From 78d974e203b30673401fa31c0af45f784121eaf1 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 08:53:07 +0100 Subject: [PATCH 01/15] Include our first set of automated tests --- .gitignore | 2 +- docs/testing-research.md | 242 +++++++++ .../Completion/CSharpCompletionSmokeTests.cs | 16 + .../Completion/RimworldXmlCompletionTests.cs | 65 +++ .../Completion/XmlPsiDiagnosticsTests.cs | 43 ++ .../ReSharperPlugin.RimworldDev.Tests.csproj | 56 +- .../RimworldDevTestEnvironmentZone.cs | 12 + .../RimworldDevTestsAssembly.cs | 7 + .../SmokeTests.cs | 16 + .../ZoneMarker.cs | 6 + .../Completion/CSharp/TestLocalVariable.cs | 8 + .../CSharp/TestLocalVariable.cs.gold | 16 + .../Rimworld/TestThingDefProperties.xml | 7 + .../Rimworld/TestThingDefProperties.xml.gold | 510 ++++++++++++++++++ .../test/data/Completion/Xml/XmlIsParsed.xml | 4 + .../data/Completion/Xml/XmlIsParsed.xml.gold | 4 + ...60ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock | 15 + .../test/data/nuget.config | 9 +- .../ReSharperPlugin.RimworldDev.csproj | 4 + .../ScopeHelper.cs | 36 +- 20 files changed, 1062 insertions(+), 16 deletions(-) create mode 100644 docs/testing-research.md create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/CSharpCompletionSmokeTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/XmlPsiDiagnosticsTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestsAssembly.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/ZoneMarker.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock diff --git a/.gitignore b/.gitignore index f8c1dbd..7844937 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,7 @@ _dotTrace* # Nuget packages/ -**/NuGetLocks/* +# NuGetLocks are committed on purpose: the lock files pin test-framework package versions # Example Mod example-mod/.idea \ No newline at end of file diff --git a/docs/testing-research.md b/docs/testing-research.md new file mode 100644 index 0000000..45f1f73 --- /dev/null +++ b/docs/testing-research.md @@ -0,0 +1,242 @@ +# Backend testing — reference + +How the ReSharper SDK test framework is used in this repo: what was learned from JetBrains' own plugins (Unity, +F#, ForTea, the plugin template), one third-party plugin (heapview) and the official docs, and — more usefully — +what it actually took to get a RimWorld XML completion gold test green here. Sources are at the end. + +## The approach + +Backend completion is tested with the **ReSharper SDK test framework: NUnit + gold files**. A test boots an +in-memory ReSharper shell (once per test assembly), creates an in-memory solution containing the test-data file(s), +runs the feature at `{caret}`, dumps the result to text and diffs it against a committed `.gold` file. First run +writes a `.tmp`; you review it and rename it to `.gold`. + +The Kotlin side is not tested. JetBrains' game-engine plugins without backend tests verify completion end-to-end from +Kotlin on TeamCity (full Rider download plus a real .NET SDK per test class); our completion logic is entirely +backend, so that route buys nothing here. + +## What exists (all green) + +`src/dotnet/ReSharperPlugin.RimworldDev.Tests`: + +| Test | Proves | +|---|---| +| `SmokeTests.ShellStarts` | the shell boots | +| `Completion/CSharpCompletionSmokeTests.TestLocalVariable` | `CodeCompletionTestBase` + gold pipeline, no RimWorld involved | +| `Completion/XmlPsiDiagnosticsTests.TestXmlIsParsed` | `.xml` in the in-memory project is parsed as XML PSI; also the `BaseTestWithSingleProject` + `ExecuteWithGold` pattern | +| `Completion/RimworldXmlCompletionTests.TestThingDefProperties` | **our provider**, backed by `Krafs.Rimworld.Ref`, lists all 249 `ThingDef` properties with C# types. Goes red when `GetAllPublicFields` is broken. | + +Run: `dotnet test src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj` +(`--filter FullyQualifiedName~RimworldXmlCompletionTests` for one fixture). ~1 min build with normal `MSB3277`/ +`NU1701`/`NU1608` noise, ~15 s of tests. + +## How it works, from `dotnet test` to a gold diff + +**1. NUnit starts, and one fixture boots a whole ReSharper.** `dotnet test` runs NUnit over our test assembly. NUnit +runs a `[SetUpFixture]` once before any test in its namespace; ours is +`RimworldDevTestsAssembly : ExtensionTestEnvironmentAssembly<…>`, and that base class *is* the ReSharper shell +bootstrapper. It does what Rider's backend process does at startup — build the component container — except +in-process, headless, and once per test run. This is why the SDK copies ~1000 DLLs into the test output: the shell +is assembled from whatever is in that folder. + +**2. The component model.** ReSharper doesn't `new` its services; nearly every class is a *component* declared with +an attribute (`[ShellComponent]`, `[SolutionComponent]`, `[PsiComponent]`, `[IntellisensePart]`, …) and created by a +container that satisfies constructor parameters from other components. Our plugin is nothing but components: +`RimworldXMLItemProvider` is one, `RimworldSymbolScope` is one, `RimworlXMLCompletionContextProvider` is one. At +startup the shell *scans* assemblies for these attributes and registers what it finds. Two consequences bit us: +the scanner reads metadata with its own reader (which choked on the net8 `JetBrains.Lifetimes`), and it only scans +assemblies the test assembly references (so the plugin was invisible until a test used a plugin type). + +**3. Zones are the on/off switches for components.** Rider, ReSharper, dotCover and JetBrains' tests all share one +component catalogue, and a *zone* is how each host says which parts of it are active. Concretely: + +- A **zone definition** is an empty interface/class marked `[ZoneDefinition]`. `IRequire` on it means + "this zone only makes sense if that one is active". Example: JetBrains' `PsiFeatureTestZone` requires the daemon, + navigation, code-editing, C#, VB, XAML… zones — it is a bundle meaning "everything a PSI feature test needs". +- A **zone marker** is a class named `ZoneMarker` marked `[ZoneMarker]`, and it applies to *every component in its + namespace and below*: those components are only loaded when all the zones the marker `IRequire<>`s are active. + The test project's marker requires our env zone, so our test-only components belong to the test host. +- Components with **no marker anywhere above them** are un-zoned and load in every host. That is our plugin's + situation, and it's why the tests didn't need to declare a plugin zone. (Unity, F# and the template do declare + one, and then the test env zone must require it, or the plugin's components are filtered out of the test shell.) +- `ITestsEnvZone` is the host zone for "I am a test run". `ExtensionTestEnvironmentAssembly` activates the + zone you give it, and everything it requires, transitively. Ours: + `RimworldDevTestEnvironmentZone : ITestsEnvZone, IRequire` — "this is a test host, and I want + the full PSI feature set". That one line is what makes C# and XML parsing, completion, the daemon and the + reference-resolution machinery exist inside the test. + +**4. Each test gets an in-memory solution.** `BaseTestWithSingleProject` (which `CodeCompletionTestBase` extends) +builds a temporary solution with one project, puts the named test-data file(s) in it, and adds references. The +project targets .NET 3.5 by default and gets its `mscorlib` etc. from small "platform" NuGet packages the framework +downloads from JetBrains' feed (hence `test/data/nuget.config` and the `NuGetLocks` lock files). Our override of +`GetReferencedAssemblies` appends Krafs' DLLs to that reference list, which is how `ScopeHelper.UpdateScopes` — which +just asks every PSI module "do you have `Verse.ThingDef`?" — finds RimWorld exactly as it does in production. + +**5. The feature runs at `{caret}`.** The framework opens the file in an in-memory text control, strips `{caret}` +and puts the caret there, then invokes the real completion pipeline: the context providers build a +`CodeCompletionContext`, every `[IntellisensePart]` items provider whose `IsAvailable` says yes gets `AddLookupItems` +called, and the lookup list is assembled with its normal relevance sorting. Our provider runs unmodified; the only +plugin-side accommodations are the `ScopeHelper` test hooks. + +**6. Dump and diff.** `CodeCompletionTestBase` serialises the lookup list (`ModernList` format) to +`.tmp`, compares it with `.gold`, and fails on any difference — or on "no gold file", which is how a +new test's first run hands you the file to review and rename. The framework also fails a test if anything *logged an +error* during it (that's how the xUnit provider and the leaked-cookie problems surfaced), so "logged N errors" in the +output means look at the `Message =` lines, not at your assertion. + +## Harness — every line in the csproj is there because of a specific crash + +Bootstrap is the template's three types (`RimworldDevTestEnvironmentZone : ITestsEnvZone, IRequire`, +`ZoneMarker`, `RimworldDevTestsAssembly : ExtensionTestEnvironmentAssembly<…>`, `[assembly: Apartment(STA)]`). +Every JetBrains example test project is `net472`; ours isn't, and that cost these: + +| Setting | Crash it fixed | +|---|---| +| `TargetFramework` **`net10.0-windows`** | Rider 2026.1's backend runs plugins on .NET 10 (the SDK bundles that runtime) and some SDK DLLs are net8-built. A net6 host dies loading them. The plugin's `net6.0` is only a compile target. | +| `UseWindowsForms` + `UseWPF` | `ShellLocks` uses WinForms timers → `FileNotFoundException` in `JetEnvironment.CreateDontRunAsync`. | +| `AssetTargetFallback=net472` | SDK packages only ship props under `build/net472/`. NuGet's default fallback list starts at `net461` and stops at the first framework a package has *any* asset for, so `LibLevelDb` (`lib/net/_._`) never got its props imported and `leveldb.dll` never reached the output. | +| `JetBrains.Microsoft.TestPlatform.TranslationLayer` `ExcludeAssets="all"` | Transitive 2019 net451 VSTest repack whose `Microsoft.TestPlatform.*` DLLs overwrite the ones `testhost` needs → `TypeLoadException` at host start. | +| Post-build copy of **net472** `JetBrains.Lifetimes`/`RdFramework` (`UseNetFrameworkJetBrainsLibs` target) | NuGet gives a .NET host the net8.0 builds; the SDK's component scanner can't read net8.0 Lifetimes metadata ("Error resolving type MaybeNullWhenAttribute… 777.0.0.0") once it scans our plugin. | +| `xunit.runner.utility.net452.dll` copied to output | The SDK's net4x xUnit provider loads it by name; NuGet gives a .NET host the `netcoreapp10` flavour. Every test "logs an error" and fails. | +| Root `Directory.Build.props`: `Lifetimes`/`RdFramework` pinned **2026.1.2** | SDK's exact requirement; floated 2026.1.3 → `MissingMethodException: RdId.Hash` when the protocol component constructs. Rider ships its own copies so production never noticed. | + +Packages: `JetBrains.ReSharper.SDK.Tests` (= `$(SdkVersion)`; a 20 KB props file that sets `JetTestProject=True`, +which makes the SDK targets copy ~1000 SDK files into the output), `Microsoft.NET.Test.Sdk`, `NUnit3TestAdapter`, +`GitHubActionsTestLogger` (our Gradle `testDotNet` passes `--logger GitHubActions`). **No explicit NUnit** — the SDK +pins `[3.13.2]` exactly. + +**The scanner only loads assemblies the test assembly references.** Until a test used a plugin type, the compiler +emitted no reference to `ReSharperPlugin.RimworldDev.dll` and our components were simply not in the shell. Any +fixture that tests the plugin must touch a plugin type (ours call `ScopeHelper.Reset()`). + +Zones: the plugin has no `ZoneMarker`; un-zoned components load everywhere, tests included. XML has no language +zone at all (`JetBrains.ReSharper.Psi.Xml.dll` defines none), so nothing to require. If a plugin zone is ever added, +the test env zone must `IRequire<>` it or every plugin component silently vanishes. + +`test/data/nuget.config` is mandatory (framework-reference packages come from `resharper-platform.jetbrains.com`). +`test/data/NuGetLocks/*.lock` **must be committed**: they pin what the *framework* downloads at run time for the +in-memory project (e.g. `JetBrains.Tests.Platform.NETFrameWork 3.5`, requested as an open range), which the csproj +never sees. Without them a new patch upload on JetBrains' feed changes the reference set under the golds. One lock +file per distinct request (file name = hash of the request); delete ones left behind by abandoned experiments. + +## Test data + +Found by walking up from the test assembly to `test/data`; per fixture `RelativeTestDataPath => @"Completion\Rimworld"`. +`DoNamedTest()` uses the **full method name**: `TestThingDefProperties` → `TestThingDefProperties.xml` (no prefix +stripping; that's `DoNamedTest2`). Gold sits beside the input as `.gold`; `.tmp` appears on mismatch. +`ExecuteWithGold(projectFile, …)` names its gold after the *source file*, so give diagnostics their own input file. +Failure to find the data root looks like "The marker item cannot be found…" then "the Shell is not running". + +## Completion tests + +```csharp +[TestFileExtension(".xml")] +public class RimworldXmlCompletionTests : CodeCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"Completion\Rimworld"; + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId tfm) => /* base + Krafs DLLs */; + [SetUp] public void Reset() { ScopeHelper.Reset(); ScopeHelper.SkipAssemblyDiscovery = true; } + [TearDown] public void Forget() { ScopeHelper.Reset(); } + [Test] public void TestThingDefProperties() => DoNamedTest(); +} +``` + +- `ModernList` = gold is the lookup list (`Completion: Basic` / `Count: N` / `Range: "<♦"` / relevance + alphabetic + sections, `<==` = selected). `Action` = gold is the document after accepting the item named by a + `// ${COMPLETE_ITEM:name}` header (`ABSENT_ITEM` asserts absence). Input needs `{caret}`. +- Empty result is `Count: 0`; `` means no provider produced a context at all — that is what stock, + schema-less XML gives in this shell, so there is no "generic XML completion" checkpoint; our provider *is* the + XML completion here. +- `LookupItemFilter(ILookupItem)` restricts the dump to chosen items (not needed yet: the 249-line ThingDef gold is a + useful regression net as-is). Also: `Sorting`, `PresentLookupItem`, `[TestSetting(typeof(Key), nameof(Key.Prop), v)]`. +- Gold formats change with SDK versions; expect regeneration on bumps. +- Later: `HighlightingTestBase` (gold = source with `|text|(0)` markers) for the value validators. + +## RimWorld types: Krafs.Rimworld.Ref + +What works: the test csproj has `` +and bakes `$(PkgKrafs_Rimworld_Ref)\ref\net472` into an `AssemblyMetadataAttribute("RimworldRefDir", …)`; the fixture +overrides `GetReferencedAssemblies` and adds every DLL there except `mscorlib*`/`System*`/`netstandard*`/`Mono.*`. +`ScopeHelper` then finds `Verse.ThingDef` exactly as it does with the real game DLL. The Krafs list is identical to +the real `Assembly-CSharp.dll`'s (the gold was first generated from the latter by accident). + +What does not work, so nobody retries it: +- `[TestPackages("Krafs.Rimworld.Ref/1.6.4871")]` restores the package but the in-memory project defaults to + **.NET 3.5** (see the lock file's `Input (NuGetFramework=net35)`), which can't consume `ref/net472` → nothing referenced. +- `[TestPlatform(".NETFramework", 4, 7, 2)]` needs a `JetBrains.Tests.Platform.NETFrameWork 4.7.2` package; the feed + stops at 4.6, and the failed restore poisons other fixtures in the run. A net35 project referencing net472-built + DLLs by path is fine. + +Plugin-side hooks added for tests (`internal`, `InternalsVisibleTo` the test assembly): +- `ScopeHelper.Reset()` — the statics otherwise hold scopes from a disposed solution (next test breaks, and the + framework reports leaked "assembly cookies" at teardown). +- `ScopeHelper.SkipAssemblyDiscovery` — without it `AddRef` finds the developer's real Steam install and adds it to + the test solution (non-hermetic, and it leaks). +- `UpdateScopes` now looks for `Verse.ThingDef` *before* the "some module has no types → not ready" bail-out; the + XML-only in-memory project is legitimately empty and used to block RimWorld detection forever. + +## When a test fails + +Failures come in five shapes; the output tells you which: + +| You see | It means | Look at | +|---|---|---| +| "There is no gold file" | new test, expected | the `.tmp` | +| "The test output differs from the gold file" | behaviour changed | `diff` the `.tmp` against the `.gold`; either fix the plugin or accept the new gold | +| `Count: 0` or `` in the `.tmp` | our provider ran but had nothing, or never ran | `ScopeHelper.UpdateScopes` returning `false` (is RimWorld referenced? did `Reset()` run?), then `IsAvailable` | +| "The test has logged N errors" | some component threw during the test; the assertion may even have passed | the `Message =` lines — usually a missing DLL or a component that couldn't construct | +| Every test fails in ~4 s with the same exception | the shell didn't boot | the first `EXCEPTION #1` — it's an environment problem (see the harness table), not a test problem | + +## Multi-file (not yet used) + +`DoTestSolution([TestName], ["Other.xml"])` for extra files in one project; `DoTestSolution(string[][])` with a project +GUID appended to a file set for a second, referenced project. `SimpleICache`s (e.g. `RimworldSymbolScope`) populate +on solution load; call `psiServices.Files.CommitAllDocuments()` before asserting. + +## Gold hygiene + +Golds are written with a UTF-8 BOM; commit as-is. Line endings are undocumented — JetBrains forces +`test/data/**/* text eol=lf`; ours is `text=auto`, add the rule before CI runs on Linux. Gitignore `*.tmp` under +test data. Keep input/gold case consistent. + +## Platform + +JetBrains' last official word (RIDER-23218, 2019): "we don't support plugin unit tests on Linux"; every surveyed +repo runs backend tests on `windows-latest`. Our CI Test job is `ubuntu-latest` and will need to move. + +## Diagnostics + +- `dotnet msbuild -getItem:JetContent -getProperty:JetTestProject` shows what the SDK will copy. +- Reflection over the DLLs in the test output (`ReflectionOnlyLoadFrom`) is the fastest way to find a type's + namespace or an attribute's constructor — nothing is documented. +- Component/zone filtering: Unity's `TestEnvironment.cs` has a `RESHARPER_LOG_CONF` recipe with TRACE loggers for + `JetBrains.Application.Environment.JetEnvironment`, `…Extensibility.CatalogComponentSource`, + `…Environment.RunsProducts`, `…Catalogs.PartCatalogZoneMapping`. +- "The test has logged N errors" fails a test even when its own assertion passed; read the `Message =` lines. + +## Sources + +Best code references: Unity's +[TestEnvironment.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/TestEnvironment.cs) +(zone comments are the real docs), +[AsmDefReferencesCompletionTests.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/Unity/AsmDef/Feature/Services/CodeCompletion/AsmDefReferencesCompletionTests.cs) + +[gold](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/data/Unity/AsmDef/CodeCompletion/AsmDefReferences/TestList01.asmdef.gold), +[TestUnityAttribute.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/Unity/TestUnityAttribute.cs); +F#'s [Common.fs](https://raw.githubusercontent.com/JetBrains/resharper-fsharp/main/ReSharper.FSharp/test/src/FSharp.Tests.Common/src/Common.fs) and +[FSharpCompletionTest.fs](https://raw.githubusercontent.com/JetBrains/resharper-fsharp/main/ReSharper.FSharp/test/src/FSharp.Tests/FSharpCompletionTest.fs); +ForTea's [T4CodeCompletionTest.cs](https://raw.githubusercontent.com/JetBrains/ForTea/master/Backend/RiderPlugin/test/src/T4CodeCompletionTest.cs) + +[Directive.tt.gold](https://raw.githubusercontent.com/JetBrains/ForTea/master/Backend/RiderPlugin/test/data/CodeCompletion/Directive.tt.gold); +heapview's [test csproj](https://raw.githubusercontent.com/controlflow/resharper-heapview/master/src/dotnet/ReSharperPlugin.HeapView.Tests/ReSharperPlugin.HeapView.Tests.csproj) and +[ci.yml](https://raw.githubusercontent.com/controlflow/resharper-heapview/master/.github/workflows/ci.yml); +the [template's Tests project](https://github.com/JetBrains/resharper-rider-plugin/tree/master/content/src/dotnet/ReSharperPlugin.SamplePlugin.Tests). + +Official docs worth reading (the rest is skeletal or stale): +[ProjectStructure](https://www.jetbrains.com/help/resharper/sdk/ProjectStructure.html), +[GoldFiles](https://www.jetbrains.com/help/resharper/sdk/GoldFiles.html), +[ExternalAnnotations_Testing](https://www.jetbrains.com/help/resharper/sdk/ExternalAnnotations_Testing.html) (`[TestReferences]`), +[Analysis_Testing](https://www.jetbrains.com/help/resharper/sdk/Analysis_Testing.html). + +Negative results: `godot-support` and `azure-tools-for-intellij` have no backend tests (Kotlin end-to-end only, TeamCity); +the docs' `ITestsZone` is stale (`ITestsEnvZone` is current); `[TestPackages]`, `[TestPlatform]`, +`CodeCompletionTestBase` and non-net472 hosts are undocumented anywhere. diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/CSharpCompletionSmokeTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/CSharpCompletionSmokeTests.cs new file mode 100644 index 0000000..dec7a60 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/CSharpCompletionSmokeTests.cs @@ -0,0 +1,16 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.Completion; + +/// +/// Proves that CodeCompletionTestBase + gold files work in this harness at all, with nothing RimWorld-specific +/// involved. If this is red, the completion pipeline itself is broken, not our provider. +/// +public class CSharpCompletionSmokeTests : CodeCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"Completion\CSharp"; + + [Test] public void TestLocalVariable() => DoNamedTest(); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs new file mode 100644 index 0000000..9c3ecb3 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.Completion; + +/// +/// The real thing: RimWorld XML completion backed by the game's types. Krafs.Rimworld.Ref (a complete reference +/// assembly for RimWorld) is referenced into the in-memory project, and ScopeHelper finds it the same way it finds +/// the real Assembly-CSharp.dll: by looking for Verse.ThingDef. +/// +[TestFileExtension(".xml")] +public class RimworldXmlCompletionTests : CodeCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"Completion\Rimworld"; + + /// + /// Every DLL from the Krafs package's ref/net472 folder except the framework ones, which the test platform + /// already provides. The folder path is baked into this assembly by the csproj from NuGet's restore. + /// + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) + { + var refDir = typeof(RimworldXmlCompletionTests).Assembly + .GetCustomAttributes() + .Single(a => a.Key == "RimworldRefDir").Value; + + var rimworldDlls = Directory.GetFiles(refDir, "*.dll") + .Where(path => + { + var name = Path.GetFileName(path); + return !name.StartsWith("mscorlib", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("System", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("Mono.", StringComparison.OrdinalIgnoreCase); + }); + + return base.GetReferencedAssemblies(targetFrameworkId).Concat(rimworldDlls); + } + + [SetUp] + public void ResetRimworldScope() + { + // ScopeHelper caches the RimWorld scope in statics; without this the second test reuses a scope from a solution + // that no longer exists. The discovery switch stops it from finding a real RimWorld install on this machine and + // adding that to the test solution on top of the Krafs reference. + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() + { + // Drop our references to the solution's modules before the framework checks that nothing is still holding them. + ScopeHelper.Reset(); + } + + [Test] public void TestThingDefProperties() => DoNamedTest(); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/XmlPsiDiagnosticsTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/XmlPsiDiagnosticsTests.cs new file mode 100644 index 0000000..7189d48 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/XmlPsiDiagnosticsTests.cs @@ -0,0 +1,43 @@ +using System.Linq; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Xml.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.Completion; + +/// +/// Dumps how the test shell sees an .xml file in the in-memory project. Exists to answer "is XML even parsed as +/// XML here?" when XML completion returns nothing. +/// +[TestFileExtension(".xml")] +public class XmlPsiDiagnosticsTests : BaseTestWithSingleProject +{ + protected override string RelativeTestDataPath => @"Completion\Xml"; + + [Test] public void TestXmlIsParsed() => DoTestSolution("XmlIsParsed.xml"); + + protected override void DoTest(Lifetime lifetime, IProject project) + { + Solution.GetPsiServices().Files.CommitAllDocuments(); + using (ReadLockCookie.Create()) + { + var projectFile = project.GetAllProjectFiles().Single(); + var sourceFile = projectFile.ToSourceFiles().Single(); + var psiFile = sourceFile.GetPrimaryPsiFile(); + + ExecuteWithGold(projectFile, writer => + { + writer.WriteLine($"ProjectFileType: {projectFile.LanguageType.Name}"); + writer.WriteLine($"PrimaryPsiLanguage: {sourceFile.PrimaryPsiLanguage.Name}"); + writer.WriteLine($"PsiFile: {psiFile?.GetType().FullName ?? ""}"); + writer.WriteLine($"XmlTags: {psiFile?.Descendants().ToEnumerable().Count() ?? -1}"); + }); + } + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index 8f8bcdd..04ce93a 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -1,12 +1,46 @@  - net10.0 + + net10.0-windows + + true + true + + net472 false + latest + + + + + + + + + + + + + + + <_Parameter1>RimworldRefDir + <_Parameter2>$(PkgKrafs_Rimworld_Ref)\ref\net472 + @@ -16,6 +50,24 @@ + + + - \ No newline at end of file + + + + <_JetNetFxLib Include="$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))jetbrains.lifetimes/2026.1.2/lib/net472/JetBrains.Lifetimes.dll" /> + <_JetNetFxLib Include="$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))jetbrains.rdframework/2026.1.2/lib/net472/JetBrains.RdFramework.dll" /> + + + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs new file mode 100644 index 0000000..c85a7ae --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs @@ -0,0 +1,12 @@ +using System.Threading; +using JetBrains.Application.BuildScript.Application.Zones; +using JetBrains.ReSharper.TestFramework; +using JetBrains.TestFramework.Application.Zones; +using NUnit.Framework; + +[assembly: Apartment(ApartmentState.STA)] + +namespace ReSharperPlugin.RimworldDev.Tests; + +[ZoneDefinition] +public class RimworldDevTestEnvironmentZone : ITestsEnvZone, IRequire; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestsAssembly.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestsAssembly.cs new file mode 100644 index 0000000..90e8fec --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestsAssembly.cs @@ -0,0 +1,7 @@ +using JetBrains.TestFramework; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests; + +[SetUpFixture] +public class RimworldDevTestsAssembly : ExtensionTestEnvironmentAssembly; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests.cs new file mode 100644 index 0000000..508e073 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests.cs @@ -0,0 +1,16 @@ +using JetBrains.TestFramework; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests; + +/// +/// Proves the ReSharper test shell boots at all. If this is red, nothing else in the project can be green. +/// +public class SmokeTests : BaseTest +{ + [Test] + public void ShellStarts() + { + Assert.That(ShellInstance, Is.Not.Null); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ZoneMarker.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ZoneMarker.cs new file mode 100644 index 0000000..56d8ed6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ZoneMarker.cs @@ -0,0 +1,6 @@ +using JetBrains.Application.BuildScript.Application.Zones; + +namespace ReSharperPlugin.RimworldDev.Tests; + +[ZoneMarker] +public class ZoneMarker : IRequire; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs new file mode 100644 index 0000000..96bfa12 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs @@ -0,0 +1,8 @@ +class C +{ + void M() + { + var myLocalVariable = 1; + myLoc{caret} + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs.gold new file mode 100644 index 0000000..fe33494 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/CSharp/TestLocalVariable.cs.gold @@ -0,0 +1,16 @@ +Completion: Basic +Prefix: "myLoc" +Count: 1 +Focus: Hard +Range: "myLoc♦" + ▲▲▲▲▲▲ + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, PrefixMatch, LocalVariablesAndParameters, NotObsolete, ObsoleteRuleApplied, NormalSelectionPriority, ClosestLocalVar, Other] +ṃỵḶọc̣alVariable int <== + +##### ALPHABETIC SORT ##### + + [Light, Generic] +ṃỵḶọc̣alVariable int <== diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml new file mode 100644 index 0000000..84ec20c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml @@ -0,0 +1,7 @@ + + + + TestThing + <{caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml.gold new file mode 100644 index 0000000..c79daa5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml.gold @@ -0,0 +1,510 @@ +Completion: Basic +Count: 249 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +allowedArchonexusCount int <== +altitudeLayer AltitudeLayer +alwaysFlee bool +alwaysHaulable bool +apparel ApparelProperties +artisticSkillPrerequisite int +autoTargetNearbyIdenticalThings bool +blockLight bool +blockPlants bool +blockWeather bool +blockWind bool +blocksAltitudes List +bringAlongOnGravship bool +building BuildingProperties +buildingPrerequisites List +burnableByRecipe bool +butcherProducts List +canBeUsedUnderRoof bool +canDeteriorateUnspawned bool +canEditAnyStyle bool +canGenerateDefaultDesignator bool +canInteractThroughCorners bool +canLoadIntoCaravan bool +canScatterOver bool +castEdgeShadows bool +category ThingCategory +clearBuildingArea bool +colorGenerator ColorGenerator +colorGeneratorInTraderStock ColorGenerator +colorPerStuff List +comps List +constructEffect EffecterDef +constructionSkillPrerequisite int +containedItemsSelectable bool +containedPawnsSelectable bool +costList List +costListForDifficulty CostListForDifficulty +costStuffCount int +coversFloor bool +damageMultipliers List +deepCommonality float +deepCountPerCell int +deepCountPerPortion int +deepLumpSizeRange IntRange +defName string +defaultPlacingRot Rot4 +defaultStuff ThingDef +description string +descriptionHyperlinks List +deselectedSelectionBracketFactor float +designateHaulable bool +designationCategory DesignationCategoryDef +designationHotKey KeyBindingDef +designatorDropdown DesignatorDropdownGroupDef +destroyOnDrop bool +destroyable bool +deteriorateFromEnvironmentalEffects bool +devNote string +disableImpassableShotOverConfigError bool +discoveryPrerequisites List +displayNumbersBetweenSameDefDistRange FloatRange +dominantStyleCategory StyleCategoryDef +dontPrint bool +drawDamagedOverlay bool +drawGUIOverlay bool +drawGUIOverlayQuality bool +drawHighlight bool +drawHighlightOnlyForHostile bool +drawOffscreen bool +drawPlaceWorkersWhileInstallBlueprintSelected bool +drawPlaceWorkersWhileSelected bool +drawStyleCategory DrawStyleCategoryDef +drawerType DrawerType +dropPodActive ThingDef +dropPodFaller ThingDef +entityCodexEntry EntityCodexEntryDef +entityDefToBuild BuildableDef +equipmentType EquipmentType +equippedAngleOffset float +equippedDistanceOffset float +equippedStatOffsets List +fertility float +fillPercent float +filth FilthProperties +filthLeaving ThingDef +forceDebugSpawnable bool +forceLeavingsAllowed bool +forceMoveItemsBeforeConstruction bool +forcePassableByFlyingPawns bool +gas GasProperties +generateAllowChance float +generateCommonality float +genericMarketSellable bool +graphicData GraphicData +gravshipSpawnPriority int +hasCustomRectForSelector bool +hasInteractionCell bool +hasTooltip bool +healthAffectsPrice bool +hiddenWhileUndiscovered bool +hideAtSnowOrSandDepth float +hideInspect bool +hideMainDesc bool +hideStats bool +highlightColor Color? +holdsRoof bool +ideoBuilding bool +ideoBuildingNamerBase RulePackDef +ignoreConfigErrors bool +ignoreIllegalLabelCharacterConfigError bool +ingestible IngestibleProperties +ingredient IngredientProperties +inspectorTabs List +interactionCellIcon ThingDef +interactionCellIconReverse bool +interactionCellOffset IntVec3 +intricate bool +isAltar bool +isAutoAttackableMapObject bool +isFrameInt bool +isMechClusterThreat bool +isSaveable bool +isTechHediff bool +isUnfinishedThing bool +killedLeavings List +killedLeavingsChance float +killedLeavingsExpandRect int +killedLeavingsPlayerHostile List +killedLeavingsRanges List +label string +leaveResourcesWhenKilled bool +maxTechLevelToBuild TechLevel +meleeHitSound SoundDef +mergeVerbGizmos bool +messageOnDeteriorateInStorage bool +minMonolithLevel int +minRewardCount int +minTechLevelToBuild TechLevel +mineable bool +minifiedDef ThingDef +minifiedDrawOffset Vector3 +minifiedDrawScale float +minifiedManualDraw bool +modExtensions List +mote MoteProperties +multipleInteractionCellOffsets List +neverMultiSelect bool +neverOverlapFloors bool +noRightClickDraftAttack bool +notifyMapRemoved bool +onlyShowInspectString bool +orderedTakeGroup OrderedTakeGroupDef +overrideMinifiedRot Rot4 +passability Traversability +pathCost int +pathCostIgnoreRepeat bool +pathfinderDangerous bool +pawnFlyer PawnFlyerProperties +placeWorkers List +plant PlantProperties +portal MapPortalProperties +possessionCount int +preventDroppingThingsOn bool +preventGravshipLandingOn bool +preventSkyfallersLandingOn bool +preventSpawningInResourcePod bool +projectile ProjectileProperties +projectileWhenLoaded ThingDef +race RaceProperties +randomStyle List +randomStyleChance float +randomizeRotationOnSpawn bool +receivesSignals bool +recipeMaker RecipeMakerProperties +recipes List +recoilPower float +recoilRelaxation float +relicChance float +repairEffect EffecterDef +replaceTags List +requireInspectedGravEngine bool +requiresFactionToAcquire FactionDef +researchPrerequisites List +resourceReadoutAlwaysShow bool +resourceReadoutPriority ResourceCountPriority +resourcesFractionWhenDeconstructed float +ritualFocus RitualFocusProperties +rotatable bool +rotateInShelves bool +saveCompressible bool +scatterableOnMapGen bool +seeThroughFog bool +selectable bool +showInSearch bool +size IntVec2 +skyfaller SkyfallerProperties +slagDef ThingDef +smallVolume bool +smeltProducts List +smeltable bool +socialPropernessMatters bool +soundDrop SoundDef +soundImpactDefault SoundDef +soundInteract SoundDef +soundOpen SoundDef +soundPickup SoundDef +soundPlayInstrument SoundDef +soundSpawned SoundDef +specialDisplayRadius float +stackLimit int +startingHpRange FloatRange +statBases List +staticSunShadowHeight float +stealable bool +storedConceptLearnOpportunity ConceptDef +stuffCategories List +stuffCategorySummary string +stuffProps StuffProperties +surfaceType SurfaceType +techHediffsTags List +techLevel TechLevel +terrainAffordanceNeeded TerrainAffordanceDef +thingCategories List +thingClass Type +thingSetMakerTags List +tickerType TickerType +tools List +tradeNeverGenerateStacked bool +tradeNeverStack bool +tradeTags List +tradeability Tradeability +uiIconColor Color +uiIconColorTwo Color +uiIconForStackCount int +uiIconOffset Vector2 +uiIconPath string +uiIconPathsStuff List +uiIconScale float +uiOrder float +useBlueprintGraphicAsGhost bool +useHitPoints bool +useSameGraphicForGhost bool +useStuffTerrainAffordance bool +violentTechHediff bool +virtualDefParent ThingDef +virtualDefs List +weaponClasses List +weaponTags List +wipesPlants bool + +##### ALPHABETIC SORT ##### + + [Light, Generic] +allowedArchonexusCount int <== +altitudeLayer AltitudeLayer +alwaysFlee bool +alwaysHaulable bool +apparel ApparelProperties +artisticSkillPrerequisite int +autoTargetNearbyIdenticalThings bool +blockLight bool +blockPlants bool +blockWeather bool +blockWind bool +blocksAltitudes List +bringAlongOnGravship bool +building BuildingProperties +buildingPrerequisites List +burnableByRecipe bool +butcherProducts List +canBeUsedUnderRoof bool +canDeteriorateUnspawned bool +canEditAnyStyle bool +canGenerateDefaultDesignator bool +canInteractThroughCorners bool +canLoadIntoCaravan bool +canScatterOver bool +castEdgeShadows bool +category ThingCategory +clearBuildingArea bool +colorGenerator ColorGenerator +colorGeneratorInTraderStock ColorGenerator +colorPerStuff List +comps List +constructEffect EffecterDef +constructionSkillPrerequisite int +containedItemsSelectable bool +containedPawnsSelectable bool +costList List +costListForDifficulty CostListForDifficulty +costStuffCount int +coversFloor bool +damageMultipliers List +deepCommonality float +deepCountPerCell int +deepCountPerPortion int +deepLumpSizeRange IntRange +defName string +defaultPlacingRot Rot4 +defaultStuff ThingDef +description string +descriptionHyperlinks List +deselectedSelectionBracketFactor float +designateHaulable bool +designationCategory DesignationCategoryDef +designationHotKey KeyBindingDef +designatorDropdown DesignatorDropdownGroupDef +destroyOnDrop bool +destroyable bool +deteriorateFromEnvironmentalEffects bool +devNote string +disableImpassableShotOverConfigError bool +discoveryPrerequisites List +displayNumbersBetweenSameDefDistRange FloatRange +dominantStyleCategory StyleCategoryDef +dontPrint bool +drawDamagedOverlay bool +drawGUIOverlay bool +drawGUIOverlayQuality bool +drawHighlight bool +drawHighlightOnlyForHostile bool +drawOffscreen bool +drawPlaceWorkersWhileInstallBlueprintSelected bool +drawPlaceWorkersWhileSelected bool +drawStyleCategory DrawStyleCategoryDef +drawerType DrawerType +dropPodActive ThingDef +dropPodFaller ThingDef +entityCodexEntry EntityCodexEntryDef +entityDefToBuild BuildableDef +equipmentType EquipmentType +equippedAngleOffset float +equippedDistanceOffset float +equippedStatOffsets List +fertility float +fillPercent float +filth FilthProperties +filthLeaving ThingDef +forceDebugSpawnable bool +forceLeavingsAllowed bool +forceMoveItemsBeforeConstruction bool +forcePassableByFlyingPawns bool +gas GasProperties +generateAllowChance float +generateCommonality float +genericMarketSellable bool +graphicData GraphicData +gravshipSpawnPriority int +hasCustomRectForSelector bool +hasInteractionCell bool +hasTooltip bool +healthAffectsPrice bool +hiddenWhileUndiscovered bool +hideAtSnowOrSandDepth float +hideInspect bool +hideMainDesc bool +hideStats bool +highlightColor Color? +holdsRoof bool +ideoBuilding bool +ideoBuildingNamerBase RulePackDef +ignoreConfigErrors bool +ignoreIllegalLabelCharacterConfigError bool +ingestible IngestibleProperties +ingredient IngredientProperties +inspectorTabs List +interactionCellIcon ThingDef +interactionCellIconReverse bool +interactionCellOffset IntVec3 +intricate bool +isAltar bool +isAutoAttackableMapObject bool +isFrameInt bool +isMechClusterThreat bool +isSaveable bool +isTechHediff bool +isUnfinishedThing bool +killedLeavings List +killedLeavingsChance float +killedLeavingsExpandRect int +killedLeavingsPlayerHostile List +killedLeavingsRanges List +label string +leaveResourcesWhenKilled bool +maxTechLevelToBuild TechLevel +meleeHitSound SoundDef +mergeVerbGizmos bool +messageOnDeteriorateInStorage bool +minMonolithLevel int +minRewardCount int +minTechLevelToBuild TechLevel +mineable bool +minifiedDef ThingDef +minifiedDrawOffset Vector3 +minifiedDrawScale float +minifiedManualDraw bool +modExtensions List +mote MoteProperties +multipleInteractionCellOffsets List +neverMultiSelect bool +neverOverlapFloors bool +noRightClickDraftAttack bool +notifyMapRemoved bool +onlyShowInspectString bool +orderedTakeGroup OrderedTakeGroupDef +overrideMinifiedRot Rot4 +passability Traversability +pathCost int +pathCostIgnoreRepeat bool +pathfinderDangerous bool +pawnFlyer PawnFlyerProperties +placeWorkers List +plant PlantProperties +portal MapPortalProperties +possessionCount int +preventDroppingThingsOn bool +preventGravshipLandingOn bool +preventSkyfallersLandingOn bool +preventSpawningInResourcePod bool +projectile ProjectileProperties +projectileWhenLoaded ThingDef +race RaceProperties +randomStyle List +randomStyleChance float +randomizeRotationOnSpawn bool +receivesSignals bool +recipeMaker RecipeMakerProperties +recipes List +recoilPower float +recoilRelaxation float +relicChance float +repairEffect EffecterDef +replaceTags List +requireInspectedGravEngine bool +requiresFactionToAcquire FactionDef +researchPrerequisites List +resourceReadoutAlwaysShow bool +resourceReadoutPriority ResourceCountPriority +resourcesFractionWhenDeconstructed float +ritualFocus RitualFocusProperties +rotatable bool +rotateInShelves bool +saveCompressible bool +scatterableOnMapGen bool +seeThroughFog bool +selectable bool +showInSearch bool +size IntVec2 +skyfaller SkyfallerProperties +slagDef ThingDef +smallVolume bool +smeltProducts List +smeltable bool +socialPropernessMatters bool +soundDrop SoundDef +soundImpactDefault SoundDef +soundInteract SoundDef +soundOpen SoundDef +soundPickup SoundDef +soundPlayInstrument SoundDef +soundSpawned SoundDef +specialDisplayRadius float +stackLimit int +startingHpRange FloatRange +statBases List +staticSunShadowHeight float +stealable bool +storedConceptLearnOpportunity ConceptDef +stuffCategories List +stuffCategorySummary string +stuffProps StuffProperties +surfaceType SurfaceType +techHediffsTags List +techLevel TechLevel +terrainAffordanceNeeded TerrainAffordanceDef +thingCategories List +thingClass Type +thingSetMakerTags List +tickerType TickerType +tools List +tradeNeverGenerateStacked bool +tradeNeverStack bool +tradeTags List +tradeability Tradeability +uiIconColor Color +uiIconColorTwo Color +uiIconForStackCount int +uiIconOffset Vector2 +uiIconPath string +uiIconPathsStuff List +uiIconScale float +uiOrder float +useBlueprintGraphicAsGhost bool +useHitPoints bool +useSameGraphicForGhost bool +useStuffTerrainAffordance bool +violentTechHediff bool +virtualDefParent ThingDef +virtualDefs List +weaponClasses List +weaponTags List +wipesPlants bool diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml new file mode 100644 index 0000000..f2dd956 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml @@ -0,0 +1,4 @@ + + text + <{caret} + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml.gold new file mode 100644 index 0000000..f77782b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Xml/XmlIsParsed.xml.gold @@ -0,0 +1,4 @@ +ProjectFileType: XML +PrimaryPsiLanguage: XML +PsiFile: JetBrains.ReSharper.Psi.Xml.Impl.Tree.XmlFile +XmlTags: 3 diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock new file mode 100644 index 0000000..8b9d669 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/13F421D86A81D99BBBFA0B1049260ED77771A9D305E9DDDCEFDFDC01B0EFA7B0.lock @@ -0,0 +1,15 @@ +# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. +# Input (NuGetFramework=net35): +# JetBrains.Tests.Platform.NETFrameWork [2.0.0.0, 2.0.0.2147483647] +# JetBrains.Tests.Platform.NETFrameWork [3.0.0.0, 3.0.0.2147483647] +# JetBrains.Tests.Platform.NETFrameWork [3.5.0.0, 3.5.0.2147483647] +JetBrains.Tests.Microsoft.Gac 1.1 +JetBrains.Tests.Platform.NETFrameWork 2.0 +JetBrains.Tests.Platform.NETFrameWork 3.0 +JetBrains.Tests.Platform.NETFrameWork 3.5 +JetBrains.Tests.Platform.NetFramework.Binaries 2.0 +JetBrains.Tests.Platform.NetFramework.Binaries 3.0 +JetBrains.Tests.Platform.NetFramework.Binaries 3.5 +JetBrains.Tests.Platform.NetFramework.Profiles 3.5 +JetBrains.Tests.Platform.NetFramework.ReferenceAssemblies 3.0 +JetBrains.Tests.Platform.NetFramework.ReferenceAssemblies 3.5 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/nuget.config b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/nuget.config index d292778..575b4ab 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/nuget.config +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/nuget.config @@ -1,21 +1,18 @@ - + - - + + - - - diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj index 017f183..fe39417 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj @@ -21,6 +21,10 @@ true + + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs index 57660cd..7f7f6c9 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs @@ -27,6 +27,26 @@ public class ScopeHelper private static List usedScopes; private static bool adding = false; + /// + /// When set, never go looking for Assembly-CSharp.dll on disk. Tests set this so that a RimWorld install on the + /// developer's machine can't leak into an in-memory test solution that already references the game types. + /// + internal static bool SkipAssemblyDiscovery; + + /// + /// Forgets every cached scope/module. Tests need this because the statics otherwise outlive the in-memory solution + /// that produced them; production never calls it. + /// + internal static void Reset() + { + allScopes = new(); + knownCustomScopes = new(); + rimworldScope = null; + rimworldModule = null; + usedScopes = null; + adding = false; + } + public static bool UpdateScopes(ISolution solution) { if (solution == null) return false; @@ -35,12 +55,6 @@ public static bool UpdateScopes(ISolution solution) allScopes = solution.PsiModules().GetModules().Select(module => module.GetPsiServices().Symbols.GetSymbolScope(module, true, true)).ToList(); - // If we haven't determined the Rimworld scope yet, our scopes may not be ready for querying. Since I'd rather - // that we were able to pull the scope from the dependencies than try to find it ourselves, let's check if the - // scopes are ready for querying first. Ofcourse, if we have no scopes at all, there's nothing to wait for - if (rimworldScope == null && allScopes.Any() && allScopes.Any(scope => !scope.GetAllShortNames().Any())) - return false; - if (rimworldScope == null) { rimworldScope = @@ -48,6 +62,14 @@ public static bool UpdateScopes(ISolution solution) if (rimworldScope == null) { + // If we haven't determined the Rimworld scope yet, our scopes may not be ready for querying. Since I'd + // rather that we were able to pull the scope from the dependencies than try to find it ourselves, let's + // check if the scopes are ready for querying first. Ofcourse, if we have no scopes at all, there's + // nothing to wait for. This check deliberately comes *after* looking for Rimworld: a module that is + // legitimately empty (an XML-only project) must not stop us from using a Rimworld module that's ready. + if (allScopes.Any() && allScopes.Any(scope => !scope.GetAllShortNames().Any())) + return false; + AddRef(solution); return false; @@ -81,7 +103,7 @@ public static ISymbolScope GetScopeForClass(string className) private static async void AddRef(ISolution solution) { - if (adding) return; + if (adding || SkipAssemblyDiscovery) return; adding = true; var path = FindRimworldDll(solution.SolutionDirectory.FullPath); From 1df3563f611276c54b8e9b6295f0aab46b51dd0e Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 11:00:18 +0100 Subject: [PATCH 02/15] Adding more experimental tests --- docs/testing-plan.md | 229 ++++++++ docs/testing-research.md | 60 +- .../RimworldCSharpCompletionTests.cs | 27 + .../Completion/RimworldCompletionTestBase.cs | 59 ++ .../Completion/RimworldXmlCompletionTests.cs | 81 ++- .../FindUsages/RimworldFindUsagesTests.cs | 48 ++ .../RimworldXmlHighlightingTests.cs | 59 ++ .../References/RimworldNavigationTests.cs | 36 ++ .../References/RimworldReferenceTests.cs | 93 ++++ .../Rimworld/Action/TestCompleteEnumValue.xml | 8 + .../Action/TestCompleteEnumValue.xml.gold | 8 + .../Rimworld/Action/TestCompleteTag.xml | 10 + .../Rimworld/Action/TestCompleteTag.xml.gold | 10 + .../test/data/Completion/Rimworld/ModTypes.cs | 12 + .../data/Completion/Rimworld/OtherDefs.xml | 9 + .../Rimworld/TestDefReferenceOtherFile.xml | 7 + .../TestDefReferenceOtherFile.xml.gold | 16 + .../Rimworld/TestDefReferenceSameFile.xml | 10 + .../TestDefReferenceSameFile.xml.gold | 16 + .../Completion/Rimworld/TestEnumValue.xml | 7 + .../Rimworld/TestEnumValue.xml.gold | 36 ++ .../Rimworld/TestListItemProperties.xml | 11 + .../Rimworld/TestListItemProperties.xml.gold | 14 + .../TestListItemWithClassProperties.xml | 11 + .../TestListItemWithClassProperties.xml.gold | 34 ++ .../TestModDefAsSuperclassReference.xml | 10 + .../TestModDefAsSuperclassReference.xml.gold | 16 + .../Rimworld/TestModDefClassProperties.xml | 7 + .../TestModDefClassProperties.xml.gold | 512 ++++++++++++++++++ .../TestModListItemClassProperties.xml | 11 + .../TestModListItemClassProperties.xml.gold | 16 + .../Rimworld/TestNestedFieldProperties.xml | 9 + .../TestNestedFieldProperties.xml.gold | 80 +++ .../Completion/Rimworld/TestParentName.xml | 13 + .../Rimworld/TestParentName.xml.gold | 14 + .../data/Completion/RimworldCSharp/Defs.xml | 12 + .../RimworldCSharp/TestDefDatabaseGetNamed.cs | 9 + .../TestDefDatabaseGetNamed.cs.gold | 16 + .../TestDefOfFieldWithPrefix.cs | 11 + .../TestDefOfFieldWithPrefix.cs.gold | 22 + .../TestDefOfFieldWithPrefixAndSemicolon.cs | 11 + ...stDefOfFieldWithPrefixAndSemicolon.cs.gold | 22 + .../test/data/FindUsages/CSharpUsages.cs | 16 + .../test/data/FindUsages/Defs.xml | 10 + .../test/data/FindUsages/OtherUsages.xml | 10 + .../data/FindUsages/TestFromCSharpString.cs | 16 + .../FindUsages/TestFromCSharpString.cs.gold | 97 ++++ .../test/data/FindUsages/TestFromDefName.xml | 10 + .../data/FindUsages/TestFromDefName.xml.gold | 70 +++ .../data/FindUsages/TestFromNameAttribute.xml | 11 + .../FindUsages/TestFromNameAttribute.xml.gold | 70 +++ .../data/Highlighting/TestInvalidBool.xml | 8 + .../Highlighting/TestInvalidBool.xml.gold | 11 + .../Highlighting/TestNoRimworldReference.xml | 8 + .../TestNoRimworldReference.xml.gold | 10 + ...2d172759429b0a03f8481a2e8f8f14cb6ec21.lock | 9 + .../test/data/References/CSharpDefs.xml | 12 + .../test/data/References/CSharpToXmlDef.cs | 21 + .../data/References/CSharpToXmlDef.cs.gold | 4 + .../Navigation/TestNavigateToCSharpField.xml | 7 + .../TestNavigateToCSharpField.xml.gold | 54 ++ .../Navigation/TestNavigateToXmlDef.xml | 10 + .../Navigation/TestNavigateToXmlDef.xml.gold | 68 +++ .../test/data/References/OtherDefs.xml | 9 + .../test/data/References/XmlToCSharp.xml | 19 + .../test/data/References/XmlToCSharp.xml.gold | 9 + .../test/data/References/XmlToXmlDef.xml | 18 + .../test/data/References/XmlToXmlDef.xml.gold | 18 + 68 files changed, 2252 insertions(+), 55 deletions(-) create mode 100644 docs/testing-plan.md create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/ModTypes.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/OtherDefs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/Defs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/CSharpUsages.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/Defs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/OtherUsages.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/279ddca82531108db2d9065f4f72d172759429b0a03f8481a2e8f8f14cb6ec21.lock create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpDefs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/OtherDefs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold diff --git a/docs/testing-plan.md b/docs/testing-plan.md new file mode 100644 index 0000000..2b754e7 --- /dev/null +++ b/docs/testing-plan.md @@ -0,0 +1,229 @@ +# Backend testing — broadening plan + +The first round (see `testing-research.md`) proved one path: `CodeCompletionTestBase` + gold files, Krafs referenced +into the in-memory project, `ScopeHelper` finding `Verse.ThingDef`, and `RimworldXMLItemProvider` listing one type's +fields at the top level of a def. + +> **Status (2026-09-18): experiment phase closed after Phase F.** See "Conclusions" at the end. Phases G and H were +> not run; they remain here as the map for whoever picks this up. + +This plan is about finding **where the harness stops working**, not about coverage. Each step changes one thing +relative to something already green, so a failure points at that one change. Stop at the first red step, fix or +record the boundary, then carry on. + +Status column: `—` not started, `✅` green, `❌` boundary found (see notes), `⚠️` green with caveats. + +## Phase A — same fixture, same single file, harder paths through the provider + +Only the input XML changes; the C# fixture is untouched. + +| # | Step | New variable | Status | +|---|---|---|---| +| 1 | Nested field: `<{caret}` | `GetContextFromHierachy` following a field into its type | ✅ | +| 2 | `
  • ` of a list: `
  • <{caret}` | `"li"` resolved via previous field's `List` type argument | ✅ | +| 3 | `
  • <{caret}` | the `li` branch, class resolved against RimWorld's scope | ✅ | +| 4 | Text value, enum: `{caret}` | the `TEXT` branch of `IsAvailable`/`AddLookupItems` | ✅ | +| 5 | Accept an item (`CodeCompletionTestType.Action` + `${COMPLETE_ITEM:…}`) | gold is the document after insertion | ❌ | + +## Phase B — the def index (`RimworldSymbolScope`) + +Still completion, but now dependent on a `SimpleICache` populating inside the test shell. + +| # | Step | New variable | Status | +|---|---|---|---| +| 6 | Def reference, same file: caret in a field typed as a `Def` subclass, target def in the same file | `RimworldSymbolScope` indexing in a test solution | ✅ | +| 7 | Same, target def in a second file | `DoTestSolution(name, ["Other.xml"])` — first multi-file test | ✅ | +| 8 | `ParentName=""` completion with `Abstract="true"` and concrete defs | attribute branch + `DefTags` abstract flag | ✅ | +| 9 | Mod-defined def class in a `.cs` file (`MyMod.CustomThingDef : ThingDef`, `li Class="MyMod.Foo"`) | mixed C#/XML project, `ExtraDefTagNames`, all-solution-scopes fallback | ⚠️ | + +## Phase C — the same pipeline from the C# side + +Phase B's XML + index setup, caret in a `.cs` file, different providers. + +| # | Step | New variable | Status | +|---|---|---|---| +| 10 | `[DefOf]` field completion against defs in a companion `.xml` | `CSharpDefsOfItemProvider` | ⚠️ | +| 11 | `DefDatabase.GetNamed("{caret}")` | `RimworldDefCSharpItemProvider` | ✅ | + +## Phase D — reference resolution (Ctrl+Click): a new kind of test + +| # | Step | New variable | Status | +|---|---|---|---| +| 12 | Hand-rolled dump on `BaseTestWithSingleProject` + `ExecuteWithGold` (proven by `XmlPsiDiagnosticsTests`): every tag → `GetReferences()` → `Resolve()` → declared element | `RimworldReferenceProvider` + `RimworldXmlReference` (XML → C# field) | ✅ | +| 13 | Same dump over Phase B input | `RimworldXmlDefReference` (XML → XML def) | ✅ | +| 14 | Same dump over Phase C input | `RimworldCSharpReferenceProvider` (C# string → XML def) | ✅ | +| 15 | (Optional) port 12–14 onto the SDK's own reference test base, if one exists — find it by reflection | the SDK base class | ✅ | + +## Phase E — daemon / highlighting + +| # | Step | New variable | Status | +|---|---|---|---| +| 16 | One invalid `bool` via `HighlightingTestBase` (gold = source with `\|text\|(0)` markers) | `CustomXmlAnalysisStage` in the test shell; daemon registration, severity filter | ✅ | +| 17 | No RimWorld reference → no highlights, no logged errors | the bail-out path; first test without Krafs | ✅ | + +## Phase F — Find Usages (known rough edge) + +| # | Step | New variable | Status | +|---|---|---|---| +| 18 | Find Usages on a ``, usages in another XML file and a C# file | `XMLTagDeclaredElement`, `RimworldSearcherFactory`/`CustomSearcher`. Read `Find Usages.md` first; may end up documenting current behaviour rather than correct behaviour | ⚠️ | + +## Phase G — environment-driven behaviour + +| # | Step | New variable | Status | +|---|---|---|---| +| 19 | Drop Krafs from references, `SkipAssemblyDiscovery = false`, copy Krafs' `Assembly-CSharp.dll` into a temp `RimWorldWin64_Data/Managed/`, point `RimworldPath` at it via `[TestSetting]`, rerun step 1's input | `ScopeHelper.AddRef` / `IAssemblyFactory.AddRef` / settings accessor — the real XML-only-mod mechanism. Expect teardown cookie/leak issues | — | +| 20 | Alt+Insert property generator via the SDK's generate test base | the generate workflow; `PropertyOrdering` makes gold order meaningful | — | + +## Phase H — beyond the current test project (boundary-finding only) + +| # | Step | New variable | Status | +|---|---|---|---| +| 21 | Test one trivial Rider-only thing (e.g. `RimworldProjectMark` parsing an `About.xml`) | the test project references the **RESHARPER** csproj, which excludes `RimworldXmlProject/`, `Remodder/`, `TemplateParameters/`; can a test project reference the Rider csproj and still boot? | — | +| 22 | Remodder `Decompiler` against a tiny Harmony-patched assembly | mostly non-PSI; cheap once 21 works | — | +| 23 | (Separate track) plain JUnit for `QuickStartUtils` setup/teardown against a temp Ludeon dir | Kotlin side, no IDE; highest-stakes code (it can destroy the user's mod list) | — | + +Out of scope until we decide what to keep: moving CI to Windows, the `.gitattributes` LF rule for golds. + +## Reading the results + +- A–C green → the completion harness generalises as-is. +- D/E → whether non-completion SDK test bases work on our `net10.0-windows` host. +- G → whether anything touching disk and settings can be made hermetic. +- H → whether the Rider-only build is testable at all. + +After each phase, fold what broke and how it was fixed into `testing-research.md`. + +## Findings log + +### Phase A–C (2026-09-18) — the completion harness generalises; the bugs found are the plugin's + +Test inputs/golds: `test/data/Completion/Rimworld/` (A, B), `…/Rimworld/Action/` (step 5), `…/RimworldCSharp/` (C). +Fixtures share `RimworldCompletionTestBase` (Krafs references + `ScopeHelper` reset). Suite: 15 green, 4 `[Ignore]`d +with a reason pointing here. Nothing needed changing in the harness itself. + +**Worked unchanged (✅):** nested fields (1), `
  • ` via `List` (2), `li Class=` (3), enum text values (4), def +references from the same file (6) and a second file (7, `DoNamedTest("Other.xml")`), `ParentName` abstract-only +filtering (8), a mod `.cs` file alongside XML in the same in-memory project (9a/9b — `MyMod.CustomThingDef` as a def +root, `li Class="MyMod.CompProperties_Custom"`), `DefDatabase.GetNamed("…")` (11), `[DefOf]` with +`Mod{caret};` (10). `RimworldSymbolScope` populates during the test solution's load with no extra plumbing; no +`CommitAllDocuments` needed for list tests. + +**Step 5 ❌ — plugin bug, not harness.** `CodeCompletionTestType.Action` works (the `${COMPLETE_ITEM:x}` directive can +sit in an XML comment; the inserted text in both golds is right), but accepting an item commits the document, and +`RimworldSymbolScope.Merge` → `AddToLocalCache` calls `sourceFile.GetPrimaryPsiFile()` *during* the commit's merge +phase. The platform logs "Trying to get PSI file for an uncommitted document" (`PsiFiles.AssertNotDirty`) twice, and +the framework fails any test that logs errors. First load doesn't trip it because nothing is dirty yet. This will +affect *any* future test that edits an XML document (Action completion, quick-fixes, generators). Root cause is the +index design: it resolves persisted offsets back to live `ITreeNode`s at merge time. Fix candidates: resolve lazily at +query time, or defer resolution until after commit. + +**Step 9c ⚠️ — plugin bug (load order).** A `MyMod.CustomThingDef` def is not offered where a `ThingDef` is expected. +Traced: when the index merges on load, `ScopeHelper.RimworldScope` is still `null` (symbol caches not ready), so +`AddDefTagToList` skips building `ExtraDefTagNames`, and nothing rebuilds it later. Likely also real on a cold open in +Rider (custom def subclasses unresolved until their file is edited) — not verified in Rider. Test has a hand-written +expected gold and is ignored. + +**Step 10 ⚠️ — narrow `IsAvailable`.** `CSharpDefsOfItemProvider` requires the caret's parent to be an +`IFieldDeclaration`. With `public static ThingDef Mod{caret}` followed by `}` (i.e. typing a new field at the end of +the class, the natural moment to complete), C# error recovery parses a **`MethodDeclaration`**; with no name typed the +caret node is whitespace. Only `Mod{caret};` works. Same parser as Rider, so presumably the same in production. Test +for the unfinished shape has a hand-written expected gold and is ignored. + +**Found by reading, not by a test:** `ScopeHelper.GetScopeForClass` searches `knownCustomScopes` twice; the second +lookup was meant to search `allScopes`, so `knownCustomScopes` is never populated and every dotted class name falls +back to `rimworldScope`. Masked whenever the mod's types live in the same module as the RimWorld reference (single +project, and every test here) because that scope includes references. A test for it needs a *second* project holding +the mod types — `DoTestSolution(string[][])`. + +**Technique that paid off:** a temporary `File.AppendAllText(Path.GetTempPath()/"rw-trace.txt", …)` in the plugin +method under test, then revert. Faster than reading framework logs to find which gate in `IsAvailable` rejected. + +### Phase D (2026-09-18) — reference resolution and real navigation both testable + +Tests: `References/RimworldReferenceTests.cs` (dump, steps 12–14) and `References/RimworldNavigationTests.cs` (step 15); +data under `test/data/References/`. Suite: 20 green, 4 ignored (unchanged from A–C). + +**Steps 12–14 ✅, first run.** The dump (`BaseTestWithSingleProject`, walk `psiFile.Descendants()`, call +`node.GetReferences()`, `Resolve()`, write one line per reference) picks up our +`IReferenceProviderFactory`s with no registration work. The dump is filtered to reference types from the plugin +assembly, because a C# file otherwise lists every ordinary type/namespace reference. What resolves: def type → class, +inherited/nested/`li`/`li Class` fields → `IField`, enum text → enum member, `Name`/`ParentName`/`defName` → the def's +`XMLTagDeclaredElement`, a def reference to a def in the same file / another file / another def type, and C# `[DefOf]` +fields and `DefDatabase.GetNamed*("…")` strings → the XML def. Missing defs and wrong-type defs are not linked (by +design; there's no "unresolved" reference). + +**Minor bug (recorded in the step 13 gold):** closing tags get Ctrl+Click only by coincidence. `GetHierarchy` treats a +closing `` identifier as *inside* its own tag, so it looks up `minifiedDef` on the field's *type* +(`ThingDef`), which happens to have that field. `` etc. get nothing. + +**Gaps, not bugs:** no reference on the `li Class="…"` value, and none on `Type`-typed text (`CompGlower`). + +**Step 15 ✅, better than planned.** The SDK ships no generic reference/resolve test base (only +`WebReferenceTestBase`; JetBrains' own resolve tests are in unpublished assemblies). But +`JetBrains.ReSharper.IntentionsTests.Navigation.AllNavigationProvidersTestBase` works: it runs *every* +context-navigation provider at an `{on}` marker (not `{caret}`) and dumps what each would do. It needs +`ExtraPath => ""` overridden. Its golds show Go to Declaration/Implementation/Type Declaration opening **decompiled Krafs +source** (`ThingDef.cs`, caret on the field), Go to Declaration on a def reference landing on the ``, and +**Find Usages / Show Usages / Highlight Usages through `CustomSearcher`** finding both occurrences of a def. So Phase F +is partly proven already: the searcher runs in the test shell. Note Find Usages on a C# field from XML reports "not +found" (usages of C# members in XML aren't searchable — expected, per `Find Usages.md`). + +Gold stability: the navigation golds embed decompiled Krafs source and some HTML-ish menu markup — pinned by the +Krafs version and the SDK version respectively; expect churn on bumps. + +### Phase E (2026-09-18) — daemon stage testable with no extra work + +Tests: `Highlighting/RimworldXmlHighlightingTests.cs`, data `test/data/Highlighting/`. + +**Steps 16–17 ✅, first run.** `HighlightingTestBase` (namespace `JetBrains.ReSharper.FeaturesTestFramework.Daemon`) +only needs `CompilerIdsLanguage => XmlLanguage.Instance` on top of the usual path/reference overrides. The gold is the +source with `|range|(n)` markers plus the list of highlightings: an invalid `bool` produces +`ReSharper Underlined Error Highlighting: Value must be "true" or "false"` on the right range, a valid one nothing. +With no Krafs reference the stage produces nothing and logs nothing. + +**Fragility worth knowing:** `CustomXmlAnalysisStageProcess` never calls `ScopeHelper.UpdateScopes`; it reads +`ScopeHelper.RimworldScope` directly. It works because the stage is declared after `CollectUsagesStage`, which resolves +references, and our reference factory calls `UpdateScopes`. Reorder the stages or change the reference factory and the +analyser silently goes quiet. + +### Phase F (2026-09-18) — Find Usages runs; C# usages are missed, cause found + +Tests: `FindUsages/RimworldFindUsagesTests.cs`, data `test/data/FindUsages/`. Same `AllNavigationProvidersTestBase` as +step 15; XML fixture and C# fixture (`[TestFileExtension]` differs) share an abstract base. + +**Step 18 ⚠️.** Starting Find Usages from a ``, from a `Name=""` attribute or from a C# `GetNamed("…")` string, +every XML usage across files is found (including `ParentName=""`) and a same-named def of another type is correctly +excluded. **Usages in C# (the `[DefOf]` field, the `DefDatabase` string) are never listed** — even when the search +starts from that C# string. This is the gap `Find Usages.md` anticipates. Cause, verified with a temporary change: +`RimworldSearcherFactory.IsCompatibleWithLanguage` accepts only `XmlLanguage`, and the platform only hands a +searcher files in languages it accepts; letting it also accept `CSharpLanguage` makes both C# usages appear (5 results +instead of 3). `HasReference` on the C# reference factory is never consulted, so it isn't the blocker. Caveat before +shipping that one-liner: `CreateReferenceSearcher` filters *elements* through the same method, so it would also start +receiving C# declared elements. The golds record current behaviour and say so in the fixture's doc comment. + +## Conclusions + +Suite at the close: **25 green, 4 `[Ignore]`d** (each with a hand-written expected gold and a reason pointing here), +~12 s of tests after a ~1 min build. + +**What the harness can test (proven):** XML and C# completion lists and insertion; the def index across files and +mixed C#/XML projects; reference resolution (dump); real IDE navigation — Go to Declaration into decompiled game +source, Find/Show/Highlight Usages; daemon highlightings. Every SDK test base tried worked on the `net10.0-windows` +host once its abstract members were supplied — no new harness fixes were needed after the proof of concept. + +**Boundaries found — all in the plugin, none in the harness:** + +| # | Problem | Where | Blocks | +|---|---|---|---| +| 1 | Index reads PSI mid-commit ("uncommitted document" logged) | `RimworldSymbolScope.AddToLocalCache` via `Merge` | any test that edits an XML document (Action completion, future quick-fix/generator tests) | +| 2 | Custom def subclasses not indexed under their base type on cold load | `ExtraDefTagNames` built only if `ScopeHelper.RimworldScope` is set at merge | step 9c | +| 3 | `[DefOf]` completion needs a following `;` | `CSharpDefsOfItemProvider.IsAvailable` (unfinished declaration parses as a method) | step 10 variant | +| 4 | Find Usages ignores C# | `RimworldSearcherFactory.IsCompatibleWithLanguage` | step 18's C# usages | +| 5 | `GetScopeForClass` searches `knownCustomScopes` twice (should be `allScopes`) | `ScopeHelper` | found by reading; needs a two-project test | +| 6 | Closing tags resolve only by coincidence | `GetHierarchy` on closing identifiers | cosmetic | +| 7 | Daemon stage relies on another stage having set the scope | `CustomXmlAnalysisStageProcess` | nothing yet; fragile | + +**Not explored:** Phase G (disk discovery via `RimworldPath`/`AddRef`, the generator) and Phase H (Rider-only code, +Remodder, Kotlin). #1 must be fixed before the generator (step 20) can be tested. Also still open from the proof of +concept: moving CI to Windows and the `.gitattributes` LF rule for golds. + diff --git a/docs/testing-research.md b/docs/testing-research.md index 45f1f73..39e9fe7 100644 --- a/docs/testing-research.md +++ b/docs/testing-research.md @@ -15,7 +15,10 @@ The Kotlin side is not tested. JetBrains' game-engine plugins without backend te Kotlin on TeamCity (full Rider download plus a real .NET SDK per test class); our completion logic is entirely backend, so that route buys nothing here. -## What exists (all green) +## What exists + +See `testing-plan.md` for the step-by-step broadening plan, its status, and the plugin bugs it has turned up. The +original proof-of-concept tests: `src/dotnet/ReSharperPlugin.RimworldDev.Tests`: @@ -117,7 +120,9 @@ the test env zone must `IRequire<>` it or every plugin component silently vanish `test/data/NuGetLocks/*.lock` **must be committed**: they pin what the *framework* downloads at run time for the in-memory project (e.g. `JetBrains.Tests.Platform.NETFrameWork 3.5`, requested as an open range), which the csproj never sees. Without them a new patch upload on JetBrains' feed changes the reference set under the golds. One lock -file per distinct request (file name = hash of the request); delete ones left behind by abandoned experiments. +file per distinct request (file name = hash of the request); delete ones left behind by abandoned experiments. The +broadening work added a second, legitimate one (`279ddca8…`, input `Microsoft.NETCore.App [2.0.0]`), requested by one +of the navigation/highlighting test bases; commit it too. ## Test data @@ -188,11 +193,54 @@ Failures come in five shapes; the output tells you which: | "The test has logged N errors" | some component threw during the test; the assertion may even have passed | the `Message =` lines — usually a missing DLL or a component that couldn't construct | | Every test fails in ~4 s with the same exception | the shell didn't boot | the first `EXCEPTION #1` — it's an environment problem (see the harness table), not a test problem | -## Multi-file (not yet used) +## Multi-file + +`DoNamedTest("Other.xml", "ModTypes.cs")` adds extra files to the one in-memory project; mixing a `.cs` file into an +`[TestFileExtension(".xml")]` fixture works (the C# compiles against Krafs like any mod). `RimworldSymbolScope` +populates on solution load with no extra calls for list tests. Not yet used: `DoTestSolution(string[][])` with a +project GUID appended to a file set for a second, referenced project — needed to test anything that depends on mod +types living in a *different* module from the RimWorld reference. + +## Action completion and edits + +`CodeCompletionTestType.Action` reads `${COMPLETE_ITEM:name}` from anywhere in the file, so in XML put it in a comment. +Any test that edits an XML document currently fails with "Trying to get PSI file for an uncommitted document" logged +from `RimworldSymbolScope.AddToLocalCache` during commit — a plugin bug, see `testing-plan.md` step 5. + +## Reference and navigation tests + +Two working routes (examples in `References/`): + +- **Dump** (`RimworldReferenceTests`): `BaseTestWithSingleProject`, `CommitAllDocuments`, then for every node of the file + `node.GetReferences()` → `Resolve()` → write a line into `ExecuteWithGold`. Filter to + `reference.GetType().Assembly == typeof(ScopeHelper).Assembly` or C# files drown in ordinary references. Tests the + reference providers in isolation, gold is compact. +- **Real navigation** (`RimworldNavigationTests`): `AllNavigationProvidersTestBase` (namespace + `JetBrains.ReSharper.IntentionsTests.Navigation`, in `JetBrains.ReSharper.FeaturesTestFramework`). Marker is `{on}` + (`{off}` asserts unavailability), **not** `{caret}`; must override `ExtraPath` (`""` is fine). Gold covers Go to + Declaration/Implementation/Type Declaration, Find Usages, Show Usages and Highlight Usages; navigation into + referenced assemblies shows decompiled source. Also exist: `NavigationProviderTestBase` (one provider) and + `ContextNavigationTestBase`. + +There is no general `ReferenceTestBase`/`ResolveTestBase` in the shipped SDK. To find test bases, grep the DLLs: +`grep -aoE '[A-Za-z]*TestBase' JetBrains.ReSharper.FeaturesTestFramework.dll | sort -u`, then inspect members with +PowerShell `ReflectionOnlyLoadFrom` (hook `ReflectionOnlyAssemblyResolve` to the bin folder and read +`ReflectionTypeLoadException.Types` when `GetTypes()` throws). Abstract members are cheapest to discover by compiling. + +## Highlighting and Find Usages tests + +- **Highlighting** (`Highlighting/`): `HighlightingTestBase` (`JetBrains.ReSharper.FeaturesTestFramework.Daemon`); must + override `CompilerIdsLanguage` (`XmlLanguage.Instance`). No markers in the input — the gold adds `|range|(n)` and a + numbered list. `HighlightingPredicate` is available to narrow what gets dumped (not needed so far). +- **Find Usages** (`FindUsages/`): no dedicated base needed — `AllNavigationProvidersTestBase` already runs + Find Usages / Show Usages / Highlight Usages. Put `{on}` on the def, extra files via `DoNamedTest("a.xml", "b.cs")`. + To start from a C# file, use a second fixture with `[TestFileExtension(".cs")]`. + +## Debugging "the provider didn't contribute" -`DoTestSolution([TestName], ["Other.xml"])` for extra files in one project; `DoTestSolution(string[][])` with a project -GUID appended to a file set for a second, referenced project. `SimpleICache`s (e.g. `RimworldSymbolScope`) populate -on solution load; call `psiServices.Files.CommitAllDocuments()` before asserting. +Fastest route: temporarily `File.AppendAllText(Path.Combine(Path.GetTempPath(), "rw-trace.txt"), …)` inside the +plugin method (e.g. log `context.NodeInFile`'s type, text and parent type in `IsAvailable`), run the one test, read +the file, revert. That's how the `[DefOf]` provider was found to see a `MethodDeclaration` for unfinished fields. ## Gold hygiene diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs new file mode 100644 index 0000000..a75ee63 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs @@ -0,0 +1,27 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.Completion; + +/// +/// Def names offered in C#, from the same def index the XML side uses. Each test brings Defs.xml along so the index has +/// something in it. +/// +[TestFileExtension(".cs")] +public class RimworldCSharpCompletionTests : RimworldCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"Completion\RimworldCSharp"; + + [Test] public void TestDefOfFieldWithPrefixAndSemicolon() => DoNamedTest("Defs.xml"); + + // Gold is hand-written: what the plugin *should* offer. Without a following ';' the C# parser recovers the unfinished + // `public static ThingDef Mod` as a MethodDeclaration, and CSharpDefsOfItemProvider.IsAvailable wants a + // FieldDeclaration, so only C#'s own name suggestions appear. + [Test, Ignore("CSharpDefsOfItemProvider needs a FieldDeclaration; unfinished declarations parse as methods; see docs/testing-plan.md step 10")] + public void TestDefOfFieldWithPrefix() => DoNamedTest("Defs.xml"); + + + [Test] public void TestDefDatabaseGetNamed() => DoNamedTest("Defs.xml"); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs new file mode 100644 index 0000000..cff8bc0 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.Completion; + +/// +/// Completion tests backed by the game's types. Krafs.Rimworld.Ref (a complete reference assembly for RimWorld) is +/// referenced into the in-memory project, and ScopeHelper finds it the same way it finds the real +/// Assembly-CSharp.dll: by looking for Verse.ThingDef. +/// +public abstract class RimworldCompletionTestBase : CodeCompletionTestBase +{ + /// + /// Every DLL from the Krafs package's ref/net472 folder except the framework ones, which the test platform + /// already provides. The folder path is baked into this assembly by the csproj from NuGet's restore. + /// + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldReferenceAssemblies()); + + public static IEnumerable RimworldReferenceAssemblies() + { + var refDir = typeof(RimworldCompletionTestBase).Assembly + .GetCustomAttributes() + .Single(a => a.Key == "RimworldRefDir").Value; + + return Directory.GetFiles(refDir, "*.dll") + .Where(path => + { + var name = Path.GetFileName(path); + return !name.StartsWith("mscorlib", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("System", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && + !name.StartsWith("Mono.", StringComparison.OrdinalIgnoreCase); + }); + } + + [SetUp] + public void ResetRimworldScope() + { + // ScopeHelper caches the RimWorld scope in statics; without this the second test reuses a scope from a solution + // that no longer exists. The discovery switch stops it from finding a real RimWorld install on this machine and + // adding that to the test solution on top of the Krafs reference. + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() + { + // Drop our references to the solution's modules before the framework checks that nothing is still holding them. + ScopeHelper.Reset(); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs index 9c3ecb3..aa971ae 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs @@ -1,65 +1,48 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; using JetBrains.ReSharper.FeaturesTestFramework.Completion; using JetBrains.ReSharper.TestFramework; -using JetBrains.Util.Dotnet.TargetFrameworkIds; using NUnit.Framework; namespace ReSharperPlugin.RimworldDev.Tests.Completion; /// -/// The real thing: RimWorld XML completion backed by the game's types. Krafs.Rimworld.Ref (a complete reference -/// assembly for RimWorld) is referenced into the in-memory project, and ScopeHelper finds it the same way it finds -/// the real Assembly-CSharp.dll: by looking for Verse.ThingDef. +/// The real thing: RimWorld XML completion backed by the game's types. Gold is the lookup list at {caret}. /// [TestFileExtension(".xml")] -public class RimworldXmlCompletionTests : CodeCompletionTestBase +public class RimworldXmlCompletionTests : RimworldCompletionTestBase { protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; protected override string RelativeTestDataPath => @"Completion\Rimworld"; - /// - /// Every DLL from the Krafs package's ref/net472 folder except the framework ones, which the test platform - /// already provides. The folder path is baked into this assembly by the csproj from NuGet's restore. - /// - protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) - { - var refDir = typeof(RimworldXmlCompletionTests).Assembly - .GetCustomAttributes() - .Single(a => a.Key == "RimworldRefDir").Value; - - var rimworldDlls = Directory.GetFiles(refDir, "*.dll") - .Where(path => - { - var name = Path.GetFileName(path); - return !name.StartsWith("mscorlib", StringComparison.OrdinalIgnoreCase) && - !name.StartsWith("System", StringComparison.OrdinalIgnoreCase) && - !name.StartsWith("netstandard", StringComparison.OrdinalIgnoreCase) && - !name.StartsWith("Mono.", StringComparison.OrdinalIgnoreCase); - }); - - return base.GetReferencedAssemblies(targetFrameworkId).Concat(rimworldDlls); - } - - [SetUp] - public void ResetRimworldScope() - { - // ScopeHelper caches the RimWorld scope in statics; without this the second test reuses a scope from a solution - // that no longer exists. The discovery switch stops it from finding a real RimWorld install on this machine and - // adding that to the test solution on top of the Krafs reference. - ScopeHelper.Reset(); - ScopeHelper.SkipAssemblyDiscovery = true; - } + [Test] public void TestThingDefProperties() => DoNamedTest(); + [Test] public void TestNestedFieldProperties() => DoNamedTest(); + [Test] public void TestListItemProperties() => DoNamedTest(); + [Test] public void TestListItemWithClassProperties() => DoNamedTest(); + [Test] public void TestEnumValue() => DoNamedTest(); + + // Phase B: the def index (RimworldSymbolScope) + [Test] public void TestDefReferenceSameFile() => DoNamedTest(); + [Test] public void TestDefReferenceOtherFile() => DoNamedTest("OtherDefs.xml"); + [Test] public void TestParentName() => DoNamedTest(); + [Test] public void TestModDefClassProperties() => DoNamedTest("ModTypes.cs"); + [Test] public void TestModListItemClassProperties() => DoNamedTest("ModTypes.cs"); + // Gold is hand-written: what the plugin *should* offer. It currently omits CustomThing, because ExtraDefTagNames is + // only built when ScopeHelper already has the RimWorld scope at merge time, and on a cold load it doesn't. + [Test, Ignore("ExtraDefTagNames not built when the def index merges before scopes are ready; see docs/testing-plan.md step 9")] + public void TestModDefAsSuperclassReference() => DoNamedTest("ModTypes.cs"); +} - [TearDown] - public void ForgetRimworldScope() - { - // Drop our references to the solution's modules before the framework checks that nothing is still holding them. - ScopeHelper.Reset(); - } +/// +/// Gold is the document after accepting the item named by the input's ${COMPLETE_ITEM:…} directive. +/// The golds are correct, but accepting an item commits the edited document, and RimworldSymbolScope.Merge reads the +/// PSI file mid-commit ("Trying to get PSI file for an uncommitted document"), which fails the test as a logged error. +/// +[TestFileExtension(".xml")] +[Ignore("RimworldSymbolScope.AddToLocalCache calls GetPrimaryPsiFile during commit merge; see docs/testing-plan.md step 5")] +public class RimworldXmlCompletionActionTests : RimworldCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.Action; + protected override string RelativeTestDataPath => @"Completion\Rimworld\Action"; - [Test] public void TestThingDefProperties() => DoNamedTest(); + [Test] public void TestCompleteTag() => DoNamedTest(); + [Test] public void TestCompleteEnumValue() => DoNamedTest(); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs new file mode 100644 index 0000000..0e00679 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.ReSharper.IntentionsTests.Navigation; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.FindUsages; + +/// +/// Find Usages (and the other navigation providers) started from a def, with usages spread across XML and C# files. +/// XMLTagDeclaredElement + RimworldSearcherFactory/CustomSearcher is the known rough edge; see "Find Usages.md". +/// The golds record *current* behaviour: usages in XML are found, usages in C# (the [DefOf] field and the +/// DefDatabase string in CSharpUsages.cs) are not, because RimworldSearcherFactory.IsCompatibleWithLanguage only +/// accepts XML. Allowing C# there makes both appear (verified), so when that's fixed these golds should gain them. +/// +public abstract class RimworldFindUsagesTestBase : AllNavigationProvidersTestBase +{ + protected override string ExtraPath => ""; + protected override string RelativeTestDataPath => @"FindUsages"; + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldCompletionTestBase.RimworldReferenceAssemblies()); + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); +} + +[TestFileExtension(".xml")] +public class RimworldFindUsagesFromXmlTests : RimworldFindUsagesTestBase +{ + [Test] public void TestFromDefName() => DoNamedTest("OtherUsages.xml", "CSharpUsages.cs"); + [Test] public void TestFromNameAttribute() => DoNamedTest(); +} + +[TestFileExtension(".cs")] +public class RimworldFindUsagesFromCSharpTests : RimworldFindUsagesTestBase +{ + [Test] public void TestFromCSharpString() => DoNamedTest("Defs.xml", "OtherUsages.xml"); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs new file mode 100644 index 0000000..cdaa2f2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.ReSharper.FeaturesTestFramework.Daemon; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Xml; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.Highlighting; + +/// +/// CustomXmlAnalysisStage: XML values checked against the C# type of the field they set. Gold is the source with the +/// highlighted ranges marked, followed by the list of highlightings. +/// +[TestFileExtension(".xml")] +public class RimworldXmlHighlightingTests : HighlightingTestBase +{ + protected override string RelativeTestDataPath => @"Highlighting"; + protected override PsiLanguageType CompilerIdsLanguage => XmlLanguage.Instance; + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldCompletionTestBase.RimworldReferenceAssemblies()); + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); + + [Test] public void TestInvalidBool() => DoNamedTest(); +} + +/// +/// The same input with no RimWorld types in the solution: the stage must quietly produce nothing (and log nothing). +/// +[TestFileExtension(".xml")] +public class RimworldXmlHighlightingWithoutRimworldTests : HighlightingTestBase +{ + protected override string RelativeTestDataPath => @"Highlighting"; + protected override PsiLanguageType CompilerIdsLanguage => XmlLanguage.Instance; + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); + + [Test] public void TestNoRimworldReference() => DoNamedTest(); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs new file mode 100644 index 0000000..3b1b961 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.ReSharper.IntentionsTests.Navigation; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.References; + +/// +/// Ctrl+Click the way the IDE does it: every context navigation provider (Go to Declaration, Find Usages, …) is run at +/// {caret} and the targets it would offer are dumped. +/// +[TestFileExtension(".xml")] +public class RimworldNavigationTests : AllNavigationProvidersTestBase +{ + protected override string ExtraPath => ""; + protected override string RelativeTestDataPath => @"References\Navigation"; + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldCompletionTestBase.RimworldReferenceAssemblies()); + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); + + [Test] public void TestNavigateToCSharpField() => DoNamedTest(); + [Test] public void TestNavigateToXmlDef() => DoNamedTest(); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs new file mode 100644 index 0000000..567b061 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using JetBrains.Application.Components; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.References; + +/// +/// Ctrl+Click, tested without the SDK's navigation machinery: walk every node of the test file, ask it for its +/// references (which is where our IReferenceProviderFactory implementations plug in), resolve each one and dump +/// "node → reference type → what it resolved to". The first file given to DoTestSolution is the one dumped; the rest +/// only exist to be resolved into. +/// +public class RimworldReferenceTests : BaseTestWithSingleProject +{ + protected override string RelativeTestDataPath => @"References"; + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldCompletionTestBase.RimworldReferenceAssemblies()); + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); + + [Test] public void TestXmlToCSharp() => DoTestSolution("XmlToCSharp.xml"); + [Test] public void TestXmlToXmlDef() => DoTestSolution("XmlToXmlDef.xml", "OtherDefs.xml"); + [Test] public void TestCSharpToXmlDef() => DoTestSolution("CSharpToXmlDef.cs", "CSharpDefs.xml"); + + protected override void DoTest(Lifetime lifetime, IProject project) + { + Solution.GetPsiServices().Files.CommitAllDocuments(); + using (ReadLockCookie.Create()) + { + var dumpedFileName = TestMethodName2FileNames().First(); + var projectFile = project.GetAllProjectFiles().Single(file => file.Name == dumpedFileName); + var sourceFile = projectFile.ToSourceFiles().Single(); + var psiFile = sourceFile.GetPrimaryPsiFile()!; + var document = sourceFile.Document; + + ExecuteWithGold(projectFile, writer => + { + foreach (var node in psiFile.Descendants().ToEnumerable()) + { + // Only the plugin's references; a C# file is otherwise full of ordinary type/namespace references. + foreach (var reference in node.GetReferences() + .Where(reference => reference.GetType().Assembly == typeof(ScopeHelper).Assembly)) + { + var start = document.GetCoordsByOffset(reference.GetDocumentRange().StartOffset.Offset); + var resolved = reference.Resolve(); + writer.WriteLine( + $"({(int)start.Line + 1},{(int)start.Column + 1}) '{reference.GetDocumentRange().GetText()}' " + + $"[{reference.GetType().Name}] -> {resolved.ResolveErrorType}: {Describe(resolved.DeclaredElement)}"); + } + } + }); + } + } + + private IEnumerable TestMethodName2FileNames() => myFileSet; + + private string[] myFileSet = []; + + protected new void DoTestSolution(params string[] fileSet) + { + myFileSet = fileSet; + base.DoTestSolution(fileSet); + } + + private static string Describe(IDeclaredElement element) => element switch + { + null => "", + ITypeElement type => $"type {type.GetClrName().FullName}", + ITypeMember member => $"{member.GetElementType().PresentableName} {member.ContainingType?.GetClrName().FullName}.{member.ShortName}", + _ => $"{element.GetType().Name} {element.ShortName}", + }; +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml new file mode 100644 index 0000000..2aabcd3 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml @@ -0,0 +1,8 @@ + + + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml.gold new file mode 100644 index 0000000..1621cb0 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteEnumValue.xml.gold @@ -0,0 +1,8 @@ + + + + + TestThing + Building{caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml new file mode 100644 index 0000000..d068a3d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml @@ -0,0 +1,10 @@ + + + + + TestThing + + <{caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml.gold new file mode 100644 index 0000000..966b725 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/Action/TestCompleteTag.xml.gold @@ -0,0 +1,10 @@ + + + + + TestThing + + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/ModTypes.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/ModTypes.cs new file mode 100644 index 0000000..31a09b6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/ModTypes.cs @@ -0,0 +1,12 @@ +namespace MyMod +{ + public class CustomThingDef : Verse.ThingDef + { + public int customField; + } + + public class CompProperties_Custom : Verse.CompProperties + { + public float customValue; + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/OtherDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/OtherDefs.xml new file mode 100644 index 0000000..0a5f5f5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/OtherDefs.xml @@ -0,0 +1,9 @@ + + + + OtherThing + + + NotAThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml new file mode 100644 index 0000000..b958ee6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml @@ -0,0 +1,7 @@ + + + + TestThingB + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml.gold new file mode 100644 index 0000000..54f3cd5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceOtherFile.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +OtherThing <== +TestThingB + +##### ALPHABETIC SORT ##### + + [Light, Generic] +OtherThing <== +TestThingB diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml new file mode 100644 index 0000000..1418c75 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml @@ -0,0 +1,10 @@ + + + + TestThingA + + + TestThingB + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml.gold new file mode 100644 index 0000000..03ebb55 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestDefReferenceSameFile.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +TestThingA <== +TestThingB + +##### ALPHABETIC SORT ##### + + [Light, Generic] +TestThingA <== +TestThingB diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml new file mode 100644 index 0000000..ab517ef --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml @@ -0,0 +1,7 @@ + + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml.gold new file mode 100644 index 0000000..f4bb505 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestEnumValue.xml.gold @@ -0,0 +1,36 @@ +Completion: Basic +Count: 12 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +Attachment 8 <== +Building 3 +Ethereal 10 +Filth 6 +Gas 7 +Item 2 +Mote 9 +None 0 +Pawn 1 +Plant 4 +Projectile 5 +PsychicEmitter 11 + +##### ALPHABETIC SORT ##### + + [Light, Generic] +Attachment 8 <== +Building 3 +Ethereal 10 +Filth 6 +Gas 7 +Item 2 +Mote 9 +None 0 +Pawn 1 +Plant 4 +Projectile 5 +PsychicEmitter 11 diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml new file mode 100644 index 0000000..4d92b7c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml @@ -0,0 +1,11 @@ + + + + TestThing + +
  • + <{caret} +
  • + +
    + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml.gold new file mode 100644 index 0000000..37ad97c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemProperties.xml.gold @@ -0,0 +1,14 @@ +Completion: Basic +Count: 1 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +compClass Type <== + +##### ALPHABETIC SORT ##### + + [Light, Generic] +compClass Type <== diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml new file mode 100644 index 0000000..904d63c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml @@ -0,0 +1,11 @@ + + + + TestThing + +
  • + <{caret} +
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml.gold new file mode 100644 index 0000000..b75e21b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml.gold @@ -0,0 +1,34 @@ +Completion: Basic +Count: 11 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +alwaysDisplayAsUsingPower bool <== +compClass Type +idlePowerDraw float +powerUpgrades List +shortCircuitInRain bool +showPowerNeededIfOff bool +soundAmbientPowered SoundDef +soundAmbientProducingPower SoundDef +soundPowerOff SoundDef +soundPowerOn SoundDef +transmitsPower bool + +##### ALPHABETIC SORT ##### + + [Light, Generic] +alwaysDisplayAsUsingPower bool <== +compClass Type +idlePowerDraw float +powerUpgrades List +shortCircuitInRain bool +showPowerNeededIfOff bool +soundAmbientPowered SoundDef +soundAmbientProducingPower SoundDef +soundPowerOff SoundDef +soundPowerOn SoundDef +transmitsPower bool diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml new file mode 100644 index 0000000..d7ad5b5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml @@ -0,0 +1,10 @@ + + + + CustomThing + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml.gold new file mode 100644 index 0000000..060bf2f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefAsSuperclassReference.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +CustomThing <== +TestThing + +##### ALPHABETIC SORT ##### + + [Light, Generic] +CustomThing <== +TestThing diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml new file mode 100644 index 0000000..0d60c17 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml @@ -0,0 +1,7 @@ + + + + CustomThing + <{caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml.gold new file mode 100644 index 0000000..9776804 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml.gold @@ -0,0 +1,512 @@ +Completion: Basic +Count: 250 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +allowedArchonexusCount int <== +altitudeLayer AltitudeLayer +alwaysFlee bool +alwaysHaulable bool +apparel ApparelProperties +artisticSkillPrerequisite int +autoTargetNearbyIdenticalThings bool +blockLight bool +blockPlants bool +blockWeather bool +blockWind bool +blocksAltitudes List +bringAlongOnGravship bool +building BuildingProperties +buildingPrerequisites List +burnableByRecipe bool +butcherProducts List +canBeUsedUnderRoof bool +canDeteriorateUnspawned bool +canEditAnyStyle bool +canGenerateDefaultDesignator bool +canInteractThroughCorners bool +canLoadIntoCaravan bool +canScatterOver bool +castEdgeShadows bool +category ThingCategory +clearBuildingArea bool +colorGenerator ColorGenerator +colorGeneratorInTraderStock ColorGenerator +colorPerStuff List +comps List +constructEffect EffecterDef +constructionSkillPrerequisite int +containedItemsSelectable bool +containedPawnsSelectable bool +costList List +costListForDifficulty CostListForDifficulty +costStuffCount int +coversFloor bool +customField int +damageMultipliers List +deepCommonality float +deepCountPerCell int +deepCountPerPortion int +deepLumpSizeRange IntRange +defName string +defaultPlacingRot Rot4 +defaultStuff ThingDef +description string +descriptionHyperlinks List +deselectedSelectionBracketFactor float +designateHaulable bool +designationCategory DesignationCategoryDef +designationHotKey KeyBindingDef +designatorDropdown DesignatorDropdownGroupDef +destroyOnDrop bool +destroyable bool +deteriorateFromEnvironmentalEffects bool +devNote string +disableImpassableShotOverConfigError bool +discoveryPrerequisites List +displayNumbersBetweenSameDefDistRange FloatRange +dominantStyleCategory StyleCategoryDef +dontPrint bool +drawDamagedOverlay bool +drawGUIOverlay bool +drawGUIOverlayQuality bool +drawHighlight bool +drawHighlightOnlyForHostile bool +drawOffscreen bool +drawPlaceWorkersWhileInstallBlueprintSelected bool +drawPlaceWorkersWhileSelected bool +drawStyleCategory DrawStyleCategoryDef +drawerType DrawerType +dropPodActive ThingDef +dropPodFaller ThingDef +entityCodexEntry EntityCodexEntryDef +entityDefToBuild BuildableDef +equipmentType EquipmentType +equippedAngleOffset float +equippedDistanceOffset float +equippedStatOffsets List +fertility float +fillPercent float +filth FilthProperties +filthLeaving ThingDef +forceDebugSpawnable bool +forceLeavingsAllowed bool +forceMoveItemsBeforeConstruction bool +forcePassableByFlyingPawns bool +gas GasProperties +generateAllowChance float +generateCommonality float +genericMarketSellable bool +graphicData GraphicData +gravshipSpawnPriority int +hasCustomRectForSelector bool +hasInteractionCell bool +hasTooltip bool +healthAffectsPrice bool +hiddenWhileUndiscovered bool +hideAtSnowOrSandDepth float +hideInspect bool +hideMainDesc bool +hideStats bool +highlightColor Color? +holdsRoof bool +ideoBuilding bool +ideoBuildingNamerBase RulePackDef +ignoreConfigErrors bool +ignoreIllegalLabelCharacterConfigError bool +ingestible IngestibleProperties +ingredient IngredientProperties +inspectorTabs List +interactionCellIcon ThingDef +interactionCellIconReverse bool +interactionCellOffset IntVec3 +intricate bool +isAltar bool +isAutoAttackableMapObject bool +isFrameInt bool +isMechClusterThreat bool +isSaveable bool +isTechHediff bool +isUnfinishedThing bool +killedLeavings List +killedLeavingsChance float +killedLeavingsExpandRect int +killedLeavingsPlayerHostile List +killedLeavingsRanges List +label string +leaveResourcesWhenKilled bool +maxTechLevelToBuild TechLevel +meleeHitSound SoundDef +mergeVerbGizmos bool +messageOnDeteriorateInStorage bool +minMonolithLevel int +minRewardCount int +minTechLevelToBuild TechLevel +mineable bool +minifiedDef ThingDef +minifiedDrawOffset Vector3 +minifiedDrawScale float +minifiedManualDraw bool +modExtensions List +mote MoteProperties +multipleInteractionCellOffsets List +neverMultiSelect bool +neverOverlapFloors bool +noRightClickDraftAttack bool +notifyMapRemoved bool +onlyShowInspectString bool +orderedTakeGroup OrderedTakeGroupDef +overrideMinifiedRot Rot4 +passability Traversability +pathCost int +pathCostIgnoreRepeat bool +pathfinderDangerous bool +pawnFlyer PawnFlyerProperties +placeWorkers List +plant PlantProperties +portal MapPortalProperties +possessionCount int +preventDroppingThingsOn bool +preventGravshipLandingOn bool +preventSkyfallersLandingOn bool +preventSpawningInResourcePod bool +projectile ProjectileProperties +projectileWhenLoaded ThingDef +race RaceProperties +randomStyle List +randomStyleChance float +randomizeRotationOnSpawn bool +receivesSignals bool +recipeMaker RecipeMakerProperties +recipes List +recoilPower float +recoilRelaxation float +relicChance float +repairEffect EffecterDef +replaceTags List +requireInspectedGravEngine bool +requiresFactionToAcquire FactionDef +researchPrerequisites List +resourceReadoutAlwaysShow bool +resourceReadoutPriority ResourceCountPriority +resourcesFractionWhenDeconstructed float +ritualFocus RitualFocusProperties +rotatable bool +rotateInShelves bool +saveCompressible bool +scatterableOnMapGen bool +seeThroughFog bool +selectable bool +showInSearch bool +size IntVec2 +skyfaller SkyfallerProperties +slagDef ThingDef +smallVolume bool +smeltProducts List +smeltable bool +socialPropernessMatters bool +soundDrop SoundDef +soundImpactDefault SoundDef +soundInteract SoundDef +soundOpen SoundDef +soundPickup SoundDef +soundPlayInstrument SoundDef +soundSpawned SoundDef +specialDisplayRadius float +stackLimit int +startingHpRange FloatRange +statBases List +staticSunShadowHeight float +stealable bool +storedConceptLearnOpportunity ConceptDef +stuffCategories List +stuffCategorySummary string +stuffProps StuffProperties +surfaceType SurfaceType +techHediffsTags List +techLevel TechLevel +terrainAffordanceNeeded TerrainAffordanceDef +thingCategories List +thingClass Type +thingSetMakerTags List +tickerType TickerType +tools List +tradeNeverGenerateStacked bool +tradeNeverStack bool +tradeTags List +tradeability Tradeability +uiIconColor Color +uiIconColorTwo Color +uiIconForStackCount int +uiIconOffset Vector2 +uiIconPath string +uiIconPathsStuff List +uiIconScale float +uiOrder float +useBlueprintGraphicAsGhost bool +useHitPoints bool +useSameGraphicForGhost bool +useStuffTerrainAffordance bool +violentTechHediff bool +virtualDefParent ThingDef +virtualDefs List +weaponClasses List +weaponTags List +wipesPlants bool + +##### ALPHABETIC SORT ##### + + [Light, Generic] +allowedArchonexusCount int <== +altitudeLayer AltitudeLayer +alwaysFlee bool +alwaysHaulable bool +apparel ApparelProperties +artisticSkillPrerequisite int +autoTargetNearbyIdenticalThings bool +blockLight bool +blockPlants bool +blockWeather bool +blockWind bool +blocksAltitudes List +bringAlongOnGravship bool +building BuildingProperties +buildingPrerequisites List +burnableByRecipe bool +butcherProducts List +canBeUsedUnderRoof bool +canDeteriorateUnspawned bool +canEditAnyStyle bool +canGenerateDefaultDesignator bool +canInteractThroughCorners bool +canLoadIntoCaravan bool +canScatterOver bool +castEdgeShadows bool +category ThingCategory +clearBuildingArea bool +colorGenerator ColorGenerator +colorGeneratorInTraderStock ColorGenerator +colorPerStuff List +comps List +constructEffect EffecterDef +constructionSkillPrerequisite int +containedItemsSelectable bool +containedPawnsSelectable bool +costList List +costListForDifficulty CostListForDifficulty +costStuffCount int +coversFloor bool +customField int +damageMultipliers List +deepCommonality float +deepCountPerCell int +deepCountPerPortion int +deepLumpSizeRange IntRange +defName string +defaultPlacingRot Rot4 +defaultStuff ThingDef +description string +descriptionHyperlinks List +deselectedSelectionBracketFactor float +designateHaulable bool +designationCategory DesignationCategoryDef +designationHotKey KeyBindingDef +designatorDropdown DesignatorDropdownGroupDef +destroyOnDrop bool +destroyable bool +deteriorateFromEnvironmentalEffects bool +devNote string +disableImpassableShotOverConfigError bool +discoveryPrerequisites List +displayNumbersBetweenSameDefDistRange FloatRange +dominantStyleCategory StyleCategoryDef +dontPrint bool +drawDamagedOverlay bool +drawGUIOverlay bool +drawGUIOverlayQuality bool +drawHighlight bool +drawHighlightOnlyForHostile bool +drawOffscreen bool +drawPlaceWorkersWhileInstallBlueprintSelected bool +drawPlaceWorkersWhileSelected bool +drawStyleCategory DrawStyleCategoryDef +drawerType DrawerType +dropPodActive ThingDef +dropPodFaller ThingDef +entityCodexEntry EntityCodexEntryDef +entityDefToBuild BuildableDef +equipmentType EquipmentType +equippedAngleOffset float +equippedDistanceOffset float +equippedStatOffsets List +fertility float +fillPercent float +filth FilthProperties +filthLeaving ThingDef +forceDebugSpawnable bool +forceLeavingsAllowed bool +forceMoveItemsBeforeConstruction bool +forcePassableByFlyingPawns bool +gas GasProperties +generateAllowChance float +generateCommonality float +genericMarketSellable bool +graphicData GraphicData +gravshipSpawnPriority int +hasCustomRectForSelector bool +hasInteractionCell bool +hasTooltip bool +healthAffectsPrice bool +hiddenWhileUndiscovered bool +hideAtSnowOrSandDepth float +hideInspect bool +hideMainDesc bool +hideStats bool +highlightColor Color? +holdsRoof bool +ideoBuilding bool +ideoBuildingNamerBase RulePackDef +ignoreConfigErrors bool +ignoreIllegalLabelCharacterConfigError bool +ingestible IngestibleProperties +ingredient IngredientProperties +inspectorTabs List +interactionCellIcon ThingDef +interactionCellIconReverse bool +interactionCellOffset IntVec3 +intricate bool +isAltar bool +isAutoAttackableMapObject bool +isFrameInt bool +isMechClusterThreat bool +isSaveable bool +isTechHediff bool +isUnfinishedThing bool +killedLeavings List +killedLeavingsChance float +killedLeavingsExpandRect int +killedLeavingsPlayerHostile List +killedLeavingsRanges List +label string +leaveResourcesWhenKilled bool +maxTechLevelToBuild TechLevel +meleeHitSound SoundDef +mergeVerbGizmos bool +messageOnDeteriorateInStorage bool +minMonolithLevel int +minRewardCount int +minTechLevelToBuild TechLevel +mineable bool +minifiedDef ThingDef +minifiedDrawOffset Vector3 +minifiedDrawScale float +minifiedManualDraw bool +modExtensions List +mote MoteProperties +multipleInteractionCellOffsets List +neverMultiSelect bool +neverOverlapFloors bool +noRightClickDraftAttack bool +notifyMapRemoved bool +onlyShowInspectString bool +orderedTakeGroup OrderedTakeGroupDef +overrideMinifiedRot Rot4 +passability Traversability +pathCost int +pathCostIgnoreRepeat bool +pathfinderDangerous bool +pawnFlyer PawnFlyerProperties +placeWorkers List +plant PlantProperties +portal MapPortalProperties +possessionCount int +preventDroppingThingsOn bool +preventGravshipLandingOn bool +preventSkyfallersLandingOn bool +preventSpawningInResourcePod bool +projectile ProjectileProperties +projectileWhenLoaded ThingDef +race RaceProperties +randomStyle List +randomStyleChance float +randomizeRotationOnSpawn bool +receivesSignals bool +recipeMaker RecipeMakerProperties +recipes List +recoilPower float +recoilRelaxation float +relicChance float +repairEffect EffecterDef +replaceTags List +requireInspectedGravEngine bool +requiresFactionToAcquire FactionDef +researchPrerequisites List +resourceReadoutAlwaysShow bool +resourceReadoutPriority ResourceCountPriority +resourcesFractionWhenDeconstructed float +ritualFocus RitualFocusProperties +rotatable bool +rotateInShelves bool +saveCompressible bool +scatterableOnMapGen bool +seeThroughFog bool +selectable bool +showInSearch bool +size IntVec2 +skyfaller SkyfallerProperties +slagDef ThingDef +smallVolume bool +smeltProducts List +smeltable bool +socialPropernessMatters bool +soundDrop SoundDef +soundImpactDefault SoundDef +soundInteract SoundDef +soundOpen SoundDef +soundPickup SoundDef +soundPlayInstrument SoundDef +soundSpawned SoundDef +specialDisplayRadius float +stackLimit int +startingHpRange FloatRange +statBases List +staticSunShadowHeight float +stealable bool +storedConceptLearnOpportunity ConceptDef +stuffCategories List +stuffCategorySummary string +stuffProps StuffProperties +surfaceType SurfaceType +techHediffsTags List +techLevel TechLevel +terrainAffordanceNeeded TerrainAffordanceDef +thingCategories List +thingClass Type +thingSetMakerTags List +tickerType TickerType +tools List +tradeNeverGenerateStacked bool +tradeNeverStack bool +tradeTags List +tradeability Tradeability +uiIconColor Color +uiIconColorTwo Color +uiIconForStackCount int +uiIconOffset Vector2 +uiIconPath string +uiIconPathsStuff List +uiIconScale float +uiOrder float +useBlueprintGraphicAsGhost bool +useHitPoints bool +useSameGraphicForGhost bool +useStuffTerrainAffordance bool +violentTechHediff bool +virtualDefParent ThingDef +virtualDefs List +weaponClasses List +weaponTags List +wipesPlants bool diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml new file mode 100644 index 0000000..e485239 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml @@ -0,0 +1,11 @@ + + + + TestThing + +
  • + <{caret} +
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml.gold new file mode 100644 index 0000000..72791cd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModListItemClassProperties.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +compClass Type <== +customValue float + +##### ALPHABETIC SORT ##### + + [Light, Generic] +compClass Type <== +customValue float diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml new file mode 100644 index 0000000..dc1cd61 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml @@ -0,0 +1,9 @@ + + + + TestThing + + <{caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml.gold new file mode 100644 index 0000000..ff4469c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestNestedFieldProperties.xml.gold @@ -0,0 +1,80 @@ +Completion: Basic +Count: 34 +Focus: Hard +Range: "<♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +addTopAltitudeBias bool <== +allowAtlasing bool +allowFlip bool +asymmetricLink AsymmetricLinkData +attachPoints List +attachments List +color Color +colorTwo Color +cornerOverlayPath string +damageData DamageGraphicData +drawOffset Vector3 +drawOffsetEast Vector3? +drawOffsetNorth Vector3? +drawOffsetSouth Vector3? +drawOffsetWest Vector3? +drawRotated bool +drawSize Vector2 +flipExtraRotation float +graphicClass Type +ignoreThingDrawColor bool +linkFlags LinkFlags +linkType LinkDrawerType +maskPath string +maxSnS Vector2 +name string +offsetSnS Vector2 +onGroundRandomRotateAngle float +overlayOpacity float +renderInstanced bool +renderQueue int +shaderParameters List +shaderType ShaderTypeDef +shadowData ShadowData +texPath string + +##### ALPHABETIC SORT ##### + + [Light, Generic] +addTopAltitudeBias bool <== +allowAtlasing bool +allowFlip bool +asymmetricLink AsymmetricLinkData +attachPoints List +attachments List +color Color +colorTwo Color +cornerOverlayPath string +damageData DamageGraphicData +drawOffset Vector3 +drawOffsetEast Vector3? +drawOffsetNorth Vector3? +drawOffsetSouth Vector3? +drawOffsetWest Vector3? +drawRotated bool +drawSize Vector2 +flipExtraRotation float +graphicClass Type +ignoreThingDrawColor bool +linkFlags LinkFlags +linkType LinkDrawerType +maskPath string +maxSnS Vector2 +name string +offsetSnS Vector2 +onGroundRandomRotateAngle float +overlayOpacity float +renderInstanced bool +renderQueue int +shaderParameters List +shaderType ShaderTypeDef +shadowData ShadowData +texPath string diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml new file mode 100644 index 0000000..bac7c15 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml @@ -0,0 +1,13 @@ + + + + + + ConcreteThing + + + + + Child + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml.gold new file mode 100644 index 0000000..f44d911 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestParentName.xml.gold @@ -0,0 +1,14 @@ +Completion: Basic +Count: 1 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +AbstractBase <== + +##### ALPHABETIC SORT ##### + + [Light, Generic] +AbstractBase <== diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/Defs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/Defs.xml new file mode 100644 index 0000000..a04cf73 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/Defs.xml @@ -0,0 +1,12 @@ + + + + ModThingA + + + ModThingB + + + ModSound + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs new file mode 100644 index 0000000..8fb4d10 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs @@ -0,0 +1,9 @@ +using Verse; + +namespace MyMod +{ + public static class Lookup + { + public static ThingDef Get() => DefDatabase.GetNamed("{caret}"); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold new file mode 100644 index 0000000..9cec850 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "public static ThingDef Get() => DefDatabase.GetNamed("♦");" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NotObsolete, ObsoleteRuleApplied, NormalSelectionPriority, Other] +ModThingA <== +ModThingB + +##### ALPHABETIC SORT ##### + + [Light, Generic] +ModThingA <== +ModThingB diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs new file mode 100644 index 0000000..34ea535 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs @@ -0,0 +1,11 @@ +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef Mod{caret} + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold new file mode 100644 index 0000000..f9f38c5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold @@ -0,0 +1,22 @@ +Completion: Basic +Prefix: "Mod" +Count: 4 +Focus: Hard +Range: "public static ThingDef Mod♦" + ▲▲▲▲ + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, PrefixMatch, NotObsolete, ObsoleteRuleApplied, NormalSelectionPriority, Other] +ṂọḍDef <== +ṂọḍThingA +ṂọḍThingB +ṂọḍThingDef + +##### ALPHABETIC SORT ##### + + [Light, Generic] +ṂọḍDef <== +ṂọḍThingA +ṂọḍThingB +ṂọḍThingDef diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs new file mode 100644 index 0000000..19f7f30 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs @@ -0,0 +1,11 @@ +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef Mod{caret}; + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold new file mode 100644 index 0000000..7aacee7 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold @@ -0,0 +1,22 @@ +Completion: Basic +Prefix: "Mod" +Count: 4 +Focus: Hard +Range: "public static ThingDef Mod♦;" + ▲▲▲▲ + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, PrefixMatch, NotObsolete, ObsoleteRuleApplied, NormalSelectionPriority, Other] +ṂọḍDef <== +ṂọḍThingA +ṂọḍThingB +ṂọḍThingDef + +##### ALPHABETIC SORT ##### + + [Light, Generic] +ṂọḍDef <== +ṂọḍThingA +ṂọḍThingB +ṂọḍThingDef diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/CSharpUsages.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/CSharpUsages.cs new file mode 100644 index 0000000..2952960 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/CSharpUsages.cs @@ -0,0 +1,16 @@ +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef SharedThing; + } + + public static class Lookup + { + public static ThingDef Get() => DefDatabase.GetNamed("SharedThing"); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/Defs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/Defs.xml new file mode 100644 index 0000000..ad89c0b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/Defs.xml @@ -0,0 +1,10 @@ + + + + SharedThing + + + LocalUser + SharedThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/OtherUsages.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/OtherUsages.xml new file mode 100644 index 0000000..41032a5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/OtherUsages.xml @@ -0,0 +1,10 @@ + + + + OtherUser + SharedThing + + + SharedThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs new file mode 100644 index 0000000..d3d7043 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs @@ -0,0 +1,16 @@ +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef SharedThing; + } + + public static class Lookup + { + public static ThingDef Get() => DefDatabase.GetNamed("Shared{on}Thing"); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs.gold new file mode 100644 index 0000000..2002354 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromCSharpString.cs.gold @@ -0,0 +1,97 @@ +## FindReferencedCodeProvider activity: + Tooltip was shown: Referenced code in 'XmlTag ThingDef/SharedThing' were not found + +## FindUsagesAdvancedProvider activity: + FindResults window with 3 results + TO: [O] |SharedThing| RANGE: (78,89) @ Defs.xml + TO: [O] |SharedThing| RANGE: (189,200) @ Defs.xml + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + +## FindUsagesProvider activity: + FindResults window with 3 results + TO: [O] |SharedThing| RANGE: (78,89) @ Defs.xml + TO: [O] |SharedThing| RANGE: (189,200) @ Defs.xml + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + +## GotoDeclarationProvider activity: + Immediate result: + TO: [O] |SharedThing| RANGE: (78,89) @ Defs.xml + Navigation result: + opened file: Defs.xml + ------------------ + + + |CARET|SharedThing + + + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] |SharedThing| RANGE: (78,89) @ Defs.xml + Navigation result: + opened file: Defs.xml + ------------------ + + + |CARET|SharedThing + + + ------------------ + + +## HighlightUsagesProvider activity: + Tooltip was shown: Usages of 'ThingDef/SharedThing' were not found in this file + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/SharedThing'`: + TO: [O] |SharedThing| RANGE: (78,89) @ Defs.xml + Menu item (enabled) : + icon: UsageOther + text: Defs.xml **SharedThing** (4) + tail: in + tooltip: **SharedThing** + Navigation result: + opened file: Defs.xml + ------------------ + + + |CARET|SharedThing + + + ------------------ + + TO: [O] |SharedThing| RANGE: (189,200) @ Defs.xml + Menu item (enabled) : + icon: UsageOther + text: Defs.xml **SharedThing** (8) + tail: in + tooltip: **SharedThing** + Navigation result: + opened file: Defs.xml + ------------------ + + LocalUser + |CARET|SharedThing + + + ------------------ + + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + Menu item (enabled) : + icon: UsageOther + text: OtherUsages.xml **SharedThing** (5) + tail: in + tooltip: **SharedThing** + Navigation result: + opened file: OtherUsages.xml + ------------------ + + OtherUser + |CARET|SharedThing + + + ------------------ + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml new file mode 100644 index 0000000..49e5d38 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml @@ -0,0 +1,10 @@ + + + + Shared{on}Thing + + + LocalUser + SharedThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml.gold new file mode 100644 index 0000000..d5c6c7f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromDefName.xml.gold @@ -0,0 +1,70 @@ +## FindReferencedCodeProvider activity: + Tooltip was shown: Referenced code in 'XmlTag ThingDef/SharedThing' were not found + +## FindUsagesAdvancedProvider activity: + FindResults window with 3 results + TO: [O] |SharedThing| RANGE: (78,89) @ TestFromDefName.xml + TO: [O] |SharedThing| RANGE: (189,200) @ TestFromDefName.xml + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + +## FindUsagesProvider activity: + FindResults window with 3 results + TO: [O] |SharedThing| RANGE: (78,89) @ TestFromDefName.xml + TO: [O] |SharedThing| RANGE: (189,200) @ TestFromDefName.xml + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + +## GotoDeclarationProvider activity: + Immediate result: + TO: [O] |SharedThing| RANGE: (78,89) @ TestFromDefName.xml + Navigation result: + caret did not move + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] |SharedThing| RANGE: (78,89) @ TestFromDefName.xml + Navigation result: + caret did not move + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/SharedThing'`: + TO: [O] |SharedThing| RANGE: (119,130) @ OtherUsages.xml + Menu item (enabled) : + icon: UsageOther + text: OtherUsages.xml **SharedThing** (5) + tail: in + tooltip: **SharedThing** + Navigation result: + opened file: OtherUsages.xml + ------------------ + + OtherUser + |CARET|SharedThing + + + ------------------ + + TO: [O] |SharedThing| RANGE: (78,89) @ TestFromDefName.xml + Menu item (enabled) : + icon: UsageOther + text: TestFromDefName.xml **SharedThing** (4) + tail: in + tooltip: **SharedThing** + Navigation result: + caret did not move + TO: [O] |SharedThing| RANGE: (189,200) @ TestFromDefName.xml + Menu item (enabled) : + icon: UsageOther + text: TestFromDefName.xml **SharedThing** (8) + tail: in + tooltip: **SharedThing** + Navigation result: + opened file: TestFromDefName.xml + ------------------ + + LocalUser + |CARET|SharedThing + + + ------------------ + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml new file mode 100644 index 0000000..e8b6876 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml @@ -0,0 +1,11 @@ + + + + + + ChildA + + + ChildB + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml.gold new file mode 100644 index 0000000..cfdc47b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/FindUsages/TestFromNameAttribute.xml.gold @@ -0,0 +1,70 @@ +## FindReferencedCodeProvider activity: + Tooltip was shown: Referenced code in 'XmlTag ThingDef/BaseThing' were not found + +## FindUsagesAdvancedProvider activity: + FindResults window with 3 results + TO: [O] RANGE: (65,76) @ TestFromNameAttribute.xml + TO: [O] RANGE: (135,146) @ TestFromNameAttribute.xml + TO: [O] RANGE: (223,234) @ TestFromNameAttribute.xml + +## FindUsagesProvider activity: + FindResults window with 3 results + TO: [O] RANGE: (65,76) @ TestFromNameAttribute.xml + TO: [O] RANGE: (135,146) @ TestFromNameAttribute.xml + TO: [O] RANGE: (223,234) @ TestFromNameAttribute.xml + +## GotoDeclarationProvider activity: + Immediate result: + TO: [O] RANGE: (65,76) @ TestFromNameAttribute.xml + Navigation result: + caret did not move + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] RANGE: (65,76) @ TestFromNameAttribute.xml + Navigation result: + caret did not move + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/BaseThing'`: + TO: [O] RANGE: (65,76) @ TestFromNameAttribute.xml + Menu item (enabled) : + icon: UsageOther + text: TestFromNameAttribute.xml (3) + tail: in + tooltip: + Navigation result: + caret did not move + TO: [O] RANGE: (135,146) @ TestFromNameAttribute.xml + Menu item (enabled) : + icon: UsageOther + text: TestFromNameAttribute.xml (5) + tail: in + tooltip: + Navigation result: + opened file: TestFromNameAttribute.xml + ------------------ + + + + ChildA + + ------------------ + + TO: [O] RANGE: (223,234) @ TestFromNameAttribute.xml + Menu item (enabled) : + icon: UsageOther + text: TestFromNameAttribute.xml (8) + tail: in + tooltip: + Navigation result: + opened file: TestFromNameAttribute.xml + ------------------ + ChildA + + + ChildB + + ------------------ + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml new file mode 100644 index 0000000..2f1e266 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml @@ -0,0 +1,8 @@ + + + + TestThing + maybe + true + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml.gold new file mode 100644 index 0000000..125df71 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidBool.xml.gold @@ -0,0 +1,11 @@ + + + + TestThing + |maybe|(0) + true + + + +--------------------------------------------------------- +(0): ReSharper Underlined Error Highlighting: Value must be "true" or "false" diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml new file mode 100644 index 0000000..2f1e266 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml @@ -0,0 +1,8 @@ + + + + TestThing + maybe + true + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml.gold new file mode 100644 index 0000000..e8abc30 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestNoRimworldReference.xml.gold @@ -0,0 +1,10 @@ + + + + TestThing + maybe + true + + + +--------------------------------------------------------- diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/279ddca82531108db2d9065f4f72d172759429b0a03f8481a2e8f8f14cb6ec21.lock b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/279ddca82531108db2d9065f4f72d172759429b0a03f8481a2e8f8f14cb6ec21.lock new file mode 100644 index 0000000..0ef5cb9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/279ddca82531108db2d9065f4f72d172759429b0a03f8481a2e8f8f14cb6ec21.lock @@ -0,0 +1,9 @@ +# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. +# Input (NuGetFramework=any): +# Microsoft.NETCore.App [2.0.0] +Microsoft.NETCore.App 2.0.0 +Microsoft.NETCore.DotNetAppHost 2.0.0 +Microsoft.NETCore.DotNetHostPolicy 2.0.0 +Microsoft.NETCore.DotNetHostResolver 2.0.0 +Microsoft.NETCore.Platforms 2.0.0 +NETStandard.Library 2.0.0 \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpDefs.xml new file mode 100644 index 0000000..a04cf73 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpDefs.xml @@ -0,0 +1,12 @@ + + + + ModThingA + + + ModThingB + + + ModSound + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs new file mode 100644 index 0000000..232091d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs @@ -0,0 +1,21 @@ +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef ModThingA; + public static ThingDef MissingThing; + public static SoundDef ModSound; + } + + public static class Lookup + { + public static ThingDef Found() => DefDatabase.GetNamed("ModThingB"); + public static ThingDef Missing() => DefDatabase.GetNamed("NoSuchThing"); + public static ThingDef WrongType() => DefDatabase.GetNamed("ModSound"); + public static SoundDef Sound() => DefDatabase.GetNamedSilentFail("ModSound"); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold new file mode 100644 index 0000000..52bba14 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold @@ -0,0 +1,4 @@ +(9,32) 'ModThingA' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/ModThingA +(11,32) 'ModSound' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/ModSound +(16,74) '"ModThingB"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/ModThingB +(19,84) '"ModSound"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/ModSound diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml new file mode 100644 index 0000000..cf5b305 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml @@ -0,0 +1,7 @@ + + + + Referrer + LocalThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold new file mode 100644 index 0000000..37da6e1 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold @@ -0,0 +1,54 @@ +## FindUsagesAdvancedProvider activity: + Tooltip was shown: Usages of 'minifiedDef' were not found + +## FindUsagesProvider activity: + Tooltip was shown: Usages of 'minifiedDef' were not found + +## GotoDeclarationProvider activity: + Immediate result: + DEO: Envoy: Field:Verse.ThingDef.minifiedDef, PsiLanguageType:UNKNOWN as Envoy: Field:Verse.ThingDef.minifiedDef, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThingDef.cs + ------------------ + public bool isTechHediff; + public RecipeMakerProperties recipeMaker; + public ThingDef |CARET|minifiedDef; + public bool isUnfinishedThing; + public bool leaveResourcesWhenKilled; + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + DEO: Envoy: Field:Verse.ThingDef.minifiedDef, PsiLanguageType:UNKNOWN as Envoy: Field:Verse.ThingDef.minifiedDef, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThingDef.cs + ------------------ + public bool isTechHediff; + public RecipeMakerProperties recipeMaker; + public ThingDef |CARET|minifiedDef; + public bool isUnfinishedThing; + public bool leaveResourcesWhenKilled; + ------------------ + + +## GotoTypeDeclarationProvider activity: + Immediate result: + DEO: Envoy: Class:Verse.ThingDef, PsiLanguageType:UNKNOWN as Envoy: Class:Verse.ThingDef, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThingDef.cs + ------------------ + namespace Verse; + + public class |CARET|ThingDef : BuildableDef + { + public System.Type thingClass; + ------------------ + + +## HighlightUsagesProvider activity: + Tooltip was shown: Usages of 'minifiedDef' were not found in this file + +## ShowUsagesProvider activity: + Tooltip was shown: Usages of 'minifiedDef' were not found + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml new file mode 100644 index 0000000..c323ea9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml @@ -0,0 +1,10 @@ + + + + LocalThing + + + Referrer + Local{on}Thing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold new file mode 100644 index 0000000..3fba082 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold @@ -0,0 +1,68 @@ +## FindReferencedCodeProvider activity: + Tooltip was shown: Referenced code in 'XmlTag ThingDef/LocalThing' were not found + +## FindUsagesAdvancedProvider activity: + FindResults window with 2 results + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + +## FindUsagesProvider activity: + FindResults window with 2 results + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + +## GotoDeclarationProvider activity: + Immediate result: + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + Navigation result: + opened file: TestNavigateToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + Navigation result: + opened file: TestNavigateToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/LocalThing'`: + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + Menu item (enabled) : + icon: UsageOther + text: TestNavigateToXmlDef.xml **LocalThing** (4) + tail: in + tooltip: **LocalThing** + Navigation result: + opened file: TestNavigateToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + Menu item (enabled) : + icon: UsageOther + text: TestNavigateToXmlDef.xml **LocalThing** (8) + tail: in + tooltip: **LocalThing** + Navigation result: + caret did not move + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/OtherDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/OtherDefs.xml new file mode 100644 index 0000000..6c28f70 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/OtherDefs.xml @@ -0,0 +1,9 @@ + + + + OtherSound + + + OtherStuff + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml new file mode 100644 index 0000000..8de3ff4 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml @@ -0,0 +1,19 @@ + + + + + Building + + Things/Test + + +
  • + CompGlower +
  • +
  • + 5 +
  • +
    + 1 +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold new file mode 100644 index 0000000..a819947 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold @@ -0,0 +1,9 @@ +(3,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef +(4,10) 'label' [RimworldXmlReference] -> OK: field Verse.Def.label +(5,10) 'category' [RimworldXmlReference] -> OK: field Verse.ThingDef.category +(5,19) 'Building' [RimworldXmlReference] -> OK: enum member Verse.ThingCategory.Building +(6,10) 'graphicData' [RimworldXmlReference] -> OK: field Verse.ThingDef.graphicData +(7,14) 'texPath' [RimworldXmlReference] -> OK: field Verse.GraphicData.texPath +(9,10) 'comps' [RimworldXmlReference] -> OK: field Verse.ThingDef.comps +(11,18) 'compClass' [RimworldXmlReference] -> OK: field Verse.CompProperties.compClass +(14,18) 'idlePowerDraw' [RimworldXmlReference] -> OK: field RimWorld.CompProperties_Power.idlePowerDraw diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml new file mode 100644 index 0000000..e6e6459 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml @@ -0,0 +1,18 @@ + + + + + + LocalThing + + + Referrer + LocalThing + true + OtherSound + MissingSound + +
  • OtherStuff
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold new file mode 100644 index 0000000..7ed4f86 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold @@ -0,0 +1,18 @@ +(3,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef +(3,20) '"BaseThing"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/BaseThing +(5,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef +(5,26) '"BaseThing"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/BaseThing +(6,10) 'defName' [RimworldXmlReference] -> OK: field Verse.Def.defName +(6,18) 'LocalThing' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/LocalThing +(8,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef +(9,10) 'defName' [RimworldXmlReference] -> OK: field Verse.Def.defName +(9,18) 'Referrer' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/Referrer +(10,10) 'minifiedDef' [RimworldXmlReference] -> OK: field Verse.ThingDef.minifiedDef +(10,22) 'LocalThing' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/LocalThing +(10,34) 'minifiedDef' [RimworldXmlReference] -> OK: field Verse.ThingDef.minifiedDef +(11,10) 'leaveResourcesWhenKilled' [RimworldXmlReference] -> OK: field Verse.ThingDef.leaveResourcesWhenKilled +(12,10) 'soundDrop' [RimworldXmlReference] -> OK: field Verse.ThingDef.soundDrop +(12,20) 'OtherSound' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/OtherSound +(13,10) 'soundInteract' [RimworldXmlReference] -> OK: field Verse.ThingDef.soundInteract +(14,10) 'stuffCategories' [RimworldXmlReference] -> OK: field Verse.BuildableDef.stuffCategories +(15,17) 'OtherStuff' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement StuffCategoryDef/OtherStuff From 3de7067f0c6a4298f37c992d4c6cfea75042260e Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 11:25:53 +0100 Subject: [PATCH 03/15] Get the tests running on Github Actions --- .gitattributes | 4 +++ .github/workflows/CI.yml | 22 +++++++++------- .github/workflows/Deploy.yml | 17 +++++++++++- docs/testing-research.md | 26 ++++++++++++++++++- .../ReSharperPlugin.RimworldDev.Tests.csproj | 3 +++ 5 files changed, 61 insertions(+), 11 deletions(-) diff --git a/.gitattributes b/.gitattributes index 3a94d56..7dfe4d2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,9 @@ # Set the default behavior, in case people don't have core.autocrlf set. * text=auto +# Backend test inputs and golds must be LF everywhere. Some golds record document offsets (navigation/Find Usages +# RANGE: (78,88)), which shift if a Windows checkout turns the inputs into CRLF. Same rule as JetBrains' own plugins. +src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/** text eol=lf + # Preserve line endings in gradle scripts gradlew* -text diff diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index d1ea6cb..c4929a2 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: Build: @@ -32,20 +33,23 @@ jobs: name: ${{ github.event.repository.name }}.CI.${{ github.head_ref || github.ref_name }} path: output Test: - runs-on: ubuntu-latest + # The ReSharper test shell needs the Windows Desktop runtime (WPF/WinForms). It does not run on Linux, and the + # reasons are recorded in docs/testing-research.md, "Platform". + runs-on: windows-latest + timeout-minutes: 30 steps: - - uses: jlumbroso/free-disk-space@main - uses: actions/checkout@v4 with: submodules: recursive - - name: configure_java - uses: actions/setup-java@v4 - with: - distribution: 'corretto' - java-version: '21' - cache: 'gradle' - name: Setup .NET uses: actions/setup-dotnet@v4 with: global-json-file: global.json - - run: ./gradlew :testDotNet --no-daemon \ No newline at end of file + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('Directory.Build.props', 'src/dotnet/**/*.csproj', 'src/dotnet/**/*.props') }} + restore-keys: nuget-${{ runner.os }}- + # Same command as Gradle's :testDotNet, without needing Java/Gradle on this runner. + - run: dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index 119b70b..8ee2a06 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -6,7 +6,22 @@ on: - '*.*.*' jobs: + Test: + # See CI.yml: the backend tests only run on Windows. Publish is gated on this job instead of on Gradle's + # :publishPlugin -> :testDotNet dependency, which would fail on the Linux runner. + runs-on: windows-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - run: dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions Publish: + needs: Test runs-on: ubuntu-latest environment: Deploy steps: @@ -25,7 +40,7 @@ jobs: with: global-json-file: global.json - name: Publish Rider Package - run: ./gradlew :publishPlugin -PBuildConfiguration="Release" -PPluginVersion="${{ github.ref_name }}" -PPublishToken="${{ secrets.PUBLISH_TOKEN }}" + run: ./gradlew :publishPlugin -x testDotNet -PBuildConfiguration="Release" -PPluginVersion="${{ github.ref_name }}" -PPublishToken="${{ secrets.PUBLISH_TOKEN }}" env: PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - run: ./gradlew :buildResharperPlugin diff --git a/docs/testing-research.md b/docs/testing-research.md index 39e9fe7..6ac8ef3 100644 --- a/docs/testing-research.md +++ b/docs/testing-research.md @@ -251,7 +251,31 @@ test data. Keep input/gold case consistent. ## Platform JetBrains' last official word (RIDER-23218, 2019): "we don't support plugin unit tests on Linux"; every surveyed -repo runs backend tests on `windows-latest`. Our CI Test job is `ubuntu-latest` and will need to move. +repo runs backend tests on `windows-latest`. **Tested here (2026-09-18, WSL Ubuntu 22.04, .NET 10 SDK): confirmed.** + +**CI:** `CI.yml`'s Test job and a new `Test` job in `Deploy.yml` (which `Publish` now `needs`) run +`dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions` on `windows-latest`. Deploy publishes with +`:publishPlugin -x testDotNet`, because the Gradle dependency would run the tests on the Linux runner. The test csproj +sets `EnableWindowsTargeting=true` so the *solution still builds* on Linux/macOS (CI Build job, Deploy, contributors); +running the tests there fails loudly ("framework Microsoft.WindowsDesktop.App not found", exit 1), not silently. +`.gitattributes` forces `test/data/** eol=lf`, which is **required**: Windows runners check out CRLF, and the +navigation/Find Usages golds contain document offsets (`RANGE: (78,88)`) that shift with CRLF (4 tests fail; the +framework normalises gold line endings but not offsets). Longest repo path on a runner is ~200 chars, under MAX_PATH; +a checkout under a long local path (e.g. the Claude scratchpad) does hit it. + +What it takes to get the shell up on Linux, layer by layer (each fix got one layer further; stopped at 5): + +| # | Symptom on Linux | Cause | Workaround that got past it | +|---|---|---|---| +| 1 | `NETSDK1100` at build | `net10.0-windows` TFM | `net10.0` + no `UseWindowsForms`/`UseWPF` on non-Windows | +| 2 | NUnit "discovered 29 of 29", then **runs 0, reports nothing** | `[assembly: Apartment(STA)]` is unsupported off Windows | drop the attribute on non-Windows. Note the silent-pass hazard | +| 3 | `JetDispatcher`: "this thread is MTA rather than STA" | JetBrains emulate STA on Unix (`JetBrains.Util.Concurrency.JetThreadApartment`) but the test bootstrap never opts in | call `JetThreadApartment.STAThread()` from a `[STAThread]` method in the `SetUpFixture` constructor | +| 4 | `TypeLoadException: System.Windows.Freezable` from `ThemedIconManagerLiveImages` | the stock .NET `WindowsBase` facade wins over JetBrains' Unix mock (`JetBrains.WindowsDesktop.Mock.Runtime`, `runtimes/unix/lib/.../WindowsBase.dll`): same assembly version, higher file version, so the build's conflict resolution drops the mock | copy the mock in and declare it in `deps.json` `runtimeTargets` (rid `unix`) with a huge `fileVersion` — the host resolves conflicts from `deps.json` versions. (JetBrains do the same trick: `JetBrains.Private.Winforms` declares `fileVersion` 42.42.42.42424) | +| 5 | `System.Windows.Forms.Primitives` 9.0 missing, from `StdApplicationUI.StatusBars.JetStatusBarIndicator` | the test environment activates the WinForms status bar; `JetBrains.Private.Winforms` ships only `System.Windows.Forms.dll`, and the Windows `Primitives` is a win-x64 R2R image (`BadImageFormatException`) | none — stopped here | + +Layers 1–3 would be cheap to keep; 4 is a post-build `deps.json` patch; 5 would need a zone configuration that keeps +Windows-UI components out of the test shell (Rider's own Linux host evidently doesn't activate them), which is +undocumented. Revisit only if Windows CI minutes become a problem. ## Diagnostics diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index 04ce93a..b47da4f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -4,6 +4,9 @@ net10.0-windows + + true true From 766b38712537d5cebc4ffdcf67879d95a3e73553 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 13:26:39 +0100 Subject: [PATCH 04/15] Only run the new tests for Publishing and if a PR has a label allowing it to run those tests --- .github/workflows/CI.yml | 21 --------- .github/workflows/Deploy.yml | 28 ++++-------- .github/workflows/Tests.yml | 44 +++++++++++++++++++ docs/testing-research.md | 19 +++++--- .../ReSharperPlugin.RimworldDev.Tests.csproj | 26 ++++++++--- .../RimworldDevTestEnvironmentZone.cs | 4 ++ .../WindowsOnlyGuard.cs | 24 ++++++++++ 7 files changed, 115 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/Tests.yml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/WindowsOnlyGuard.cs diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index c4929a2..0beb2de 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -32,24 +32,3 @@ jobs: with: name: ${{ github.event.repository.name }}.CI.${{ github.head_ref || github.ref_name }} path: output - Test: - # The ReSharper test shell needs the Windows Desktop runtime (WPF/WinForms). It does not run on Linux, and the - # reasons are recorded in docs/testing-research.md, "Platform". - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - global-json-file: global.json - - name: Cache NuGet packages - uses: actions/cache@v4 - with: - path: ~/.nuget/packages - key: nuget-${{ runner.os }}-${{ hashFiles('Directory.Build.props', 'src/dotnet/**/*.csproj', 'src/dotnet/**/*.props') }} - restore-keys: nuget-${{ runner.os }}- - # Same command as Gradle's :testDotNet, without needing Java/Gradle on this runner. - - run: dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index 8ee2a06..5090ee3 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -6,26 +6,17 @@ on: - '*.*.*' jobs: - Test: - # See CI.yml: the backend tests only run on Windows. Publish is gated on this job instead of on Gradle's - # :publishPlugin -> :testDotNet dependency, which would fail on the Linux runner. - runs-on: windows-latest - timeout-minutes: 30 - steps: - - uses: actions/checkout@v4 - with: - submodules: recursive - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: 10.0.x - - run: dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions Publish: - needs: Test - runs-on: ubuntu-latest + # Windows so that :publishPlugin's dependency on :testDotNet runs the real backend test suite before anything is + # published (the ReSharper test shell only runs on Windows; on Linux every test reports as skipped). Releases are + # rare, so the Windows runner cost is acceptable here, unlike on every PR commit (see Tests.yml). + runs-on: windows-latest environment: Deploy + defaults: + run: + # ./gradlew and the output/* globs below are written for bash; Windows runners ship Git Bash. + shell: bash steps: - - uses: jlumbroso/free-disk-space@main - uses: actions/checkout@v4 with: submodules: recursive @@ -40,7 +31,7 @@ jobs: with: global-json-file: global.json - name: Publish Rider Package - run: ./gradlew :publishPlugin -x testDotNet -PBuildConfiguration="Release" -PPluginVersion="${{ github.ref_name }}" -PPublishToken="${{ secrets.PUBLISH_TOKEN }}" + run: ./gradlew :publishPlugin -PBuildConfiguration="Release" -PPluginVersion="${{ github.ref_name }}" -PPublishToken="${{ secrets.PUBLISH_TOKEN }}" env: PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - run: ./gradlew :buildResharperPlugin @@ -51,6 +42,5 @@ jobs: - name: Upload binaries to release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash run: | gh release upload ${{ github.ref_name }} output/* \ No newline at end of file diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml new file mode 100644 index 0000000..4c915a0 --- /dev/null +++ b/.github/workflows/Tests.yml @@ -0,0 +1,44 @@ +name: Tests + +# Separate from CI.yml so that adding or removing the 'feature-testing' label re-runs only the tests, on the other OS, +# without touching the Build check. +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, labeled, unlabeled] + workflow_dispatch: + inputs: + windows: + description: Run the full backend test suite on Windows + type: boolean + default: false + +jobs: + Test: + # The backend tests boot the ReSharper test shell, which only runs on Windows (docs/testing-research.md, "Platform"). + # Windows runners cost more, so by default this runs on Linux, where the suite builds and every test reports as + # skipped (WindowsOnlyGuard). The 'feature-testing' PR label (or the workflow_dispatch checkbox) runs it for real. + # Other label changes don't re-run anything. + if: >- + (github.event.action != 'labeled' && github.event.action != 'unlabeled') || + github.event.label.name == 'feature-testing' + runs-on: ${{ (contains(github.event.pull_request.labels.*.name, 'feature-testing') || inputs.windows) && 'windows-latest' || 'ubuntu-latest' }} + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ hashFiles('Directory.Build.props', 'src/dotnet/**/*.csproj', 'src/dotnet/**/*.props') }} + restore-keys: nuget-${{ runner.os }}- + # Same command as Gradle's :testDotNet, without needing Java/Gradle on this runner. + - run: dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions diff --git a/docs/testing-research.md b/docs/testing-research.md index 6ac8ef3..9bfb17d 100644 --- a/docs/testing-research.md +++ b/docs/testing-research.md @@ -253,11 +253,20 @@ test data. Keep input/gold case consistent. JetBrains' last official word (RIDER-23218, 2019): "we don't support plugin unit tests on Linux"; every surveyed repo runs backend tests on `windows-latest`. **Tested here (2026-09-18, WSL Ubuntu 22.04, .NET 10 SDK): confirmed.** -**CI:** `CI.yml`'s Test job and a new `Test` job in `Deploy.yml` (which `Publish` now `needs`) run -`dotnet test ReSharperPlugin.RimworldDev.sln --logger GitHubActions` on `windows-latest`. Deploy publishes with -`:publishPlugin -x testDotNet`, because the Gradle dependency would run the tests on the Linux runner. The test csproj -sets `EnableWindowsTargeting=true` so the *solution still builds* on Linux/macOS (CI Build job, Deploy, contributors); -running the tests there fails loudly ("framework Microsoft.WindowsDesktop.App not found", exit 1), not silently. +**Off Windows** the test project targets plain `net10.0` (no WinForms/WPF), so it builds and `dotnet test` succeeds with +every test **reported as skipped**, each with the reason: `WindowsOnlyGuard` is a `[SetUpFixture]` outside any +namespace, so it runs before the one that boots the shell and `Assert.Ignore`s everything on non-Windows. Two +approaches that look right but aren't: an assembly-level `[Platform(Include = "Win")]` makes the adapter print only "No +test is available" (exit 0, nothing reported — reads as a pass), and so does leaving `[Apartment(STA)]` in on Linux, +hence it's under `#if WINDOWS` (defined for the `net10.0-windows` build only). + +**CI:** runner cost rules out Windows by default. `Tests.yml` runs `dotnet test ReSharperPlugin.RimworldDev.sln --logger +GitHubActions` on `ubuntu-latest` (29 skipped), and on `windows-latest` (the real suite) when the PR has the +`feature-testing` label or the manual run's "windows" box is ticked. PR runs trigger on `labeled`/`unlabeled` too, so +adding the label re-runs just the tests on Windows; it lives apart from `CI.yml` so label changes don't touch the Build +check. Deploy's `Publish` job runs on `windows-latest` (releases are rare enough for the cost), so `:publishPlugin` -> +`:testDotNet` runs the real suite and a failing test stops the release; its steps use `shell: bash` for `./gradlew` +and the `output/*` globs. `.gitattributes` forces `test/data/** eol=lf`, which is **required**: Windows runners check out CRLF, and the navigation/Find Usages golds contain document offsets (`RANGE: (78,88)`) that shift with CRLF (4 tests fail; the framework normalises gold line endings but not offsets). Longest repo path on a runner is ~200 chars, under MAX_PATH; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index b47da4f..adad02f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -4,13 +4,21 @@ net10.0-windows - - true true true + + + + + net10.0 + false + false + + + - <_JetNetFxLib Include="$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))jetbrains.lifetimes/2026.1.2/lib/net472/JetBrains.Lifetimes.dll" /> - <_JetNetFxLib Include="$([MSBuild]::EnsureTrailingSlash('$(NuGetPackageRoot)'))jetbrains.rdframework/2026.1.2/lib/net472/JetBrains.RdFramework.dll" /> + + <_JetPinnedLib Include="@(RuntimeCopyLocalItems)" + Condition="'%(RuntimeCopyLocalItems.Extension)' == '.dll' And ('%(RuntimeCopyLocalItems.NuGetPackageId)' == 'JetBrains.Lifetimes' Or '%(RuntimeCopyLocalItems.NuGetPackageId)' == 'JetBrains.RdFramework')" /> + <_JetNetFxLib Include="@(_JetPinnedLib->'%(RootDir)%(Directory)..\net472\%(Filename)%(Extension)')" /> - + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs index c85a7ae..9a842ac 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs @@ -4,7 +4,11 @@ using JetBrains.TestFramework.Application.Zones; using NUnit.Framework; +#if WINDOWS +// WINDOWS is defined for the net10.0-windows build only. Elsewhere NUnit can't honour STA and would silently run nothing; +// the tests are skipped there by WindowsOnlyGuard instead. [assembly: Apartment(ApartmentState.STA)] +#endif namespace ReSharperPlugin.RimworldDev.Tests; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/WindowsOnlyGuard.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/WindowsOnlyGuard.cs new file mode 100644 index 0000000..b02f408 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/WindowsOnlyGuard.cs @@ -0,0 +1,24 @@ +using System; +using NUnit.Framework; + +/// +/// Everything in this assembly boots the ReSharper test shell, which needs the Windows Desktop runtime (WPF/WinForms) +/// and can't run elsewhere (docs/testing-research.md, "Platform"). Off Windows this reports every test as skipped, with +/// the reason, instead of letting the shell crash on startup. +/// +/// +/// Deliberately outside any namespace: NUnit runs set-up fixtures from the outermost namespace in, so this runs before +/// RimworldDevTestsAssembly (which boots the shell), and Assert.Ignore here marks each test beneath it as +/// ignored. An assembly-level [Platform] doesn't work for this: the adapter then reports "No test is available" +/// and nothing else, which reads as a pass. OS-agnostic tests would need their own assembly. +/// +[SetUpFixture] +public class WindowsOnlyGuard +{ + [OneTimeSetUp] + public void RequireWindows() + { + if (!OperatingSystem.IsWindows()) + Assert.Ignore("The ReSharper test shell only runs on Windows; run the suite on Windows (CI: add the 'feature-testing' PR label)."); + } +} From 49541ba04390179fa9732ee22e4ce5584d6741ea Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 16:09:06 +0100 Subject: [PATCH 05/15] Update tests to use globals file --- .github/workflows/Tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Tests.yml b/.github/workflows/Tests.yml index 4c915a0..f523817 100644 --- a/.github/workflows/Tests.yml +++ b/.github/workflows/Tests.yml @@ -33,7 +33,7 @@ jobs: - name: Setup .NET uses: actions/setup-dotnet@v4 with: - dotnet-version: 10.0.x + global-json-file: global.json - name: Cache NuGet packages uses: actions/cache@v4 with: From c6832ee2e117ab405f83fa1e233a2ac9047fee76 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 18 Sep 2026 17:21:27 +0100 Subject: [PATCH 06/15] Move the SymbolScope away from storing tree nodes during Merge and into looking them up at the time when they're needed. This should (hopefully?) prevent errors about trying to access an uncommited PSI doc --- .../Completion/RimworldXmlCompletionTests.cs | 12 +- .../SymbolScope/RimworldSymbolScopeTests.cs | 96 +++++++ .../SymbolScope/TestDefNodesFollowEdits.xml | 7 + .../ScopeHelper.cs | 20 ++ .../SymbolScope/RimworldSymbolScope.cs | 257 +++++++++++------- .../SymbolScope/RimworldXmlDefSymbol.cs | 25 +- 6 files changed, 307 insertions(+), 110 deletions(-) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs index aa971ae..5e8ad45 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs @@ -25,19 +25,17 @@ public class RimworldXmlCompletionTests : RimworldCompletionTestBase [Test] public void TestParentName() => DoNamedTest(); [Test] public void TestModDefClassProperties() => DoNamedTest("ModTypes.cs"); [Test] public void TestModListItemClassProperties() => DoNamedTest("ModTypes.cs"); - // Gold is hand-written: what the plugin *should* offer. It currently omits CustomThing, because ExtraDefTagNames is - // only built when ScopeHelper already has the RimWorld scope at merge time, and on a cold load it doesn't. - [Test, Ignore("ExtraDefTagNames not built when the def index merges before scopes are ready; see docs/testing-plan.md step 9")] - public void TestModDefAsSuperclassReference() => DoNamedTest("ModTypes.cs"); + // A def of a mod's ThingDef subclass is offered where a ThingDef is expected, even though the def index merges + // before the scopes are ready on load (docs/testing-plan.md step 9). + [Test] public void TestModDefAsSuperclassReference() => DoNamedTest("ModTypes.cs"); } /// /// Gold is the document after accepting the item named by the input's ${COMPLETE_ITEM:…} directive. -/// The golds are correct, but accepting an item commits the edited document, and RimworldSymbolScope.Merge reads the -/// PSI file mid-commit ("Trying to get PSI file for an uncommitted document"), which fails the test as a logged error. +/// Accepting an item commits the edited document, which runs RimworldSymbolScope.Merge mid-commit; these guard that the +/// index doesn't read PSI there (docs/testing-plan.md step 5). /// [TestFileExtension(".xml")] -[Ignore("RimworldSymbolScope.AddToLocalCache calls GetPrimaryPsiFile during commit merge; see docs/testing-plan.md step 5")] public class RimworldXmlCompletionActionTests : RimworldCompletionTestBase { protected override CodeCompletionTestType TestType => CodeCompletionTestType.Action; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs new file mode 100644 index 0000000..1267849 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs @@ -0,0 +1,96 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Application.Components; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.SymbolScope; +using ReSharperPlugin.RimworldDev.Tests.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.SymbolScope; + +/// +/// The def index stores offsets and finds tree nodes on demand (docs/testing-plan.md step 5). Each commit below re-runs +/// Build + Merge for the file, which used to read PSI mid-commit and log an error. After each edit, the node handed back +/// must be the one at the def's current position, not a node cached from an earlier tree. +/// +public class RimworldSymbolScopeTests : BaseTestWithSingleProject +{ + private const string FileName = "TestDefNodesFollowEdits.xml"; + private const string ThingALine = " ThingA\n"; + + protected override string RelativeTestDataPath => @"SymbolScope"; + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + base.GetReferencedAssemblies(targetFrameworkId).Concat(RimworldCompletionTestBase.RimworldReferenceAssemblies()); + + [SetUp] + public void ResetRimworldScope() + { + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + [TearDown] + public void ForgetRimworldScope() => ScopeHelper.Reset(); + + [Test] public void TestDefNodesFollowEdits() => DoTestSolution(FileName); + + protected override void DoTest(Lifetime lifetime, IProject project) + { + var files = Solution.GetPsiServices().Files; + var scope = Solution.GetComponent(); + var sourceFile = project.GetAllProjectFiles().Single(file => file.Name == FileName).ToSourceFiles().Single(); + var document = sourceFile.Document; + + files.CommitAllDocuments(); + AssertAllDefs(scope, sourceFile); + + // Everything moves down a line + using (WriteLockCookie.Create()) + document.InsertText(document.GetText().IndexOf(""), "\n"); + files.CommitAllDocuments(); + AssertAllDefs(scope, sourceFile); + + // ThingB's name lands exactly where ThingA's was, which the index has a node cached for + using (WriteLockCookie.Create()) + { + var start = document.GetText().IndexOf(ThingALine); + document.DeleteText(new TextRange(start, start + ThingALine.Length)); + } + files.CommitAllDocuments(); + using (ReadLockCookie.Create()) + { + Assert.That(scope.GetTagByDef("ThingDef", "ThingA"), Is.Null); + AssertDefNode(scope.GetTagByDef("ThingDef", "ThingB"), sourceFile, ">ThingB<", 1); + } + } + + private static void AssertAllDefs(RimworldSymbolScope scope, IPsiSourceFile sourceFile) + { + using (ReadLockCookie.Create()) + { + AssertDefNode(scope.GetTagByDef("ThingDef", "ThingA"), sourceFile, ">ThingA<", 1); + AssertDefNode(scope.GetTagByDef("ThingDef", "ThingB"), sourceFile, ">ThingB<", 1); + AssertDefNode(scope.GetTagByDef("ThingDef", "BaseThing"), sourceFile, "\"BaseThing\"", 0); + Assert.That(scope.IsDefAbstract("ThingDef/BaseThing"), Is.True); + Assert.That(scope.IsDefAbstract("ThingDef/ThingA"), Is.False); + } + } + + // The node must be live and sit where `marker` (plus `skip` characters) is in the document's current text + private static void AssertDefNode(ITreeNode node, IPsiSourceFile sourceFile, string marker, int skip) + { + Assert.That(node, Is.Not.Null); + Assert.That(node.IsValid(), Is.True); + Assert.That(node.GetSourceFile(), Is.EqualTo(sourceFile)); + Assert.That(node.GetDocumentRange().StartOffset.Offset, + Is.EqualTo(sourceFile.Document.GetText().IndexOf(marker) + skip)); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml new file mode 100644 index 0000000..eb75650 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml @@ -0,0 +1,7 @@ + + + ThingA + ThingB + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs index 7f7f6c9..57d55e8 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs @@ -339,6 +339,26 @@ public static List GetAllSuperTypes(string clrName) return items; } + /// + /// The other def types a def class can be referenced as: its superclasses below Verse.Def, nearest first, as + /// short names (MyMod.CustomThingDef -> ThingDef, BuildableDef). Null when the class can't be + /// resolved, which for a mod's class can just mean the symbol caches aren't ready yet. + /// + [CanBeNull] + public static List GetDefSuperClassNames(string clrName) + { + using (CompilationContextCookie.GetOrCreate(UniversalModuleReferenceContext.Instance)) + { + if (GetScopeForClass(clrName)?.GetTypeElementByCLRName(clrName) is not { } typeElement) return null; + + return typeElement.GetAllSuperClasses() + .Select(superClass => superClass.GetClrName()) + .TakeWhile(superClass => superClass.FullName != "Verse.Def") + .Select(superClass => superClass.ShortName) + .ToList(); + } + } + public static bool ExtendsFromVerseDef(string clrName) { if (RimworldScope is null) return false; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs index f3b434a..1b12668 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs @@ -1,13 +1,11 @@ using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using JetBrains; using JetBrains.Annotations; using JetBrains.Application.Parts; -using JetBrains.Application.Parts; using JetBrains.Application.Threading; -using JetBrains.Collections; using JetBrains.Lifetimes; -using JetBrains.Metadata.Reader.API; using JetBrains.ProjectModel; using JetBrains.ReSharper.Psi; using JetBrains.ReSharper.Psi.Caches; @@ -16,37 +14,67 @@ using JetBrains.ReSharper.Psi.Resolve; using JetBrains.ReSharper.Psi.Tree; using JetBrains.ReSharper.Psi.Util; -using JetBrains.ReSharper.Psi.Xml.Impl.Tree; using JetBrains.ReSharper.Psi.Xml.Tree; using ReSharperPlugin.RimworldDev.TypeDeclaration; namespace ReSharperPlugin.RimworldDev.SymbolScope; -public struct DefTag +/// +/// Where a def is declared: the offset of its <defName> text or Name="" value in its file. The tree +/// node itself is only looked up when someone asks for it (). +/// +public readonly struct DefTag { - public DefTag(ITreeNode treeNode, bool isAbstract = false) + public DefTag(IPsiSourceFile sourceFile, int documentOffset, bool isAbstract) { - TreeNode = treeNode; + SourceFile = sourceFile; + DocumentOffset = documentOffset; IsAbstract = isAbstract; } - public ITreeNode TreeNode { get; } + public IPsiSourceFile SourceFile { get; } + public int DocumentOffset { get; } public bool IsAbstract { get; } } +/// +/// Index of every def in the solution, keyed by "{defType}/{defName}". +/// +/// Merge/MergeLoaded only record where each def lives; they must not touch PSI, because Merge runs in the middle of a +/// document commit, where asking for a PSI file asserts ("Trying to get PSI file for an uncommitted document"). Tree +/// nodes are looked up when queried, and the superclass aliases in are resolved on the +/// first query after a change, once the RimWorld and mod types can actually be resolved. +/// [PsiComponent(Instantiation.ContainerAsyncPrimaryThread)] public class RimworldSymbolScope : SimpleICache> { - private Dictionary DefTags = new(); - private Dictionary ExtraDefTagNames = new(); + // Version of the persisted RimworldXmlDefSymbol format; bump it whenever the marshaller changes + private const long PersistentVersion = 2; + + private readonly ISolution _solution; + + // Written in Merge/Drop (write lock), read by queries (read lock), so the two never overlap + private readonly Dictionary DefTags = new(); + + // "ThingDef/CustomThing" -> "MyMod.CustomThingDef/CustomThing". Queries can run on several threads at once, so it's + // rebuilt under a lock and swapped in whole. + private Dictionary _extraDefTagNames = new(); + private volatile bool _extraDefTagNamesStale; + private readonly object _extraDefTagNamesLock = new(); + + // Offset -> defName/Name value node for each XML file we've looked into. Keyed weakly on the IFile so the nodes + // go away with the tree instead of being kept alive by the index. + private readonly ConditionalWeakTable> _defNodesByFile = new(); + private Dictionary _declaredElements = new(); private SymbolTable _symbolTable; public RimworldSymbolScope (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, - long? version = null) - : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version) + ISolution solution) + : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, PersistentVersion) { + _solution = solution; } protected override bool IsApplicable(IPsiSourceFile sourceFile) @@ -54,8 +82,24 @@ protected override bool IsApplicable(IPsiSourceFile sourceFile) return base.IsApplicable(sourceFile) && sourceFile.LanguageType.Name == "XML"; } + /// + /// The current aliases, rebuilding them first if they're stale (see ). Call it + /// once per query and use the result throughout: each call may retry the rebuild, and another query may swap in a + /// new dictionary between calls. + /// + private Dictionary GetExtraDefTagNames() + { + if (!_extraDefTagNamesStale) return _extraDefTagNames; + + lock (_extraDefTagNamesLock) + { + if (_extraDefTagNamesStale) RebuildExtraDefTagNames(); + return _extraDefTagNames; + } + } + public bool HasTag(DefNameValue defName) => - DefTags.ContainsKey(defName.TagId) || ExtraDefTagNames.ContainsKey(defName.TagId); + DefTags.ContainsKey(defName.TagId) || GetExtraDefTagNames().ContainsKey(defName.TagId); [CanBeNull] public ITreeNode GetTagByDef(string defType, string defName) @@ -69,31 +113,32 @@ public ITreeNode GetTagByDef(string defType, string defName) [CanBeNull] public ITreeNode GetTagByDef(string defId) { - if (!DefTags.ContainsKey(defId)) + if (!DefTags.TryGetValue(defId, out var defTag)) return null; - return DefTags[defId].TreeNode; + return FindDefNode(defTag); } public bool IsDefAbstract(string defId) { - return DefTags.ContainsKey(defId) && DefTags[defId].IsAbstract; + return DefTags.TryGetValue(defId, out var defTag) && defTag.IsAbstract; } public DefNameValue GetDefName(DefNameValue value) => - ExtraDefTagNames.TryGetValue(value.TagId, out var defTag) ? new DefNameValue(defTag) : value; + GetExtraDefTagNames().TryGetValue(value.TagId, out var defTag) ? new DefNameValue(defTag) : value; public List GetDefsByType(string defType) { + var extraDefTagNames = GetExtraDefTagNames(); + return DefTags .Keys .Where(key => key.StartsWith($"{defType}/")) - .Select(defId => ExtraDefTagNames.ContainsKey(defId) ? ExtraDefTagNames[defId] : defId) - .ToList() + .Select(defId => extraDefTagNames.TryGetValue(defId, out var aliasedDefId) ? aliasedDefId : defId) .Concat( - ExtraDefTagNames.Keys - .Where(key => key.StartsWith($"{defType}/")) - .Select(key => ExtraDefTagNames[key]) + extraDefTagNames + .Where(alias => alias.Key.StartsWith($"{defType}/")) + .Select(alias => alias.Value) ).ToList(); } @@ -125,7 +170,7 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup) .Children() .FirstOrDefault(element => element is IXmlValueToken)? .GetUnquotedText(); - + var defNameTag = tag.GetNestedTags("defName"). FirstOrDefault()?. Children(). @@ -133,10 +178,15 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup) tag.GetAttribute("Name")?. Children(). FirstOrDefault(element => element is IXmlValueToken); - + if (defName is null) continue; - defs.Add(new RimworldXmlDefSymbol(defNameTag, defName, tag.GetTagName())); + // Only defs identified by a Name="" attribute can be abstract parents + var isAbstract = defNameTag is IXmlValueToken && + tag.GetAttribute("Abstract") is { } attribute && + attribute.UnquotedValue.ToLower() == "true"; + + defs.Add(new RimworldXmlDefSymbol(defNameTag, defName, tag.GetTagName(), isAbstract)); } return defs; @@ -161,76 +211,14 @@ public override void Drop(IPsiSourceFile sourceFile) base.Drop(sourceFile); } + // Runs inside Merge, i.e. mid-commit: must not ask for PSI (see the class comment) private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List cacheItem) { - ScopeHelper.UpdateScopes(sourceFile.GetSolution()); - if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return; - cacheItem?.ForEach(item => { - var matchingDefTag = xmlFile - .GetNestedTags("Defs/*/defName").FirstOrDefault(tag => - tag.Children().ElementAt(1).GetTreeStartOffset().Offset == - item.DocumentOffset) ?? - xmlFile - .GetNestedTags("Defs/*") - .FirstOrDefault(tag => - tag.GetAttribute("Name")? - .Children() - .FirstOrDefault(element => element is IXmlValueToken)? - .GetTreeStartOffset().Offset == item.DocumentOffset - )? - .GetAttribute("Name")? - .Children() - .FirstOrDefault(element => element is IXmlValueToken); - - if (matchingDefTag is null) return; - - // If the DefName is in a [Name=""] Attribute, it'll be matched to a XmlValueToken, which doesn't have any - // children. Otherwise, it'll be matched to the XmlTag for , where we want the first child as the - // string value - var xmlTag = matchingDefTag is IXmlValueToken ? matchingDefTag : matchingDefTag.Children().ElementAt(1); - - AddDefTagToList(item, xmlTag); + DefTags[$"{item.DefType}/{item.DefName}"] = new DefTag(sourceFile, item.DocumentOffset, item.IsAbstract); + if (item.DefType.Contains(".")) _extraDefTagNamesStale = true; }); - - void AddDefTagToList(RimworldXmlDefSymbol item, ITreeNode xmlTag) - { - using (CompilationContextCookie.GetOrCreate(UniversalModuleReferenceContext.Instance)) - { - if (item.DefType.Contains(".") && ScopeHelper.RimworldScope is not null) - { - var superClasses = ScopeHelper.GetScopeForClass(item.DefType)? - .GetTypeElementByCLRName(item.DefType)? - .GetAllSuperClasses().ToList() ?? new(); - - foreach (var superClass in superClasses) - { - if (superClass.GetClrName().FullName == "Verse.Def") break; - - var subDefType = superClass.GetClrName().ShortName; - if (!ExtraDefTagNames.ContainsKey($"{subDefType}/{item.DefName}")) - { - ExtraDefTagNames.Add($"{subDefType}/{item.DefName}", $"{item.DefType}/{item.DefName}"); - } - else - { - ExtraDefTagNames[$"{subDefType}/{item.DefName}"] = $"{item.DefType}/{item.DefName}"; - } - } - } - - var isAbstract = xmlTag is IXmlValueToken && - xmlTag.Parent?.Parent is XmlTagHeaderNode defTypeTag && - defTypeTag.GetAttribute("Abstract") is {} attribute && - attribute.UnquotedValue.ToLower() == "true"; - - if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) - DefTags.Add($"{item.DefType}/{item.DefName}", new DefTag(xmlTag, isAbstract)); - else - DefTags[$"{item.DefType}/{item.DefName}"] = new DefTag(xmlTag, isAbstract); - } - } } private void RemoveFromLocalCache(IPsiSourceFile sourceFile) @@ -239,8 +227,12 @@ private void RemoveFromLocalCache(IPsiSourceFile sourceFile) items?.ForEach(item => { - if (DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) - DefTags.Remove($"{item.DefType}/{item.DefName}"); + var defId = $"{item.DefType}/{item.DefName}"; + + if (DefTags.TryGetValue(defId, out var defTag) && defTag.SourceFile.Equals(sourceFile)) + DefTags.Remove(defId); + + if (item.DefType.Contains(".")) _extraDefTagNamesStale = true; }); } @@ -250,6 +242,85 @@ private void PopulateLocalCache() AddToLocalCache(sourceFile, cacheItem); } + /// + /// Maps each def whose type is a mod class (<MyMod.CustomThingDef>) to every RimWorld superclass short + /// name up to Verse.Def, so that a ThingDef reference finds it. Stays stale, so the next query tries + /// again, until RimWorld's scope is ready and every such type resolves; on a cold load neither is true when the + /// index is merged. + /// + private void RebuildExtraDefTagNames() + { + var customDefs = DefTags.Keys + .Select(defId => new DefNameValue(defId)) + .Where(defId => defId.DefType.Contains(".")) + .ToList(); + + var extraDefTagNames = new Dictionary(); + var allResolved = true; + + if (customDefs.Any()) + { + if (!ScopeHelper.UpdateScopes(_solution)) return; + + foreach (var def in customDefs) + { + if (ScopeHelper.GetDefSuperClassNames(def.DefType) is not { } superClassNames) + { + allResolved = false; + continue; + } + + foreach (var superClassName in superClassNames) + extraDefTagNames[$"{superClassName}/{def.DefName}"] = def.TagId; + } + } + + _extraDefTagNames = extraDefTagNames; + _extraDefTagNamesStale = !allResolved; + } + + [CanBeNull] + private ITreeNode FindDefNode(DefTag defTag) + { + var sourceFile = defTag.SourceFile; + if (!sourceFile.IsValid()) return null; + + // Queries normally run on committed documents, but don't turn a stray one into the assertion this design avoids + if (!sourceFile.GetPsiServices().Files.IsCommitted(sourceFile)) return null; + if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return null; + + if (_defNodesByFile.TryGetValue(xmlFile, out var nodes) && + nodes.TryGetValue(defTag.DocumentOffset, out var cachedNode) && + cachedNode.IsValid() && + cachedNode.GetTreeStartOffset().Offset == defTag.DocumentOffset) + return cachedNode; + + // Not looked at yet, or reparsed since (an incremental reparse can keep the same IFile) + nodes = FindDefNodes(xmlFile); + _defNodesByFile.AddOrUpdate(xmlFile, nodes); + + return nodes.TryGetValue(defTag.DocumentOffset, out var node) ? node : null; + } + + // The same nodes Build records offsets for: the text inside , and the value of Name="" + private static Dictionary FindDefNodes(IXmlFile xmlFile) + { + var nodes = new Dictionary(); + + foreach (var tag in xmlFile.GetNestedTags("Defs/*")) + { + if (tag.GetNestedTags("defName").FirstOrDefault()?.Children().ElementAtOrDefault(1) is + { } defNameValue) + nodes[defNameValue.GetTreeStartOffset().Offset] = defNameValue; + + if (tag.GetAttribute("Name")?.Children().FirstOrDefault(element => element is IXmlValueToken) is + { } nameValue) + nodes[nameValue.GetTreeStartOffset().Offset] = nameValue; + } + + return nodes; + } + public void AddDeclaredElement(ISolution solution, ITreeNode owner, string defType, string defName, bool caseSensitiveName) { @@ -279,4 +350,4 @@ public ISymbolTable GetSymbolTable(ISolution solution) return _symbolTable; } -} \ No newline at end of file +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs index fd58ba0..869573d 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs @@ -9,35 +9,39 @@ public class RimworldXmlDefSymbol { public static readonly IUnsafeMarshaller> Marshaller = UnsafeMarshallers.GetCollectionMarshaller(new UniversalMarshaller(Read, Write), (size) => new List()); - + public string DefName { get; } public string DefType { get; } public int DocumentOffset { get; } - - // public IXmlTag Tag { get; } - public RimworldXmlDefSymbol(ITreeNode tag, string defName, string defType) + public bool IsAbstract { get; } + + public RimworldXmlDefSymbol(ITreeNode tag, string defName, string defType, bool isAbstract) { DefName = defName; DefType = defType; DocumentOffset = tag.GetTreeStartOffset().Offset; + IsAbstract = isAbstract; } - - public RimworldXmlDefSymbol(int documentOffset, string defName, string defType) + + public RimworldXmlDefSymbol(int documentOffset, string defName, string defType, bool isAbstract) { DefName = defName; DefType = defType; DocumentOffset = documentOffset; + IsAbstract = isAbstract; } - + + // Changing what's read/written here means bumping RimworldSymbolScope.PersistentVersion, or old caches get misread private static RimworldXmlDefSymbol Read(UnsafeReader reader) { var defType = reader.ReadString(); var defName = reader.ReadString(); var documentOffset = reader.ReadInt(); - - return new RimworldXmlDefSymbol(documentOffset, defName, defType); + var isAbstract = reader.ReadBool(); + + return new RimworldXmlDefSymbol(documentOffset, defName, defType, isAbstract); } private static void Write(UnsafeWriter writer, RimworldXmlDefSymbol value) @@ -45,5 +49,6 @@ private static void Write(UnsafeWriter writer, RimworldXmlDefSymbol value) writer.Write(value.DefType); writer.Write(value.DefName); writer.Write(value.DocumentOffset); + writer.Write(value.IsAbstract); } -} \ No newline at end of file +} From 3a7ae8ca633c560ece27e3fed23b28728f7b7e4c Mon Sep 17 00:00:00 2001 From: Gareth Date: Sun, 20 Sep 2026 13:17:15 +0100 Subject: [PATCH 07/15] Adding a base set of tests, completion tests are verified and expanded on --- docs/testing-plan.md | 14 +- .../AcceptCompletion/RimworldXmlTests.cs | 21 + .../RimworldCSharpCompletionTests.cs | 7 +- .../RimworldCompletionTestBase.cs | 2 +- .../RimworldXmlCompletionTests.cs | 41 +- .../FindUsages/RimworldFindUsagesTests.cs | 2 +- .../RimworldXmlHighlightingTests.cs | 2 +- .../ReSharperPlugin.RimworldDev.Tests.csproj | 4 + .../References/RimworldNavigationTests.cs | 2 +- .../References/RimworldReferenceTests.cs | 2 +- .../CSharpCompletion.cs} | 6 +- .../ShellStartsTest.cs} | 4 +- .../XmlParsing.cs} | 6 +- .../SymbolScope/RimworldSymbolScopeTests.cs | 2 +- .../Rimworld}/TestCompleteEnumValue.xml | 0 .../Rimworld}/TestCompleteEnumValue.xml.gold | 0 .../Rimworld}/TestCompleteTag.xml | 0 .../Rimworld}/TestCompleteTag.xml.gold | 0 .../TestListItemWithClassProperties.xml.gold | 34 -- .../TestModDefClassProperties.xml.gold | 512 ------------------ .../Rimworld/TestThingDefProperties.xml.gold | 510 ----------------- .../Rimworld/ModTypes.cs | 0 .../Rimworld/OtherDefs.xml | 0 .../Rimworld/StuffCategoryDefs.xml | 22 + .../Rimworld/TestBooleanPropertyValue.xml | 5 + .../TestBooleanPropertyValue.xml.gold | 16 + .../Rimworld/TestDefName.xml | 3 + .../Rimworld/TestDefName.xml.gold | 16 + .../Rimworld/TestDefReferenceOtherFile.xml | 0 .../TestDefReferenceOtherFile.xml.gold | 0 .../Rimworld/TestDefReferenceSameFile.xml | 0 .../TestDefReferenceSameFile.xml.gold | 0 .../Rimworld/TestDefsFilterByType.xml | 7 + .../Rimworld/TestDefsFilterByType.xml.gold | 16 + .../Rimworld/TestEnumValue.xml | 0 .../Rimworld/TestEnumValue.xml.gold | 0 .../Rimworld/TestListItemProperties.xml | 0 .../Rimworld/TestListItemProperties.xml.gold | 0 .../TestListItemWithClassProperties.xml | 2 +- .../TestListItemWithClassProperties.xml.gold | 16 + .../TestModDefAsSuperclassReference.xml | 0 .../TestModDefAsSuperclassReference.xml.gold | 0 .../Rimworld/TestModDefClassProperties.xml | 2 +- .../TestModDefClassProperties.xml.gold | 21 + .../TestModListItemClassProperties.xml | 0 .../TestModListItemClassProperties.xml.gold | 0 .../Rimworld/TestNestedFieldProperties.xml | 0 .../TestNestedFieldProperties.xml.gold | 0 .../Rimworld/TestParentName.xml | 0 .../Rimworld/TestParentName.xml.gold | 0 .../Rimworld/TestStructPropertyValue.xml | 5 + .../Rimworld/TestStructPropertyValue.xml.gold | 22 + .../Rimworld/TestThingDefProperties.xml | 2 +- .../Rimworld/TestThingDefProperties.xml.gold | 19 + .../RimworldCSharp/Defs.xml | 0 .../RimworldCSharp/TestDefDatabaseGetNamed.cs | 0 .../TestDefDatabaseGetNamed.cs.gold | 0 .../TestDefOfFieldWithPrefix.cs | 0 .../TestDefOfFieldWithPrefix.cs.gold | 0 .../TestDefOfFieldWithPrefixAndSemicolon.cs | 0 ...stDefOfFieldWithPrefixAndSemicolon.cs.gold | 0 .../CSharp/TestLocalVariable.cs | 0 .../CSharp/TestLocalVariable.cs.gold | 0 .../Xml/XmlIsParsed.xml | 0 .../Xml/XmlIsParsed.xml.gold | 0 65 files changed, 242 insertions(+), 1103 deletions(-) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/AcceptCompletion/RimworldXmlTests.cs rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{Completion => CompletionSuggestions}/RimworldCSharpCompletionTests.cs (92%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{Completion => CompletionSuggestions}/RimworldCompletionTestBase.cs (97%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{Completion => CompletionSuggestions}/RimworldXmlCompletionTests.cs (56%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{Completion/CSharpCompletionSmokeTests.cs => SmokeTests/CSharpCompletion.cs} (70%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{SmokeTests.cs => SmokeTests/ShellStartsTest.cs} (75%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/{Completion/XmlPsiDiagnosticsTests.cs => SmokeTests/XmlParsing.cs} (88%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion/Rimworld/Action => AcceptCompletion/Rimworld}/TestCompleteEnumValue.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion/Rimworld/Action => AcceptCompletion/Rimworld}/TestCompleteEnumValue.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion/Rimworld/Action => AcceptCompletion/Rimworld}/TestCompleteTag.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion/Rimworld/Action => AcceptCompletion/Rimworld}/TestCompleteTag.xml.gold (100%) delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestListItemWithClassProperties.xml.gold delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestModDefClassProperties.xml.gold delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Completion/Rimworld/TestThingDefProperties.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/ModTypes.cs (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/OtherDefs.xml (100%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/StuffCategoryDefs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml.gold create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestDefReferenceOtherFile.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestDefReferenceOtherFile.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestDefReferenceSameFile.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestDefReferenceSameFile.xml.gold (100%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestEnumValue.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestEnumValue.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestListItemProperties.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestListItemProperties.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestListItemWithClassProperties.xml (87%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestModDefAsSuperclassReference.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestModDefAsSuperclassReference.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestModDefClassProperties.xml (88%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestModListItemClassProperties.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestModListItemClassProperties.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestNestedFieldProperties.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestNestedFieldProperties.xml.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestParentName.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestParentName.xml.gold (100%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/Rimworld/TestThingDefProperties.xml (84%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/Defs.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefDatabaseGetNamed.cs (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefOfFieldWithPrefix.cs (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => CompletionSuggestions}/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => SmokeTests}/CSharp/TestLocalVariable.cs (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => SmokeTests}/CSharp/TestLocalVariable.cs.gold (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => SmokeTests}/Xml/XmlIsParsed.xml (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{Completion => SmokeTests}/Xml/XmlIsParsed.xml.gold (100%) diff --git a/docs/testing-plan.md b/docs/testing-plan.md index 2b754e7..5fa0432 100644 --- a/docs/testing-plan.md +++ b/docs/testing-plan.md @@ -117,12 +117,20 @@ affect *any* future test that edits an XML document (Action completion, quick-fi index design: it resolves persisted offsets back to live `ITreeNode`s at merge time. Fix candidates: resolve lazily at query time, or defer resolution until after commit. +**Fixed (2026-09-18):** the index now stores `(sourceFile, offset, isAbstract)` and `GetTagByDef` finds the node on +query (`isAbstract` is computed in `Build` and persisted). Both Action tests pass unchanged against their golds. +`SymbolScope/RimworldSymbolScopeTests` edits a def file and checks the node handed back is the live one at the def's new +offset, including a def moving onto another def's old offset (checked by removing the cached-node check: it fails). + **Step 9c ⚠️ — plugin bug (load order).** A `MyMod.CustomThingDef` def is not offered where a `ThingDef` is expected. Traced: when the index merges on load, `ScopeHelper.RimworldScope` is still `null` (symbol caches not ready), so `AddDefTagToList` skips building `ExtraDefTagNames`, and nothing rebuilds it later. Likely also real on a cold open in Rider (custom def subclasses unresolved until their file is edited) — not verified in Rider. Test has a hand-written expected gold and is ignored. +**Fixed (2026-09-18)** with step 5: `ExtraDefTagNames` is rebuilt on the first query after a custom-typed def changes, +and stays stale until the scopes are ready and every custom def type resolves. The hand-written gold now passes. + **Step 10 ⚠️ — narrow `IsAvailable`.** `CSharpDefsOfItemProvider` requires the caret's parent to be an `IFieldDeclaration`. With `public static ThingDef Mod{caret}` followed by `}` (i.e. typing a new field at the end of the class, the natural moment to complete), C# error recovery parses a **`MethodDeclaration`**; with no name typed the @@ -215,8 +223,8 @@ host once its abstract members were supplied — no new harness fixes were neede | # | Problem | Where | Blocks | |---|---|---|---| -| 1 | Index reads PSI mid-commit ("uncommitted document" logged) | `RimworldSymbolScope.AddToLocalCache` via `Merge` | any test that edits an XML document (Action completion, future quick-fix/generator tests) | -| 2 | Custom def subclasses not indexed under their base type on cold load | `ExtraDefTagNames` built only if `ScopeHelper.RimworldScope` is set at merge | step 9c | +| 1 | ✅ Fixed — Index reads PSI mid-commit ("uncommitted document" logged) | `RimworldSymbolScope.AddToLocalCache` via `Merge` | any test that edits an XML document (Action completion, future quick-fix/generator tests) | +| 2 | ✅ Fixed — Custom def subclasses not indexed under their base type on cold load | `ExtraDefTagNames` built only if `ScopeHelper.RimworldScope` is set at merge | step 9c | | 3 | `[DefOf]` completion needs a following `;` | `CSharpDefsOfItemProvider.IsAvailable` (unfinished declaration parses as a method) | step 10 variant | | 4 | Find Usages ignores C# | `RimworldSearcherFactory.IsCompatibleWithLanguage` | step 18's C# usages | | 5 | `GetScopeForClass` searches `knownCustomScopes` twice (should be `allScopes`) | `ScopeHelper` | found by reading; needs a two-project test | @@ -224,6 +232,6 @@ host once its abstract members were supplied — no new harness fixes were neede | 7 | Daemon stage relies on another stage having set the scope | `CustomXmlAnalysisStageProcess` | nothing yet; fragile | **Not explored:** Phase G (disk discovery via `RimworldPath`/`AddRef`, the generator) and Phase H (Rider-only code, -Remodder, Kotlin). #1 must be fixed before the generator (step 20) can be tested. Also still open from the proof of +Remodder, Kotlin). #1 (now fixed) was the blocker for testing the generator (step 20). Also still open from the proof of concept: moving CI to Windows and the `.gitattributes` LF rule for golds. diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/AcceptCompletion/RimworldXmlTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/AcceptCompletion/RimworldXmlTests.cs new file mode 100644 index 0000000..6c0e466 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/AcceptCompletion/RimworldXmlTests.cs @@ -0,0 +1,21 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; + +namespace ReSharperPlugin.RimworldDev.Tests.AcceptCompletion; + +/// +/// Gold is the document after accepting the item named by the input's ${COMPLETE_ITEM:…} directive. +/// Accepting an item commits the edited document, which runs RimworldSymbolScope.Merge mid-commit; these guard that the +/// index doesn't read PSI there (docs/testing-plan.md step 5). +/// +[TestFileExtension(".xml")] +public class RimworldXmlTests : RimworldCompletionTestBase +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.Action; + protected override string RelativeTestDataPath => @"AcceptCompletion\Rimworld"; + + [Test] public void TestCompleteTag() => DoNamedTest(); + [Test] public void TestCompleteEnumValue() => DoNamedTest(); +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs similarity index 92% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs index a75ee63..02de60c 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCSharpCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs @@ -2,7 +2,7 @@ using JetBrains.ReSharper.TestFramework; using NUnit.Framework; -namespace ReSharperPlugin.RimworldDev.Tests.Completion; +namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; /// /// Def names offered in C#, from the same def index the XML side uses. Each test brings Defs.xml along so the index has @@ -12,7 +12,7 @@ namespace ReSharperPlugin.RimworldDev.Tests.Completion; public class RimworldCSharpCompletionTests : RimworldCompletionTestBase { protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; - protected override string RelativeTestDataPath => @"Completion\RimworldCSharp"; + protected override string RelativeTestDataPath => @"CompletionSuggestions\RimworldCSharp"; [Test] public void TestDefOfFieldWithPrefixAndSemicolon() => DoNamedTest("Defs.xml"); @@ -21,7 +21,6 @@ public class RimworldCSharpCompletionTests : RimworldCompletionTestBase // FieldDeclaration, so only C#'s own name suggestions appear. [Test, Ignore("CSharpDefsOfItemProvider needs a FieldDeclaration; unfinished declarations parse as methods; see docs/testing-plan.md step 10")] public void TestDefOfFieldWithPrefix() => DoNamedTest("Defs.xml"); - - + [Test] public void TestDefDatabaseGetNamed() => DoNamedTest("Defs.xml"); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCompletionTestBase.cs similarity index 97% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCompletionTestBase.cs index cff8bc0..e1afa57 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldCompletionTestBase.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCompletionTestBase.cs @@ -7,7 +7,7 @@ using JetBrains.Util.Dotnet.TargetFrameworkIds; using NUnit.Framework; -namespace ReSharperPlugin.RimworldDev.Tests.Completion; +namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; /// /// Completion tests backed by the game's types. Krafs.Rimworld.Ref (a complete reference assembly for RimWorld) is diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs similarity index 56% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs index 5e8ad45..c09aef8 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Completion/RimworldXmlCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs @@ -2,7 +2,7 @@ using JetBrains.ReSharper.TestFramework; using NUnit.Framework; -namespace ReSharperPlugin.RimworldDev.Tests.Completion; +namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; /// /// The real thing: RimWorld XML completion backed by the game's types. Gold is the lookup list at {caret}. @@ -11,36 +11,31 @@ namespace ReSharperPlugin.RimworldDev.Tests.Completion; public class RimworldXmlCompletionTests : RimworldCompletionTestBase { protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; - protected override string RelativeTestDataPath => @"Completion\Rimworld"; + protected override string RelativeTestDataPath => @"CompletionSuggestions\Rimworld"; + // Test Property Names + [Test] public void TestDefName() => DoNamedTest(); [Test] public void TestThingDefProperties() => DoNamedTest(); + [Test] public void TestModDefClassProperties() => DoNamedTest("ModTypes.cs"); [Test] public void TestNestedFieldProperties() => DoNamedTest(); [Test] public void TestListItemProperties() => DoNamedTest(); [Test] public void TestListItemWithClassProperties() => DoNamedTest(); + [Test] public void TestModListItemClassProperties() => DoNamedTest("ModTypes.cs"); + + // Test Property Values + [Test] public void TestBooleanPropertyValue() => DoNamedTest(); [Test] public void TestEnumValue() => DoNamedTest(); - // Phase B: the def index (RimworldSymbolScope) + [Test] public void TestStructPropertyValue() => DoNamedTest(); + + // Test Def references [Test] public void TestDefReferenceSameFile() => DoNamedTest(); [Test] public void TestDefReferenceOtherFile() => DoNamedTest("OtherDefs.xml"); + + [Test] + public void TestDefsFilterByType() => DoNamedTest("OtherDefs.xml", "StuffCategoryDefs.xml"); [Test] public void TestParentName() => DoNamedTest(); - [Test] public void TestModDefClassProperties() => DoNamedTest("ModTypes.cs"); - [Test] public void TestModListItemClassProperties() => DoNamedTest("ModTypes.cs"); - // A def of a mod's ThingDef subclass is offered where a ThingDef is expected, even though the def index merges - // before the scopes are ready on load (docs/testing-plan.md step 9). + + // When a property expects a specific DefType (like ThingDef), modded classes that extend that should be offered [Test] public void TestModDefAsSuperclassReference() => DoNamedTest("ModTypes.cs"); -} - -/// -/// Gold is the document after accepting the item named by the input's ${COMPLETE_ITEM:…} directive. -/// Accepting an item commits the edited document, which runs RimworldSymbolScope.Merge mid-commit; these guard that the -/// index doesn't read PSI there (docs/testing-plan.md step 5). -/// -[TestFileExtension(".xml")] -public class RimworldXmlCompletionActionTests : RimworldCompletionTestBase -{ - protected override CodeCompletionTestType TestType => CodeCompletionTestType.Action; - protected override string RelativeTestDataPath => @"Completion\Rimworld\Action"; - - [Test] public void TestCompleteTag() => DoNamedTest(); - [Test] public void TestCompleteEnumValue() => DoNamedTest(); -} +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs index 0e00679..431e08f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs @@ -4,7 +4,7 @@ using JetBrains.ReSharper.TestFramework; using JetBrains.Util.Dotnet.TargetFrameworkIds; using NUnit.Framework; -using ReSharperPlugin.RimworldDev.Tests.Completion; +using ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; namespace ReSharperPlugin.RimworldDev.Tests.FindUsages; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs index cdaa2f2..d966f67 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs @@ -6,7 +6,7 @@ using JetBrains.ReSharper.TestFramework; using JetBrains.Util.Dotnet.TargetFrameworkIds; using NUnit.Framework; -using ReSharperPlugin.RimworldDev.Tests.Completion; +using ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; namespace ReSharperPlugin.RimworldDev.Tests.Highlighting; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index adad02f..a01143c 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -69,6 +69,10 @@ CopyToOutputDirectory="PreserveNewest" Link="xunit.runner.utility.net452.dll" Visible="false" /> + + + + + + + TestThing + +
  • + {caret} +
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateInListItem.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateInListItem.xml.gold new file mode 100644 index 0000000..1051304 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateInListItem.xml.gold @@ -0,0 +1,17 @@ +Provided elements: + 0: stat:RimWorld.StatDef + 1: value:System.Single + + + + + + TestThing + +
  • + + {caret} +
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml new file mode 100644 index 0000000..8435b40 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml @@ -0,0 +1,11 @@ + + + + + TestThing + + Things/Test + {caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml.gold new file mode 100644 index 0000000..e9377fa --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateProperties.xml.gold @@ -0,0 +1,47 @@ +Provided elements: + 0: drawSize:UnityEngine.Vector2 + 1: graphicClass:System.Type + 2: shadowData:Verse.ShadowData + 3: shaderType:Verse.ShaderTypeDef + 4: color:UnityEngine.Color + 5: damageData:Verse.DamageGraphicData + 6: shaderParameters:System.Collections.Generic.List`1 + 7: maskPath:System.String + 8: name:System.String + 9: colorTwo:UnityEngine.Color + 10: drawOffset:UnityEngine.Vector3 + 11: drawOffsetNorth:System.Nullable`1 + 12: drawOffsetEast:System.Nullable`1 + 13: drawOffsetSouth:System.Nullable`1 + 14: drawOffsetWest:System.Nullable`1 + 15: onGroundRandomRotateAngle:System.Single + 16: drawRotated:System.Boolean + 17: allowFlip:System.Boolean + 18: flipExtraRotation:System.Single + 19: renderInstanced:System.Boolean + 20: allowAtlasing:System.Boolean + 21: renderQueue:System.Int32 + 22: overlayOpacity:System.Single + 23: attachments:System.Collections.Generic.List`1 + 24: attachPoints:System.Collections.Generic.List`1 + 25: addTopAltitudeBias:System.Boolean + 26: ignoreThingDrawColor:System.Boolean + 27: maxSnS:UnityEngine.Vector2 + 28: offsetSnS:UnityEngine.Vector2 + 29: linkType:Verse.LinkDrawerType + 30: linkFlags:Verse.LinkFlags + 31: asymmetricLink:Verse.AsymmetricLinkData + 32: cornerOverlayPath:System.String + + + + + + TestThing + + Things/Test + + {caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/02a274601026edaa3d1d80ed70c45d04374a05b3a1fdf3241e8aa0a7fc1cb635.lock b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/02a274601026edaa3d1d80ed70c45d04374a05b3a1fdf3241e8aa0a7fc1cb635.lock new file mode 100644 index 0000000..087bc50 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/NuGetLocks/02a274601026edaa3d1d80ed70c45d04374a05b3a1fdf3241e8aa0a7fc1cb635.lock @@ -0,0 +1,4 @@ +# Please commit this file, it's crucial for tests stability. Even if you believe it's not yours, it still needs to be committed. +# Input (NuGetFramework=net35): +# JetBrains.Annotations [2025.2.0] +JetBrains.Annotations 2025.2.0 \ No newline at end of file From 8bb6a29f08f3b7db537fb611b4c9e9cc3ff69098 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 21 Sep 2026 22:45:30 +0100 Subject: [PATCH 11/15] Adjust the comments on various test and validate/expand the navigation tests --- .../RimworldCSharpCompletionTests.cs | 10 ++- .../RimworldXmlCompletionTests.cs | 3 - .../Generate/RimworldGenerateTests.cs | 4 +- .../RimworldXmlHighlightingTests.cs | 6 +- .../RimworldNavigationAfterEdits.cs | 59 +++++++++++++ .../Navigation/RimworldNavigationTests.cs | 32 ++++++++ .../References/RimworldNavigationTests.cs | 20 ----- .../SmokeTests/CSharpCompletion.cs | 7 +- .../SmokeTests/ShellStartsTest.cs | 6 +- .../SmokeTests/XmlParsing.cs | 7 +- .../SymbolScope/RimworldSymbolScopeTests.cs | 82 ------------------- .../MovingDefs.xml} | 3 +- .../Navigation/TestNavigateAfterDefsMove.xml | 7 ++ .../TestNavigateAfterDefsMove.xml.gold | 74 +++++++++++++++++ .../TestNavigatePropertyToCSharpField.xml} | 0 ...estNavigatePropertyToCSharpField.xml.gold} | 0 .../TestNavigateValueToCSharpEnum.xml | 5 ++ .../TestNavigateValueToCSharpEnum.xml.gold | 54 ++++++++++++ .../TestNavigateValueToXmlDef.xml} | 0 .../TestNavigateValueToXmlDef.xml.gold} | 26 +++--- 20 files changed, 271 insertions(+), 134 deletions(-) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{SymbolScope/TestDefNodesFollowEdits.xml => Navigation/MovingDefs.xml} (70%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{References/Navigation/TestNavigateToCSharpField.xml => Navigation/TestNavigatePropertyToCSharpField.xml} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{References/Navigation/TestNavigateToCSharpField.xml.gold => Navigation/TestNavigatePropertyToCSharpField.xml.gold} (100%) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml.gold rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{References/Navigation/TestNavigateToXmlDef.xml => Navigation/TestNavigateValueToXmlDef.xml} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/{References/Navigation/TestNavigateToXmlDef.xml.gold => Navigation/TestNavigateValueToXmlDef.xml.gold} (74%) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs index 62a536e..4133d09 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs @@ -14,10 +14,12 @@ public class RimworldCSharpCompletionTests(ProjectLayout layout) : RimworldCompl [Test] public void TestDefOfFieldWithPrefixAndSemicolon() => DoNamedTest("Defs.xml"); - // Gold is hand-written: what the plugin *should* offer. Without a following ';' the C# parser recovers the unfinished - // `public static ThingDef Mod` as a MethodDeclaration, and CSharpDefsOfItemProvider.IsAvailable wants a - // FieldDeclaration, so only C#'s own name suggestions appear. - [Test, Ignore("CSharpDefsOfItemProvider needs a FieldDeclaration; unfinished declarations parse as methods; see docs/testing-plan.md step 10")] + /// + /// Because of how we've set up the ItemLookup, if we do `public static ThingDef Mod{caret};`, it'll suggest our + /// correct ThingDefs, however if we leave the semi-colon off and do `public static ThingDef Mod{caret}` then it + /// won't. + /// + [Test, Ignore("CSharpDefsOfItemProvider needs to be adjusted first")] public void TestDefOfFieldWithPrefix() => DoNamedTest("Defs.xml"); [Test] public void TestDefDatabaseGetNamed() => DoNamedTest("Defs.xml"); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs index b8bbe74..c064415 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs @@ -5,9 +5,6 @@ namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; -/// -/// The real thing: RimWorld XML completion backed by the game's types. Gold is the lookup list at {caret}. -/// [ProjectLayouts(ProjectLayout.CSharpProject)] [TestFileExtension(".xml")] public class RimworldXmlCompletionTests(ProjectLayout layout) : RimworldCompletionTestBase(layout) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs index 85aa3ff..2e8ff20 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs @@ -5,8 +5,7 @@ namespace ReSharperPlugin.RimworldDev.Tests.Generate; /// -/// Alt+Insert on a def. Gold is the properties the menu offers, in the order it offers them, followed by the document -/// after generating the ones the input selects. +/// This tests the `Alt+Insert` Generation menu inside a Def. /// [ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] [TestFileExtension(".xml")] @@ -14,7 +13,6 @@ public class RimworldGenerateTests(ProjectLayout layout) : RimworldGenerateTestB { protected override string RelativeTestDataPath => @"Generate"; - // The dump spells out generic type arguments only where they resolve, which is not in a mod's own project [ProjectLayouts(ProjectLayout.XmlProject)] [Test] public void TestGenerateProperties() => DoNamedTest(); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs index 6e75475..c1facd9 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs @@ -12,8 +12,10 @@ public class RimworldXmlHighlightingTests(ProjectLayout layout) : RimworldHighli [Test] public void TestValidValues() => DoNamedTest(); - // Float values are only reported when loaded up XmlProjects, they don't actually get matched properly in CSharp - // projects + /// + /// Float values are only reported when loaded up XmlProjects, they don't actually get matched properly in CSharp + /// projects + /// [ProjectLayouts(ProjectLayout.XmlProject)] [Test] public void TestInvalidValues() => DoNamedTest(); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs new file mode 100644 index 0000000..4a84318 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs @@ -0,0 +1,59 @@ +using System.Linq; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.Navigation; + +/// +/// We previously had an issue around keeping stale references to ITreeNodes in our Symbol Cache, which meant that if +/// Rider attempted to access it from our Symbol Cache (for navigation for example) after the file had been edited but +/// before we'd rebuilt our cache then we'd get an error about trying to read an uncommited PSI Document. The fix for +/// that was to store data that allows us to look up the real ITreeNode, and then do that lookup on demand rather than +/// storing the ITreeNode itself. +/// +/// These tests exist to act as a regression test against that behavior coming back. +/// +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldNavigationAfterEditTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) +{ + private const string DefsFile = "MovingDefs.xml"; + private const string ThingALine = " ThingA\n"; + + protected override string ExtraPath => ""; + protected override string RelativeTestDataPath => "Navigation"; + + [Test] public void TestNavigateAfterDefsMove() => DoNamedTest(DefsFile); + + protected override void DoTest(Lifetime lifetime, IProject testProject) + { + var files = Solution.GetPsiServices().Files; + var document = testProject.GetAllProjectFiles().Single(file => file.Name == DefsFile).ToSourceFiles().Single() + .Document; + + files.CommitAllDocuments(); + using (ReadLockCookie.Create()) + { + var psiFile = testProject.GetAllProjectFiles().Single(file => file.Name != DefsFile).ToSourceFiles() + .Single().GetPrimaryPsiFile()!; + + foreach (var node in psiFile.Descendants().ToEnumerable()) + foreach (var reference in node.GetReferences()) + reference.Resolve(); + } + + using (WriteLockCookie.Create()) + document.InsertText(document.GetText().IndexOf(" -->"), ThingALine); + files.CommitAllDocuments(); + + base.DoTest(lifetime, testProject); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs new file mode 100644 index 0000000..b83d3f2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs @@ -0,0 +1,32 @@ +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; +using System.Linq; + +namespace ReSharperPlugin.RimworldDev.Tests.Navigation; + +/// +/// These tests essentially emulate us doing a Ctrl+Click in the IDE. The gold files should show what options were +/// presented to the IDE (not the user) from what sources. The distinction is that the same reference/result may be +/// presented to the IDE form multiple sources and that'll be deduplicated to a single option for the IDE. +/// +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldNavigationTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) +{ + protected override string ExtraPath => ""; + protected override string RelativeTestDataPath => "Navigation"; + + [Test] public void TestNavigatePropertyToCSharpField() => DoNamedTest(); + [Test] public void TestNavigateValueToXmlDef() => DoNamedTest(); + [Test] public void TestNavigateValueToCSharpEnum() => DoNamedTest(); +} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs deleted file mode 100644 index 6a40788..0000000 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldNavigationTests.cs +++ /dev/null @@ -1,20 +0,0 @@ -using JetBrains.ReSharper.TestFramework; -using NUnit.Framework; -using ReSharperPlugin.RimworldDev.Tests.TestBases; - -namespace ReSharperPlugin.RimworldDev.Tests.References; - -/// -/// Ctrl+Click the way the IDE does it: every context navigation provider (Go to Declaration, Find Usages, …) is run at -/// {caret} and the targets it would offer are dumped. -/// -[ProjectLayouts(ProjectLayout.CSharpProject)] -[TestFileExtension(".xml")] -public class RimworldNavigationTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) -{ - protected override string ExtraPath => ""; - protected override string RelativeTestDataPath => @"References\Navigation"; - - [Test] public void TestNavigateToCSharpField() => DoNamedTest(); - [Test] public void TestNavigateToXmlDef() => DoNamedTest(); -} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs index 454b2a4..b4ee495 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs @@ -4,8 +4,11 @@ namespace ReSharperPlugin.RimworldDev.Tests.SmokeTests; /// -/// Proves that CodeCompletionTestBase + gold files work in this harness at all, with nothing RimWorld-specific -/// involved. If this is red, the completion pipeline itself is broken, not our provider. +/// The smoke tests are here to prove that tests, in general, are functioning. This is in case we do an upgrade and find +/// our automated tests are no longer functioning. It allows us to narrow it down to whether tests are broken, parsing +/// is broken, completion is broken or just our specific tests are broken. +/// +/// This test is specific to completion and just tests if we can assert on autocompleting a C# variable. /// public class CSharpCompletion : CodeCompletionTestBase { diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs index 28d0f41..0c40d0e 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs @@ -4,7 +4,11 @@ namespace ReSharperPlugin.RimworldDev.Tests.SmokeTests; /// -/// Proves the ReSharper test shell boots at all. If this is red, nothing else in the project can be green. +/// The smoke tests are here to prove that tests, in general, are functioning. This is in case we do an upgrade and find +/// our automated tests are no longer functioning. It allows us to narrow it down to whether tests are broken, parsing +/// is broken, completion is broken or just our specific tests are broken. +/// +/// This test is just checking that we can start tests in the first place. /// public class ShellStartsTest : BaseTest { diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs index 23d7584..d737160 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs @@ -12,8 +12,11 @@ namespace ReSharperPlugin.RimworldDev.Tests.SmokeTests; /// -/// Dumps how the test shell sees an .xml file in the in-memory project. Exists to answer "is XML even parsed as -/// XML here?" when XML completion returns nothing. +/// The smoke tests are here to prove that tests, in general, are functioning. This is in case we do an upgrade and find +/// our automated tests are no longer functioning. It allows us to narrow it down to whether tests are broken, parsing +/// is broken, completion is broken or just our specific tests are broken. +/// +/// This test is about whether we can parse XML files. /// [TestFileExtension(".xml")] public class XmlParsing : BaseTestWithSingleProject diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs deleted file mode 100644 index bb8d42a..0000000 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SymbolScope/RimworldSymbolScopeTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -using JetBrains.Application.Components; -using JetBrains.Lifetimes; -using JetBrains.ProjectModel; -using JetBrains.ReSharper.Psi.Tree; -using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Resources.Shell; -using JetBrains.ReSharper.TestFramework; -using JetBrains.Util; -using NUnit.Framework; -using ReSharperPlugin.RimworldDev.SymbolScope; -using ReSharperPlugin.RimworldDev.Tests.TestBases; -using System.Linq; - -namespace ReSharperPlugin.RimworldDev.Tests.SymbolScope; - -/// -/// The def index stores offsets and finds tree nodes on demand (docs/testing-plan.md step 5). Each commit below re-runs -/// Build + Merge for the file, which used to read PSI mid-commit and log an error. After each edit, the node handed back -/// must be the one at the def's current position, not a node cached from an earlier tree. -/// -[ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] -public class RimworldSymbolScopeTests(ProjectLayout layout) : RimworldSolutionTestBase(layout) -{ - private const string FileName = "TestDefNodesFollowEdits.xml"; - private const string ThingALine = " ThingA\n"; - - protected override string RelativeTestDataPath => @"SymbolScope"; - - [Test] public void TestDefNodesFollowEdits() => DoLayoutTestSolution(FileName); - - protected override void DoTest(Lifetime lifetime, IProject project) - { - var files = Solution.GetPsiServices().Files; - var scope = Solution.GetComponent(); - var sourceFile = project.GetAllProjectFiles().Single(file => file.Name == FileName).ToSourceFiles().Single(); - var document = sourceFile.Document; - - files.CommitAllDocuments(); - AssertAllDefs(scope, sourceFile); - - // Everything moves down a line - using (WriteLockCookie.Create()) - document.InsertText(document.GetText().IndexOf(""), "\n"); - files.CommitAllDocuments(); - AssertAllDefs(scope, sourceFile); - - // ThingB's name lands exactly where ThingA's was, which the index has a node cached for - using (WriteLockCookie.Create()) - { - var start = document.GetText().IndexOf(ThingALine); - document.DeleteText(new TextRange(start, start + ThingALine.Length)); - } - files.CommitAllDocuments(); - using (ReadLockCookie.Create()) - { - Assert.That(scope.GetTagByDef("ThingDef", "ThingA"), Is.Null); - AssertDefNode(scope.GetTagByDef("ThingDef", "ThingB"), sourceFile, ">ThingB<", 1); - } - } - - private static void AssertAllDefs(RimworldSymbolScope scope, IPsiSourceFile sourceFile) - { - using (ReadLockCookie.Create()) - { - AssertDefNode(scope.GetTagByDef("ThingDef", "ThingA"), sourceFile, ">ThingA<", 1); - AssertDefNode(scope.GetTagByDef("ThingDef", "ThingB"), sourceFile, ">ThingB<", 1); - AssertDefNode(scope.GetTagByDef("ThingDef", "BaseThing"), sourceFile, "\"BaseThing\"", 0); - Assert.That(scope.IsDefAbstract("ThingDef/BaseThing"), Is.True); - Assert.That(scope.IsDefAbstract("ThingDef/ThingA"), Is.False); - } - } - - // The node must be live and sit where `marker` (plus `skip` characters) is in the document's current text - private static void AssertDefNode(ITreeNode node, IPsiSourceFile sourceFile, string marker, int skip) - { - Assert.That(node, Is.Not.Null); - Assert.That(node.IsValid(), Is.True); - Assert.That(node.GetSourceFile(), Is.EqualTo(sourceFile)); - Assert.That(node.GetDocumentRange().StartOffset.Offset, - Is.EqualTo(sourceFile.Document.GetText().IndexOf(marker) + skip)); - } -} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml similarity index 70% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml index eb75650..2a0c4d3 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SymbolScope/TestDefNodesFollowEdits.xml +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml @@ -1,7 +1,6 @@ + ThingA ThingB - - diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml new file mode 100644 index 0000000..ab517df --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml @@ -0,0 +1,7 @@ + + + + Referrer + Thing{on}A + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml.gold new file mode 100644 index 0000000..f0cc060 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateAfterDefsMove.xml.gold @@ -0,0 +1,74 @@ +## FindReferencedCodeProvider activity: + Tooltip was shown: Referenced code in 'XmlTag ThingDef/ThingA' were not found + +## FindUsagesAdvancedProvider activity: + FindResults window with 2 results + TO: [O] |ThingA| RANGE: (118,124) @ TestNavigateAfterDefsMove.xml + TO: [O] |ThingA| RANGE: (136,142) @ MovingDefs.xml + +## FindUsagesProvider activity: + FindResults window with 2 results + TO: [O] |ThingA| RANGE: (118,124) @ TestNavigateAfterDefsMove.xml + TO: [O] |ThingA| RANGE: (136,142) @ MovingDefs.xml + +## GotoDeclarationProvider activity: + Immediate result: + TO: [O] |ThingA| RANGE: (136,142) @ MovingDefs.xml + Navigation result: + opened file: MovingDefs.xml + ------------------ + --> + + |CARET|ThingA + ThingB + + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] |ThingA| RANGE: (136,142) @ MovingDefs.xml + Navigation result: + opened file: MovingDefs.xml + ------------------ + --> + + |CARET|ThingA + ThingB + + ------------------ + + +## HighlightUsagesProvider activity: + Immediate result: + TO: [O] |ThingA| RANGE: (118,124) @ TestNavigateAfterDefsMove.xml + Navigation result: + caret did not move + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/ThingA'`: + TO: [O] |ThingA| RANGE: (136,142) @ MovingDefs.xml + Menu item (enabled) : + icon: UsageOther + text: MovingDefs.xml **ThingA** (5) + tail: in + tooltip: **ThingA** + Navigation result: + opened file: MovingDefs.xml + ------------------ + --> + + |CARET|ThingA + ThingB + + ------------------ + + TO: [O] |ThingA| RANGE: (118,124) @ TestNavigateAfterDefsMove.xml + Menu item (enabled) : + icon: UsageOther + text: TestNavigateAfterDefsMove.xml **ThingA** (5) + tail: in + tooltip: **ThingA** + Navigation result: + caret did not move + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml.gold similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToCSharpField.xml.gold rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml.gold diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml new file mode 100644 index 0000000..2d9f8fe --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml @@ -0,0 +1,5 @@ + + + High{on}Priority + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml.gold new file mode 100644 index 0000000..e75477a --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToCSharpEnum.xml.gold @@ -0,0 +1,54 @@ +## FindUsagesAdvancedProvider activity: + Tooltip was shown: Usages of 'HighPriority' were not found + +## FindUsagesProvider activity: + Tooltip was shown: Usages of 'HighPriority' were not found + +## GotoDeclarationProvider activity: + Immediate result: + DEO: Envoy: EnumMember:Verse.AI.ThinkTreeDutyHook.HighPriority, PsiLanguageType:UNKNOWN as Envoy: EnumMember:Verse.AI.ThinkTreeDutyHook.HighPriority, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThinkTreeDutyHook.cs + ------------------ + { + None, + |CARET|HighPriority, + MediumPriority, + } + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + DEO: Envoy: EnumMember:Verse.AI.ThinkTreeDutyHook.HighPriority, PsiLanguageType:UNKNOWN as Envoy: EnumMember:Verse.AI.ThinkTreeDutyHook.HighPriority, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThinkTreeDutyHook.cs + ------------------ + { + None, + |CARET|HighPriority, + MediumPriority, + } + ------------------ + + +## GotoTypeDeclarationProvider activity: + Immediate result: + DEO: Envoy: Enum:Verse.AI.ThinkTreeDutyHook, PsiLanguageType:UNKNOWN as Envoy: Enum:Verse.AI.ThinkTreeDutyHook, PsiLanguageType:UNKNOWN RANGE: (0,0) @ Assembly-CSharp + Navigation result: + opened file: ThinkTreeDutyHook.cs + ------------------ + namespace Verse.AI; + + public enum |CARET|ThinkTreeDutyHook + { + None, + ------------------ + + +## HighlightUsagesProvider activity: + Tooltip was shown: Usages of 'HighPriority' were not found in this file + +## ShowUsagesProvider activity: + Tooltip was shown: Usages of 'HighPriority' were not found + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml.gold similarity index 74% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml.gold index 3fba082..25a4ac5 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/Navigation/TestNavigateToXmlDef.xml.gold +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml.gold @@ -3,19 +3,19 @@ ## FindUsagesAdvancedProvider activity: FindResults window with 2 results - TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml - TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateValueToXmlDef.xml ## FindUsagesProvider activity: FindResults window with 2 results - TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml - TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateValueToXmlDef.xml ## GotoDeclarationProvider activity: Immediate result: - TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml Navigation result: - opened file: TestNavigateToXmlDef.xml + opened file: TestNavigateValueToXmlDef.xml ------------------ @@ -27,9 +27,9 @@ ## GotoImplementationProvider activity: Immediate result: - TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml Navigation result: - opened file: TestNavigateToXmlDef.xml + opened file: TestNavigateValueToXmlDef.xml ------------------ @@ -41,14 +41,14 @@ ## ShowUsagesProvider activity: Async context menu shown `Usages of 'ThingDef/LocalThing'`: - TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml Menu item (enabled) : icon: UsageOther - text: TestNavigateToXmlDef.xml **LocalThing** (4) + text: TestNavigateValueToXmlDef.xml **LocalThing** (4) tail: in tooltip: **LocalThing** Navigation result: - opened file: TestNavigateToXmlDef.xml + opened file: TestNavigateValueToXmlDef.xml ------------------ @@ -57,10 +57,10 @@ ------------------ - TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateValueToXmlDef.xml Menu item (enabled) : icon: UsageOther - text: TestNavigateToXmlDef.xml **LocalThing** (8) + text: TestNavigateValueToXmlDef.xml **LocalThing** (8) tail: in tooltip: **LocalThing** Navigation result: From 1e0142a6ef7380da391894ac7daa882dba5e485b Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 21 Sep 2026 23:12:58 +0100 Subject: [PATCH 12/15] Extract our a ReferenceTestBase to make Reference tests more uniform --- docs/testing-plan.md | 83 +++++++++++++++++-- .../References/RimworldReferenceTests.cs | 78 +++-------------- .../TestBases/RimworldReferenceTestBase.cs | 61 ++++++++++++++ .../TestBases/RimworldSolutionTestBase.cs | 7 -- ...SharpToXmlDef.cs => TestCSharpToXmlDef.cs} | 0 ...Def.cs.gold => TestCSharpToXmlDef.cs.gold} | 0 .../{XmlToCSharp.xml => TestXmlToCSharp.xml} | 0 ...harp.xml.gold => TestXmlToCSharp.xml.gold} | 0 .../{XmlToXmlDef.xml => TestXmlToXmlDef.xml} | 0 ...lDef.xml.gold => TestXmlToXmlDef.xml.gold} | 0 10 files changed, 149 insertions(+), 80 deletions(-) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{CSharpToXmlDef.cs => TestCSharpToXmlDef.cs} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{CSharpToXmlDef.cs.gold => TestCSharpToXmlDef.cs.gold} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{XmlToCSharp.xml => TestXmlToCSharp.xml} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{XmlToCSharp.xml.gold => TestXmlToCSharp.xml.gold} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{XmlToXmlDef.xml => TestXmlToXmlDef.xml} (100%) rename src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/{XmlToXmlDef.xml.gold => TestXmlToXmlDef.xml.gold} (100%) diff --git a/docs/testing-plan.md b/docs/testing-plan.md index 9fbed9c..9501c86 100644 --- a/docs/testing-plan.md +++ b/docs/testing-plan.md @@ -74,13 +74,13 @@ Phase B's XML + index setup, caret in a `.cs` file, different providers. | # | Step | New variable | Status | |---|---|---|---| | 19 | Drop Krafs from references, `SkipAssemblyDiscovery = false`, copy Krafs' `Assembly-CSharp.dll` into a temp `RimWorldWin64_Data/Managed/`, point `RimworldPath` at it via `[TestSetting]`, rerun step 1's input | `ScopeHelper.AddRef` / `IAssemblyFactory.AddRef` / settings accessor — the real XML-only-mod mechanism. Expect teardown cookie/leak issues | — | -| 20 | Alt+Insert property generator via the SDK's generate test base | the generate workflow; `PropertyOrdering` makes gold order meaningful | — | +| 20 | Alt+Insert property generator via the SDK's generate test base | the generate workflow; `PropertyOrdering` makes gold order meaningful | ✅ | ## Phase H — beyond the current test project (boundary-finding only) | # | Step | New variable | Status | |---|---|---|---| -| 21 | Test one trivial Rider-only thing (e.g. `RimworldProjectMark` parsing an `About.xml`) | the test project references the **RESHARPER** csproj, which excludes `RimworldXmlProject/`, `Remodder/`, `TemplateParameters/`; can a test project reference the Rider csproj and still boot? | — | +| 21 | Test one trivial Rider-only thing (e.g. `RimworldProjectMark` parsing an `About.xml`) | the test project references the **RESHARPER** csproj, which excludes `RimworldXmlProject/`, `Remodder/`, `TemplateParameters/`; can a test project reference the Rider csproj and still boot? | ❌ | | 22 | Remodder `Decompiler` against a tiny Harmony-patched assembly | mostly non-PSI; cheap once 21 works | — | | 23 | (Separate track) plain JUnit for `QuickStartUtils` setup/teardown against a temp Ludeon dir | Kotlin side, no IDE; highest-stakes code (it can destroy the user's mod list) | — | @@ -121,8 +121,12 @@ query time, or defer resolution until after commit. **Fixed (2026-09-18):** the index now stores `(sourceFile, offset, isAbstract)` and `GetTagByDef` finds the node on query (`isAbstract` is computed in `Build` and persisted). Both Action tests pass unchanged against their golds. -`SymbolScope/RimworldSymbolScopeTests` edits a def file and checks the node handed back is the live one at the def's new -offset, including a def moving onto another def's old offset (checked by removing the cached-node check: it fails). +`RimworldNavigationAfterEditTests` resolves the references in an open file, grows a comment above the defs it points at +by exactly one def line, then Ctrl+Clicks `ThingA`: it has to land on `ThingA`, which now sits at `ThingB`'s old offset +while `ThingB`'s node survives the reparse. Checked against the plugin: removing the offset comparison in `FindDefNode`, +or the whole cached-node check, sends it to `ThingB`; the pre-fix index fails it with the "uncommitted document" error. +(It replaced a white-box `RimworldSymbolScopeTests`, which deleted a def instead; the reparse threw those nodes away, +so it never caught a missing offset comparison.) **Step 9c ⚠️ — plugin bug (load order).** A `MyMod.CustomThingDef` def is not offered where a `ThingDef` is expected. Traced: when the index merges on load, `ScopeHelper.RimworldScope` is still `null` (symbol caches not ready), so @@ -276,10 +280,9 @@ case). | Suite | Layouts | Why | |---|---|---| | `RimworldXmlHighlightingTests` | both | `TestInvalidValues` narrowed to the XML layout (boundary 8) | -| `RimworldSymbolScopeTests` | both | | | `AcceptCompletion.RimworldXmlTests` | both | | | `RimworldXmlCompletionTests` | C# only | keyword-less type column and empty def-name lists in the XML layout | -| `RimworldNavigationTests` | C# only | nothing to navigate to in the XML layout; not investigated | +| `RimworldNavigationTests`, `RimworldNavigationAfterEditTests` | C# only | nothing to navigate to in the XML layout; not investigated | | `RimworldFindUsagesFromXmlTests` | C# only | no usages found in the XML layout; not investigated | | `RimworldReferenceTests` | C# only | its one `.cs`-driven test lands in the referenceless project | | `RimworldCSharpCompletionTests`, `RimworldFindUsagesFromCSharpTests`, `…WithoutRimworldTests` | C# only | the layout isn't what they're about | @@ -335,6 +338,74 @@ project bare. `GetProjectProperties` is virtual but doesn't know which project i from `CreateProjectDescriptor`. Once that behaves, turn the completion suite on with the attribute and gold both layouts. +### Phase G step 20 (2026-09-20) — the Generate menu tests like any other feature + +Tests: `Generate/RimworldGenerateTests.cs`, data `test/data/Generate/`, base `TestBases/RimworldGenerateTestBase.cs`. + +`GenerateTestBase` (namespace `JetBrains.ReSharper.FeaturesTestFramework.Generate`) is language-agnostic, so the XML +generator drives it unchanged. The gold is the list of properties the menu offers, in the order it offers them, +followed by the document after generating the selected ones - which pins `PropertyOrdering` (`drawSize` and +`graphicClass` come before `name`, not alphabetical order) and the filter that hides tags the def already has. + +Input directives are `${NAME:value}` in an XML comment, the same shape the accept-completion tests use: +`${KIND:RimworldPropertyGenerator}` (required - the base asserts on it), then either `${SELECTALL:true}` or numbered +`${SELECT0:…}`, `${SELECT1:…}` whose values are the `TestDescriptor` strings from the dump (`drawSize:UnityEngine +.Vector2`). The numbering matters: the reader stops at the first index it can't find. The base also decapitalises the +first test file, so the input and gold are `testGenerateProperties.xml`, not `Test…`. + +Two things the base has to supply that the IDE supplies in real life, both in `RimworldGenerateTestBase`: + +- `ScopeHelper.UpdateScopes` before the workflow is created. The generator reads `ScopeHelper.RimworldScope` directly + and offers nothing when it is null; in the IDE some other feature has filled it in by then. Same fragility as the + daemon stage (see Phase E). +- A write lock around the test. `DefPropertiesGeneratorBuilderXml.Process` calls `ModificationUtil.AddChildAfter` + without one, and the harness - unlike the IDE action - doesn't hold one, so the run logs "This operation requires a + writer lock" and fails on the logged error. + +`TestGenerateProperties` runs in the XML layout only: the descriptor dump prints generic type arguments +(`List\`1[T -> Verse.ShaderParameter]`) where they resolve, which they don't in a mod's own project. The offered +fields and their order are the same in both. `TestGenerateInListItem` runs in both. + +### Phase H step 21 (2026-09-20) — the Rider build compiles into the tests, but its solution components don't + +Swapping the test project's `ProjectReference` to `ReSharperPlugin.RimworldDev.Rider.csproj` (plus the +`InternalsVisibleTo` the ReSharper csproj carries, since the tests use `ScopeHelper`'s internals) **compiles cleanly** +- both backends already reference `JetBrains.Rider.SDK` through `Directory.Build.props`, so the Remodder and project +model packages come along without complaint. + +It does not run. 40 of 44 tests then fail with: + +``` +The component ReSharperPlugin.RimworldDev.RimworldXmlProject.RimworldProjectMarkProvider +constructor requires JetBrains.ProjectModel.ProjectsHost.ISolutionMark, which we do not have. +``` + +`RimworldProjectMarkProvider` is a `[SolutionInstanceComponent]` taking `ISolutionMark`, and a test solution is built +in memory rather than opened from a `.sln`, so the container has no solution mark to give it. The container's +dependency check runs for every test that opens a solution, which is nearly all of them. + +Gating it out doesn't work as easily as it looks: a `[ZoneMarker]` on the `RimworldXmlProject` namespace requiring +`JetBrains.Rider.Model.IRiderModelZone` changes nothing, because the test environment activates that zone too. The +plugin has no zone markers at all today, so every component it defines is fair game wherever the assembly is scanned. + +**Giving the tests a real `.sln` doesn't help.** `BaseTestWithExistingSolution` opens one from test data, but it only +renames the in-memory solution's file path - the container still has no `ISolutionMark` descriptor at all ("Could not +find the component's ISolutionMark descriptor"). The mark is made by the project *host* (`SolutionMarkFactory` in +`JetBrains.Platform.ProjectModel.Host.dll`), which this shell never starts. + +The one base that does run that pipeline, `BaseTestWithExistingSolutionLoadedByMsbuild`, wants a `global.json` under +the test project root and then fails fetching `JetBrains.MSBuildForTests` from `packages.jetbrains.team` - a JetBrains +internal test-data feed we have no access to. So that route is closed as well. + +To make the Rider build testable, one of these has to happen first, and both are plugin changes rather than test ones: + +- take `ISolution` in that constructor and look the mark up on demand, so the component can be built without one; or +- define a zone the test environment does not activate and mark the Rider-only namespaces with it, which means giving + the plugin a zone graph it currently doesn't have. + +Until then the tests stay on the ReSharper build, and `RimworldXmlProject/`, `Remodder/` and `TemplateParameters/` are +out of reach. Everything reverted; the suite is back to 41 passed, 3 skipped. + ### Phase F (2026-09-18) — Find Usages runs; C# usages are missed, cause found Tests: `FindUsages/RimworldFindUsagesTests.cs`, data `test/data/FindUsages/`. Same `AllNavigationProvidersTestBase` as diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs index feb688c..4f67cb9 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs @@ -1,80 +1,24 @@ -using JetBrains.Application.Components; -using JetBrains.Lifetimes; -using JetBrains.ProjectModel; -using JetBrains.ReSharper.Psi.Files; -using JetBrains.ReSharper.Psi.Resolve; -using JetBrains.ReSharper.Psi.Tree; -using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Resources.Shell; using JetBrains.ReSharper.TestFramework; using NUnit.Framework; using ReSharperPlugin.RimworldDev.Tests.TestBases; -using System.Collections.Generic; -using System.IO; -using System.Linq; namespace ReSharperPlugin.RimworldDev.Tests.References; -/// -/// Ctrl+Click, tested without the SDK's navigation machinery: walk every node of the test file, ask it for its -/// references (which is where our IReferenceProviderFactory implementations plug in), resolve each one and dump -/// "node → reference type → what it resolved to". The first file given to DoTestSolution is the one dumped; the rest -/// only exist to be resolved into. -/// [ProjectLayouts(ProjectLayout.CSharpProject)] -public class RimworldReferenceTests(ProjectLayout layout) : RimworldSolutionTestBase(layout) +[TestFileExtension(".xml")] +public class RimworldReferencesFromXmlTests(ProjectLayout layout) : RimworldReferenceTestBase(layout) { protected override string RelativeTestDataPath => @"References"; - [Test] public void TestXmlToCSharp() => DoTestSolution("XmlToCSharp.xml"); - [Test] public void TestXmlToXmlDef() => DoTestSolution("XmlToXmlDef.xml", "OtherDefs.xml"); - [Test] public void TestCSharpToXmlDef() => DoTestSolution("CSharpToXmlDef.cs", "CSharpDefs.xml"); - - protected override void DoTest(Lifetime lifetime, IProject project) - { - Solution.GetPsiServices().Files.CommitAllDocuments(); - using (ReadLockCookie.Create()) - { - var dumpedFileName = TestMethodName2FileNames().First(); - var projectFile = project.GetAllProjectFiles().Single(file => file.Name == dumpedFileName); - var sourceFile = projectFile.ToSourceFiles().Single(); - var psiFile = sourceFile.GetPrimaryPsiFile()!; - var document = sourceFile.Document; - - ExecuteWithGold(projectFile, writer => - { - foreach (var node in psiFile.Descendants().ToEnumerable()) - { - // Only the plugin's references; a C# file is otherwise full of ordinary type/namespace references. - foreach (var reference in node.GetReferences() - .Where(reference => reference.GetType().Assembly == typeof(ScopeHelper).Assembly)) - { - var start = document.GetCoordsByOffset(reference.GetDocumentRange().StartOffset.Offset); - var resolved = reference.Resolve(); - writer.WriteLine( - $"({(int)start.Line + 1},{(int)start.Column + 1}) '{reference.GetDocumentRange().GetText()}' " + - $"[{reference.GetType().Name}] -> {resolved.ResolveErrorType}: {Describe(resolved.DeclaredElement)}"); - } - } - }); - } - } - - private IEnumerable TestMethodName2FileNames() => myFileSet; - - private string[] myFileSet = []; + [Test] public void TestXmlToCSharp() => DoNamedTest(); + [Test] public void TestXmlToXmlDef() => DoNamedTest("OtherDefs.xml"); +} - protected new void DoTestSolution(params string[] fileSet) - { - myFileSet = fileSet; - DoLayoutTestSolution(fileSet); - } +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".cs")] +public class RimworldReferencesFromCSharpTests(ProjectLayout layout) : RimworldReferenceTestBase(layout) +{ + protected override string RelativeTestDataPath => @"References"; - private static string Describe(IDeclaredElement element) => element switch - { - null => "", - ITypeElement type => $"type {type.GetClrName().FullName}", - ITypeMember member => $"{member.GetElementType().PresentableName} {member.ContainingType?.GetClrName().FullName}.{member.ShortName}", - _ => $"{element.GetType().Name} {element.ShortName}", - }; + [Test] public void TestCSharpToXmlDef() => DoNamedTest("CSharpDefs.xml"); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs new file mode 100644 index 0000000..96f184c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs @@ -0,0 +1,61 @@ +using System.Linq; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Resources.Shell; + +namespace ReSharperPlugin.RimworldDev.Tests.TestBases; + +/// +/// Ctrl+Click, tested without the SDK's navigation machinery: walk every node of the test file, ask it for its +/// references (which is where our IReferenceProviderFactory implementations plug in), resolve each one and dump +/// "node → reference type → what it resolved to". The file named after the test is the one dumped; the files given to +/// DoNamedTest only exist to be resolved into. +/// +public abstract class RimworldReferenceTestBase(ProjectLayout layout) : RimworldSolutionTestBase(layout) +{ + protected void DoNamedTest(params string[] otherFiles) => + ProjectLayoutSupport.BuildSolution(Layout, TestName, otherFiles, RelativeTestDataPath, + files => DoTestSolution(files), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); + + protected override void DoTest(Lifetime lifetime, IProject project) + { + Solution.GetPsiServices().Files.CommitAllDocuments(); + using (ReadLockCookie.Create()) + { + var projectFile = project.GetAllProjectFiles().Single(file => file.Name == TestName); + var sourceFile = projectFile.ToSourceFiles().Single(); + var psiFile = sourceFile.GetPrimaryPsiFile()!; + var document = sourceFile.Document; + + ExecuteWithGold(projectFile, writer => + { + foreach (var node in psiFile.Descendants().ToEnumerable()) + { + // Only the plugin's references; a C# file is otherwise full of ordinary type/namespace references. + foreach (var reference in node.GetReferences() + .Where(reference => reference.GetType().Assembly == typeof(ScopeHelper).Assembly)) + { + var start = document.GetCoordsByOffset(reference.GetDocumentRange().StartOffset.Offset); + var resolved = reference.Resolve(); + writer.WriteLine( + $"({(int)start.Line + 1},{(int)start.Column + 1}) '{reference.GetDocumentRange().GetText()}' " + + $"[{reference.GetType().Name}] -> {resolved.ResolveErrorType}: {Describe(resolved.DeclaredElement)}"); + } + } + }); + } + } + + private static string Describe(IDeclaredElement element) => element switch + { + null => "", + ITypeElement type => $"type {type.GetClrName().FullName}", + ITypeMember member => $"{member.GetElementType().PresentableName} {member.ContainingType?.GetClrName().FullName}.{member.ShortName}", + _ => $"{element.GetType().Name} {element.ShortName}", + }; +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs index 5cf1503..a115078 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.ProjectModel; using JetBrains.ProjectModel.Update; using JetBrains.Util; @@ -39,10 +38,4 @@ protected override Pair base.CreateProjectDescriptor(projectName, outputAssemblyName, absoluteFileSet, ProjectLayoutSupport.Libraries(Layout, projectName, ProjectName, libraries), projectGuid, projectLocation); - - /// For fixtures that name their own files instead of going through DoNamedTest. - protected void DoLayoutTestSolution(params string[] files) => - ProjectLayoutSupport.BuildSolution(Layout, files.First(), files.Skip(1), RelativeTestDataPath, - files => DoTestSolution(files), - (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/CSharpToXmlDef.cs.gold rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToCSharp.xml.gold rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold similarity index 100% rename from src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/XmlToXmlDef.xml.gold rename to src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold From 20a45f719b03ad521e5dc2e9f20f83aca486c048 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 21 Sep 2026 23:23:50 +0100 Subject: [PATCH 13/15] Refactor RimworldReferenceTestBase to use SDK helpers for reference tests rather than our own hand-written golds --- .../TestBases/RimworldReferenceTestBase.cs | 89 ++++++++----------- .../TestBases/RimworldSolutionTestBase.cs | 41 --------- .../References/TestCSharpToXmlDef.cs.gold | 31 ++++++- .../data/References/TestXmlToCSharp.xml.gold | 39 ++++++-- .../data/References/TestXmlToXmlDef.xml.gold | 56 ++++++++---- 5 files changed, 133 insertions(+), 123 deletions(-) delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs index 96f184c..6a5130f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs @@ -1,61 +1,48 @@ -using System.Linq; -using JetBrains.Lifetimes; +using System; +using System.Collections.Generic; using JetBrains.ProjectModel; -using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.Files; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; using JetBrains.ReSharper.Psi.Resolve; -using JetBrains.ReSharper.Psi.Tree; -using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.TestFramework; namespace ReSharperPlugin.RimworldDev.Tests.TestBases; -/// -/// Ctrl+Click, tested without the SDK's navigation machinery: walk every node of the test file, ask it for its -/// references (which is where our IReferenceProviderFactory implementations plug in), resolve each one and dump -/// "node → reference type → what it resolved to". The file named after the test is the one dumped; the files given to -/// DoNamedTest only exist to be resolved into. -/// -public abstract class RimworldReferenceTestBase(ProjectLayout layout) : RimworldSolutionTestBase(layout) +/// Reference resolution the way the SDK's own resolve tests check it, in the layout the fixture asks for. +public abstract class RimworldReferenceTestBase : ReferenceTestBase, IProjectLayoutFixture { - protected void DoNamedTest(params string[] otherFiles) => - ProjectLayoutSupport.BuildSolution(Layout, TestName, otherFiles, RelativeTestDataPath, - files => DoTestSolution(files), - (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); - - protected override void DoTest(Lifetime lifetime, IProject project) + protected RimworldReferenceTestBase(ProjectLayout layout) { - Solution.GetPsiServices().Files.CommitAllDocuments(); - using (ReadLockCookie.Create()) - { - var projectFile = project.GetAllProjectFiles().Single(file => file.Name == TestName); - var sourceFile = projectFile.ToSourceFiles().Single(); - var psiFile = sourceFile.GetPrimaryPsiFile()!; - var document = sourceFile.Document; - - ExecuteWithGold(projectFile, writer => - { - foreach (var node in psiFile.Descendants().ToEnumerable()) - { - // Only the plugin's references; a C# file is otherwise full of ordinary type/namespace references. - foreach (var reference in node.GetReferences() - .Where(reference => reference.GetType().Assembly == typeof(ScopeHelper).Assembly)) - { - var start = document.GetCoordsByOffset(reference.GetDocumentRange().StartOffset.Offset); - var resolved = reference.Resolve(); - writer.WriteLine( - $"({(int)start.Line + 1},{(int)start.Column + 1}) '{reference.GetDocumentRange().GetText()}' " + - $"[{reference.GetType().Name}] -> {resolved.ResolveErrorType}: {Describe(resolved.DeclaredElement)}"); - } - } - }); - } + Layout = layout; + (ProjectName, SecondProjectName) = ProjectLayoutSupport.ProjectNames(layout, ProjectName, SecondProjectName); } - private static string Describe(IDeclaredElement element) => element switch - { - null => "", - ITypeElement type => $"type {type.GetClrName().FullName}", - ITypeMember member => $"{member.GetElementType().PresentableName} {member.ContainingType?.GetClrName().FullName}.{member.ShortName}", - _ => $"{element.GetType().Name} {element.ShortName}", - }; + public ProjectLayout Layout { get; } + + /// Off for fixtures that check what happens with no RimWorld types around. + protected virtual bool ReferenceRimworld => true; + + protected override bool CanReuseSolution(ISolution solution) => + ProjectLayoutSupport.CanReuse(base.CanReuseSolution(solution), solution, ProjectName); + + protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => + ProjectLayoutSupport.ReferencedAssemblies(base.GetReferencedAssemblies(targetFrameworkId), ReferenceRimworld); + + protected override Pair>> + CreateProjectDescriptor(string projectName, string outputAssemblyName, + ICollection absoluteFileSet, + ICollection>> libraries, Guid projectGuid, + FileSystemPath projectLocation = null) => + base.CreateProjectDescriptor(projectName, outputAssemblyName, absoluteFileSet, + ProjectLayoutSupport.Libraries(Layout, projectName, ProjectName, libraries), projectGuid, projectLocation); + + protected override void DoNamedTest(params string[] otherFiles) => + ProjectLayoutSupport.BuildSolution(Layout, TestName, otherFiles, RelativeTestDataPath, + _ => base.DoNamedTest(otherFiles), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); + + // Only the plugin's references; a C# file is otherwise full of ordinary type/namespace references. + protected override bool AcceptReference(IReference reference) => + reference.GetType().Assembly == typeof(ScopeHelper).Assembly; } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs deleted file mode 100644 index a115078..0000000 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldSolutionTestBase.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using System.Collections.Generic; -using JetBrains.ProjectModel; -using JetBrains.ProjectModel.Update; -using JetBrains.Util; -using JetBrains.Util.Dotnet.TargetFrameworkIds; -using JetBrains.ReSharper.TestFramework; - -namespace ReSharperPlugin.RimworldDev.Tests.TestBases; - -/// -/// A solution built from test data, in the layout the fixture asks for. Tests that drive the PSI themselves rather -/// than through a feature's test base start here. -/// -public abstract class RimworldSolutionTestBase : BaseTestWithSingleProject, IProjectLayoutFixture -{ - protected RimworldSolutionTestBase(ProjectLayout layout) - { - Layout = layout; - (ProjectName, SecondProjectName) = ProjectLayoutSupport.ProjectNames(layout, ProjectName, SecondProjectName); - } - - public ProjectLayout Layout { get; } - - /// Off for fixtures that check what happens with no RimWorld types around. - protected virtual bool ReferenceRimworld => true; - - protected override bool CanReuseSolution(ISolution solution) => - ProjectLayoutSupport.CanReuse(base.CanReuseSolution(solution), solution, ProjectName); - - protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId targetFrameworkId) => - ProjectLayoutSupport.ReferencedAssemblies(base.GetReferencedAssemblies(targetFrameworkId), ReferenceRimworld); - - protected override Pair>> - CreateProjectDescriptor(string projectName, string outputAssemblyName, - ICollection absoluteFileSet, - ICollection>> libraries, Guid projectGuid, - FileSystemPath projectLocation = null) => - base.CreateProjectDescriptor(projectName, outputAssemblyName, absoluteFileSet, - ProjectLayoutSupport.Libraries(Layout, projectName, ProjectName, libraries), projectGuid, projectLocation); -} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold index 52bba14..f943c76 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold @@ -1,4 +1,27 @@ -(9,32) 'ModThingA' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/ModThingA -(11,32) 'ModSound' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/ModSound -(16,74) '"ModThingB"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/ModThingB -(19,84) '"ModSound"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/ModSound +using RimWorld; +using Verse; + +namespace MyMod +{ + [DefOf] + public static class MyThingDefOf + { + public static ThingDef |ModThingA|(0); + public static ThingDef MissingThing; + public static SoundDef |ModSound|(1); + } + + public static class Lookup + { + public static ThingDef Found() => DefDatabase.GetNamed(|"ModThingB"|(2)); + public static ThingDef Missing() => DefDatabase.GetNamed("NoSuchThing"); + public static ThingDef WrongType() => DefDatabase.GetNamed("ModSound"); + public static SoundDef Sound() => DefDatabase.GetNamedSilentFail(|"ModSound"|(3)); + } +} + +------------------------------------------------ +0: result=OK declaredElem=ThingDef/ModThingA +1: result=OK declaredElem=SoundDef/ModSound +2: result=OK declaredElem=ThingDef/ModThingB +3: result=OK declaredElem=SoundDef/ModSound diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold index a819947..10fb5d4 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold @@ -1,9 +1,30 @@ -(3,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef -(4,10) 'label' [RimworldXmlReference] -> OK: field Verse.Def.label -(5,10) 'category' [RimworldXmlReference] -> OK: field Verse.ThingDef.category -(5,19) 'Building' [RimworldXmlReference] -> OK: enum member Verse.ThingCategory.Building -(6,10) 'graphicData' [RimworldXmlReference] -> OK: field Verse.ThingDef.graphicData -(7,14) 'texPath' [RimworldXmlReference] -> OK: field Verse.GraphicData.texPath -(9,10) 'comps' [RimworldXmlReference] -> OK: field Verse.ThingDef.comps -(11,18) 'compClass' [RimworldXmlReference] -> OK: field Verse.CompProperties.compClass -(14,18) 'idlePowerDraw' [RimworldXmlReference] -> OK: field RimWorld.CompProperties_Power.idlePowerDraw + + + <|ThingDef|(0)> + <|label|(1)>test thing + <|category|(2)>|Building|(3) + <|graphicData|(4)> + <|texPath|(5)>Things/Test + + <|comps|(6)> +
  • + <|compClass|(7)>CompGlower +
  • +
  • + <|idlePowerDraw|(8)>5 +
  • + + 1 +
    +
    + +------------------------------------------------ +0: result=OK declaredElem=Verse.ThingDef +1: result=OK declaredElem=System.String Verse.Def.label +2: result=OK declaredElem=Verse.ThingCategory Verse.ThingDef.category +3: result=OK declaredElem=Verse.ThingCategory Verse.ThingCategory.Building +4: result=OK declaredElem=Verse.GraphicData Verse.ThingDef.graphicData +5: result=OK declaredElem=System.String Verse.GraphicData.texPath +6: result=OK declaredElem=System.Collections.Generic.List`1[T -> Verse.CompProperties] Verse.ThingDef.comps +7: result=OK declaredElem=System.Type Verse.CompProperties.compClass +8: result=OK declaredElem=System.Single RimWorld.CompProperties_Power.idlePowerDraw diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold index 7ed4f86..1791b75 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold @@ -1,18 +1,38 @@ -(3,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef -(3,20) '"BaseThing"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/BaseThing -(5,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef -(5,26) '"BaseThing"' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/BaseThing -(6,10) 'defName' [RimworldXmlReference] -> OK: field Verse.Def.defName -(6,18) 'LocalThing' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/LocalThing -(8,6) 'ThingDef' [RimworldXmlReference] -> OK: type Verse.ThingDef -(9,10) 'defName' [RimworldXmlReference] -> OK: field Verse.Def.defName -(9,18) 'Referrer' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/Referrer -(10,10) 'minifiedDef' [RimworldXmlReference] -> OK: field Verse.ThingDef.minifiedDef -(10,22) 'LocalThing' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement ThingDef/LocalThing -(10,34) 'minifiedDef' [RimworldXmlReference] -> OK: field Verse.ThingDef.minifiedDef -(11,10) 'leaveResourcesWhenKilled' [RimworldXmlReference] -> OK: field Verse.ThingDef.leaveResourcesWhenKilled -(12,10) 'soundDrop' [RimworldXmlReference] -> OK: field Verse.ThingDef.soundDrop -(12,20) 'OtherSound' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement SoundDef/OtherSound -(13,10) 'soundInteract' [RimworldXmlReference] -> OK: field Verse.ThingDef.soundInteract -(14,10) 'stuffCategories' [RimworldXmlReference] -> OK: field Verse.BuildableDef.stuffCategories -(15,17) 'OtherStuff' [RimworldXmlDefReference] -> OK: XMLTagDeclaredElement StuffCategoryDef/OtherStuff + + + <|ThingDef|(0) Name=|"BaseThing"|(1) Abstract="True"> +
    + <|ThingDef|(2) ParentName=|"BaseThing"|(3)> + <|defName|(4)>|LocalThing|(5) +
    + <|ThingDef|(6)> + <|defName|(7)>|Referrer|(8) + <|minifiedDef|(9)>|LocalThing|(10) + <|leaveResourcesWhenKilled|(12)>true + <|soundDrop|(13)>|OtherSound|(14) + <|soundInteract|(15)>MissingSound + <|stuffCategories|(16)> +
  • |OtherStuff|(17)
  • + + +
    + +------------------------------------------------ +0: result=OK declaredElem=Verse.ThingDef +1: result=OK declaredElem=ThingDef/BaseThing +2: result=OK declaredElem=Verse.ThingDef +3: result=OK declaredElem=ThingDef/BaseThing +4: result=OK declaredElem=System.String Verse.Def.defName +5: result=OK declaredElem=ThingDef/LocalThing +6: result=OK declaredElem=Verse.ThingDef +7: result=OK declaredElem=System.String Verse.Def.defName +8: result=OK declaredElem=ThingDef/Referrer +9: result=OK declaredElem=Verse.ThingDef Verse.ThingDef.minifiedDef +10: result=OK declaredElem=ThingDef/LocalThing +11: result=OK declaredElem=Verse.ThingDef Verse.ThingDef.minifiedDef +12: result=OK declaredElem=System.Boolean Verse.ThingDef.leaveResourcesWhenKilled +13: result=OK declaredElem=Verse.SoundDef Verse.ThingDef.soundDrop +14: result=OK declaredElem=SoundDef/OtherSound +15: result=OK declaredElem=Verse.SoundDef Verse.ThingDef.soundInteract +16: result=OK declaredElem=System.Collections.Generic.List`1[T -> RimWorld.StuffCategoryDef] Verse.BuildableDef.stuffCategories +17: result=OK declaredElem=StuffCategoryDef/OtherStuff From 17acc9aab8b6c33500cb3aeb2371a04df25879f0 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 21 Sep 2026 23:38:36 +0100 Subject: [PATCH 14/15] Validate the FindUsages tests --- .../FindUsages/RimworldFindUsagesTests.cs | 14 ++++++-------- .../Navigation/RimworldNavigationAfterEdits.cs | 1 - .../Navigation/RimworldNavigationTests.cs | 1 - .../TestBases/RimworldNavigationTestBase.cs | 2 ++ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs index 37e9ba6..5973ee9 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs @@ -12,23 +12,21 @@ namespace ReSharperPlugin.RimworldDev.Tests.FindUsages; /// DefDatabase string in CSharpUsages.cs) are not, because RimworldSearcherFactory.IsCompatibleWithLanguage only /// accepts XML. Allowing C# there makes both appear (verified), so when that's fixed these golds should gain them. ///
    -public abstract class RimworldFindUsagesTestBase(ProjectLayout layout) : RimworldNavigationTestBase(layout) -{ - protected override string ExtraPath => ""; - protected override string RelativeTestDataPath => @"FindUsages"; -} - [ProjectLayouts(ProjectLayout.CSharpProject)] [TestFileExtension(".xml")] -public class RimworldFindUsagesFromXmlTests(ProjectLayout layout) : RimworldFindUsagesTestBase(layout) +public class RimworldFindUsagesFromXmlTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) { + protected override string RelativeTestDataPath => "FindUsages"; + [Test] public void TestFromDefName() => DoNamedTest("OtherUsages.xml", "CSharpUsages.cs"); [Test] public void TestFromNameAttribute() => DoNamedTest(); } [ProjectLayouts(ProjectLayout.CSharpProject)] [TestFileExtension(".cs")] -public class RimworldFindUsagesFromCSharpTests(ProjectLayout layout) : RimworldFindUsagesTestBase(layout) +public class RimworldFindUsagesFromCSharpTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) { + protected override string RelativeTestDataPath => "FindUsages"; + [Test] public void TestFromCSharpString() => DoNamedTest("Defs.xml", "OtherUsages.xml"); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs index 4a84318..24afacf 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs @@ -28,7 +28,6 @@ public class RimworldNavigationAfterEditTests(ProjectLayout layout) : RimworldNa private const string DefsFile = "MovingDefs.xml"; private const string ThingALine = " ThingA\n"; - protected override string ExtraPath => ""; protected override string RelativeTestDataPath => "Navigation"; [Test] public void TestNavigateAfterDefsMove() => DoNamedTest(DefsFile); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs index b83d3f2..d5bb1a2 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs @@ -21,7 +21,6 @@ namespace ReSharperPlugin.RimworldDev.Tests.Navigation; [TestFileExtension(".xml")] public class RimworldNavigationTests(ProjectLayout layout) : RimworldNavigationTestBase(layout) { - protected override string ExtraPath => ""; protected override string RelativeTestDataPath => "Navigation"; [Test] public void TestNavigatePropertyToCSharpField() => DoNamedTest(); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs index d0b49c2..79e4097 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs @@ -12,6 +12,8 @@ namespace ReSharperPlugin.RimworldDev.Tests.TestBases; /// Navigation and Find Usages the way the IDE runs them, in the layout the fixture asks for. public abstract class RimworldNavigationTestBase : AllNavigationProvidersTestBase, IProjectLayoutFixture { + protected sealed override string ExtraPath => ""; + protected RimworldNavigationTestBase(ProjectLayout layout) { Layout = layout; From 13e972408aadc67e8eefe60bd55b46abce6ea913 Mon Sep 17 00:00:00 2001 From: Gareth Date: Mon, 21 Sep 2026 23:58:12 +0100 Subject: [PATCH 15/15] Fix an issue with [DefOf] autocomplete where it doesn't work if there's no trailing `;` typed in yet --- docs/testing-plan.md | 451 ------------------ docs/testing-research.md | 323 ------------- .../RimworldCSharpCompletionTests.cs | 10 +- .../CSharpDefsOfItemProvider.cs | 22 +- 4 files changed, 18 insertions(+), 788 deletions(-) delete mode 100644 docs/testing-plan.md delete mode 100644 docs/testing-research.md diff --git a/docs/testing-plan.md b/docs/testing-plan.md deleted file mode 100644 index 9501c86..0000000 --- a/docs/testing-plan.md +++ /dev/null @@ -1,451 +0,0 @@ -# Backend testing — broadening plan - -The first round (see `testing-research.md`) proved one path: `CodeCompletionTestBase` + gold files, Krafs referenced -into the in-memory project, `ScopeHelper` finding `Verse.ThingDef`, and `RimworldXMLItemProvider` listing one type's -fields at the top level of a def. - -> **Status (2026-09-18): experiment phase closed after Phase F.** See "Conclusions" at the end. Phases G and H were -> not run; they remain here as the map for whoever picks this up. - -This plan is about finding **where the harness stops working**, not about coverage. Each step changes one thing -relative to something already green, so a failure points at that one change. Stop at the first red step, fix or -record the boundary, then carry on. - -Status column: `—` not started, `✅` green, `❌` boundary found (see notes), `⚠️` green with caveats. - -## Phase A — same fixture, same single file, harder paths through the provider - -Only the input XML changes; the C# fixture is untouched. - -| # | Step | New variable | Status | -|---|---|---|---| -| 1 | Nested field: `<{caret}` | `GetContextFromHierachy` following a field into its type | ✅ | -| 2 | `
  • ` of a list: `
  • <{caret}` | `"li"` resolved via previous field's `List` type argument | ✅ | -| 3 | `
  • <{caret}` | the `li` branch, class resolved against RimWorld's scope | ✅ | -| 4 | Text value, enum: `{caret}` | the `TEXT` branch of `IsAvailable`/`AddLookupItems` | ✅ | -| 5 | Accept an item (`CodeCompletionTestType.Action` + `${COMPLETE_ITEM:…}`) | gold is the document after insertion | ❌ | - -## Phase B — the def index (`RimworldSymbolScope`) - -Still completion, but now dependent on a `SimpleICache` populating inside the test shell. - -| # | Step | New variable | Status | -|---|---|---|---| -| 6 | Def reference, same file: caret in a field typed as a `Def` subclass, target def in the same file | `RimworldSymbolScope` indexing in a test solution | ✅ | -| 7 | Same, target def in a second file | `DoTestSolution(name, ["Other.xml"])` — first multi-file test | ✅ | -| 8 | `ParentName=""` completion with `Abstract="true"` and concrete defs | attribute branch + `DefTags` abstract flag | ✅ | -| 9 | Mod-defined def class in a `.cs` file (`MyMod.CustomThingDef : ThingDef`, `li Class="MyMod.Foo"`) | mixed C#/XML project, `ExtraDefTagNames`, all-solution-scopes fallback | ⚠️ | - -## Phase C — the same pipeline from the C# side - -Phase B's XML + index setup, caret in a `.cs` file, different providers. - -| # | Step | New variable | Status | -|---|---|---|---| -| 10 | `[DefOf]` field completion against defs in a companion `.xml` | `CSharpDefsOfItemProvider` | ⚠️ | -| 11 | `DefDatabase.GetNamed("{caret}")` | `RimworldDefCSharpItemProvider` | ✅ | - -## Phase D — reference resolution (Ctrl+Click): a new kind of test - -| # | Step | New variable | Status | -|---|---|---|---| -| 12 | Hand-rolled dump on `BaseTestWithSingleProject` + `ExecuteWithGold` (proven by `XmlPsiDiagnosticsTests`): every tag → `GetReferences()` → `Resolve()` → declared element | `RimworldReferenceProvider` + `RimworldXmlReference` (XML → C# field) | ✅ | -| 13 | Same dump over Phase B input | `RimworldXmlDefReference` (XML → XML def) | ✅ | -| 14 | Same dump over Phase C input | `RimworldCSharpReferenceProvider` (C# string → XML def) | ✅ | -| 15 | (Optional) port 12–14 onto the SDK's own reference test base, if one exists — find it by reflection | the SDK base class | ✅ | - -## Phase E — daemon / highlighting - -| # | Step | New variable | Status | -|---|---|---|---| -| 16 | One invalid `bool` via `HighlightingTestBase` (gold = source with `\|text\|(0)` markers) | `CustomXmlAnalysisStage` in the test shell; daemon registration, severity filter | ✅ | -| 17 | No RimWorld reference → no highlights, no logged errors | the bail-out path; first test without Krafs | ✅ | -| 17b | Every validating branch across a pair of same-shaped inputs, valid and invalid: int, float, IntRange, FloatRange, Vector2, Vector3, enum, struct, the types the stage skips, unresolvable fields and whitespace-padded values | the rest of the switch in `CustomXmlAnalysisStageProcess`; found boundaries 8 and 9 | ✅ | -| 17c | The same suite again in a mod's real layout — XML in a referenceless project, RimWorld referenced by a C# project beside it | `[ProjectLayouts]` + `ProjectLayoutSupport`; project descriptors and per-project references | ✅ | - -## Phase F — Find Usages (known rough edge) - -| # | Step | New variable | Status | -|---|---|---|---| -| 18 | Find Usages on a ``, usages in another XML file and a C# file | `XMLTagDeclaredElement`, `RimworldSearcherFactory`/`CustomSearcher`. Read `Find Usages.md` first; may end up documenting current behaviour rather than correct behaviour | ⚠️ | - -## Phase G — environment-driven behaviour - -| # | Step | New variable | Status | -|---|---|---|---| -| 19 | Drop Krafs from references, `SkipAssemblyDiscovery = false`, copy Krafs' `Assembly-CSharp.dll` into a temp `RimWorldWin64_Data/Managed/`, point `RimworldPath` at it via `[TestSetting]`, rerun step 1's input | `ScopeHelper.AddRef` / `IAssemblyFactory.AddRef` / settings accessor — the real XML-only-mod mechanism. Expect teardown cookie/leak issues | — | -| 20 | Alt+Insert property generator via the SDK's generate test base | the generate workflow; `PropertyOrdering` makes gold order meaningful | ✅ | - -## Phase H — beyond the current test project (boundary-finding only) - -| # | Step | New variable | Status | -|---|---|---|---| -| 21 | Test one trivial Rider-only thing (e.g. `RimworldProjectMark` parsing an `About.xml`) | the test project references the **RESHARPER** csproj, which excludes `RimworldXmlProject/`, `Remodder/`, `TemplateParameters/`; can a test project reference the Rider csproj and still boot? | ❌ | -| 22 | Remodder `Decompiler` against a tiny Harmony-patched assembly | mostly non-PSI; cheap once 21 works | — | -| 23 | (Separate track) plain JUnit for `QuickStartUtils` setup/teardown against a temp Ludeon dir | Kotlin side, no IDE; highest-stakes code (it can destroy the user's mod list) | — | - -Out of scope until we decide what to keep: moving CI to Windows, the `.gitattributes` LF rule for golds. - -## Reading the results - -- A–C green → the completion harness generalises as-is. -- D/E → whether non-completion SDK test bases work on our `net10.0-windows` host. -- G → whether anything touching disk and settings can be made hermetic. -- H → whether the Rider-only build is testable at all. - -After each phase, fold what broke and how it was fixed into `testing-research.md`. - -## Findings log - -### Phase A–C (2026-09-18) — the completion harness generalises; the bugs found are the plugin's - -Test inputs/golds: `test/data/Completion/Rimworld/` (A, B), `…/Rimworld/Action/` (step 5), `…/RimworldCSharp/` (C). -Fixtures share `RimworldCompletionTestBase` (Krafs references + `ScopeHelper` reset). Suite: 15 green, 4 `[Ignore]`d -with a reason pointing here. Nothing needed changing in the harness itself. - -**Worked unchanged (✅):** nested fields (1), `
  • ` via `List` (2), `li Class=` (3), enum text values (4), def -references from the same file (6) and a second file (7, `DoNamedTest("Other.xml")`), `ParentName` abstract-only -filtering (8), a mod `.cs` file alongside XML in the same in-memory project (9a/9b — `MyMod.CustomThingDef` as a def -root, `li Class="MyMod.CompProperties_Custom"`), `DefDatabase.GetNamed("…")` (11), `[DefOf]` with -`Mod{caret};` (10). `RimworldSymbolScope` populates during the test solution's load with no extra plumbing; no -`CommitAllDocuments` needed for list tests. - -**Step 5 ❌ — plugin bug, not harness.** `CodeCompletionTestType.Action` works (the `${COMPLETE_ITEM:x}` directive can -sit in an XML comment; the inserted text in both golds is right), but accepting an item commits the document, and -`RimworldSymbolScope.Merge` → `AddToLocalCache` calls `sourceFile.GetPrimaryPsiFile()` *during* the commit's merge -phase. The platform logs "Trying to get PSI file for an uncommitted document" (`PsiFiles.AssertNotDirty`) twice, and -the framework fails any test that logs errors. First load doesn't trip it because nothing is dirty yet. This will -affect *any* future test that edits an XML document (Action completion, quick-fixes, generators). Root cause is the -index design: it resolves persisted offsets back to live `ITreeNode`s at merge time. Fix candidates: resolve lazily at -query time, or defer resolution until after commit. - -**Fixed (2026-09-18):** the index now stores `(sourceFile, offset, isAbstract)` and `GetTagByDef` finds the node on -query (`isAbstract` is computed in `Build` and persisted). Both Action tests pass unchanged against their golds. -`RimworldNavigationAfterEditTests` resolves the references in an open file, grows a comment above the defs it points at -by exactly one def line, then Ctrl+Clicks `ThingA`: it has to land on `ThingA`, which now sits at `ThingB`'s old offset -while `ThingB`'s node survives the reparse. Checked against the plugin: removing the offset comparison in `FindDefNode`, -or the whole cached-node check, sends it to `ThingB`; the pre-fix index fails it with the "uncommitted document" error. -(It replaced a white-box `RimworldSymbolScopeTests`, which deleted a def instead; the reparse threw those nodes away, -so it never caught a missing offset comparison.) - -**Step 9c ⚠️ — plugin bug (load order).** A `MyMod.CustomThingDef` def is not offered where a `ThingDef` is expected. -Traced: when the index merges on load, `ScopeHelper.RimworldScope` is still `null` (symbol caches not ready), so -`AddDefTagToList` skips building `ExtraDefTagNames`, and nothing rebuilds it later. Likely also real on a cold open in -Rider (custom def subclasses unresolved until their file is edited) — not verified in Rider. Test has a hand-written -expected gold and is ignored. - -**Fixed (2026-09-18)** with step 5: `ExtraDefTagNames` is rebuilt on the first query after a custom-typed def changes, -and stays stale until the scopes are ready and every custom def type resolves. The hand-written gold now passes. - -**Step 10 ⚠️ — narrow `IsAvailable`.** `CSharpDefsOfItemProvider` requires the caret's parent to be an -`IFieldDeclaration`. With `public static ThingDef Mod{caret}` followed by `}` (i.e. typing a new field at the end of -the class, the natural moment to complete), C# error recovery parses a **`MethodDeclaration`**; with no name typed the -caret node is whitespace. Only `Mod{caret};` works. Same parser as Rider, so presumably the same in production. Test -for the unfinished shape has a hand-written expected gold and is ignored. - -**Found by reading, not by a test:** `ScopeHelper.GetScopeForClass` searches `knownCustomScopes` twice; the second -lookup was meant to search `allScopes`, so `knownCustomScopes` is never populated and every dotted class name falls -back to `rimworldScope`. Masked whenever the mod's types live in the same module as the RimWorld reference (single -project, and every test here) because that scope includes references. A test for it needs a *second* project holding -the mod types — `DoTestSolution(string[][])`. - -**Technique that paid off:** a temporary `File.AppendAllText(Path.GetTempPath()/"rw-trace.txt", …)` in the plugin -method under test, then revert. Faster than reading framework logs to find which gate in `IsAvailable` rejected. - -### Phase D (2026-09-18) — reference resolution and real navigation both testable - -Tests: `References/RimworldReferenceTests.cs` (dump, steps 12–14) and `References/RimworldNavigationTests.cs` (step 15); -data under `test/data/References/`. Suite: 20 green, 4 ignored (unchanged from A–C). - -**Steps 12–14 ✅, first run.** The dump (`BaseTestWithSingleProject`, walk `psiFile.Descendants()`, call -`node.GetReferences()`, `Resolve()`, write one line per reference) picks up our -`IReferenceProviderFactory`s with no registration work. The dump is filtered to reference types from the plugin -assembly, because a C# file otherwise lists every ordinary type/namespace reference. What resolves: def type → class, -inherited/nested/`li`/`li Class` fields → `IField`, enum text → enum member, `Name`/`ParentName`/`defName` → the def's -`XMLTagDeclaredElement`, a def reference to a def in the same file / another file / another def type, and C# `[DefOf]` -fields and `DefDatabase.GetNamed*("…")` strings → the XML def. Missing defs and wrong-type defs are not linked (by -design; there's no "unresolved" reference). - -**Minor bug (recorded in the step 13 gold):** closing tags get Ctrl+Click only by coincidence. `GetHierarchy` treats a -closing `` identifier as *inside* its own tag, so it looks up `minifiedDef` on the field's *type* -(`ThingDef`), which happens to have that field. `` etc. get nothing. - -**Gaps, not bugs:** no reference on the `li Class="…"` value, and none on `Type`-typed text (`CompGlower`). - -**Step 15 ✅, better than planned.** The SDK ships no generic reference/resolve test base (only -`WebReferenceTestBase`; JetBrains' own resolve tests are in unpublished assemblies). But -`JetBrains.ReSharper.IntentionsTests.Navigation.AllNavigationProvidersTestBase` works: it runs *every* -context-navigation provider at an `{on}` marker (not `{caret}`) and dumps what each would do. It needs -`ExtraPath => ""` overridden. Its golds show Go to Declaration/Implementation/Type Declaration opening **decompiled Krafs -source** (`ThingDef.cs`, caret on the field), Go to Declaration on a def reference landing on the ``, and -**Find Usages / Show Usages / Highlight Usages through `CustomSearcher`** finding both occurrences of a def. So Phase F -is partly proven already: the searcher runs in the test shell. Note Find Usages on a C# field from XML reports "not -found" (usages of C# members in XML aren't searchable — expected, per `Find Usages.md`). - -Gold stability: the navigation golds embed decompiled Krafs source and some HTML-ish menu markup — pinned by the -Krafs version and the SDK version respectively; expect churn on bumps. - -### Phase E (2026-09-18) — daemon stage testable with no extra work - -Tests: `Highlighting/RimworldXmlHighlightingTests.cs`, data `test/data/Highlighting/`. - -**Steps 16–17 ✅, first run.** `HighlightingTestBase` (namespace `JetBrains.ReSharper.FeaturesTestFramework.Daemon`) -only needs `CompilerIdsLanguage => XmlLanguage.Instance` on top of the usual path/reference overrides. The gold is the -source with `|range|(n)` markers plus the list of highlightings: an invalid `bool` produces -`ReSharper Underlined Error Highlighting: Value must be "true" or "false"` on the right range, a valid one nothing. -With no Krafs reference the stage produces nothing and logs nothing. - -**Fragility worth knowing:** `CustomXmlAnalysisStageProcess` never calls `ScopeHelper.UpdateScopes`; it reads -`ScopeHelper.RimworldScope` directly. It works because the stage is declared after `CollectUsagesStage`, which resolves -references, and our reference factory calls `UpdateScopes`. Reorder the stages or change the reference factory and the -analyser silently goes quiet. - -### Phase E, expanded (2026-09-20) — the rest of the value validators - -`Highlighting/RimworldXmlHighlightingTests.cs` covers the whole switch in three tests: `TestValidValues` and -`TestInvalidValues` over two inputs with the **same structure**, and `TestWithoutRimworld`. Between them the pair holds -every validating branch, both operands of each range, the types the stage deliberately skips (string, -`IntVec2`/`IntVec3`, `Color`, `Nullable<>`), the single-value shorthand the ranges and `Vector2` allow, values whose -field resolves to no type at all (unknown def type, misspelled field, RimWorld's `statBases` syntax, unknown -`li Class`) and whitespace-padded values. A value reported in one input and not in its twin is the difference between -them. - -Two gaps turned up: - -- **Floats are checked in a real mod but not in a C# project** (boundary 8). `TestInvalidValues` carries the float - fields along with everything else and runs in the XML layout only, because in the C# one those values go unreported. - - `GetContextFromHierachy` identifies a field's type by `field.Type.GetLongPresentableName(CSharpLanguage.Instance)` and - looks that string up in the symbol scope. What that returns depends on whether the type resolves in the module the - file belongs to — `DeclaredTypeBase.GetPresentableName` is: - - ```csharp - var typeElement = GetTypeElement(); - if (typeElement == null) return GetUnresolvedPresentation(...); // "System.Single" - return typePresenter.GetPresentableName(this, style); // "float" - ``` - - Resolved, it reaches `CSharpTypePresenter`, whose default style carries `UseKeywordsForPredefinedTypes`, so a - predefined type prints as its keyword — not a CLR name, resolves to nothing. Unresolved, it reaches - `DeclaredTypeFromReflectionClassType.GetUnresolvedPresentation`, which prints `myClrTypeName.FullName` because - `GetLongPresentableName` asks for `DefaultWithQualifiedName` — and that *does* resolve. - - In the shell the `.xml` is a file of the C# test project, `System.Single` resolves through its mscorlib, and floats go - unchecked. In a mod, `Defs` belong to the plugin's own XML project, where it doesn't resolve, so the value is checked. - Confirmed from both ends: the author moved the same file between the two projects in a real solution and watched the - highlightings appear and disappear. Float validation therefore works by accident and only outside a C# project; - `bool`/`string`/`int` work everywhere only because of the hand-written keyword switch, and `Verse.FloatRange` has no - keyword to print so it behaves the same either way. Ruled out along the way, each by experiment: the Krafs reference - assemblies (the real `Assembly-CSharp.dll` from a Steam install behaves the same), the filtered reference set (every - framework DLL from the package changes nothing) and the compilation-context cookie (no cookie, the universal context - and the file's own resolve context all answer `float`). - - **The fix is to read the CLR name off `field.Type` instead of its presentation**, which removes the fork entirely - (adding `case "float"` only patches one keyword of a dozen). It was tried and reverted: it changes what completion - and references resolve too, which the current tests don't cover, and `Nullable<>` has to be looked through at the - same time or every nullable value gets reported as wrong. Left for a bug-fix pass. -- **Leading whitespace silences a tag** (boundary 9). Trailing whitespace is fine — it is included in both the checked - text and the highlighted range — but a value written on its own line has a whitespace token before the text token, so - `element.Parent.Children().FirstOrDefault(x => x is XmlFloatingTextToken) != element` sends the stage home. That is - also why `ProcessBoolean`/`ProcessEnum` get away with comparing untrimmed text. - -One more quirk, harmless but worth knowing: `Vector2`'s regex is lazy and `float.TryParse` accepts a thousands -separator, so `(1,2,3)` in a `Vector2` field parses its second component as `"2,3"` → 23 and reports nothing. - -### Both project layouts (2026-09-20) — the harness can now build what a mod actually looks like - -A mod keeps its `Defs` in the plugin's own XML project, not in a C# project, and that changes what the plugin sees -(boundary 8). A fixture now declares which layouts it runs in, and one class covers both: - -```csharp -[ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] -[TestFileExtension(".xml")] -public class RimworldXmlHighlightingTests(ProjectLayout layout) : RimworldHighlightingTestBase(layout) -``` - -`ProjectLayoutsAttribute` is an `IFixtureBuilder2` that builds one fixture per layout listed, so every `[Test]` in the -class runs once in each — against **one shared gold**. A test that passes in only one layout therefore *fails*, which -is the point. `--filter FullyQualifiedName~XmlProject` runs a single layout, which is how to regenerate a gold since -both layouts write the same `.tmp`. - -**Declaring a layout is mandatory** for every fixture that touches the plugin: the layout-aware bases take it as a -constructor argument with no default, so a fixture without the attribute has nothing to build it from. Each fixture -therefore says which model it is testing, and the ones that only work in one say so, with a comment explaining what -fails in the other. (The smoke tests stay on the SDK's own bases — they exist to answer "do tests work at all".) - -A test can narrow itself further: the same attribute on a `[Test]` method limits that test to the layouts it lists and -skips it in the others, so one awkward test doesn't hold its whole suite back (`TestInvalidValues` is the current -case). - -| Suite | Layouts | Why | -|---|---|---| -| `RimworldXmlHighlightingTests` | both | `TestInvalidValues` narrowed to the XML layout (boundary 8) | -| `AcceptCompletion.RimworldXmlTests` | both | | -| `RimworldXmlCompletionTests` | C# only | keyword-less type column and empty def-name lists in the XML layout | -| `RimworldNavigationTests`, `RimworldNavigationAfterEditTests` | C# only | nothing to navigate to in the XML layout; not investigated | -| `RimworldFindUsagesFromXmlTests` | C# only | no usages found in the XML layout; not investigated | -| `RimworldReferenceTests` | C# only | its one `.cs`-driven test lands in the referenceless project | -| `RimworldCSharpCompletionTests`, `RimworldFindUsagesFromCSharpTests`, `…WithoutRimworldTests` | C# only | the layout isn't what they're about | - -Two things to know when adding a suite to the XML layout: - -- The C# project has to hold *something*, which is `test/data/ModAssembly.cs` — one file for every suite, reached with - the `..\` prefix the split computes from `RelativeTestDataPath`. Don't put copies in the suite folders: a completion - fixture picks up stray `.cs` files in its own data folder, and a spare one changes what its tests see. -- The layout bases set the project names in their constructor and override `CanReuseSolution` to check them, because - the framework otherwise hands a fixture a solution another fixture built in the other layout. - -`ProjectLayout.cs` holds the enum, the attribute, the `IProjectLayoutFixture` marker and `ProjectLayoutSupport`, which -has all the behaviour. Changing an existing fixture's layouts is the attribute and nothing else. The only plumbing left -is per **SDK test class**, because C# has no mixins: a layout-aware base has to exist for each of them, and the four -that do live together in `TestBases/` (`RimworldHighlightingTestBase`, `RimworldCompletionTestBase`, -`RimworldNavigationTestBase`, `RimworldSolutionTestBase`). They are identical apart from which SDK class they extend — a constructor, `Layout`, -`ReferenceRimworld`, and four one-line overrides that hand off to `ProjectLayoutSupport`: - -| Override | Hands off to | -|---|---| -| constructor | `ProjectNames` — names the two projects before the solution is built | -| `CanReuseSolution` | `CanReuse` — the framework otherwise hands over a solution built in the other layout | -| `GetReferencedAssemblies` | `ReferencedAssemblies` — the game's assemblies, unless `ReferenceRimworld` is off | -| `CreateProjectDescriptor` | `Libraries` — strips every reference off the XML project | -| `DoNamedTest` (or `DoLayoutTestSolution`) | `BuildSolution` — `.cs` data to the C# project, the Defs to the XML one | - -Resetting `ScopeHelper` around each test, and skipping a test whose own `[ProjectLayouts]` excludes the fixture's -layout, are both done by the attribute (it is an NUnit `ITestAction`), so a base needs no `SetUp` for either. To cover -an SDK test class that has no base yet, copy one of the four and change what it extends. - -The highlighting suite reproduces the real behaviour exactly: every invalid value in `TestInvalidValues` is reported -in the XML layout, and the float ones are not in the C# project - which is what the author sees when moving the same -file between the two projects in a real solution. Since the two layouts share a gold, that test carries -`[ProjectLayouts(XmlProject)]` and skips in the C# one until boundary 8 is fixed. `TestValidValues` runs in both. - -That also settled the other open question: the whitespace-padded values behave the same in both layouts, so **the -whitespace gap (boundary 9) is real plugin behaviour, not a harness artifact.** - -XML completion declares the C# layout only. Running it in both was tried and surfaces two differences, neither fixed: - -1. The popup's type column loses its C# keywords — `Int32`, `Boolean`, `Single`, `List` instead of `int`, `bool`, - `float`, `List`. Same cause as the float highlighting, cosmetic but user-visible. -2. Def-name completion comes back empty (`Count: 0`) in the emulated layout. It tracks the reference stripping — put the - XML project's references back and the list fills — and everything the provider needs still works when probed - directly (the def index finds both defs, `GetTagByDef` returns live nodes, the type resolves and its supertypes - include `Verse.Def`). A real XML-only mod completing def names is the plugin's headline feature, so treat this as the - emulation being harsher than the real project model until someone confirms otherwise in a real mod. - -Next step for #2: give the XML project the project properties the real host uses (`RimworldXmlProjectHost` builds -`ProjectLanguage.JAVASCRIPT` with a `NetFramework` target framework of *null version*) instead of stripping a C# -project bare. `GetProjectProperties` is virtual but doesn't know which project it's building, so it needs a field set -from `CreateProjectDescriptor`. Once that behaves, turn the completion suite on with the attribute and gold both -layouts. - -### Phase G step 20 (2026-09-20) — the Generate menu tests like any other feature - -Tests: `Generate/RimworldGenerateTests.cs`, data `test/data/Generate/`, base `TestBases/RimworldGenerateTestBase.cs`. - -`GenerateTestBase` (namespace `JetBrains.ReSharper.FeaturesTestFramework.Generate`) is language-agnostic, so the XML -generator drives it unchanged. The gold is the list of properties the menu offers, in the order it offers them, -followed by the document after generating the selected ones - which pins `PropertyOrdering` (`drawSize` and -`graphicClass` come before `name`, not alphabetical order) and the filter that hides tags the def already has. - -Input directives are `${NAME:value}` in an XML comment, the same shape the accept-completion tests use: -`${KIND:RimworldPropertyGenerator}` (required - the base asserts on it), then either `${SELECTALL:true}` or numbered -`${SELECT0:…}`, `${SELECT1:…}` whose values are the `TestDescriptor` strings from the dump (`drawSize:UnityEngine -.Vector2`). The numbering matters: the reader stops at the first index it can't find. The base also decapitalises the -first test file, so the input and gold are `testGenerateProperties.xml`, not `Test…`. - -Two things the base has to supply that the IDE supplies in real life, both in `RimworldGenerateTestBase`: - -- `ScopeHelper.UpdateScopes` before the workflow is created. The generator reads `ScopeHelper.RimworldScope` directly - and offers nothing when it is null; in the IDE some other feature has filled it in by then. Same fragility as the - daemon stage (see Phase E). -- A write lock around the test. `DefPropertiesGeneratorBuilderXml.Process` calls `ModificationUtil.AddChildAfter` - without one, and the harness - unlike the IDE action - doesn't hold one, so the run logs "This operation requires a - writer lock" and fails on the logged error. - -`TestGenerateProperties` runs in the XML layout only: the descriptor dump prints generic type arguments -(`List\`1[T -> Verse.ShaderParameter]`) where they resolve, which they don't in a mod's own project. The offered -fields and their order are the same in both. `TestGenerateInListItem` runs in both. - -### Phase H step 21 (2026-09-20) — the Rider build compiles into the tests, but its solution components don't - -Swapping the test project's `ProjectReference` to `ReSharperPlugin.RimworldDev.Rider.csproj` (plus the -`InternalsVisibleTo` the ReSharper csproj carries, since the tests use `ScopeHelper`'s internals) **compiles cleanly** -- both backends already reference `JetBrains.Rider.SDK` through `Directory.Build.props`, so the Remodder and project -model packages come along without complaint. - -It does not run. 40 of 44 tests then fail with: - -``` -The component ReSharperPlugin.RimworldDev.RimworldXmlProject.RimworldProjectMarkProvider -constructor requires JetBrains.ProjectModel.ProjectsHost.ISolutionMark, which we do not have. -``` - -`RimworldProjectMarkProvider` is a `[SolutionInstanceComponent]` taking `ISolutionMark`, and a test solution is built -in memory rather than opened from a `.sln`, so the container has no solution mark to give it. The container's -dependency check runs for every test that opens a solution, which is nearly all of them. - -Gating it out doesn't work as easily as it looks: a `[ZoneMarker]` on the `RimworldXmlProject` namespace requiring -`JetBrains.Rider.Model.IRiderModelZone` changes nothing, because the test environment activates that zone too. The -plugin has no zone markers at all today, so every component it defines is fair game wherever the assembly is scanned. - -**Giving the tests a real `.sln` doesn't help.** `BaseTestWithExistingSolution` opens one from test data, but it only -renames the in-memory solution's file path - the container still has no `ISolutionMark` descriptor at all ("Could not -find the component's ISolutionMark descriptor"). The mark is made by the project *host* (`SolutionMarkFactory` in -`JetBrains.Platform.ProjectModel.Host.dll`), which this shell never starts. - -The one base that does run that pipeline, `BaseTestWithExistingSolutionLoadedByMsbuild`, wants a `global.json` under -the test project root and then fails fetching `JetBrains.MSBuildForTests` from `packages.jetbrains.team` - a JetBrains -internal test-data feed we have no access to. So that route is closed as well. - -To make the Rider build testable, one of these has to happen first, and both are plugin changes rather than test ones: - -- take `ISolution` in that constructor and look the mark up on demand, so the component can be built without one; or -- define a zone the test environment does not activate and mark the Rider-only namespaces with it, which means giving - the plugin a zone graph it currently doesn't have. - -Until then the tests stay on the ReSharper build, and `RimworldXmlProject/`, `Remodder/` and `TemplateParameters/` are -out of reach. Everything reverted; the suite is back to 41 passed, 3 skipped. - -### Phase F (2026-09-18) — Find Usages runs; C# usages are missed, cause found - -Tests: `FindUsages/RimworldFindUsagesTests.cs`, data `test/data/FindUsages/`. Same `AllNavigationProvidersTestBase` as -step 15; XML fixture and C# fixture (`[TestFileExtension]` differs) share an abstract base. - -**Step 18 ⚠️.** Starting Find Usages from a ``, from a `Name=""` attribute or from a C# `GetNamed("…")` string, -every XML usage across files is found (including `ParentName=""`) and a same-named def of another type is correctly -excluded. **Usages in C# (the `[DefOf]` field, the `DefDatabase` string) are never listed** — even when the search -starts from that C# string. This is the gap `Find Usages.md` anticipates. Cause, verified with a temporary change: -`RimworldSearcherFactory.IsCompatibleWithLanguage` accepts only `XmlLanguage`, and the platform only hands a -searcher files in languages it accepts; letting it also accept `CSharpLanguage` makes both C# usages appear (5 results -instead of 3). `HasReference` on the C# reference factory is never consulted, so it isn't the blocker. Caveat before -shipping that one-liner: `CreateReferenceSearcher` filters *elements* through the same method, so it would also start -receiving C# declared elements. The golds record current behaviour and say so in the fixture's doc comment. - -## Conclusions - -Suite at the close: **25 green, 4 `[Ignore]`d** (each with a hand-written expected gold and a reason pointing here), -~12 s of tests after a ~1 min build. - -**What the harness can test (proven):** XML and C# completion lists and insertion; the def index across files and -mixed C#/XML projects; reference resolution (dump); real IDE navigation — Go to Declaration into decompiled game -source, Find/Show/Highlight Usages; daemon highlightings. Every SDK test base tried worked on the `net10.0-windows` -host once its abstract members were supplied — no new harness fixes were needed after the proof of concept. - -**Boundaries found — all in the plugin, none in the harness:** - -| # | Problem | Where | Blocks | -|---|---|---|---| -| 1 | ✅ Fixed — Index reads PSI mid-commit ("uncommitted document" logged) | `RimworldSymbolScope.AddToLocalCache` via `Merge` | any test that edits an XML document (Action completion, future quick-fix/generator tests) | -| 2 | ✅ Fixed — Custom def subclasses not indexed under their base type on cold load | `ExtraDefTagNames` built only if `ScopeHelper.RimworldScope` is set at merge | step 9c | -| 3 | `[DefOf]` completion needs a following `;` | `CSharpDefsOfItemProvider.IsAvailable` (unfinished declaration parses as a method) | step 10 variant | -| 4 | Find Usages ignores C# | `RimworldSearcherFactory.IsCompatibleWithLanguage` | step 18's C# usages | -| 5 | `GetScopeForClass` searches `knownCustomScopes` twice (should be `allScopes`) | `ScopeHelper` | found by reading; needs a two-project test | -| 6 | Closing tags resolve only by coincidence | `GetHierarchy` on closing identifiers | cosmetic | -| 7 | Daemon stage relies on another stage having set the scope | `CustomXmlAnalysisStageProcess` | nothing yet; fragile | -| 8 | Predefined field types are identified by a *presented* name, which differs between a mod's XML project and a C# project, so floats are validated in a real mod but not in a C# project. Reading the CLR name off the type fixes it (tried, reverted — it reaches further than the highlighting tests cover; left for a bug-fix pass, and `Nullable<>` needs looking through at the same time or every nullable value is reported as wrong) | `GetContextFromHierachy`'s `GetLongPresentableName` + the `bool`/`string`/`int` keyword switch | `TestInvalidValues` runs in the XML layout only; any future test over a `float`, `double`, `long`, … field | -| 9 | ✅ Confirmed in both layouts — a value with leading whitespace (typically one written on its own line) is never validated | the "first text child" guard in `CustomXmlAnalysisStageProcess.ProcessAfterInterior` — the whitespace is a separate, earlier token | any multi-line value | - -**Not explored:** Phase G (disk discovery via `RimworldPath`/`AddRef`, the generator) and Phase H (Rider-only code, -Remodder, Kotlin). #1 (now fixed) was the blocker for testing the generator (step 20). Also still open from the proof of -concept: moving CI to Windows and the `.gitattributes` LF rule for golds. - diff --git a/docs/testing-research.md b/docs/testing-research.md deleted file mode 100644 index 9bfb17d..0000000 --- a/docs/testing-research.md +++ /dev/null @@ -1,323 +0,0 @@ -# Backend testing — reference - -How the ReSharper SDK test framework is used in this repo: what was learned from JetBrains' own plugins (Unity, -F#, ForTea, the plugin template), one third-party plugin (heapview) and the official docs, and — more usefully — -what it actually took to get a RimWorld XML completion gold test green here. Sources are at the end. - -## The approach - -Backend completion is tested with the **ReSharper SDK test framework: NUnit + gold files**. A test boots an -in-memory ReSharper shell (once per test assembly), creates an in-memory solution containing the test-data file(s), -runs the feature at `{caret}`, dumps the result to text and diffs it against a committed `.gold` file. First run -writes a `.tmp`; you review it and rename it to `.gold`. - -The Kotlin side is not tested. JetBrains' game-engine plugins without backend tests verify completion end-to-end from -Kotlin on TeamCity (full Rider download plus a real .NET SDK per test class); our completion logic is entirely -backend, so that route buys nothing here. - -## What exists - -See `testing-plan.md` for the step-by-step broadening plan, its status, and the plugin bugs it has turned up. The -original proof-of-concept tests: - -`src/dotnet/ReSharperPlugin.RimworldDev.Tests`: - -| Test | Proves | -|---|---| -| `SmokeTests.ShellStarts` | the shell boots | -| `Completion/CSharpCompletionSmokeTests.TestLocalVariable` | `CodeCompletionTestBase` + gold pipeline, no RimWorld involved | -| `Completion/XmlPsiDiagnosticsTests.TestXmlIsParsed` | `.xml` in the in-memory project is parsed as XML PSI; also the `BaseTestWithSingleProject` + `ExecuteWithGold` pattern | -| `Completion/RimworldXmlCompletionTests.TestThingDefProperties` | **our provider**, backed by `Krafs.Rimworld.Ref`, lists all 249 `ThingDef` properties with C# types. Goes red when `GetAllPublicFields` is broken. | - -Run: `dotnet test src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj` -(`--filter FullyQualifiedName~RimworldXmlCompletionTests` for one fixture). ~1 min build with normal `MSB3277`/ -`NU1701`/`NU1608` noise, ~15 s of tests. - -## How it works, from `dotnet test` to a gold diff - -**1. NUnit starts, and one fixture boots a whole ReSharper.** `dotnet test` runs NUnit over our test assembly. NUnit -runs a `[SetUpFixture]` once before any test in its namespace; ours is -`RimworldDevTestsAssembly : ExtensionTestEnvironmentAssembly<…>`, and that base class *is* the ReSharper shell -bootstrapper. It does what Rider's backend process does at startup — build the component container — except -in-process, headless, and once per test run. This is why the SDK copies ~1000 DLLs into the test output: the shell -is assembled from whatever is in that folder. - -**2. The component model.** ReSharper doesn't `new` its services; nearly every class is a *component* declared with -an attribute (`[ShellComponent]`, `[SolutionComponent]`, `[PsiComponent]`, `[IntellisensePart]`, …) and created by a -container that satisfies constructor parameters from other components. Our plugin is nothing but components: -`RimworldXMLItemProvider` is one, `RimworldSymbolScope` is one, `RimworlXMLCompletionContextProvider` is one. At -startup the shell *scans* assemblies for these attributes and registers what it finds. Two consequences bit us: -the scanner reads metadata with its own reader (which choked on the net8 `JetBrains.Lifetimes`), and it only scans -assemblies the test assembly references (so the plugin was invisible until a test used a plugin type). - -**3. Zones are the on/off switches for components.** Rider, ReSharper, dotCover and JetBrains' tests all share one -component catalogue, and a *zone* is how each host says which parts of it are active. Concretely: - -- A **zone definition** is an empty interface/class marked `[ZoneDefinition]`. `IRequire` on it means - "this zone only makes sense if that one is active". Example: JetBrains' `PsiFeatureTestZone` requires the daemon, - navigation, code-editing, C#, VB, XAML… zones — it is a bundle meaning "everything a PSI feature test needs". -- A **zone marker** is a class named `ZoneMarker` marked `[ZoneMarker]`, and it applies to *every component in its - namespace and below*: those components are only loaded when all the zones the marker `IRequire<>`s are active. - The test project's marker requires our env zone, so our test-only components belong to the test host. -- Components with **no marker anywhere above them** are un-zoned and load in every host. That is our plugin's - situation, and it's why the tests didn't need to declare a plugin zone. (Unity, F# and the template do declare - one, and then the test env zone must require it, or the plugin's components are filtered out of the test shell.) -- `ITestsEnvZone` is the host zone for "I am a test run". `ExtensionTestEnvironmentAssembly` activates the - zone you give it, and everything it requires, transitively. Ours: - `RimworldDevTestEnvironmentZone : ITestsEnvZone, IRequire` — "this is a test host, and I want - the full PSI feature set". That one line is what makes C# and XML parsing, completion, the daemon and the - reference-resolution machinery exist inside the test. - -**4. Each test gets an in-memory solution.** `BaseTestWithSingleProject` (which `CodeCompletionTestBase` extends) -builds a temporary solution with one project, puts the named test-data file(s) in it, and adds references. The -project targets .NET 3.5 by default and gets its `mscorlib` etc. from small "platform" NuGet packages the framework -downloads from JetBrains' feed (hence `test/data/nuget.config` and the `NuGetLocks` lock files). Our override of -`GetReferencedAssemblies` appends Krafs' DLLs to that reference list, which is how `ScopeHelper.UpdateScopes` — which -just asks every PSI module "do you have `Verse.ThingDef`?" — finds RimWorld exactly as it does in production. - -**5. The feature runs at `{caret}`.** The framework opens the file in an in-memory text control, strips `{caret}` -and puts the caret there, then invokes the real completion pipeline: the context providers build a -`CodeCompletionContext`, every `[IntellisensePart]` items provider whose `IsAvailable` says yes gets `AddLookupItems` -called, and the lookup list is assembled with its normal relevance sorting. Our provider runs unmodified; the only -plugin-side accommodations are the `ScopeHelper` test hooks. - -**6. Dump and diff.** `CodeCompletionTestBase` serialises the lookup list (`ModernList` format) to -`.tmp`, compares it with `.gold`, and fails on any difference — or on "no gold file", which is how a -new test's first run hands you the file to review and rename. The framework also fails a test if anything *logged an -error* during it (that's how the xUnit provider and the leaked-cookie problems surfaced), so "logged N errors" in the -output means look at the `Message =` lines, not at your assertion. - -## Harness — every line in the csproj is there because of a specific crash - -Bootstrap is the template's three types (`RimworldDevTestEnvironmentZone : ITestsEnvZone, IRequire`, -`ZoneMarker`, `RimworldDevTestsAssembly : ExtensionTestEnvironmentAssembly<…>`, `[assembly: Apartment(STA)]`). -Every JetBrains example test project is `net472`; ours isn't, and that cost these: - -| Setting | Crash it fixed | -|---|---| -| `TargetFramework` **`net10.0-windows`** | Rider 2026.1's backend runs plugins on .NET 10 (the SDK bundles that runtime) and some SDK DLLs are net8-built. A net6 host dies loading them. The plugin's `net6.0` is only a compile target. | -| `UseWindowsForms` + `UseWPF` | `ShellLocks` uses WinForms timers → `FileNotFoundException` in `JetEnvironment.CreateDontRunAsync`. | -| `AssetTargetFallback=net472` | SDK packages only ship props under `build/net472/`. NuGet's default fallback list starts at `net461` and stops at the first framework a package has *any* asset for, so `LibLevelDb` (`lib/net/_._`) never got its props imported and `leveldb.dll` never reached the output. | -| `JetBrains.Microsoft.TestPlatform.TranslationLayer` `ExcludeAssets="all"` | Transitive 2019 net451 VSTest repack whose `Microsoft.TestPlatform.*` DLLs overwrite the ones `testhost` needs → `TypeLoadException` at host start. | -| Post-build copy of **net472** `JetBrains.Lifetimes`/`RdFramework` (`UseNetFrameworkJetBrainsLibs` target) | NuGet gives a .NET host the net8.0 builds; the SDK's component scanner can't read net8.0 Lifetimes metadata ("Error resolving type MaybeNullWhenAttribute… 777.0.0.0") once it scans our plugin. | -| `xunit.runner.utility.net452.dll` copied to output | The SDK's net4x xUnit provider loads it by name; NuGet gives a .NET host the `netcoreapp10` flavour. Every test "logs an error" and fails. | -| Root `Directory.Build.props`: `Lifetimes`/`RdFramework` pinned **2026.1.2** | SDK's exact requirement; floated 2026.1.3 → `MissingMethodException: RdId.Hash` when the protocol component constructs. Rider ships its own copies so production never noticed. | - -Packages: `JetBrains.ReSharper.SDK.Tests` (= `$(SdkVersion)`; a 20 KB props file that sets `JetTestProject=True`, -which makes the SDK targets copy ~1000 SDK files into the output), `Microsoft.NET.Test.Sdk`, `NUnit3TestAdapter`, -`GitHubActionsTestLogger` (our Gradle `testDotNet` passes `--logger GitHubActions`). **No explicit NUnit** — the SDK -pins `[3.13.2]` exactly. - -**The scanner only loads assemblies the test assembly references.** Until a test used a plugin type, the compiler -emitted no reference to `ReSharperPlugin.RimworldDev.dll` and our components were simply not in the shell. Any -fixture that tests the plugin must touch a plugin type (ours call `ScopeHelper.Reset()`). - -Zones: the plugin has no `ZoneMarker`; un-zoned components load everywhere, tests included. XML has no language -zone at all (`JetBrains.ReSharper.Psi.Xml.dll` defines none), so nothing to require. If a plugin zone is ever added, -the test env zone must `IRequire<>` it or every plugin component silently vanishes. - -`test/data/nuget.config` is mandatory (framework-reference packages come from `resharper-platform.jetbrains.com`). -`test/data/NuGetLocks/*.lock` **must be committed**: they pin what the *framework* downloads at run time for the -in-memory project (e.g. `JetBrains.Tests.Platform.NETFrameWork 3.5`, requested as an open range), which the csproj -never sees. Without them a new patch upload on JetBrains' feed changes the reference set under the golds. One lock -file per distinct request (file name = hash of the request); delete ones left behind by abandoned experiments. The -broadening work added a second, legitimate one (`279ddca8…`, input `Microsoft.NETCore.App [2.0.0]`), requested by one -of the navigation/highlighting test bases; commit it too. - -## Test data - -Found by walking up from the test assembly to `test/data`; per fixture `RelativeTestDataPath => @"Completion\Rimworld"`. -`DoNamedTest()` uses the **full method name**: `TestThingDefProperties` → `TestThingDefProperties.xml` (no prefix -stripping; that's `DoNamedTest2`). Gold sits beside the input as `.gold`; `.tmp` appears on mismatch. -`ExecuteWithGold(projectFile, …)` names its gold after the *source file*, so give diagnostics their own input file. -Failure to find the data root looks like "The marker item cannot be found…" then "the Shell is not running". - -## Completion tests - -```csharp -[TestFileExtension(".xml")] -public class RimworldXmlCompletionTests : CodeCompletionTestBase -{ - protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; - protected override string RelativeTestDataPath => @"Completion\Rimworld"; - protected override IEnumerable GetReferencedAssemblies(TargetFrameworkId tfm) => /* base + Krafs DLLs */; - [SetUp] public void Reset() { ScopeHelper.Reset(); ScopeHelper.SkipAssemblyDiscovery = true; } - [TearDown] public void Forget() { ScopeHelper.Reset(); } - [Test] public void TestThingDefProperties() => DoNamedTest(); -} -``` - -- `ModernList` = gold is the lookup list (`Completion: Basic` / `Count: N` / `Range: "<♦"` / relevance + alphabetic - sections, `<==` = selected). `Action` = gold is the document after accepting the item named by a - `// ${COMPLETE_ITEM:name}` header (`ABSENT_ITEM` asserts absence). Input needs `{caret}`. -- Empty result is `Count: 0`; `` means no provider produced a context at all — that is what stock, - schema-less XML gives in this shell, so there is no "generic XML completion" checkpoint; our provider *is* the - XML completion here. -- `LookupItemFilter(ILookupItem)` restricts the dump to chosen items (not needed yet: the 249-line ThingDef gold is a - useful regression net as-is). Also: `Sorting`, `PresentLookupItem`, `[TestSetting(typeof(Key), nameof(Key.Prop), v)]`. -- Gold formats change with SDK versions; expect regeneration on bumps. -- Later: `HighlightingTestBase` (gold = source with `|text|(0)` markers) for the value validators. - -## RimWorld types: Krafs.Rimworld.Ref - -What works: the test csproj has `` -and bakes `$(PkgKrafs_Rimworld_Ref)\ref\net472` into an `AssemblyMetadataAttribute("RimworldRefDir", …)`; the fixture -overrides `GetReferencedAssemblies` and adds every DLL there except `mscorlib*`/`System*`/`netstandard*`/`Mono.*`. -`ScopeHelper` then finds `Verse.ThingDef` exactly as it does with the real game DLL. The Krafs list is identical to -the real `Assembly-CSharp.dll`'s (the gold was first generated from the latter by accident). - -What does not work, so nobody retries it: -- `[TestPackages("Krafs.Rimworld.Ref/1.6.4871")]` restores the package but the in-memory project defaults to - **.NET 3.5** (see the lock file's `Input (NuGetFramework=net35)`), which can't consume `ref/net472` → nothing referenced. -- `[TestPlatform(".NETFramework", 4, 7, 2)]` needs a `JetBrains.Tests.Platform.NETFrameWork 4.7.2` package; the feed - stops at 4.6, and the failed restore poisons other fixtures in the run. A net35 project referencing net472-built - DLLs by path is fine. - -Plugin-side hooks added for tests (`internal`, `InternalsVisibleTo` the test assembly): -- `ScopeHelper.Reset()` — the statics otherwise hold scopes from a disposed solution (next test breaks, and the - framework reports leaked "assembly cookies" at teardown). -- `ScopeHelper.SkipAssemblyDiscovery` — without it `AddRef` finds the developer's real Steam install and adds it to - the test solution (non-hermetic, and it leaks). -- `UpdateScopes` now looks for `Verse.ThingDef` *before* the "some module has no types → not ready" bail-out; the - XML-only in-memory project is legitimately empty and used to block RimWorld detection forever. - -## When a test fails - -Failures come in five shapes; the output tells you which: - -| You see | It means | Look at | -|---|---|---| -| "There is no gold file" | new test, expected | the `.tmp` | -| "The test output differs from the gold file" | behaviour changed | `diff` the `.tmp` against the `.gold`; either fix the plugin or accept the new gold | -| `Count: 0` or `` in the `.tmp` | our provider ran but had nothing, or never ran | `ScopeHelper.UpdateScopes` returning `false` (is RimWorld referenced? did `Reset()` run?), then `IsAvailable` | -| "The test has logged N errors" | some component threw during the test; the assertion may even have passed | the `Message =` lines — usually a missing DLL or a component that couldn't construct | -| Every test fails in ~4 s with the same exception | the shell didn't boot | the first `EXCEPTION #1` — it's an environment problem (see the harness table), not a test problem | - -## Multi-file - -`DoNamedTest("Other.xml", "ModTypes.cs")` adds extra files to the one in-memory project; mixing a `.cs` file into an -`[TestFileExtension(".xml")]` fixture works (the C# compiles against Krafs like any mod). `RimworldSymbolScope` -populates on solution load with no extra calls for list tests. Not yet used: `DoTestSolution(string[][])` with a -project GUID appended to a file set for a second, referenced project — needed to test anything that depends on mod -types living in a *different* module from the RimWorld reference. - -## Action completion and edits - -`CodeCompletionTestType.Action` reads `${COMPLETE_ITEM:name}` from anywhere in the file, so in XML put it in a comment. -Any test that edits an XML document currently fails with "Trying to get PSI file for an uncommitted document" logged -from `RimworldSymbolScope.AddToLocalCache` during commit — a plugin bug, see `testing-plan.md` step 5. - -## Reference and navigation tests - -Two working routes (examples in `References/`): - -- **Dump** (`RimworldReferenceTests`): `BaseTestWithSingleProject`, `CommitAllDocuments`, then for every node of the file - `node.GetReferences()` → `Resolve()` → write a line into `ExecuteWithGold`. Filter to - `reference.GetType().Assembly == typeof(ScopeHelper).Assembly` or C# files drown in ordinary references. Tests the - reference providers in isolation, gold is compact. -- **Real navigation** (`RimworldNavigationTests`): `AllNavigationProvidersTestBase` (namespace - `JetBrains.ReSharper.IntentionsTests.Navigation`, in `JetBrains.ReSharper.FeaturesTestFramework`). Marker is `{on}` - (`{off}` asserts unavailability), **not** `{caret}`; must override `ExtraPath` (`""` is fine). Gold covers Go to - Declaration/Implementation/Type Declaration, Find Usages, Show Usages and Highlight Usages; navigation into - referenced assemblies shows decompiled source. Also exist: `NavigationProviderTestBase` (one provider) and - `ContextNavigationTestBase`. - -There is no general `ReferenceTestBase`/`ResolveTestBase` in the shipped SDK. To find test bases, grep the DLLs: -`grep -aoE '[A-Za-z]*TestBase' JetBrains.ReSharper.FeaturesTestFramework.dll | sort -u`, then inspect members with -PowerShell `ReflectionOnlyLoadFrom` (hook `ReflectionOnlyAssemblyResolve` to the bin folder and read -`ReflectionTypeLoadException.Types` when `GetTypes()` throws). Abstract members are cheapest to discover by compiling. - -## Highlighting and Find Usages tests - -- **Highlighting** (`Highlighting/`): `HighlightingTestBase` (`JetBrains.ReSharper.FeaturesTestFramework.Daemon`); must - override `CompilerIdsLanguage` (`XmlLanguage.Instance`). No markers in the input — the gold adds `|range|(n)` and a - numbered list. `HighlightingPredicate` is available to narrow what gets dumped (not needed so far). -- **Find Usages** (`FindUsages/`): no dedicated base needed — `AllNavigationProvidersTestBase` already runs - Find Usages / Show Usages / Highlight Usages. Put `{on}` on the def, extra files via `DoNamedTest("a.xml", "b.cs")`. - To start from a C# file, use a second fixture with `[TestFileExtension(".cs")]`. - -## Debugging "the provider didn't contribute" - -Fastest route: temporarily `File.AppendAllText(Path.Combine(Path.GetTempPath(), "rw-trace.txt"), …)` inside the -plugin method (e.g. log `context.NodeInFile`'s type, text and parent type in `IsAvailable`), run the one test, read -the file, revert. That's how the `[DefOf]` provider was found to see a `MethodDeclaration` for unfinished fields. - -## Gold hygiene - -Golds are written with a UTF-8 BOM; commit as-is. Line endings are undocumented — JetBrains forces -`test/data/**/* text eol=lf`; ours is `text=auto`, add the rule before CI runs on Linux. Gitignore `*.tmp` under -test data. Keep input/gold case consistent. - -## Platform - -JetBrains' last official word (RIDER-23218, 2019): "we don't support plugin unit tests on Linux"; every surveyed -repo runs backend tests on `windows-latest`. **Tested here (2026-09-18, WSL Ubuntu 22.04, .NET 10 SDK): confirmed.** - -**Off Windows** the test project targets plain `net10.0` (no WinForms/WPF), so it builds and `dotnet test` succeeds with -every test **reported as skipped**, each with the reason: `WindowsOnlyGuard` is a `[SetUpFixture]` outside any -namespace, so it runs before the one that boots the shell and `Assert.Ignore`s everything on non-Windows. Two -approaches that look right but aren't: an assembly-level `[Platform(Include = "Win")]` makes the adapter print only "No -test is available" (exit 0, nothing reported — reads as a pass), and so does leaving `[Apartment(STA)]` in on Linux, -hence it's under `#if WINDOWS` (defined for the `net10.0-windows` build only). - -**CI:** runner cost rules out Windows by default. `Tests.yml` runs `dotnet test ReSharperPlugin.RimworldDev.sln --logger -GitHubActions` on `ubuntu-latest` (29 skipped), and on `windows-latest` (the real suite) when the PR has the -`feature-testing` label or the manual run's "windows" box is ticked. PR runs trigger on `labeled`/`unlabeled` too, so -adding the label re-runs just the tests on Windows; it lives apart from `CI.yml` so label changes don't touch the Build -check. Deploy's `Publish` job runs on `windows-latest` (releases are rare enough for the cost), so `:publishPlugin` -> -`:testDotNet` runs the real suite and a failing test stops the release; its steps use `shell: bash` for `./gradlew` -and the `output/*` globs. -`.gitattributes` forces `test/data/** eol=lf`, which is **required**: Windows runners check out CRLF, and the -navigation/Find Usages golds contain document offsets (`RANGE: (78,88)`) that shift with CRLF (4 tests fail; the -framework normalises gold line endings but not offsets). Longest repo path on a runner is ~200 chars, under MAX_PATH; -a checkout under a long local path (e.g. the Claude scratchpad) does hit it. - -What it takes to get the shell up on Linux, layer by layer (each fix got one layer further; stopped at 5): - -| # | Symptom on Linux | Cause | Workaround that got past it | -|---|---|---|---| -| 1 | `NETSDK1100` at build | `net10.0-windows` TFM | `net10.0` + no `UseWindowsForms`/`UseWPF` on non-Windows | -| 2 | NUnit "discovered 29 of 29", then **runs 0, reports nothing** | `[assembly: Apartment(STA)]` is unsupported off Windows | drop the attribute on non-Windows. Note the silent-pass hazard | -| 3 | `JetDispatcher`: "this thread is MTA rather than STA" | JetBrains emulate STA on Unix (`JetBrains.Util.Concurrency.JetThreadApartment`) but the test bootstrap never opts in | call `JetThreadApartment.STAThread()` from a `[STAThread]` method in the `SetUpFixture` constructor | -| 4 | `TypeLoadException: System.Windows.Freezable` from `ThemedIconManagerLiveImages` | the stock .NET `WindowsBase` facade wins over JetBrains' Unix mock (`JetBrains.WindowsDesktop.Mock.Runtime`, `runtimes/unix/lib/.../WindowsBase.dll`): same assembly version, higher file version, so the build's conflict resolution drops the mock | copy the mock in and declare it in `deps.json` `runtimeTargets` (rid `unix`) with a huge `fileVersion` — the host resolves conflicts from `deps.json` versions. (JetBrains do the same trick: `JetBrains.Private.Winforms` declares `fileVersion` 42.42.42.42424) | -| 5 | `System.Windows.Forms.Primitives` 9.0 missing, from `StdApplicationUI.StatusBars.JetStatusBarIndicator` | the test environment activates the WinForms status bar; `JetBrains.Private.Winforms` ships only `System.Windows.Forms.dll`, and the Windows `Primitives` is a win-x64 R2R image (`BadImageFormatException`) | none — stopped here | - -Layers 1–3 would be cheap to keep; 4 is a post-build `deps.json` patch; 5 would need a zone configuration that keeps -Windows-UI components out of the test shell (Rider's own Linux host evidently doesn't activate them), which is -undocumented. Revisit only if Windows CI minutes become a problem. - -## Diagnostics - -- `dotnet msbuild -getItem:JetContent -getProperty:JetTestProject` shows what the SDK will copy. -- Reflection over the DLLs in the test output (`ReflectionOnlyLoadFrom`) is the fastest way to find a type's - namespace or an attribute's constructor — nothing is documented. -- Component/zone filtering: Unity's `TestEnvironment.cs` has a `RESHARPER_LOG_CONF` recipe with TRACE loggers for - `JetBrains.Application.Environment.JetEnvironment`, `…Extensibility.CatalogComponentSource`, - `…Environment.RunsProducts`, `…Catalogs.PartCatalogZoneMapping`. -- "The test has logged N errors" fails a test even when its own assertion passed; read the `Message =` lines. - -## Sources - -Best code references: Unity's -[TestEnvironment.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/TestEnvironment.cs) -(zone comments are the real docs), -[AsmDefReferencesCompletionTests.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/Unity/AsmDef/Feature/Services/CodeCompletion/AsmDefReferencesCompletionTests.cs) + -[gold](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/data/Unity/AsmDef/CodeCompletion/AsmDefReferences/TestList01.asmdef.gold), -[TestUnityAttribute.cs](https://github.com/JetBrains/resharper-unity/blob/master/resharper/resharper-unity/test/src/Unity.Tests/Unity/TestUnityAttribute.cs); -F#'s [Common.fs](https://raw.githubusercontent.com/JetBrains/resharper-fsharp/main/ReSharper.FSharp/test/src/FSharp.Tests.Common/src/Common.fs) and -[FSharpCompletionTest.fs](https://raw.githubusercontent.com/JetBrains/resharper-fsharp/main/ReSharper.FSharp/test/src/FSharp.Tests/FSharpCompletionTest.fs); -ForTea's [T4CodeCompletionTest.cs](https://raw.githubusercontent.com/JetBrains/ForTea/master/Backend/RiderPlugin/test/src/T4CodeCompletionTest.cs) + -[Directive.tt.gold](https://raw.githubusercontent.com/JetBrains/ForTea/master/Backend/RiderPlugin/test/data/CodeCompletion/Directive.tt.gold); -heapview's [test csproj](https://raw.githubusercontent.com/controlflow/resharper-heapview/master/src/dotnet/ReSharperPlugin.HeapView.Tests/ReSharperPlugin.HeapView.Tests.csproj) and -[ci.yml](https://raw.githubusercontent.com/controlflow/resharper-heapview/master/.github/workflows/ci.yml); -the [template's Tests project](https://github.com/JetBrains/resharper-rider-plugin/tree/master/content/src/dotnet/ReSharperPlugin.SamplePlugin.Tests). - -Official docs worth reading (the rest is skeletal or stale): -[ProjectStructure](https://www.jetbrains.com/help/resharper/sdk/ProjectStructure.html), -[GoldFiles](https://www.jetbrains.com/help/resharper/sdk/GoldFiles.html), -[ExternalAnnotations_Testing](https://www.jetbrains.com/help/resharper/sdk/ExternalAnnotations_Testing.html) (`[TestReferences]`), -[Analysis_Testing](https://www.jetbrains.com/help/resharper/sdk/Analysis_Testing.html). - -Negative results: `godot-support` and `azure-tools-for-intellij` have no backend tests (Kotlin end-to-end only, TeamCity); -the docs' `ITestsZone` is stale (`ITestsEnvZone` is current); `[TestPackages]`, `[TestPlatform]`, -`CodeCompletionTestBase` and non-net472 hosts are undocumented anywhere. diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs index 4133d09..684cc14 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs @@ -13,14 +13,6 @@ public class RimworldCSharpCompletionTests(ProjectLayout layout) : RimworldCompl protected override string RelativeTestDataPath => @"CompletionSuggestions\RimworldCSharp"; [Test] public void TestDefOfFieldWithPrefixAndSemicolon() => DoNamedTest("Defs.xml"); - - /// - /// Because of how we've set up the ItemLookup, if we do `public static ThingDef Mod{caret};`, it'll suggest our - /// correct ThingDefs, however if we leave the semi-colon off and do `public static ThingDef Mod{caret}` then it - /// won't. - /// - [Test, Ignore("CSharpDefsOfItemProvider needs to be adjusted first")] - public void TestDefOfFieldWithPrefix() => DoNamedTest("Defs.xml"); - + [Test] public void TestDefOfFieldWithPrefix() => DoNamedTest("Defs.xml"); [Test] public void TestDefDatabaseGetNamed() => DoNamedTest("Defs.xml"); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/CSharpDefsOfItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/CSharpDefsOfItemProvider.cs index ec4da4a..4a9c835 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/CSharpDefsOfItemProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/CSharpDefsOfItemProvider.cs @@ -21,9 +21,9 @@ protected override bool IsAvailable(CSharpCodeCompletionContext context) { var node = context.NodeInFile; if (!node.Language.IsLanguage(CSharpLanguage.Instance)) return false; - if (node.Parent is not IFieldDeclaration fieldDeclaration) return false; - if (fieldDeclaration.Type is not IDeclaredType) return false; - if (fieldDeclaration.GetContainingTypeElement() is not IClass containingClass) return false; + if (GetFieldType(node) is null) return false; + if (node.Parent is not ICSharpTypeMemberDeclaration memberDeclaration) return false; + if (memberDeclaration.GetContainingTypeElement() is not IClass containingClass) return false; if (containingClass .GetAttributeInstances(AttributesSource.Self) .FirstOrDefault(attribute => attribute.GetClrName().FullName == "RimWorld.DefOf") is null) return false; @@ -35,8 +35,7 @@ protected override bool AddLookupItems(CSharpCodeCompletionContext context, IIte { var node = context.NodeInFile; - if (node.Parent is not IFieldDeclaration fieldDeclaration) return false; - if (fieldDeclaration.Type is not IDeclaredType fieldType) return false; + if (GetFieldType(node) is not { } fieldType) return false; var defTypeName = fieldType.GetClrName().ShortName; var xmlSymbolTable = context.NodeInFile.GetSolution().GetComponent(); @@ -57,4 +56,17 @@ protected override bool AddLookupItems(CSharpCodeCompletionContext context, IIte return base.AddLookupItems(context, collector); } + + private static IDeclaredType GetFieldType(ITreeNode node) => node.Parent switch + { + // We have two possibilities we need to account for: With a `;` already in place and without one. With it in + // place, Rider reports that it's a IFieldDeclaration. Without it, it'll report that it's a IMethodDeclaration. + // In the case that there's no trailing `;` and Rider thinks it's a method Declaration, we can also check that + // there's no left parenthesis, which would be the opening of the signature for the method. If there's not, + // we'll just keep treating it like a field. + IFieldDeclaration fieldDeclaration => fieldDeclaration.Type as IDeclaredType, + IMethodDeclaration { LPar: null } methodDeclaration when methodDeclaration.NameIdentifier == node => + methodDeclaration.Type as IDeclaredType, + _ => null + }; } \ No newline at end of file