diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ab953..3285d76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2023.1.1 +* Taking some advice from the Jetbrains team, I've pushed the XML Def detection/storage into a `SimpleICache`, massively improving performance + ## 2023.1 * The first release of this plugin. * On top of the features from the Alphas, it also includes automatically detecting and using Rimworlds `Assembly-CSharp.dll` if it's not already part of your project diff --git a/build.gradle b/build.gradle index 1467f17..6669d98 100644 --- a/build.gradle +++ b/build.gradle @@ -160,16 +160,6 @@ rdgen { } } -patchPluginXml { - // TODO: See also org.jetbrains.changelog: https://github.com/JetBrains/gradle-changelog-plugin - def changelogText = file("${rootDir}/CHANGELOG.md").text - def changelogMatches = changelogText =~ /(?s)(-.+?)(?=##|$)/ - - changeNotes = changelogMatches.collect { - it[1].replaceAll(/(?s)\r?\n/, "
\n") - }.take(1).join('') -} - prepareSandbox { dependsOn compileDotNet diff --git a/buildPlugin.ps1 b/buildPlugin.ps1 index 6631623..5b4ee26 100644 --- a/buildPlugin.ps1 +++ b/buildPlugin.ps1 @@ -1,5 +1,5 @@ Param( - $Version = "2023.1.0" + $Version = "2023.1.1" ) Set-StrictMode -Version Latest diff --git a/global.json b/global.json new file mode 100644 index 0000000..7f412b9 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.202", + "rollForward": "latestMajor", + "allowPrerelease": true + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index f069c51..37eb0da 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ DotnetPluginId=ReSharperPlugin.RimworldDev DotnetSolution=ReSharperPlugin.RimworldDev.sln RiderPluginId=com.jetbrains.rider.plugins.rimworlddev -PluginVersion=2023.1.0 +PluginVersion=2023.1.1 BuildConfiguration=Release diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..ccebba7 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..bbfe5a5 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://cache-redirector.jetbrains.com/services.gradle.org/distributions/gradle-7.3-all.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj index c83b28d..471c655 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs index a80cb7b..12c8fd8 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs @@ -2,15 +2,16 @@ using System.Linq; using JetBrains.DataFlow; using JetBrains.Lifetimes; +using JetBrains.ProjectModel; using JetBrains.ReSharper.Psi; using JetBrains.ReSharper.Psi.Caches; -using JetBrains.ReSharper.Psi.Modules; using JetBrains.ReSharper.Psi.Resolve; using JetBrains.ReSharper.Psi.Tree; using JetBrains.ReSharper.Psi.Util; using JetBrains.ReSharper.Psi.Xml; using JetBrains.ReSharper.Psi.Xml.Impl.Tree; using JetBrains.ReSharper.Psi.Xml.Tree; +using ReSharperPlugin.RimworldDev.SymbolScope; namespace ReSharperPlugin.RimworldDev.TypeDeclaration; @@ -91,12 +92,12 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol !classContext.GetAllSuperTypes().Any(superType => superType.GetClrName().FullName == "Verse.Def")) return new ReferenceCollection(); - var tagId = $"{classContext.ShortName}/{element.GetText()}"; - if (!RimworldXMLDefUtil.DefTags.ContainsKey(tagId)) return new ReferenceCollection(); - - var tag = RimworldXMLDefUtil.DefTags[tagId]; - + var xmlSymbolTable = element.GetSolution().GetComponent(); + var tagId = $"{classContext.ShortName}/{element.GetText()}"; + if (xmlSymbolTable.GetTagByDef(classContext.ShortName, element.GetText()) is not { } tag) + return new ReferenceCollection(); + return new ReferenceCollection(new RimworldXmlDefReference(element, tag.GetNestedTags("defName").FirstOrDefault() ?? tag, tagId)); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworlXMLCompletionContextProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworlXMLCompletionContextProvider.cs index 4e10eab..1431573 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworlXMLCompletionContextProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworlXMLCompletionContextProvider.cs @@ -59,11 +59,6 @@ public override ISpecificCodeCompletionContext GetCompletionContext(CodeCompleti if (treeNode == null) return (ISpecificCodeCompletionContext) null; TreeTextRange treeRange = reference == null ? XmlCodeCompletionContextProvider.GetElementRange(treeNode) : reference.GetTreeTextRange(); - - if (1 == 1) - { - RimworldXMLDefUtil.UpdateDefs(xmlFile.GetSolution()); - } DocumentRange documentRange = unterminatedContext.ToDocumentRange(treeRange); if (!documentRange.IsValid()) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLDefUtil.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLDefUtil.cs deleted file mode 100644 index 82384ec..0000000 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLDefUtil.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using JetBrains.ProjectModel; -using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.Xml.Tree; -using JetBrains.Util; - -namespace ReSharperPlugin.RimworldDev; - -public class RimworldXMLDefUtil -{ - public static Dictionary DefTags = new(); - - public static void UpdateDefs(ISolution solution) - { - foreach (var project in solution.GetAllProjects()) - { - var files = project.GetAllProjectFiles(file => file.LanguageType.Name == "XML"); - - foreach (var projectFile in files) - { - if (projectFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) continue; - - var tags = xmlFile.GetNestedTags("Defs/*").Where(tag => - { - var defNameTag = tag.GetNestedTags("defName").FirstOrDefault(); - if (defNameTag is null) return false; - - return true; - }); - - foreach (var tag in tags) - { - var defName = tag.GetNestedTags("defName").FirstOrDefault()?.InnerText; - if (defName is null || DefTags.ContainsKey($"{tag.GetTagName()}/{defName}")) continue; - - DefTags.Add($"{tag.GetTagName()}/{defName}", tag); - } - } - } - } -} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs index 2c054cb..baa4d97 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs @@ -1,11 +1,10 @@ -using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using JetBrains.Application.Progress; +using JetBrains.ProjectModel; using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure; using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure.LookupItems; -using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure.LookupItems.Impl; using JetBrains.ReSharper.Feature.Services.Navigation.Requests; using JetBrains.ReSharper.Feature.Services.Occurrences; using JetBrains.ReSharper.Features.Intellisense.CodeCompletion.CSharp; @@ -20,7 +19,7 @@ using JetBrains.ReSharper.Psi.VB.Util; using JetBrains.ReSharper.Psi.Xml; using JetBrains.ReSharper.Psi.Xml.Impl.Tree; -using JetBrains.Util; +using ReSharperPlugin.RimworldDev.SymbolScope; using ReSharperPlugin.RimworldDev.TypeDeclaration; namespace ReSharperPlugin.RimworldDev; @@ -179,13 +178,15 @@ protected void AddTextLookupItems(RimworldXmlCodeCompletionContext context, IIte var className = classContext.ShortName; - var keys = RimworldXMLDefUtil.DefTags.Keys + var xmlSymbolTable = context.TreeNode!.GetSolution().GetSolution().GetComponent(); + + var keys = xmlSymbolTable.DefTags.Keys .Where(key => key.StartsWith($"{className}/")) .Select(key => key.Substring(className.Length + 1)); foreach (var key in keys) { - var item = RimworldXMLDefUtil.DefTags[$"{className}/{key}"]; + var item = xmlSymbolTable.GetTagByDef(className, key); var lookup = LookupFactory.CreateDeclaredElementLookupItem(context, key, new DeclaredElementInstance(new XMLTagDeclaredElement(item, key, false))); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs new file mode 100644 index 0000000..c0bc0ed --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains; +using JetBrains.Annotations; +using JetBrains.Application.Threading; +using JetBrains.Collections; +using JetBrains.Lifetimes; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Caches; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Xml.Tree; + +namespace ReSharperPlugin.RimworldDev.SymbolScope; + +[PsiComponent] +public class RimworldSymbolScope : SimpleICache> +{ + public Dictionary DefTags = new(); + + public RimworldSymbolScope + (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, long? version = null) + : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version) + { + } + + protected override bool IsApplicable(IPsiSourceFile sourceFile) + { + return base.IsApplicable(sourceFile) && sourceFile.LanguageType.Name == "XML"; + } + + [CanBeNull] + public IXmlTag GetTagByDef(string defType, string defName) + { + return GetTagByDef($"{defType}/{defName}"); + } + + [CanBeNull] + public IXmlTag GetTagByDef(string defId) + { + if (!DefTags.ContainsKey(defId)) + return null; + + return DefTags[defId]; + } + + public override object Build(IPsiSourceFile sourceFile, bool isStartup) + { + if (!IsApplicable(sourceFile)) + return null; + + if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return null; + + var tags = xmlFile.GetNestedTags("Defs/*").Where(tag => + { + var defNameTag = tag.GetNestedTags("defName").FirstOrDefault(); + return defNameTag is not null; + }); + + List defs = new(); + + foreach (var tag in tags) + { + var defName = tag.GetNestedTags("defName").FirstOrDefault()?.InnerText; + if (defName is null) continue; + + defs.Add(new RimworldXmlDefSymbol(tag, defName, tag.GetTagName())); + } + + return defs; + } + + public override void Merge(IPsiSourceFile sourceFile, object builtPart) + { + RemoveFromLocalCache(sourceFile); + AddToLocalCache(sourceFile, builtPart as List); + base.Merge(sourceFile, builtPart); + } + + public override void MergeLoaded(object data) + { + PopulateLocalCache(); + base.MergeLoaded(data); + } + + public override void Drop(IPsiSourceFile sourceFile) + { + RemoveFromLocalCache(sourceFile); + base.Drop(sourceFile); + } + + private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List cacheItem) + { + if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return; + + cacheItem?.ForEach(item => + { + if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) + DefTags.Add($"{item.DefType}/{item.DefName}", xmlFile.GetNestedTags("Defs/*").FirstOrDefault(tag => tag.GetTreeStartOffset().Offset == item.DocumentOffset)); + }); + } + + private void RemoveFromLocalCache(IPsiSourceFile sourceFile) + { + var items = Map!.GetValueSafe(sourceFile); + + items?.ForEach(item => + { + if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) + DefTags.Remove($"{item.DefType}/{item.DefName}"); + }); + } + + private void PopulateLocalCache() + { + foreach (var (sourceFile, cacheItem) in Map) + AddToLocalCache(sourceFile, cacheItem); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs new file mode 100644 index 0000000..9bf0b60 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using JetBrains.ReSharper.Psi.Xml.Tree; +using JetBrains.Serialization; +using JetBrains.Util.PersistentMap; + +namespace ReSharperPlugin.RimworldDev.SymbolScope; + +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(IXmlTag tag, string defName, string defType) + { + DefName = defName; + DefType = defType; + DocumentOffset = tag.GetTreeStartOffset().Offset; + } + + public RimworldXmlDefSymbol(int documentOffset, string defName, string defType) + { + DefName = defName; + DefType = defType; + DocumentOffset = documentOffset; + } + + private static RimworldXmlDefSymbol Read(UnsafeReader reader) + { + var defType = reader.ReadString(); + var defName = reader.ReadString(); + var documentOffset = reader.ReadInt(); + + return new RimworldXmlDefSymbol(documentOffset, defName, defType); + } + + private static void Write(UnsafeWriter writer, RimworldXmlDefSymbol value) + { + writer.Write(value.DefType); + writer.Write(value.DefName); + writer.Write(value.DocumentOffset); + } +} \ No newline at end of file diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml index 8cf3121..9e55e50 100644 --- a/src/rider/main/resources/META-INF/plugin.xml +++ b/src/rider/main/resources/META-INF/plugin.xml @@ -16,7 +16,7 @@ in your mods!

First release, first set of features

+

Massive performance update that stems from better caching of XML Defs

]]>