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..0beb2de 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -5,6 +5,7 @@ on: branches: - main pull_request: + workflow_dispatch: jobs: Build: @@ -31,21 +32,3 @@ jobs: with: name: ${{ github.event.repository.name }}.CI.${{ github.head_ref || github.ref_name }} path: output - Test: - runs-on: ubuntu-latest - 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 diff --git a/.github/workflows/Deploy.yml b/.github/workflows/Deploy.yml index 119b70b..5090ee3 100644 --- a/.github/workflows/Deploy.yml +++ b/.github/workflows/Deploy.yml @@ -7,10 +7,16 @@ on: jobs: Publish: - 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 @@ -36,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..f523817 --- /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: + 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/.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/build.gradle.kts b/build.gradle.kts index e7b8979..aea3e4f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -190,7 +190,10 @@ tasks.runIde { // part of a plugin, but there are dangers about keeping plugins in sync autoReload = false - val exampleModSolution = layout.projectDirectory.file("example-mod/AshAndDust.sln").asFile.absolutePath + // What the sandbox opens. Defaults to the checked-in example mod; the highlighting fixtures under example-mods/ + // are opened with e.g. -PrunIdeSolution=example-mods/XmlOnlyMod (a folder is fine, it doesn't have to be a .sln). + val solutionToOpen = providers.gradleProperty("runIdeSolution").getOrElse("example-mod/AshAndDust.sln") + val exampleModSolution = layout.projectDirectory.file(solutionToOpen).asFile.absolutePath argumentProviders += CommandLineArgumentProvider { listOf(exampleModSolution) 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..bbbd20a --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/AcceptCompletion/RimworldXmlTests.cs @@ -0,0 +1,18 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.AcceptCompletion; + +[ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldXmlTests(ProjectLayout layout) : RimworldCompletionTestBase(layout) +{ + 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/CompletionSuggestions/RimworldCSharpCompletionTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs new file mode 100644 index 0000000..684cc14 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldCSharpCompletionTests.cs @@ -0,0 +1,18 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; + +[TestFileExtension(".cs")] +[ProjectLayouts(ProjectLayout.CSharpProject)] +public class RimworldCSharpCompletionTests(ProjectLayout layout) : RimworldCompletionTestBase(layout) +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"CompletionSuggestions\RimworldCSharp"; + + [Test] public void TestDefOfFieldWithPrefixAndSemicolon() => DoNamedTest("Defs.xml"); + [Test] 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 new file mode 100644 index 0000000..c064415 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/CompletionSuggestions/RimworldXmlCompletionTests.cs @@ -0,0 +1,40 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.CompletionSuggestions; + +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldXmlCompletionTests(ProjectLayout layout) : RimworldCompletionTestBase(layout) +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + 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(); + + [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(); + + // When a property expects a specific DefType (like ThingDef), modded classes that extend that should be offered + [Test] public void TestModDefAsSuperclassReference() => DoNamedTest("ModTypes.cs"); +} \ 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 new file mode 100644 index 0000000..5973ee9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/FindUsages/RimworldFindUsagesTests.cs @@ -0,0 +1,32 @@ +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.References; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +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. +/// +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +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) : RimworldNavigationTestBase(layout) +{ + protected override string RelativeTestDataPath => "FindUsages"; + + [Test] public void TestFromCSharpString() => DoNamedTest("Defs.xml", "OtherUsages.xml"); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs new file mode 100644 index 0000000..2e8ff20 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Generate/RimworldGenerateTests.cs @@ -0,0 +1,20 @@ +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.Generate; + +/// +/// This tests the `Alt+Insert` Generation menu inside a Def. +/// +[ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldGenerateTests(ProjectLayout layout) : RimworldGenerateTestBase(layout) +{ + protected override string RelativeTestDataPath => @"Generate"; + + [ProjectLayouts(ProjectLayout.XmlProject)] + [Test] public void TestGenerateProperties() => DoNamedTest(); + + [Test] public void TestGenerateInListItem() => DoNamedTest(); +} 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..c1facd9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Highlighting/RimworldXmlHighlightingTests.cs @@ -0,0 +1,31 @@ +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.Highlighting; + +[ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldXmlHighlightingTests(ProjectLayout layout) : RimworldHighlightingTestBase(layout) +{ + protected override string RelativeTestDataPath => @"Highlighting"; + + [Test] public void TestValidValues() => DoNamedTest(); + + /// + /// 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(); +} + +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldXmlHighlightingWithoutRimworldTests(ProjectLayout layout) : RimworldHighlightingTestBase(layout) +{ + protected override string RelativeTestDataPath => @"Highlighting"; + protected override bool ReferenceRimworld => false; + + [Test] public void TestWithoutRimworld() => 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..24afacf --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationAfterEdits.cs @@ -0,0 +1,58 @@ +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 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..d5bb1a2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/Navigation/RimworldNavigationTests.cs @@ -0,0 +1,31 @@ +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 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/ProjectLayout.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ProjectLayout.cs new file mode 100644 index 0000000..5f50e1b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ProjectLayout.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using JetBrains.ProjectModel; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using NUnit.Framework; +using NUnit.Framework.Interfaces; +using NUnit.Framework.Internal; +using NUnit.Framework.Internal.Builders; + +namespace ReSharperPlugin.RimworldDev.Tests; + +/// How the solution under test is laid out. It changes what the plugin sees, so most suites want both. +public enum ProjectLayout +{ + /// Everything in one C# project, which is what the SDK's test bases build by default. + CSharpProject, + + /// + /// What a mod looks like: the Defs in a project of their own with no references, RimWorld referenced by a C# + /// project beside it. Some values are validated here and not in a C# project; see boundary 8 in + /// docs/testing-plan.md. + /// + XmlProject +} + +/// A fixture built for one layout. The layout-aware test bases implement it; tests don't need to. +public interface IProjectLayoutFixture +{ + ProjectLayout Layout { get; } +} + +/// +/// On a fixture, builds one of it per layout listed, so every test in the class runs once in each: +/// +/// [ProjectLayouts(ProjectLayout.XmlProject, ProjectLayout.CSharpProject)] +/// +/// On a test method, narrows that one test to the layouts listed and skips it in the others. +/// +/// It also resets ScopeHelper around every test it applies to, which is why the bases don't need a SetUp for it. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public class ProjectLayoutsAttribute(params ProjectLayout[] layouts) : NUnitAttribute, IFixtureBuilder2, ITestAction +{ + public IReadOnlyList Layouts { get; } = layouts; + + public IEnumerable BuildFrom(ITypeInfo typeInfo) => BuildFrom(typeInfo, MatchEverything.Instance); + + public IEnumerable BuildFrom(ITypeInfo typeInfo, IPreFilter filter) => + layouts.Select(layout => + new NUnitTestFixtureBuilder().BuildFrom(typeInfo, filter, new TestFixtureParameters(layout))); + + public ActionTargets Targets => ActionTargets.Test; + + public void BeforeTest(ITest test) + { + if (test.Fixture is IProjectLayoutFixture fixture && !Layouts.Contains(fixture.Layout)) + Assert.Ignore($"Only runs in {string.Join(", ", Layouts)}"); + + // ScopeHelper caches the RimWorld scope in statics; without this the next 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. + ScopeHelper.Reset(); + ScopeHelper.SkipAssemblyDiscovery = true; + } + + public void AfterTest(ITest test) => ScopeHelper.Reset(); + + /// NUnit's own empty filter is internal, and this is all of it that gets used. + private class MatchEverything : IPreFilter + { + public static readonly IPreFilter Instance = new MatchEverything(); + + public bool IsMatch(Type type) => true; + public bool IsMatch(Type type, MethodInfo method) => true; + } +} + +/// +/// Everything takes, so that a test base wiring it up is five one-line +/// overrides and nothing else. A base for an SDK test class that doesn't have one yet is a copy of those five. +/// +public static class ProjectLayoutSupport +{ + private const string XmlProjectName = "RimworldXmlProject"; + private const string CSharpProjectName = "ModAssembly"; + private const string CSharpProjectFile = "ModAssembly.cs"; + + /// What the two projects are called, given what the SDK named them. Call it from the base's constructor. + public static (string ProjectName, string SecondProjectName) ProjectNames( + ProjectLayout layout, string projectName, string secondProjectName) => + layout == ProjectLayout.XmlProject + ? (XmlProjectName, CSharpProjectName) + : (projectName, secondProjectName); + + /// A solution built by a fixture in the other layout has the wrong projects in it to reuse. + public static bool CanReuse(bool baseResult, ISolution solution, string projectName) => + baseResult && solution.GetAllProjects().Any(project => project.Name == projectName); + + /// The game's assemblies, unless the fixture is one that checks life without them. + public static IEnumerable ReferencedAssemblies(IEnumerable baseAssemblies, bool referenceRimworld) => + referenceRimworld ? baseAssemblies.Concat(RimworldReferenceAssemblies()) : baseAssemblies; + + /// + /// 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. + /// + private static IEnumerable RimworldReferenceAssemblies() + { + var refDir = typeof(ProjectLayoutSupport).Assembly + .GetCustomAttributes() + .Single(attribute => attribute.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); + }); + } + + /// Only the XML project loses its references; that is what makes it behave like a mod's Defs folder. + public static ICollection>> Libraries( + ProjectLayout layout, string projectName, string mainProjectName, + ICollection>> libraries) => + layout == ProjectLayout.XmlProject && projectName == mainProjectName + ? libraries + .Select(pair => + new KeyValuePair>(pair.Key, EmptyList.Instance)) + .ToList() + : libraries; + + /// + /// Builds the solution the way the layout wants it: one project, or the Defs in the XML project and the .cs files + /// in the mod's assembly beside it. The C# project holds test/data/ModAssembly.cs, shared by every suite. + /// + public static void BuildSolution(ProjectLayout layout, string testFile, IEnumerable otherFiles, + string relativeTestDataPath, Action singleProject, Action twoProjects) + { + var others = otherFiles.AsList(); + + if (layout != ProjectLayout.XmlProject) + { + singleProject(new[] { testFile }.Concat(others).ToArray()); + return; + } + + var depth = relativeTestDataPath.Split('/', '\\').Count(part => part.Length > 0); + var sharedFile = string.Concat(Enumerable.Repeat(@"..\", depth)) + CSharpProjectFile; + + twoProjects( + new[] { testFile }.Concat(others.Where(file => !file.EndsWith(".cs"))).ToArray(), + others.Where(file => file.EndsWith(".cs")).Concat(new[] { sharedFile }).ToArray()); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index 8f8bcdd..a01143c 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -1,12 +1,57 @@  + + net10.0-windows + + true + true + + + + net10.0 + false + false + + + + + net472 false + latest + + + + + + + + + + + + + + + <_Parameter1>RimworldRefDir + <_Parameter2>$(PkgKrafs_Rimworld_Ref)\ref\net472 + @@ -16,6 +61,34 @@ + + + - \ No newline at end of file + + + + + + + + + <_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/References/RimworldReferenceTests.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs new file mode 100644 index 0000000..4f67cb9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/References/RimworldReferenceTests.cs @@ -0,0 +1,24 @@ +using JetBrains.ReSharper.TestFramework; +using NUnit.Framework; +using ReSharperPlugin.RimworldDev.Tests.TestBases; + +namespace ReSharperPlugin.RimworldDev.Tests.References; + +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".xml")] +public class RimworldReferencesFromXmlTests(ProjectLayout layout) : RimworldReferenceTestBase(layout) +{ + protected override string RelativeTestDataPath => @"References"; + + [Test] public void TestXmlToCSharp() => DoNamedTest(); + [Test] public void TestXmlToXmlDef() => DoNamedTest("OtherDefs.xml"); +} + +[ProjectLayouts(ProjectLayout.CSharpProject)] +[TestFileExtension(".cs")] +public class RimworldReferencesFromCSharpTests(ProjectLayout layout) : RimworldReferenceTestBase(layout) +{ + protected override string RelativeTestDataPath => @"References"; + + [Test] public void TestCSharpToXmlDef() => DoNamedTest("CSharpDefs.xml"); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs new file mode 100644 index 0000000..9a842ac --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/RimworldDevTestEnvironmentZone.cs @@ -0,0 +1,16 @@ +using System.Threading; +using JetBrains.Application.BuildScript.Application.Zones; +using JetBrains.ReSharper.TestFramework; +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; + +[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/CSharpCompletion.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs new file mode 100644 index 0000000..b4ee495 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/CSharpCompletion.cs @@ -0,0 +1,19 @@ +using JetBrains.ReSharper.FeaturesTestFramework.Completion; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.SmokeTests; + +/// +/// 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 +{ + protected override CodeCompletionTestType TestType => CodeCompletionTestType.ModernList; + protected override string RelativeTestDataPath => @"SmokeTests\CSharp"; + + [Test] public void TestLocalVariable() => DoNamedTest(); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs new file mode 100644 index 0000000..0c40d0e --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/ShellStartsTest.cs @@ -0,0 +1,20 @@ +using JetBrains.TestFramework; +using NUnit.Framework; + +namespace ReSharperPlugin.RimworldDev.Tests.SmokeTests; + +/// +/// 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 +{ + [Test] + public void ShellStarts() + { + Assert.That(ShellInstance, Is.Not.Null); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs new file mode 100644 index 0000000..d737160 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/SmokeTests/XmlParsing.cs @@ -0,0 +1,46 @@ +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.SmokeTests; + +/// +/// 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 +{ + protected override string RelativeTestDataPath => @"SmokeTests\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/TestBases/RimworldCompletionTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldCompletionTestBase.cs new file mode 100644 index 0000000..bd68604 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldCompletionTestBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using JetBrains.ReSharper.FeaturesTestFramework.Completion; + +namespace ReSharperPlugin.RimworldDev.Tests.TestBases; + +/// +/// Completion tests backed by the game's types, in the layout the fixture asks for. 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, IProjectLayoutFixture +{ + protected RimworldCompletionTestBase(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); + + protected override void DoNamedTest(params string[] otherFiles) => + ProjectLayoutSupport.BuildSolution(Layout, TestName, otherFiles, RelativeTestDataPath, + _ => base.DoNamedTest(otherFiles), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldGenerateTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldGenerateTestBase.cs new file mode 100644 index 0000000..2db7c44 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldGenerateTestBase.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using JetBrains.Lifetimes; +using JetBrains.ReSharper.Feature.Services.Generate; +using JetBrains.ReSharper.Resources.Shell; +using JetBrains.ReSharper.FeaturesTestFramework.Generate; +using JetBrains.ReSharper.Psi; +using JetBrains.TextControl; + +namespace ReSharperPlugin.RimworldDev.Tests.TestBases; + +/// The Generate menu the way the IDE runs it, in the layout the fixture asks for. +public abstract class RimworldGenerateTestBase : GenerateTestBase, IProjectLayoutFixture +{ + protected RimworldGenerateTestBase(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); + + // GenerateTestBase decapitalises the first test file, and the gold is named after the file it ends up with + protected override void DoNamedTest(params string[] otherFiles) => + ProjectLayoutSupport.BuildSolution(Layout, ModifyTestFiles([TestName]).Single(), otherFiles, + RelativeTestDataPath, + _ => base.DoNamedTest(otherFiles), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); + + // The generator writes tags straight into the tree, which in the IDE runs under the action's own write lock + protected override void DoTest(Lifetime lifetime, IProject testProject) + { + using (WriteLockCookie.Create()) + base.DoTest(lifetime, testProject); + } + + // The generator reads the scopes from ScopeHelper's statics, which in the IDE another feature has filled in by now + protected override IGeneratorWorkflow CreateWorkflow(string kind, ITextControl textControl, + IPsiSourceFile sourceFile) + { + ScopeHelper.UpdateScopes(Solution); + + return base.CreateWorkflow(kind, textControl, sourceFile); + } +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldHighlightingTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldHighlightingTestBase.cs new file mode 100644 index 0000000..18f8f61 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldHighlightingTestBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using JetBrains.ReSharper.FeaturesTestFramework.Daemon; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Xml; + +namespace ReSharperPlugin.RimworldDev.Tests.TestBases; + +/// Highlighting tests against the game's types, in the layout the fixture asks for. +public abstract class RimworldHighlightingTestBase : HighlightingTestBase, IProjectLayoutFixture +{ + protected override PsiLanguageType CompilerIdsLanguage => XmlLanguage.Instance; + + protected RimworldHighlightingTestBase(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); + + protected override void DoNamedTest(params string[] auxFiles) => + ProjectLayoutSupport.BuildSolution(Layout, TestName, auxFiles, RelativeTestDataPath, + _ => base.DoNamedTest(auxFiles), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs new file mode 100644 index 0000000..79e4097 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldNavigationTestBase.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using JetBrains.ReSharper.IntentionsTests.Navigation; + +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; + (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); + + protected override void DoNamedTest(params string[] otherFiles) => + ProjectLayoutSupport.BuildSolution(Layout, TestName, otherFiles, RelativeTestDataPath, + _ => base.DoNamedTest(otherFiles), + (xmlProjectFiles, cSharpProjectFiles) => DoTestSolution(xmlProjectFiles, cSharpProjectFiles)); +} 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..6a5130f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/TestBases/RimworldReferenceTestBase.cs @@ -0,0 +1,48 @@ +using System; +using System.Collections.Generic; +using JetBrains.ProjectModel; +using JetBrains.ProjectModel.Update; +using JetBrains.Util; +using JetBrains.Util.Dotnet.TargetFrameworkIds; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.TestFramework; + +namespace ReSharperPlugin.RimworldDev.Tests.TestBases; + +/// 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 RimworldReferenceTestBase(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); + + 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/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)."); + } +} 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/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml new file mode 100644 index 0000000..2aabcd3 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml @@ -0,0 +1,8 @@ + + + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml.gold new file mode 100644 index 0000000..1621cb0 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteEnumValue.xml.gold @@ -0,0 +1,8 @@ + + + + + TestThing + Building{caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml new file mode 100644 index 0000000..d068a3d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml @@ -0,0 +1,10 @@ + + + + + TestThing + + <{caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml.gold new file mode 100644 index 0000000..966b725 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/AcceptCompletion/Rimworld/TestCompleteTag.xml.gold @@ -0,0 +1,10 @@ + + + + + TestThing + + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/ModTypes.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/ModTypes.cs new file mode 100644 index 0000000..31a09b6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/OtherDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/OtherDefs.xml new file mode 100644 index 0000000..0a5f5f5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/OtherDefs.xml @@ -0,0 +1,9 @@ + + + + OtherThing + + + NotAThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/StuffCategoryDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/StuffCategoryDefs.xml new file mode 100644 index 0000000..84f9491 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/StuffCategoryDefs.xml @@ -0,0 +1,22 @@ + + + + + Metallic + + metal + BuildingDestroyed_Metal_Small + BuildingDestroyed_Metal_Medium + BuildingDestroyed_Metal_Big + + + + Woody + + wood + BuildingDestroyed_Wood_Small + BuildingDestroyed_Wood_Medium + BuildingDestroyed_Wood_Big + + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml new file mode 100644 index 0000000..613c44f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml @@ -0,0 +1,5 @@ + + + {caret} + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml.gold new file mode 100644 index 0000000..62a7967 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestBooleanPropertyValue.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +false <== +true + +##### ALPHABETIC SORT ##### + + [Light, Generic] +false <== +true diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml new file mode 100644 index 0000000..776ddfa --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml.gold new file mode 100644 index 0000000..3bffd35 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefName.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Prefix: "Thou" +Count: 1 +Focus: Hard +Range: " + + + TestThingB + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceOtherFile.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceOtherFile.xml.gold new file mode 100644 index 0000000..54f3cd5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestDefReferenceSameFile.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceSameFile.xml new file mode 100644 index 0000000..1418c75 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceSameFile.xml @@ -0,0 +1,10 @@ + + + + TestThingA + + + TestThingB + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceSameFile.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefReferenceSameFile.xml.gold new file mode 100644 index 0000000..03ebb55 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml new file mode 100644 index 0000000..94779dd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml @@ -0,0 +1,7 @@ + + + +
  • {caret} + + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml.gold new file mode 100644 index 0000000..315b4b2 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestDefsFilterByType.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Count: 2 +Focus: Hard +Range: "
  • ♦" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +Metallic <== +Woody + +##### ALPHABETIC SORT ##### + + [Light, Generic] +Metallic <== +Woody diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestEnumValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestEnumValue.xml new file mode 100644 index 0000000..ab517ef --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestEnumValue.xml @@ -0,0 +1,7 @@ + + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestEnumValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestEnumValue.xml.gold new file mode 100644 index 0000000..f4bb505 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestListItemProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemProperties.xml new file mode 100644 index 0000000..4d92b7c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemProperties.xml @@ -0,0 +1,11 @@ + + + + TestThing + +
  • + <{caret} +
  • + +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemProperties.xml.gold new file mode 100644 index 0000000..37ad97c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml new file mode 100644 index 0000000..34c8310 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml @@ -0,0 +1,11 @@ + + + + TestThing + +
  • + + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml.gold new file mode 100644 index 0000000..6e59e3d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestListItemWithClassProperties.xml.gold @@ -0,0 +1,16 @@ +Completion: Basic +Prefix: "always" +Count: 1 +Focus: Hard +Range: " + + + CustomThing + + + TestThing + {caret} + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefAsSuperclassReference.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefAsSuperclassReference.xml.gold new file mode 100644 index 0000000..060bf2f --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml new file mode 100644 index 0000000..8c51d4e --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml @@ -0,0 +1,7 @@ + + + + CustomThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml.gold new file mode 100644 index 0000000..3a865f6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModDefClassProperties.xml.gold @@ -0,0 +1,21 @@ +Completion: Basic +Prefix: "cus" +Count: 3 +Focus: Hard +Range: " + + + TestThing + +
  • + <{caret} +
  • +
    +
    +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModListItemClassProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestModListItemClassProperties.xml.gold new file mode 100644 index 0000000..72791cd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestNestedFieldProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestNestedFieldProperties.xml new file mode 100644 index 0000000..dc1cd61 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestNestedFieldProperties.xml @@ -0,0 +1,9 @@ + + + + TestThing + + <{caret} + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestNestedFieldProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestNestedFieldProperties.xml.gold new file mode 100644 index 0000000..ff4469c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestParentName.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestParentName.xml new file mode 100644 index 0000000..bac7c15 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestParentName.xml @@ -0,0 +1,13 @@ + + + + + + ConcreteThing + + + + + Child + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestParentName.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestParentName.xml.gold new file mode 100644 index 0000000..f44d911 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml new file mode 100644 index 0000000..5508cd4 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml @@ -0,0 +1,5 @@ + + + {caret} + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml.gold new file mode 100644 index 0000000..96458cb --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestStructPropertyValue.xml.gold @@ -0,0 +1,22 @@ +Completion: Basic +Count: 5 +Focus: Hard +Range: "" + +##### RELEVANCE SORT ##### + + [FromSingleCompletion, FromLightAndDynamicEvaluation, NormalSelectionPriority, Other] +East Rot4 <== +Invalid Rot4 +North Rot4 +South Rot4 +West Rot4 + +##### ALPHABETIC SORT ##### + + [Light, Generic] +East Rot4 <== +Invalid Rot4 +North Rot4 +South Rot4 +West Rot4 diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml new file mode 100644 index 0000000..c858832 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml @@ -0,0 +1,7 @@ + + + + TestThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml.gold new file mode 100644 index 0000000..f0d90ab --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/Rimworld/TestThingDefProperties.xml.gold @@ -0,0 +1,19 @@ +Completion: Basic +Prefix: "altit" +Count: 2 +Focus: Hard +Range: " + +##### ALPHABETIC SORT ##### + + [Light, Generic] +ạḷṭịṭudeLayer AltitudeLayer <== +blocksẠḷṭịṭudes List diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/Defs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/Defs.xml new file mode 100644 index 0000000..a04cf73 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/Defs.xml @@ -0,0 +1,12 @@ + + + + ModThingA + + + ModThingB + + + ModSound + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefDatabaseGetNamed.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefDatabaseGetNamed.cs new file mode 100644 index 0000000..8fb4d10 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefDatabaseGetNamed.cs.gold new file mode 100644 index 0000000..9cec850 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefix.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefix.cs new file mode 100644 index 0000000..34ea535 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefix.cs.gold new file mode 100644 index 0000000..f9f38c5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs new file mode 100644 index 0000000..19f7f30 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/RimworldCSharp/TestDefOfFieldWithPrefixAndSemicolon.cs.gold new file mode 100644 index 0000000..7aacee7 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/CompletionSuggestions/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/Generate/testGenerateInListItem.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateInListItem.xml new file mode 100644 index 0000000..b459e85 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Generate/testGenerateInListItem.xml @@ -0,0 +1,12 @@ + + + + + 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/Highlighting/TestInvalidValues.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidValues.xml new file mode 100644 index 0000000..37b376c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidValues.xml @@ -0,0 +1,65 @@ + + + + UnknownDefType + seven and a half + + + TestThing + + 1~2~3 + 75 + + maybe + 7.5 + lots + 1~2~3 + half~2 + (1,up) + Bulding + Sideways + + (one,two) + (one,two,three) + (one,two,three) + + lots + + lots + + lots + + + (1,wide) + (1,2) + lots + (one,two,three) + (1,up,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + lots +
  • + lots +
    + +
  • + lots +
  • +
    +
    + + TestThingSecondOperand + 1~two + 0.5~high + left,2 + + (1,2,down) + + +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidValues.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidValues.xml.gold new file mode 100644 index 0000000..aee75f6 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestInvalidValues.xml.gold @@ -0,0 +1,84 @@ + + + + UnknownDefType + seven and a half + + + TestThing + + 1~2~3 + 75 + + |maybe|(0) + |7.5|(1) + |lots|(2) + |1~2~3|(3) + |half~2|(4) + |(1,up)|(5) + |Bulding|(6) + |Sideways|(7) + + (one,two) + (one,two,three) + (one,two,three) + + lots + + lots + + |lots |(8) + + + |(1,wide)|(9) + |(1,2)|(10) + |lots|(11) + (one,two,three) + (1,up,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + |lots|(12) +
  • + lots +
    + +
  • + lots +
  • +
    +
    + + TestThingSecondOperand + |1~two|(13) + |0.5~high|(14) + |left,2|(15) + + |(1,2,down)|(16) + + +
    + +--------------------------------------------------------- +(0): ReSharper Underlined Error Highlighting: Value must be "true" or "false" +(1): ReSharper Underlined Error Highlighting: Value must be a whole number with no decimal points +(2): ReSharper Underlined Error Highlighting: Value must be a valid number +(3): ReSharper Underlined Error Highlighting: Your value must be two integers in a format similar to "1~2" +(4): ReSharper Underlined Error Highlighting: "half" is not a valid float +(5): ReSharper Underlined Error Highlighting: "up" is not a valid number +(6): ReSharper Underlined Error Highlighting: "Bulding" is not a valid value for ThingCategory +(7): ReSharper Underlined Error Highlighting: "Sideways" is not a valid value for Rot4 +(8): ReSharper Underlined Error Highlighting: Value must be a whole number with no decimal points +(9): ReSharper Underlined Error Highlighting: "wide" is not a valid number +(10): ReSharper Underlined Error Highlighting: Your value must be in a format similar to (1,2,3) +(11): ReSharper Underlined Error Highlighting: Value must be a valid number +(12): ReSharper Underlined Error Highlighting: Value must be a valid number +(13): ReSharper Underlined Error Highlighting: "two" is not a valid integer +(14): ReSharper Underlined Error Highlighting: "high" is not a valid float +(15): ReSharper Underlined Error Highlighting: "left" is not a valid number +(16): ReSharper Underlined Error Highlighting: "down" is not a valid number diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml new file mode 100644 index 0000000..6b7d5e3 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml @@ -0,0 +1,65 @@ + + + + UnknownDefType + 75 + + + TestThing + + 1~2~3 + 75 + + True + -75 + 1.5 + 2~8 + 0.5~1 + (0.5,-0.25) + Building + North + + (1,2) + (0,0,-1) + (1,1,1) + + 75 + + 75 + + 75 + + + (1,1) + (0,0.2,0) + 12.5 + (0.5,0.5,0.5) + (0,0.2,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + 100 +
  • + 100 +
    + +
  • + 100 +
  • +
    +
    + + TestThingSecondOperand + 4 + 2 + 1.5 + + (0,0,0) + + +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml.gold new file mode 100644 index 0000000..f820948 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestValidValues.xml.gold @@ -0,0 +1,67 @@ + + + + UnknownDefType + 75 + + + TestThing + + 1~2~3 + 75 + + True + -75 + 1.5 + 2~8 + 0.5~1 + (0.5,-0.25) + Building + North + + (1,2) + (0,0,-1) + (1,1,1) + + 75 + + 75 + + 75 + + + (1,1) + (0,0.2,0) + 12.5 + (0.5,0.5,0.5) + (0,0.2,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + 100 +
  • + 100 +
    + +
  • + 100 +
  • +
    +
    + + TestThingSecondOperand + 4 + 2 + 1.5 + + (0,0,0) + + +
    + +--------------------------------------------------------- diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml new file mode 100644 index 0000000..37b376c --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml @@ -0,0 +1,65 @@ + + + + UnknownDefType + seven and a half + + + TestThing + + 1~2~3 + 75 + + maybe + 7.5 + lots + 1~2~3 + half~2 + (1,up) + Bulding + Sideways + + (one,two) + (one,two,three) + (one,two,three) + + lots + + lots + + lots + + + (1,wide) + (1,2) + lots + (one,two,three) + (1,up,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + lots +
  • + lots +
    + +
  • + lots +
  • +
    +
    + + TestThingSecondOperand + 1~two + 0.5~high + left,2 + + (1,2,down) + + +
    diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml.gold new file mode 100644 index 0000000..e2f6c34 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Highlighting/TestWithoutRimworld.xml.gold @@ -0,0 +1,67 @@ + + + + UnknownDefType + seven and a half + + + TestThing + + 1~2~3 + 75 + + maybe + 7.5 + lots + 1~2~3 + half~2 + (1,up) + Bulding + Sideways + + (one,two) + (one,two,three) + (one,two,three) + + lots + + lots + + lots + + + (1,wide) + (1,2) + lots + (one,two,three) + (1,up,0) + + + +
  • Artifact
  • +
    + +
  • + MaxHitPoints + lots +
  • + lots +
    + +
  • + lots +
  • +
    +
    + + TestThingSecondOperand + 1~two + 0.5~high + left,2 + + (1,2,down) + + +
    + +--------------------------------------------------------- diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/ModAssembly.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/ModAssembly.cs new file mode 100644 index 0000000..c35d192 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/ModAssembly.cs @@ -0,0 +1,5 @@ +namespace ModAssembly; + +public class ModEntry +{ +} diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml new file mode 100644 index 0000000..2a0c4d3 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/MovingDefs.xml @@ -0,0 +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/Navigation/TestNavigatePropertyToCSharpField.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml new file mode 100644 index 0000000..cf5b305 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml @@ -0,0 +1,7 @@ + + + + Referrer + LocalThing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.xml.gold new file mode 100644 index 0000000..37da6e1 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigatePropertyToCSharpField.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/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/Navigation/TestNavigateValueToXmlDef.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml new file mode 100644 index 0000000..c323ea9 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml @@ -0,0 +1,10 @@ + + + + LocalThing + + + Referrer + Local{on}Thing + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.xml.gold new file mode 100644 index 0000000..25a4ac5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/Navigation/TestNavigateValueToXmlDef.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) @ TestNavigateValueToXmlDef.xml + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateValueToXmlDef.xml + +## FindUsagesProvider activity: + FindResults window with 2 results + 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) @ TestNavigateValueToXmlDef.xml + Navigation result: + opened file: TestNavigateValueToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + +## GotoImplementationProvider activity: + Immediate result: + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml + Navigation result: + opened file: TestNavigateValueToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + +## ShowUsagesProvider activity: + Async context menu shown `Usages of 'ThingDef/LocalThing'`: + TO: [O] |LocalThing| RANGE: (78,88) @ TestNavigateValueToXmlDef.xml + Menu item (enabled) : + icon: UsageOther + text: TestNavigateValueToXmlDef.xml **LocalThing** (4) + tail: in + tooltip: **LocalThing** + Navigation result: + opened file: TestNavigateValueToXmlDef.xml + ------------------ + + + |CARET|LocalThing + + + ------------------ + + TO: [O] |LocalThing| RANGE: (187,197) @ TestNavigateValueToXmlDef.xml + Menu item (enabled) : + icon: UsageOther + text: TestNavigateValueToXmlDef.xml **LocalThing** (8) + tail: in + tooltip: **LocalThing** + Navigation result: + caret did not move + 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 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/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/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/TestCSharpToXmlDef.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs new file mode 100644 index 0000000..232091d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.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/TestCSharpToXmlDef.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold new file mode 100644 index 0000000..f943c76 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestCSharpToXmlDef.cs.gold @@ -0,0 +1,27 @@ +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 b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml new file mode 100644 index 0000000..8de3ff4 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml @@ -0,0 +1,19 @@ + + + + + Building + + Things/Test + + +
  • + CompGlower +
  • +
  • + 5 +
  • +
    + 1 +
    +
    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 new file mode 100644 index 0000000..10fb5d4 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToCSharp.xml.gold @@ -0,0 +1,30 @@ + + + <|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 b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml new file mode 100644 index 0000000..e6e6459 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml @@ -0,0 +1,18 @@ + + + + + + LocalThing + + + Referrer + LocalThing + true + OtherSound + MissingSound + +
  • OtherStuff
  • +
    +
    +
    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 new file mode 100644 index 0000000..1791b75 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/References/TestXmlToXmlDef.xml.gold @@ -0,0 +1,38 @@ + + + <|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 diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/CSharp/TestLocalVariable.cs b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/CSharp/TestLocalVariable.cs new file mode 100644 index 0000000..96bfa12 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/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/SmokeTests/CSharp/TestLocalVariable.cs.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/CSharp/TestLocalVariable.cs.gold new file mode 100644 index 0000000..fe33494 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/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/SmokeTests/Xml/XmlIsParsed.xml b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/Xml/XmlIsParsed.xml new file mode 100644 index 0000000..f2dd956 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/Xml/XmlIsParsed.xml @@ -0,0 +1,4 @@ + + text + <{caret} + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/Xml/XmlIsParsed.xml.gold b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/Xml/XmlIsParsed.xml.gold new file mode 100644 index 0000000..f77782b --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/test/data/SmokeTests/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/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/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 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..57d55e8 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); @@ -317,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 +}