From 6a32107e6fc62d50b58cb01551ebafacbc24d8bd Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 17 Apr 2026 19:02:19 +0100 Subject: [PATCH 1/5] Include a symbol scope for keyed translations, autocomplete in the C# and references back to the XML --- .../.idea/.idea.AshAndDust/.idea/.name | 1 - .../.idea.AshAndDust/.idea/encodings.xml | 4 - .../.idea.AshAndDust/.idea/indexLayout.xml | 8 - .../.idea/.idea.AshAndDust/.idea/vcs.xml | 6 - ...orldKeyedTranslationsCSharpItemProvider.cs | 58 ++++++ .../RimworldCSharpKeyedTranslationProvider.cs | 55 ++++++ .../RimworldKeyedTranslationReference.cs | 62 +++++++ .../ScopeHelper.cs | 15 +- .../RimworldKeyedTranslationSymbol.cs | 47 +++++ .../RimworldKeyedTranslationSymbolScope.cs | 170 ++++++++++++++++++ .../SymbolScope/RimworldSymbolScope.cs | 11 +- .../SymbolScope/RimworldXmlDefSymbol.cs | 2 - .../TypeDeclaration/XMLTagDeclaredElement.cs | 10 +- 13 files changed, 416 insertions(+), 33 deletions(-) delete mode 100644 example-mod/.idea/.idea.AshAndDust/.idea/.name delete mode 100644 example-mod/.idea/.idea.AshAndDust/.idea/encodings.xml delete mode 100644 example-mod/.idea/.idea.AshAndDust/.idea/indexLayout.xml delete mode 100644 example-mod/.idea/.idea.AshAndDust/.idea/vcs.xml create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldKeyedTranslationReference.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbol.cs create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs diff --git a/example-mod/.idea/.idea.AshAndDust/.idea/.name b/example-mod/.idea/.idea.AshAndDust/.idea/.name deleted file mode 100644 index 82c5ebf..0000000 --- a/example-mod/.idea/.idea.AshAndDust/.idea/.name +++ /dev/null @@ -1 +0,0 @@ -AshAndDust \ No newline at end of file diff --git a/example-mod/.idea/.idea.AshAndDust/.idea/encodings.xml b/example-mod/.idea/.idea.AshAndDust/.idea/encodings.xml deleted file mode 100644 index df87cf9..0000000 --- a/example-mod/.idea/.idea.AshAndDust/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/example-mod/.idea/.idea.AshAndDust/.idea/indexLayout.xml b/example-mod/.idea/.idea.AshAndDust/.idea/indexLayout.xml deleted file mode 100644 index 7b08163..0000000 --- a/example-mod/.idea/.idea.AshAndDust/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/example-mod/.idea/.idea.AshAndDust/.idea/vcs.xml b/example-mod/.idea/.idea.AshAndDust/.idea/vcs.xml deleted file mode 100644 index 6c0b863..0000000 --- a/example-mod/.idea/.idea.AshAndDust/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs new file mode 100644 index 0000000..f7d1069 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs @@ -0,0 +1,58 @@ +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure; +using JetBrains.ReSharper.Feature.Services.CodeCompletion.Infrastructure.LookupItems; +using JetBrains.ReSharper.Feature.Services.CSharp.CodeCompletion.Infrastructure; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Impl.Tree; +using JetBrains.ReSharper.Psi.CSharp.Parsing; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Tree; +using ReSharperPlugin.RimworldDev.SymbolScope; +using ReSharperPlugin.RimworldDev.TypeDeclaration; + +namespace ReSharperPlugin.RimworldDev.ItemCompletion; + +[Language(typeof(CSharpLanguage))] +public class RimworldKeyedTranslationsCSharpItemProvider: ItemsProviderOfSpecificContext +{ + private static RimworldCSharpLookupFactory LookupFactory = new(); + + protected override bool IsAvailable(CSharpCodeCompletionContext context) + { + var node = context.NodeInFile; + if (!node.Language.IsLanguage(CSharpLanguage.Instance)) return false; + if (node is not CSharpGenericToken) return false; + if (node.NodeType != CSharpTokenType.STRING_LITERAL_REGULAR) return false; + + return true; + } + + protected override bool AddLookupItems(CSharpCodeCompletionContext context, IItemsCollector collector) + { + var node = context.NodeInFile; + + if (node.Parent?.NextSibling?.NextSibling is not ICSharpIdentifier identifier || + identifier.GetText() != "Translate") + return false; + + var xmlSymbolTable = context.NodeInFile.GetSolution().GetComponent(); + + foreach (var key in xmlSymbolTable.GetKeys()) + { + var keyTag = xmlSymbolTable.GetKeyTag(key); + if (keyTag == null) continue; + + var lookup = LookupFactory.CreateDeclaredElementLookupItem( + context, + key, + new DeclaredElementInstance(new XMLTagDeclaredElement(keyTag, $"English/{key}", false)) + ); + + collector.Add(lookup); + } + + return base.AddLookupItems(context, collector); + } + +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs new file mode 100644 index 0000000..0f1dab7 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs @@ -0,0 +1,55 @@ +using JetBrains.DataFlow; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Caches; +using JetBrains.ReSharper.Psi.CSharp; +using JetBrains.ReSharper.Psi.CSharp.Tree; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Util; +using ReSharperPlugin.RimworldDev.SymbolScope; + +namespace ReSharperPlugin.RimworldDev.TypeDeclaration; + +[ReferenceProviderFactory] +public class RimworldCSharpKeyedTranslationProvider : IReferenceProviderFactory +{ + public RimworldCSharpKeyedTranslationProvider(Lifetime lifetime) => + Changed = new Signal(lifetime, GetType().FullName); + + public IReferenceFactory CreateFactory(IPsiSourceFile sourceFile, IFile file, IWordIndex wordIndexForChecks) + { + return sourceFile.PrimaryPsiLanguage.Is() + ? new RimworldCSharpKeyedTranslationReferenceFactory() + : null; + } + + public ISignal Changed { get; } +} + +public class RimworldCSharpKeyedTranslationReferenceFactory : IReferenceFactory +{ + public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection oldReferences) + { + if (element is not ICSharpLiteralExpression || + element.NextSibling?.NextSibling is not ICSharpIdentifier identifier || + identifier.GetText() != "Translate") + return new ReferenceCollection(); + + var key = element.GetUnquotedText(); + var xmlSymbolTable = element.GetSolution().GetComponent(); + + var tag = xmlSymbolTable.GetKeyTag(key); + if (tag is null) + return new ReferenceCollection(); + + return new ReferenceCollection(new RimworldKeyedTranslationReference(element, tag, "English", key)); + } + + public bool HasReference(ITreeNode element, IReferenceNameContainer names) + { + if (element.NodeType.ToString() != "TEXT") return false; + return !element.Parent.GetText().Contains("defName") && names.Contains(element.GetText()); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldKeyedTranslationReference.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldKeyedTranslationReference.cs new file mode 100644 index 0000000..6c04ec5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldKeyedTranslationReference.cs @@ -0,0 +1,62 @@ +using JetBrains.Annotations; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using ReSharperPlugin.RimworldDev.SymbolScope; + +namespace ReSharperPlugin.RimworldDev.TypeDeclaration; + +public class RimworldKeyedTranslationReference : TreeReferenceBase +{ + private readonly ITreeNode myTypeElement; + + private string myName; + private string keyName; + private string language; + + public RimworldKeyedTranslationReference([NotNull] ITreeNode owner, ITreeNode typeElement, string laguage, string keyName) : base(owner) + { + myTypeElement = typeElement; + myName = $"{laguage}/{keyName}"; + this.keyName = keyName; + this.language = laguage; + } + + public override ISymbolTable GetReferenceSymbolTable(bool useReferenceName) + { + var symbolScope = myOwner.GetSolution().GetComponent(); + + symbolScope.AddDeclaredElement( + myOwner.GetSolution(), + myTypeElement, + language, + keyName, + false + ); + + return symbolScope.GetSymbolTable(myOwner.GetSolution()); + } + + public override ResolveResultWithInfo ResolveWithoutCache() + { + return GetReferenceSymbolTable(true).GetResolveResult(GetName()); + } + + public override string GetName() => myName; + + public override IReference BindTo(IDeclaredElement element) => + BindTo(element, EmptySubstitution.INSTANCE); + + public override IReference BindTo( + IDeclaredElement element, + ISubstitution substitution) + { + return this; + } + + public override TreeTextRange GetTreeTextRange() => myOwner.GetTreeTextRange(); + + public override IAccessContext GetAccessContext() => new ElementAccessContext(myOwner); +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs index 57660cd..d43d313 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Xml; using JetBrains.Annotations; -using JetBrains.Application.Threading.Tasks; using JetBrains.Metadata.Reader.API; using JetBrains.ProjectModel; using JetBrains.ProjectModel.model2.Assemblies.Interfaces; @@ -12,8 +11,8 @@ using JetBrains.ReSharper.Psi.Caches; using JetBrains.ReSharper.Psi.Modules; using JetBrains.ReSharper.Psi.Util; +using JetBrains.ReSharper.Resources.Shell; using JetBrains.Util; -using JetBrains.Util.Threading.Tasks; using ReSharperPlugin.RimworldDev.Settings; namespace ReSharperPlugin.RimworldDev; @@ -48,11 +47,14 @@ public static bool UpdateScopes(ISolution solution) if (rimworldScope == null) { - AddRef(solution); + using (WriteLockCookie.Create()) + { + AddRef(solution); + } return false; } - + rimworldModule = solution.PsiModules().GetModules() .First(module => module.GetPsiServices().Symbols.GetSymbolScope(module, true, true) @@ -90,8 +92,9 @@ private static async void AddRef(ISolution solution) var moduleReferenceResolveContext = (IModuleReferenceResolveContext)UniversalModuleReferenceContext.Instance; - await solution.Locks.Tasks.YieldTo(solution.GetSolutionLifetimes().MaximumLifetime, Scheduling.MainDispatcher, - TaskPriority.Low); + // await solution.Locks.Tasks.YieldTo(solution.GetSolutionLifetimes().MaximumLifetime, + // Scheduling.MainDispatcher, + // TaskPriority.Low); solution.GetComponent().AddRef(path.ToAssemblyLocation(), "ScopeHelper::AddRef", moduleReferenceResolveContext); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbol.cs new file mode 100644 index 0000000..c345768 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbol.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.Serialization; +using JetBrains.Util.PersistentMap; + +namespace ReSharperPlugin.RimworldDev.SymbolScope; + +public class RimworldKeyedTranslationSymbol +{ + public static readonly IUnsafeMarshaller> Marshaller = + UnsafeMarshallers.GetCollectionMarshaller(new UniversalMarshaller(Read, Write), (size) => new List()); + + public string KeyName { get; } + public string Langauge { get; } + + public int DocumentOffset { get; } + + public RimworldKeyedTranslationSymbol(ITreeNode tag, string keyName, string language) + { + KeyName = keyName; + Langauge = language; + DocumentOffset = tag.GetTreeStartOffset().Offset; + } + + public RimworldKeyedTranslationSymbol(int documentOffset, string keyName, string language) + { + KeyName = keyName; + Langauge = language; + DocumentOffset = documentOffset; + } + + private static RimworldKeyedTranslationSymbol Read(UnsafeReader reader) + { + var keyName = reader.ReadString(); + var langauge = reader.ReadString(); + var documentOffset = reader.ReadInt(); + + return new RimworldKeyedTranslationSymbol(documentOffset, langauge, keyName); + } + + private static void Write(UnsafeWriter writer, RimworldKeyedTranslationSymbol value) + { + writer.Write(value.KeyName); + writer.Write(value.Langauge); + writer.Write(value.DocumentOffset); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs new file mode 100644 index 0000000..656828a --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs @@ -0,0 +1,170 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains; +using JetBrains.Annotations; +using JetBrains.Application.Parts; +using JetBrains.Application.Threading; +using JetBrains.Lifetimes; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Caches; +using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Xml.Tree; +using ReSharperPlugin.RimworldDev.TypeDeclaration; + +namespace ReSharperPlugin.RimworldDev.SymbolScope; + +[PsiComponent(Instantiation.ContainerAsyncPrimaryThread)] +public class RimworldKeyedTranslationSymbolScope: SimpleICache> +{ + private Dictionary> KeyedTranslations = new(); + private Dictionary _declaredElements = new(); + private SymbolTable _symbolTable; + + public List GetKeys() + { + if (!KeyedTranslations.ContainsKey("English")) + KeyedTranslations["English"] = new Dictionary(); + + return KeyedTranslations["English"].Keys.ToList(); + } + + public IXmlTag GetKeyTag(string key) + { + if (!KeyedTranslations.ContainsKey("English")) + KeyedTranslations["English"] = new Dictionary(); + + if (!KeyedTranslations["English"].ContainsKey(key)) + return null; + + return KeyedTranslations["English"][key]; + } + + public RimworldKeyedTranslationSymbolScope( + Lifetime lifetime, + [NotNull] IShellLocks locks, + [NotNull] IPersistentIndexManager persistentIndexManager, + long? version = null + ) : base(lifetime, locks, persistentIndexManager, RimworldKeyedTranslationSymbol.Marshaller, version) + { + } + + 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("LanguageData/*").Where(tag => true); + + var symbols = tags.Select(tag => new RimworldKeyedTranslationSymbol( + tag, + tag.GetName().XmlName, + "English" + )).ToList(); + + if (symbols.Count == 0) return null; + return symbols; + } + + 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 RemoveFromLocalCache(IPsiSourceFile sourceFile) + { + var items = Map!.GetValueSafe(sourceFile); + + items?.ForEach(item => + { + if (KeyedTranslations.ContainsKey(item.Langauge) && KeyedTranslations[item.Langauge].ContainsKey(item.KeyName)) + KeyedTranslations[item.Langauge].Remove(item.KeyName); + }); + } + + private void PopulateLocalCache() + { + foreach (var (sourceFile, cacheItem) in Map) + AddToLocalCache(sourceFile, cacheItem); + } + + private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List cacheItem) + { + if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return; + + cacheItem?.ForEach(item => + { + var matchingItem = xmlFile.GetNestedTags($"LanguageData/{item.KeyName}").FirstOrDefault(); + + if (matchingItem is null) return; + + AddKeyToList(item, matchingItem); + }); + } + + private void AddKeyToList(RimworldKeyedTranslationSymbol item, IXmlTag xmlTag) + { + if (!KeyedTranslations.ContainsKey(item.Langauge)) + KeyedTranslations[item.Langauge] = new Dictionary(); + + if (!KeyedTranslations[item.Langauge].ContainsKey(item.KeyName)) + KeyedTranslations[item.Langauge].Add(item.KeyName, xmlTag); + else + KeyedTranslations[item.Langauge][item.KeyName] = xmlTag; + } + + protected override bool IsApplicable(IPsiSourceFile sourceFile) + { + return base.IsApplicable(sourceFile) && sourceFile.LanguageType.Name == "XML"; + } + + public void AddDeclaredElement( + ISolution solution, + ITreeNode owner, + string language, + string keyName, + bool caseSensitiveName) + { + if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices()); + + if (_declaredElements.ContainsKey($"{language}/{keyName}")) + { + _declaredElements[$"{language}/{keyName}"].Update(owner); + return; + } + + var declaredElement = new XMLTagDeclaredElement( + owner, + $"{language}/{keyName}", + caseSensitiveName + ); + + // @TODO: We seem to get "Key Already Exists" errors. Race condition? + _declaredElements.Add($"{language}/{keyName}", declaredElement); + _symbolTable.AddSymbol(declaredElement); + } + + public ISymbolTable GetSymbolTable(ISolution solution) + { + if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices()); + + return _symbolTable; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs index f3b434a..45a6dc7 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs @@ -3,9 +3,7 @@ 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; @@ -43,9 +41,12 @@ public class RimworldSymbolScope : SimpleICache> private SymbolTable _symbolTable; public RimworldSymbolScope - (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, - long? version = null) - : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version) + ( + Lifetime lifetime, + [NotNull] IShellLocks locks, + [NotNull] IPersistentIndexManager persistentIndexManager, + long? version = null + ) : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version) { } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs index fd58ba0..e4bbbde 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs @@ -15,8 +15,6 @@ public class RimworldXmlDefSymbol public int DocumentOffset { get; } - // public IXmlTag Tag { get; } - public RimworldXmlDefSymbol(ITreeNode tag, string defName, string defType) { DefName = defName; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs index 63c9121..7010924 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Xml; using JetBrains.ReSharper.Psi; using JetBrains.ReSharper.Psi.Tree; @@ -22,6 +21,15 @@ public XMLTagDeclaredElement(ITreeNode owner, string defType, string defName, bo PresentationLanguage = owner.Language; } + public XMLTagDeclaredElement(ITreeNode owner, string keyName, bool caseSensitiveName) + { + this.owner = owner; + myPsiServices = owner.GetPsiServices(); + ShortName = $"{keyName}"; + CaseSensitiveName = caseSensitiveName; + PresentationLanguage = owner.Language; + } + public void Update(ITreeNode newOwner) { owner = newOwner; From c25aeae5511966dbb4b35ae0624e334dd090c485 Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 17 Apr 2026 20:17:35 +0100 Subject: [PATCH 2/5] Make it easier to swap between Rider versions --- example-mod/Source/AshAndDust.csproj | 4 +- .../RimworldXmlProjectHost.cs | 53 ++++++++++++------- 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/example-mod/Source/AshAndDust.csproj b/example-mod/Source/AshAndDust.csproj index 5f458eb..e62d280 100644 --- a/example-mod/Source/AshAndDust.csproj +++ b/example-mod/Source/AshAndDust.csproj @@ -33,7 +33,7 @@ False - D:\SteamLibrary\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\Assembly-CSharp.dll + ..\..\..\RimWorldWin64_Data\Managed\Assembly-CSharp.dll False @@ -41,7 +41,7 @@ - D:\SteamLibrary\steamapps\common\RimWorld\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll + ..\..\..\RimWorldWin64_Data\Managed\UnityEngine.CoreModule.dll False diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXmlProject/RimworldXmlProjectHost.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXmlProject/RimworldXmlProjectHost.cs index 4b71ed0..8af9cfc 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXmlProject/RimworldXmlProjectHost.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXmlProject/RimworldXmlProjectHost.cs @@ -26,7 +26,7 @@ public class RimworldXmlProjectHost : SolutionFileProjectHostBase private readonly FileSystemWildcardService myWildcardService; private readonly ProjectFilePropertiesFactory myProjectFilePropertiesFactory; private bool hasReloaded = false; - + public RimworldXmlProjectHost( IPlatformManager platformManager, ProjectFilePropertiesFactory projectFilePropertiesFactory, @@ -40,13 +40,18 @@ public RimworldXmlProjectHost( myStructureBuilder = new RimworldProjectStructureBuilder(myProjectFilePropertiesFactory); myWildcardService = wildcardService; } - + public override bool IsApplicable(IProjectMark projectMark) { return projectMark.Guid.ToString() == "f2a71f9b-5d33-465a-a702-920d77279781"; } - protected override void Reload(ProjectHostReloadChange change, VirtualFileSystemPath logPath) + // To swap between Rider 2025.3 and Rider 2026.1, swap the `override` between these two methods + protected void Reload(ProjectHostReloadChange change, FileSystemPath logPath) => Reload(change); + + protected override void Reload(ProjectHostReloadChange change, VirtualFileSystemPath logPath) => Reload(change); + + protected void Reload(ProjectHostReloadChange change) { if (change.ProjectMark is not RimworldProjectMark projectMark) return; @@ -70,26 +75,31 @@ protected override void Reload(ProjectHostReloadChange change, VirtualFileSystem { targetFramework }, defaultLanguage, EmptyList.InstanceList); - + // This is a quick fix suggested by Jetbrains to fix where Files/Folders get created when adding them to our project - var config = projectProperties.TryGetConfiguration(projectProperties.ActiveConfigurations.TargetFrameworkIds.FirstNotNull()); + var config = + projectProperties.TryGetConfiguration(projectProperties.ActiveConfigurations + .TargetFrameworkIds.FirstNotNull()); config?.UpdatePropertyCollection(x => x[MSBuildProjectUtil.BaseDirectoryProperty] = projectMark.Location.Parent.Parent.FullPath); - - var customDescriptor = new RimworldProjectDescriptor(projectMark.Guid, projectProperties, null, projectMark.Name, + + var customDescriptor = new RimworldProjectDescriptor(projectMark.Guid, projectProperties, null, + projectMark.Name, siteProjectLocation, projectMark.Location); var byProjectLocation = ProjectDescriptor.CreateWithoutItemsByProjectDescriptor(customDescriptor); - - myStructureBuilder.Build(byProjectLocation, ProjectFolderFilter.Instance, GetLoadFolders(projectMark.Location.Parent.Parent)); - myWildcardService.RegisterDirectory(projectMark, siteProjectLocation, targetFramework, ProjectFolderFilter.Instance); - + + myStructureBuilder.Build(byProjectLocation, ProjectFolderFilter.Instance, + GetLoadFolders(projectMark.Location.Parent.Parent)); + myWildcardService.RegisterDirectory(projectMark, siteProjectLocation, targetFramework, + ProjectFolderFilter.Instance); + change.Descriptors = new ProjectHostChangeDescriptors(byProjectLocation) { ProjectReferencesDescriptor = BuildReferences(targetFramework, projectMark) }; } - + private List GetLoadFolders(VirtualFileSystemPath basePath) { var loadFolders = new List(); @@ -109,7 +119,7 @@ private List GetLoadFolders(VirtualFileSystemPath basePath) } versionList.Sort(); - + var folderTags = document.GetElementsByTagName(versionList.Last())[0].ChildNodes; for (var i = 0; i < folderTags.Count; i++) { @@ -133,16 +143,22 @@ private static VirtualFileSystemPath GetProjectLocation([NotNull] IProjectMark p return location; } - private static ProjectReferencesDescriptor BuildReferences([NotNull] TargetFrameworkId targetFrameworkId, [NotNull] IProjectMark projectMark) + private static ProjectReferencesDescriptor BuildReferences([NotNull] TargetFrameworkId targetFrameworkId, + [NotNull] IProjectMark projectMark) { - if (projectMark is not RimworldProjectMark rimworldProjectMark || rimworldProjectMark.Dependencies.Count == 0) return null; + if (projectMark is not RimworldProjectMark rimworldProjectMark || + rimworldProjectMark.Dependencies.Count == 0) return null; var pairList = new List>(); rimworldProjectMark.Dependencies.ForEach(dependency => { - var reference = new ProjectToProjectReferenceBySearchDescriptor(targetFrameworkId, dependency.ToProjectSearchDescriptor()); - pairList.Add(new Pair(reference, ProjectReferenceProperties.Instance) ); + var reference = + new ProjectToProjectReferenceBySearchDescriptor(targetFrameworkId, + dependency.ToProjectSearchDescriptor()); + pairList.Add( + new Pair(reference, + ProjectReferenceProperties.Instance)); }); return new ProjectReferencesDescriptor(pairList); @@ -161,6 +177,7 @@ public bool Filter(VirtualFileSystemPath path) => path.Name.Equals("bin", StringComparison.OrdinalIgnoreCase) || path.Name.EndsWith(".DotSettings.user", StringComparison.OrdinalIgnoreCase) || path.Name.Equals("node_modules", StringComparison.OrdinalIgnoreCase) || - !new List {"About", "Defs", "Patches", "Languages", "Sounds", "Textures", "News"}.Any(it => path.FullPath.Contains(it)); + !new List { "About", "Defs", "Patches", "Languages", "Sounds", "Textures", "News" }.Any(it => + path.FullPath.Contains(it)); } } \ No newline at end of file From 58a53288ed45486ea474002e0db928bbbbbf1bc6 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 18 Apr 2026 00:06:18 +0100 Subject: [PATCH 3/5] Implement Find Usages on Keyed Translations and also extend Find Usages for XML Defs to find them in C# files --- .../Navigation/RimworldSearcherFactory.cs | 7 +- ...rldXmlKeyedTranslationReferenceProvider.cs | 64 +++++++++++++++++++ 2 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs index e9e1ece..256ab7f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.CSharp; using JetBrains.ReSharper.Psi.ExtensionsAPI; using JetBrains.ReSharper.Psi.Search; using JetBrains.ReSharper.Psi.Xml; @@ -10,7 +11,8 @@ namespace ReSharperPlugin.RimworldDev.Navigation; [PsiSharedComponent] public class RimworldSearcherFactory(SearchDomainFactory searchDomainFactory) : DomainSpecificSearcherFactoryBase { - public override bool IsCompatibleWithLanguage(PsiLanguageType languageType) => languageType.Is(); + public override bool IsCompatibleWithLanguage(PsiLanguageType languageType) => + languageType.Is() || languageType.Is(); public override ISearchDomain GetDeclaredElementSearchDomain(IDeclaredElement declaredElement) { @@ -21,8 +23,7 @@ public override IDomainSpecificSearcher CreateReferenceSearcher( IDeclaredElementsSet elements, ReferenceSearcherParameters referenceSearcherParameters) { - elements = new DeclaredElementsSet(elements.Where(element => - IsCompatibleWithLanguage(element.PresentationLanguage))); + elements = new DeclaredElementsSet(elements.Where(element => element.PresentationLanguage.Is())); return new CustomSearcher(this, elements, referenceSearcherParameters, false); } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs new file mode 100644 index 0000000..90dbe52 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs @@ -0,0 +1,64 @@ +using System; +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.Resolve; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Web.WebConfig; +using JetBrains.ReSharper.Psi.Xml; +using JetBrains.ReSharper.Psi.Xml.Impl.Tree; +using ReSharperPlugin.RimworldDev.SymbolScope; + +namespace ReSharperPlugin.RimworldDev.TypeDeclaration; + +[ReferenceProviderFactory] +public class RimworldXmlKeyedTranslationReferenceProviderFactory : IReferenceProviderFactory +{ + public RimworldXmlKeyedTranslationReferenceProviderFactory(Lifetime lifetime) => + Changed = new Signal(lifetime, GetType().FullName); + + public IReferenceFactory CreateFactory(IPsiSourceFile sourceFile, IFile file, IWordIndex wordIndexForChecks) + { + return sourceFile.PrimaryPsiLanguage.Is() && sourceFile.GetExtensionWithDot() + .Equals(".xml", StringComparison.CurrentCultureIgnoreCase) + ? new RimworldXmlKeyedTranslationReferenceProvider() + : null; + } + + public ISignal Changed { get; } +} + +// This reference provider only serves to attach a reference on the Keyed Translation to itself. It's a hacky workaround +// to allow us to do Find Usages on Keyed Translations +public class RimworldXmlKeyedTranslationReferenceProvider : IReferenceFactory +{ + public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection oldReferences) + { + if ( + element is not XmlIdentifier identifier || + identifier?.Parent?.Parent?.Parent is not XmlTag { } LangaugeDataTag || + LangaugeDataTag.Children().First() is not XmlTagHeaderNode languageDataHeaderNode || + languageDataHeaderNode.Children().ElementAt(1) is not XmlIdentifier languageDataIdentifier || + languageDataIdentifier.GetText() != "LanguageData" + ) + return new ReferenceCollection(); + + var keyName = identifier.GetText(); + var xmlSymbolTable = element.GetSolution().GetComponent(); + + var tag = xmlSymbolTable.GetKeyTag(keyName); + if (tag is null) + return new ReferenceCollection(); + + return new ReferenceCollection(new RimworldKeyedTranslationReference(element, tag, "English", keyName)); + } + + public bool HasReference(ITreeNode element, IReferenceNameContainer names) + { + if (element.NodeType.ToString() != "TEXT") return false; + return !element.Parent.GetText().Contains("defName") && names.Contains(element.GetText()); + } +} \ No newline at end of file From 6978d21b597ad9df9551e8dae87599248eff5236 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 18 Apr 2026 10:24:51 +0100 Subject: [PATCH 4/5] Fetch the keyed translations language from the file path instead of hardcoding it to English. Other places where we hardcode it will have to be changed as well. --- .../SymbolScope/RimworldKeyedTranslationSymbolScope.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs index 656828a..69602b7 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using JetBrains; using JetBrains.Annotations; using JetBrains.Application.Parts; @@ -56,13 +57,19 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup) { if (!IsApplicable(sourceFile)) return null; if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return null; + if (!sourceFile.DisplayName.Contains("Languages")) return null; + var languageMatch = Regex.Match(sourceFile.DisplayName, @"Languages\\(.*?)\\Keyed"); + if (!languageMatch.Success) return null; + + var language = languageMatch.Groups[1].Value; + var tags = xmlFile.GetNestedTags("LanguageData/*").Where(tag => true); var symbols = tags.Select(tag => new RimworldKeyedTranslationSymbol( tag, tag.GetName().XmlName, - "English" + language )).ToList(); if (symbols.Count == 0) return null; From 5159a9c9e175a243d632a57adc00835e48d1be14 Mon Sep 17 00:00:00 2001 From: Gareth Date: Sat, 18 Apr 2026 13:20:19 +0100 Subject: [PATCH 5/5] Support multiple languages for Keyed Translations --- ...orldKeyedTranslationsCSharpItemProvider.cs | 7 +- .../RimworldCSharpKeyedTranslationProvider.cs | 13 ++- ...rldXmlKeyedTranslationReferenceProvider.cs | 8 +- .../RimworldKeyedTranslationSymbolScope.cs | 105 +++++++++++++----- 4 files changed, 97 insertions(+), 36 deletions(-) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs index f7d1069..65c1637 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ItemCompletion/RimworldKeyedTranslationsCSharpItemProvider.cs @@ -40,13 +40,14 @@ protected override bool AddLookupItems(CSharpCodeCompletionContext context, IIte foreach (var key in xmlSymbolTable.GetKeys()) { - var keyTag = xmlSymbolTable.GetKeyTag(key); - if (keyTag == null) continue; + var nullableKeyTag = xmlSymbolTable.GetTranslationKey(key); + if (!nullableKeyTag.HasValue) continue; + var keyTag = nullableKeyTag.Value; var lookup = LookupFactory.CreateDeclaredElementLookupItem( context, key, - new DeclaredElementInstance(new XMLTagDeclaredElement(keyTag, $"English/{key}", false)) + new DeclaredElementInstance(new XMLTagDeclaredElement(keyTag.Tag, $"{keyTag.Language}/{keyTag.KeyName}", false)) ); collector.Add(lookup); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs index 0f1dab7..63c821f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldCSharpKeyedTranslationProvider.cs @@ -1,3 +1,4 @@ +using System.Linq; using JetBrains.DataFlow; using JetBrains.Lifetimes; using JetBrains.ProjectModel; @@ -40,11 +41,15 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection var key = element.GetUnquotedText(); var xmlSymbolTable = element.GetSolution().GetComponent(); - var tag = xmlSymbolTable.GetKeyTag(key); - if (tag is null) + if (!xmlSymbolTable.HasTranslationKey(key)) return new ReferenceCollection(); - - return new ReferenceCollection(new RimworldKeyedTranslationReference(element, tag, "English", key)); + + var tags = xmlSymbolTable.GetAllTagsForKey(key); + return new ReferenceCollection( + tags.Select(tag => + new RimworldKeyedTranslationReference(element, tag.Tag, tag.Language, tag.KeyName + )).ToList() + ); } public bool HasReference(ITreeNode element, IReferenceNameContainer names) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs index 90dbe52..a5691e6 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlKeyedTranslationReferenceProvider.cs @@ -49,11 +49,13 @@ element is not XmlIdentifier identifier || var keyName = identifier.GetText(); var xmlSymbolTable = element.GetSolution().GetComponent(); - var tag = xmlSymbolTable.GetKeyTag(keyName); - if (tag is null) + var nullableTag = xmlSymbolTable.GetTranslationKey(keyName); + if (nullableTag is null) return new ReferenceCollection(); - return new ReferenceCollection(new RimworldKeyedTranslationReference(element, tag, "English", keyName)); + var tag = nullableTag.Value; + + return new ReferenceCollection(new RimworldKeyedTranslationReference(element, tag.Tag, tag.Language, tag.KeyName)); } public bool HasReference(ITreeNode element, IReferenceNameContainer names) diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs index 69602b7..ee37b46 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldKeyedTranslationSymbolScope.cs @@ -18,30 +18,72 @@ namespace ReSharperPlugin.RimworldDev.SymbolScope; +public struct TranslationKey +{ + public TranslationKey(string language, string keyName, IXmlTag tag) + { + Language = language; + KeyName = keyName; + Tag = tag; + } + + public string Language { get; } + public string KeyName { get; } + + public IXmlTag Tag { get; } +} + [PsiComponent(Instantiation.ContainerAsyncPrimaryThread)] public class RimworldKeyedTranslationSymbolScope: SimpleICache> { - private Dictionary> KeyedTranslations = new(); - private Dictionary _declaredElements = new(); - private SymbolTable _symbolTable; + private Dictionary> keyedTranslations; + private Dictionary declaredElements = new(); + private SymbolTable symbolTable; + private string defaultLanguage = "English"; + private string ideLanguage = "English"; + public List GetKeys() { - if (!KeyedTranslations.ContainsKey("English")) - KeyedTranslations["English"] = new Dictionary(); - - return KeyedTranslations["English"].Keys.ToList(); + var keys = new List(); + foreach (var language in keyedTranslations.Values) + { + keys.AddRange(language.Keys); + } + + return keys.Distinct().ToList(); } - public IXmlTag GetKeyTag(string key) + public TranslationKey? GetTranslationKey(string key) { - if (!KeyedTranslations.ContainsKey("English")) - KeyedTranslations["English"] = new Dictionary(); + if (!keyedTranslations.ContainsKey(ideLanguage)) + keyedTranslations[ideLanguage] = new (); - if (!KeyedTranslations["English"].ContainsKey(key)) + if (!keyedTranslations[ideLanguage].ContainsKey(key)) return null; - return KeyedTranslations["English"][key]; + return keyedTranslations[ideLanguage][key]; + } + + public List GetAllTagsForKey(string key) + { + var tags = new List(); + + foreach (var language in keyedTranslations.Values) + { + if (!language.ContainsKey(key)) continue; + tags.Add(language[key]); + } + + return tags; + } + + public bool HasTranslationKey(string key) + { + if (keyedTranslations[ideLanguage].ContainsKey(key)) return true; + if (ideLanguage != defaultLanguage && keyedTranslations[defaultLanguage].ContainsKey(key)) return true; + + return keyedTranslations.Any(language => language.Value.ContainsKey(key)); } public RimworldKeyedTranslationSymbolScope( @@ -51,6 +93,15 @@ public RimworldKeyedTranslationSymbolScope( long? version = null ) : base(lifetime, locks, persistentIndexManager, RimworldKeyedTranslationSymbol.Marshaller, version) { + keyedTranslations = new() + { + { defaultLanguage, new() }, + }; + + if (defaultLanguage != ideLanguage) + { + keyedTranslations.Add(ideLanguage, new()); + } } public override object Build(IPsiSourceFile sourceFile, bool isStartup) @@ -101,8 +152,8 @@ private void RemoveFromLocalCache(IPsiSourceFile sourceFile) items?.ForEach(item => { - if (KeyedTranslations.ContainsKey(item.Langauge) && KeyedTranslations[item.Langauge].ContainsKey(item.KeyName)) - KeyedTranslations[item.Langauge].Remove(item.KeyName); + if (keyedTranslations.ContainsKey(item.Langauge) && keyedTranslations[item.Langauge].ContainsKey(item.KeyName)) + keyedTranslations[item.Langauge].Remove(item.KeyName); }); } @@ -128,13 +179,15 @@ private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List(); + if (!keyedTranslations.ContainsKey(item.Langauge)) + keyedTranslations[item.Langauge] = new (); + + var translationKey = new TranslationKey(item.Langauge, item.KeyName, xmlTag); - if (!KeyedTranslations[item.Langauge].ContainsKey(item.KeyName)) - KeyedTranslations[item.Langauge].Add(item.KeyName, xmlTag); + if (!keyedTranslations[item.Langauge].ContainsKey(item.KeyName)) + keyedTranslations[item.Langauge].Add(item.KeyName, translationKey); else - KeyedTranslations[item.Langauge][item.KeyName] = xmlTag; + keyedTranslations[item.Langauge][item.KeyName] = translationKey; } protected override bool IsApplicable(IPsiSourceFile sourceFile) @@ -149,11 +202,11 @@ public void AddDeclaredElement( string keyName, bool caseSensitiveName) { - if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices()); + if (symbolTable == null) symbolTable = new SymbolTable(solution.GetPsiServices()); - if (_declaredElements.ContainsKey($"{language}/{keyName}")) + if (declaredElements.ContainsKey($"{language}/{keyName}")) { - _declaredElements[$"{language}/{keyName}"].Update(owner); + declaredElements[$"{language}/{keyName}"].Update(owner); return; } @@ -164,14 +217,14 @@ public void AddDeclaredElement( ); // @TODO: We seem to get "Key Already Exists" errors. Race condition? - _declaredElements.Add($"{language}/{keyName}", declaredElement); - _symbolTable.AddSymbol(declaredElement); + declaredElements.Add($"{language}/{keyName}", declaredElement); + symbolTable.AddSymbol(declaredElement); } public ISymbolTable GetSymbolTable(ISolution solution) { - if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices()); + if (symbolTable == null) symbolTable = new SymbolTable(solution.GetPsiServices()); - return _symbolTable; + return symbolTable; } } \ No newline at end of file