diff --git a/CHANGELOG.md b/CHANGELOG.md index c338468..6906898 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 2023.4 + * Added support for Custom Def Classes + * Implemented XML Def Find Usages, so you can just Right-Click a defName and see all usages of that def in XML + * Add a new "Generate" menu feature to add props to your XML + * Fixed an issue where some classes (like `ThingDef`) wouldn't resolve correctly, meaning that references weren't made to them + * Fixed an issue where some elements in lists weren't resolved correctly + + ## 2023.3.3 * Fixed some missed API Changes in Rider 2023.3.3 diff --git a/Find Usages.md b/Find Usages.md new file mode 100644 index 0000000..934e911 --- /dev/null +++ b/Find Usages.md @@ -0,0 +1,45 @@ +## Find Usages + +This is a trail of thoughts/possible solutions and references that I went down for Find Usages. They mostly exist to +document what I went through, and as a reference if I want to try and understand my thought process later down the line +when I try to expand the Find Usages functionality to finding XML Defs from C# (think DefOf usage) + +### Reference Points + * `SearchProcessorBase` appears to be where the we can look to see if nodes are being visited + * `NamedThingsSearchSourceFileProcessor::GetReferences` is where I think our reference is being filtered out + * `FomderSearchDomainVisitor::Add` and `AsyncSearchResult::Add` are where the references are being added to the result set + * `CSharpReferenceSearcher::ProcessElement` is where we're seeing some of the C# references being processed? + +### Thoughts/Notes +So far we've been abusing the `ShortName` of our `XmlTagDeclaredElement` to store both the defName and the defType, but +that appears to be one of the things limiting us from being able to find usages. `ShortName` should probably refer only +to the `defName` of our def, but trying to do that quickly just results in references not existing, presumably because +we can't grab them from the symbol table. + +In this branch I've done work to actually move over to a singleton Symbol Table, which is good work, however since I think +it's almost a pre-requisite for the rest of our work, we really need to separate it from this branch and put it into main. +This branch is getting pretty messy because we're trying to do a whole number of things. Likewise, the ShortName migration +should probably be done separately as well so that we're not just throwing a bunch of shit against the wall to see what sticks. + +I'm not sure we need/want to implement a separate `IFindUsagesContextSearch` ourselves. I think what we want to be looking +at is possibly just our own `SearcherFactory` and `DomainSearcher`. I think what we've got so far is a start however the +ShortName issue has caused us to make hacky workarounds to swap the ShortName on demand rather than just construct it properly. +With that kind of approach, there's no real way to say what we do or do not need. Still, with a propper ShortName we were +actually hitting `RimworldReferenceProvider::hasReference` for the first time ever. We were visiting nodes, collecting references +it's just that those references were being filtered out at some point, so we need to investigate that. + +One problem though is that we're still trying to do a "Find Usages" on an actual usage of the def. If we try to do it on the +defName itself, we don't get what we need. Is that because we've attached the declaredElement to the xmlTag instead of the +string? No clue. Could be something else. I think once we get the ShortName sorted and the SymbolTable sorted, we can either +reference the Unity/YAML plugin to see what they've got or we can post to slack for help. Once we've cleaned up that is. + +----- + +It looks like we may be looking in the wrong place. We implemented a SearchFactory for LateBoundReferences, but we're +not serving up a LateBoundReference (I think), we're serving up a regular reference. + +----- + +That was definitely the wrong place. Implementing a DeclaredElementReferenceSearcher works well, but we need to do a +`SwapName` on the `RimworldXmlDefReference` object in the middle of the call inside Rider, which obviously isn't going to +work for us. This just goes back to the idea of having to rework the ShortName to be the defName and not the defType. \ No newline at end of file diff --git a/README.md b/README.md index b3a214f..cc3ef91 100644 --- a/README.md +++ b/README.md @@ -21,21 +21,9 @@ into the definitions on which the XML sits. * Rimworld Mod support in New Solution * Custom Rimworld XML Projects * Basic validation for some XML Values - -## Roadmap - -The major feature for the next version of this plugin (Version 1.1) is custom error reporting for your XML. For example -if you try to use `NorthSouth` it will point out that's an invalid option. Likewise, if you pass in -`2.5` it will tell you that you need to pass in a range of numbers, not a single number. - -Additionally, here are some other features I intend to include at some point. These features are not slated for the 1.1 -release and are a list of things to do "at some point in time". In no particular order, they are: - - * Autocompleting and referencing classes in `Class=""` attributes in XML Tags - * Autocompleting and providing references when referring to C# classes in XML Values like in `` or `` - * Handle Def tags with custom classes instead of just Rimworld classes (such as ``) - * Supporting Patches - + * Support for Custom Def Classes (Such as ``) + * The ability to perform Find Usages on XML Defs + * A "Generate" menu option to generate properties for a given XML Def ## Quick Architecture For Developers diff --git a/TODO.md b/TODO.md index 6500698..8145c97 100644 --- a/TODO.md +++ b/TODO.md @@ -3,8 +3,6 @@ * General features: * Support LookupItems for Class="" attributes * Support LookupItems for things like or - * If we know a bit of XML should be an enum like Gender, we should be able to check that it's a valid enum value - * Handle Defs with custom classes instead of Rimworld classes * We need to be able to support "LoadAlias", such as "StorageSettings.priority" * \
  • handling @@ -12,8 +10,6 @@ * XML Autocomplete * Make auto completing to other XMLTags look nicer and work faster - * When linking to other def (`DefNameHere`) also include defs where the tag is a custom class - that extends from the def we're looking for ` * Documentation * Re-read and document References.RimworldXmlReference @@ -27,4 +23,7 @@ * Investigate the possibility of tying a version number to our types in the SymbolScope so that we can don't cross reference between versions * Right now we only try to load the latest version of the XML, since we aren't built for loading multiple copies of the same def - * Refactor all reading of `About.xml` into a single class that holds that information for that file \ No newline at end of file + * Refactor all reading of `About.xml` into a single class that holds that information for that file + * Look at refactoring RimworldSymbolScope to see if we can make it cleaner + * Refactor the fetching of classes to a central location. We're all over the place, using different scopes from `ScopeHelper` + when we really should just let `ScopeHelper` deal with it all internally as well as the scope swapping \ No newline at end of file diff --git a/buildPlugin.ps1 b/buildPlugin.ps1 index 1229a42..77156c2 100644 --- a/buildPlugin.ps1 +++ b/buildPlugin.ps1 @@ -1,5 +1,5 @@ Param( - $Version = "2023.3.2" + $Version = "2023.4" ) Set-StrictMode -Version Latest diff --git a/gradle.properties b/gradle.properties index 89d0680..fac652b 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.3.3 +PluginVersion=2023.4 BuildConfiguration=Release diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index 7b8e73e..2dae672 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -6,7 +6,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs new file mode 100644 index 0000000..52fada5 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs @@ -0,0 +1,103 @@ +// using System.Collections.Generic; +// using System.Linq; +// using JetBrains.Annotations; +// using JetBrains.Application.Threading; +// using JetBrains.Lifetimes; +// using JetBrains.ReSharper.Psi; +// using JetBrains.ReSharper.Psi.Caches; +// using JetBrains.ReSharper.Psi.Files; +// using JetBrains.ReSharper.Psi.Xml.Tree; +// using JetBrains.Util; +// +// namespace ReSharperPlugin.RimworldDev.DevTools; +// +// [PsiComponent] +// public class PropertyUsageChecker : SimpleICache> +// { +// private static readonly Dictionary UsageCount = new(); +// +// public static string GetUsageOutput(int minUsages = 100) +// { +// var usages = UsageCount; +// var itemsToOutput = (from entry in usages orderby entry.Value descending select entry).ToList() +// .Where(item => item.Value > minUsages) +// .Select(usage => $"{{\"{usage.Key}\", {usage.Value}}}") +// .ToList(); +// +// return string.Join(",\r\n", itemsToOutput); +// } +// +// public PropertyUsageChecker +// (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, +// long? version = null) +// : base(lifetime, locks, persistentIndexManager, PropertyUsageItem.Marshaller, version) +// { +// } +// +// protected override bool IsApplicable(IPsiSourceFile sourceFile) +// { +// return base.IsApplicable(sourceFile) && sourceFile.LanguageType.Name == "XML"; +// } +// +// public override object Build(IPsiSourceFile sourceFile, bool isStartup) +// { +// ScopeHelper.UpdateScopes(sourceFile.GetSolution()); +// +// if (!IsApplicable(sourceFile)) +// return null; +// +// if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return null; +// +// List usages = new(); +// +// var tags = xmlFile.GetNestedTags("Defs/*").Where(tag => +// { +// var defNameTag = tag.GetNestedTags("defName").FirstOrDefault(); +// return defNameTag is not null; +// }); +// +// foreach (var tag in tags) +// { +// BuildFromTag(tag, false); +// } +// +// return usages; +// } +// +// private void BuildFromTag(IXmlTag tag, bool isNested = true) +// { +// if (isNested) +// { +// var hierachy = RimworldXMLItemProvider.GetHierarchy(tag); +// var fieldName = hierachy.Pop(); +// +// var context = +// RimworldXMLItemProvider.GetContextFromHierachy(hierachy, ScopeHelper.RimworldScope, +// ScopeHelper.AllScopes); +// +// +// if (context is not null) +// { +// var fields = RimworldXMLItemProvider.GetAllPublicFields(context, +// ScopeHelper.GetScopeForClass(context.GetClrName().FullName)); +// var field = fields.FirstOrDefault(field => field.ShortName == fieldName); +// +// if (field is not null) +// { +// var className = field.GetContainingType().GetClrName().FullName; +// var fullName = $"{className}::{field.ShortName}"; +// +// UsageCount.TryAdd(fullName, 0); +// UsageCount[fullName]++; +// } +// } +// } +// +// var nested = tag.GetNestedTags("*"); +// +// foreach (var nestedTag in nested) +// { +// BuildFromTag(nestedTag); +// } +// } +// } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs new file mode 100644 index 0000000..50ed45e --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using JetBrains.Serialization; +using JetBrains.Util.PersistentMap; + +namespace ReSharperPlugin.RimworldDev.DevTools; + +public class PropertyUsageItem +{ + public readonly string FullName; + + public PropertyUsageItem(string fullName) + { + FullName = fullName; + } + + public static readonly IUnsafeMarshaller> Marshaller = + UnsafeMarshallers.GetCollectionMarshaller(new UniversalMarshaller(Read, Write), _ => new List()); + + private static PropertyUsageItem Read(UnsafeReader reader) + { + var fullName = reader.ReadString(); + + return new PropertyUsageItem(fullName); + } + + private static void Write(UnsafeWriter writer, PropertyUsageItem value) + { + writer.Write(value.FullName); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs new file mode 100644 index 0000000..4185279 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs @@ -0,0 +1,161 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Application.DataContext; +using JetBrains.ProjectModel.DataContext; +using JetBrains.ReSharper.Feature.Services.Generate; +using JetBrains.ReSharper.Feature.Services.Generate.Actions; +using JetBrains.ReSharper.Feature.Services.Generate.Workflows; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.DataContext; +using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Xml; +using JetBrains.ReSharper.Psi.Xml.Impl.Tree; +using JetBrains.ReSharper.Psi.Xml.Parsing; +using JetBrains.ReSharper.Psi.Xml.Resources; +using JetBrains.ReSharper.Psi.Xml.Tree; + +namespace ReSharperPlugin.RimworldDev.Generator; + +[GenerateProvider] +public class DefPropertyGeneratorItemProvider : IGenerateWorkflowProvider +{ + public IEnumerable CreateWorkflow(IDataContext dataContext) + { + yield return new GenerateDefPropertiesWorkflowXml(); + } +} + +public class GenerateDefPropertiesWorkflowXml : GenerateCodeWorkflowBase +{ + public GenerateDefPropertiesWorkflowXml() + : base(kind: "RimworldPropertyGenerator", + icon: PsiXmlThemedIcons.XmlNode.Id, + title: "&Property Generator", + actionGroup: GenerateActionGroup.CLR_LANGUAGE, + windowTitle: "Def Properties", + description: "Add Properties to your Def", + actionId: "Generate.DefProperties") + { + } + + public override double Order => 100; + + public override bool IsAvailable(IDataContext dataContext) + { + var solution = dataContext.GetData(ProjectModelDataConstants.SOLUTION); + if (solution == null) + return false; + + var sourceFile = dataContext.GetData(PsiDataConstants.SOURCE_FILE); + if (sourceFile == null) + return false; + + if (!sourceFile.PrimaryPsiLanguage.IsLanguage(XmlLanguage.Instance)) return false; + + return true; + } + public override bool IsEnabled(ITreeNode context) + { + return true; + } + + public override bool IsEmptyInputAllowed(IGeneratorContext context) + { + return true; + } +} + +[GeneratorBuilder("RimworldPropertyGenerator", typeof(XmlLanguage))] +public class DefPropertiesGeneratorBuilderXml : GeneratorBuilderBase +{ + protected override bool IsAvailable(GeneratorContextBase context) + { + var anchor = context.Anchor; + + if (anchor is not XmlWhitespaceToken) return false; + if (anchor.Parent is XmlTagHeaderNode) return false; + if (anchor.Parent?.Parent is null or IXmlFile) return false; + + return true; + } + + protected override bool HasProcessableElements(GeneratorContextBase context, + IEnumerable elements) + { + return true; + } + + protected override void Process(GeneratorContextBase context) + { + var anchor = context.Anchor; + if (anchor is not ITreeNode) return; + if (anchor?.Parent is not XmlTag parentTag) return; + + var factory = XmlElementFactory.GetInstance(context.Anchor); + + foreach (var inputElement in context.InputElements) + { + if (inputElement is not GeneratorDeclaredElement declaredElement) continue; + var name = declaredElement.DeclaredElement.ShortName; + var newTag = factory.CreateTagForTag(parentTag, $"<{name}>"); + + anchor = ModificationUtil.AddChildAfter(anchor, newTag); + + context.OutputElements.Add(inputElement); + } + } +} + +[GeneratorElementProvider("RimworldPropertyGenerator", typeof(XmlLanguage))] +public class DefGeneratorElementProvider : GeneratorProviderBase +{ + public override void Populate(GeneratorContextBase context) + { + var anchor = context.Anchor; + + if (ScopeHelper.RimworldScope is null) return; + if (anchor is not XmlWhitespaceToken) return; + if (anchor.Parent is XmlTagHeaderNode) return; + if (anchor.Parent?.Parent is null or IXmlFile) return; + + var hierarchy = RimworldXMLItemProvider.GetHierarchy(context.Anchor); + var currentClass = + RimworldXMLItemProvider.GetContextFromHierachy(hierarchy, ScopeHelper.RimworldScope, ScopeHelper.AllScopes); + + var fields = RimworldXMLItemProvider.GetAllPublicFields(currentClass, ScopeHelper.RimworldScope); + + var alreadyDefinedTags = anchor.Parent + .Children() + .Where(child => child is XmlTag) + .Select(tag => (tag.Children() + .First(child => child is XmlTagHeaderNode) as XmlTagHeaderNode)? + .ContainerName + ) + .Where(name => name is not null) + .ToList(); + + var publicFields = fields.Where( + field => + field.IsField + && field.AccessibilityDomain.DomainType == AccessibilityDomain.AccessibilityDomainType.PUBLIC + && !alreadyDefinedTags.Contains(field.ShortName) + && !field.GetAttributeInstances(AttributesSource.All) + .Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute") + ).ToList().OrderByDescending(field => + { + var className = field.ContainingType?.GetClrName().FullName; + if (className is null) return 0; + + var fullName = $"{className}::{field.ShortName}"; + + if (!PropertyOrdering.PropertyUsageCount.ContainsKey(fullName)) return 0; + return PropertyOrdering.PropertyUsageCount[fullName]; + }); + + foreach (var field in publicFields) + { + context.ProvidedElements.Add(new GeneratorDeclaredElement(field)); + } + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs new file mode 100644 index 0000000..1bf3812 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs @@ -0,0 +1,521 @@ +using System.Collections.Generic; + +namespace ReSharperPlugin.RimworldDev.Generator; + +public class PropertyOrdering +{ + public static Dictionary PropertyUsageCount = new() + { + { "Verse.Def::defName", 9979 }, + { "Verse.Def::label", 4765 }, + { "Verse.Def::description", 2720 }, + { "Verse.GraphicData::texPath", 1910 }, + { "Verse.GraphicData::drawSize", 1186 }, + { "RimWorld.ThoughtStage::label", 1160 }, + { "Verse.ThingDef::graphicData", 1146 }, + { "Verse.GraphicData::graphicClass", 1078 }, + { "RimWorld.ThoughtStage::description", 977 }, + { "RimWorld.ThoughtStage::baseMoodEffect", 969 }, + { "Verse.BuildableDef::statBases", 956 }, + { "Verse.Sound.SubSoundDef::grains", 917 }, + { "Verse.SoundDef::subSounds", 904 }, + { "Verse.Sound.SubSoundDef::volumeRange", 891 }, + { "RimWorld.BackstoryDef::titleShort", 829 }, + { "RimWorld.BackstoryDef::baseDesc", 829 }, + { "RimWorld.BackstoryDef::title", 828 }, + { "RimWorld.BackstoryDef::slot", 827 }, + { "RimWorld.ThoughtDef::stages", 824 }, + { "RimWorld.BackstoryDef::skillGains", 820 }, + { "Verse.SoundDef::context", 770 }, + { "RimWorld.BackstoryDef::requiredWorkTags", 747 }, + { "Verse.BodyPartRecord::def", 722 }, + { "Verse.BodyPartRecord::coverage", 694 }, + { "RimWorld.BackstoryDef::workDisables", 685 }, + { "Verse.Def::ignoreIllegalLabelCharacterConfigError", 605 }, + { "RimWorld.BackstoryDef::shuffleable", 603 }, + { "RimWorld.BackstoryDef::identifier", 602 }, + { "Verse.GeneSymbolPack+WeightedSymbol::symbol", 584 }, + { "Verse.Sound.AudioGrain_Folder::clipFolderPath", 569 }, + { "RimWorld.BackstoryDef::spawnCategories", 536 }, + { "Verse.GraphicData::shadowData", 523 }, + { "Verse.ShadowData::volume", 522 }, + { "Verse.SoundDef::maxSimultaneous", 492 }, + { "Verse.Sound.SubSoundDef::pitchRange", 474 }, + { "RimWorld.BackstoryDef::bodyTypeMale", 465 }, + { "RimWorld.BackstoryDef::bodyTypeFemale", 465 }, + { "Verse.BodyPartRecord::customLabel", 436 }, + { "RimWorld.ThoughtDef::durationDays", 424 }, + { "RimWorld.PreceptComp_Thought::thought", 418 }, + { "Verse.AI.ThinkNode::subNodes", 403 }, + { "Verse.Sound.SubSoundDef::distRange", 401 }, + { "Verse.GraphicData::shaderType", 399 }, + { "Verse.ThingDef::comps", 380 }, + { "Verse.SubEffecterDef::subEffecterClass", 374 }, + { "RimWorld.ThoughtDef::thoughtClass", 373 }, + { "Verse.BuildableDef::costList", 334 }, + { "Verse.Sound.AudioGrain_Clip::clipPath", 334 }, + { "Verse.Def::descriptionHyperlinks", 330 }, + { "Verse.Tool::capacities", 327 }, + { "Verse.Tool::cooldownTime", 327 }, + { "RimWorld.ThoughtDef::stackLimit", 326 }, + { "Verse.Tool::power", 319 }, + { "Verse.BuildableDef::altitudeLayer", 310 }, + { "Verse.Sound.SubSoundDef::sustainLoop", 302 }, + { "Verse.PawnCapacityModifier::capacity", 299 }, + { "RimWorld.BackstoryDef::bodyTypeGlobal", 298 }, + { "Verse.ThingDef::size", 296 }, + { "Verse.GraphicData::color", 293 }, + { "Verse.Tool::label", 291 }, + { "RimWorld.ThoughtDef::workerClass", 289 }, + { "Verse.ThingDef::thingClass", 270 }, + { "Verse.ThingDef::fillPercent", 262 }, + { "Verse.BuildableDef::pathCost", 261 }, + { "RimWorld.StockGenerator::countRange", 260 }, + { "Verse.JobDef::driverClass", 256 }, + { "Verse.PawnKindLifeStage::bodyGraphicData", 251 }, + { "Verse.SubEffecterDef::spawnLocType", 250 }, + { "Verse.ShadowData::offset", 250 }, + { "Verse.BuildableDef::passability", 249 }, + { "Verse.JobDef::reportString", 242 }, + { "Verse.ThingDef::building", 241 }, + { "Verse.SoundDef::priorityMode", 239 }, + { "Verse.BodyPartRecord::depth", 237 }, + { "Verse.Tool::linkedBodyPartsGroup", 214 }, + { "Verse.SubEffecterDef::scale", 211 }, + { "Verse.PawnKindLifeStage::dessicatedBodyGraphicData", 209 }, + { "Verse.LifeStageAge::def", 207 }, + { "Verse.BodyPartRecord::parts", 207 }, + { "Verse.PawnCapacityModifier::offset", 200 }, + { "RimWorld.RitualRoleBehavior::roleId", 199 }, + { "RimWorld.RitualRoleBehavior::dutyDef", 199 }, + { "Verse.Sound.SubSoundDef::muteWhenPaused", 199 }, + { "RimWorld.ThoughtStage::baseOpinionOffset", 190 }, + { "Verse.SubEffecterDef::fleckDef", 189 }, + { "Verse.IngredientCount::filter", 188 }, + { "Verse.LifeStageAge::minAge", 185 }, + { "Verse.ThingDef::recipeMaker", 184 }, + { "Verse.BodyPartRecord::groups", 183 }, + { "Verse.SoundDef::sustain", 182 }, + { "RimWorld.PreceptComp_KnowsMemoryThought::eventDef", 179 }, + { "Verse.HediffDef::stages", 177 }, + { "RimWorld.PreceptDef::issue", 176 }, + { "RimWorld.StatDef::displayPriorityInCategory", 176 }, + { "Verse.BuildableDef::costStuffCount", 174 }, + { "RimWorld.StatDef::defaultBaseValue", 173 }, + { "Verse.EffecterDef::children", 171 }, + { "RimWorld.PreceptDef::impact", 170 }, + { "Verse.ThingDef::rotatable", 167 }, + { "Verse.CompProperties::compClass", 167 }, + { "RimWorld.StatDef::category", 166 }, + { "Verse.HediffStage::capMods", 165 }, + { "Verse.RecipeDef::jobString", 165 }, + { "RimWorld.StatDef::toStringStyle", 162 }, + { "Verse.ThingDef::tickerType", 158 }, + { "Verse.FleckDef::graphicData", 156 }, + { "Verse.HediffStage::label", 155 }, + { "RimWorld.StatDef::minValue", 155 }, + { "Verse.BuildableDef::stuffCategories", 153 }, + { "Verse.BodyPartRecord::height", 153 }, + { "Verse.HediffDef::comps", 152 }, + { "Verse.ThingDef::thingCategories", 152 }, + { "Verse.SubEffecterDef::positionRadius", 151 }, + { "Verse.HediffStage::minSeverity", 151 }, + { "Verse.FleckDef::fadeOutTime", 149 }, + { "Verse.FleckDef::solidTime", 149 }, + { "Verse.Sound.SubSoundDef::onCamera", 149 }, + { "Verse.FleckDef::altitudeLayer", 147 }, + { "RimWorld.ThoughtDef::nullifyingTraits", 147 }, + { "Verse.SubEffecterDef::burstCount", 146 }, + { "Verse.JobDef::allowOpportunisticPrefix", 142 }, + { "RimWorld.BackstoryDef::forcedTraits", 141 }, + { "Verse.RecipeDef::ingredients", 139 }, + { "RimWorld.PreceptDef::displayOrderInIssue", 138 }, + { "RimWorld.PreceptDef::comps", 136 }, + { "RimWorld.ThoughtDef::validWhileDespawned", 135 }, + { "Verse.GraphicData::damageData", 135 }, + { "Verse.RecipeMakerProperties::researchPrerequisite", 133 }, + { "Verse.VerbProperties::warmupTime", 132 }, + { "RimWorld.StyleItemDef::texPath", 132 }, + { "Verse.PawnKindDef::combatPower", 132 }, + { "RimWorld.IdeoSymbolPack::adjective", 132 }, + { "RimWorld.IdeoSymbolPack::member", 132 }, + { "RimWorld.WorkGiverDef::giverClass", 132 }, + { "Verse.ThingDef::tradeTags", 130 }, + { "Verse.GeneDef::displayOrderInCategory", 130 }, + { "Verse.VerbProperties::range", 129 }, + { "Verse.RulePackDef::include", 129 }, + { "RimWorld.RitualStage::defaultDuty", 129 }, + { "RimWorld.RitualStage::endTriggers", 129 }, + { "Verse.BuildableDef::designationCategory", 129 }, + { "RimWorld.WorkGiverDef::verb", 126 }, + { "RimWorld.WorkGiverDef::gerund", 126 }, + { "RimWorld.BodyTypeDef+WoundAnchor::rotation", 126 }, + { "RimWorld.BodyTypeDef+WoundAnchor::offset", 126 }, + { "RimWorld.BodyTypeDef+WoundAnchor::range", 126 }, + { "RimWorld.BodyTypeDef+WoundAnchor::debugColor", 126 }, + { "RimWorld.WorkGiverDef::workType", 125 }, + { "Verse.GeneDef::iconPath", 125 }, + { "RimWorld.StyleItemDef::styleTags", 124 }, + { "Verse.PawnKindDef::race", 124 }, + { "RimWorld.WorkGiverDef::priorityInType", 124 }, + { "RimWorld.IdeoSymbolPack::ideoName", 122 }, + { "RimWorld.WorkGiverDef::requiredCapacities", 119 }, + { "Verse.ThingDef::mote", 118 }, + { "Verse.MoteProperties::solidTime", 118 }, + { "Verse.ThingDef::tools", 118 }, + { "Verse.RecipeDef::fixedIngredientFilter", 117 }, + { "RimWorld.ThoughtDef::developmentalStageFilter", 117 }, + { "Verse.BuildableDef::researchPrerequisites", 117 }, + { "Verse.HediffDef::labelNoun", 116 }, + { "Verse.SubEffecterDef::speed", 116 }, + { "Verse.ThingDef::uiIconScale", 115 }, + { "RimWorld.PreceptDef::displayOrderInImpact", 115 }, + { "Verse.FleckDef::fadeInTime", 114 }, + { "RimWorld.ThoughtDef::stackLimitForSameOtherPawn", 114 }, + { "Verse.ResearchProjectDef::researchViewX", 113 }, + { "Verse.ResearchProjectDef::researchViewY", 113 }, + { "Verse.BuildableDef::uiOrder", 112 }, + { "RimWorld.PreceptComp::description", 112 }, + { "Verse.LifeStageAge::soundWounded", 111 }, + { "Verse.BuildableDef::terrainAffordanceNeeded", 110 }, + { "Verse.RecipeMakerProperties::displayPriority", 110 }, + { "Verse.LifeStageAge::soundCall", 110 }, + { "RimWorld.QuestScriptDef::root", 108 }, + { "RimWorld.RuleDef::symbol", 108 }, + { "RimWorld.RuleDef::resolvers", 108 }, + { "RimWorld.ColorDef::color", 106 }, + { "RimWorld.ThoughtDef::doNotApplyToQuestLodgers", 105 }, + { "Verse.ThingDef::race", 105 }, + { "Verse.PawnKindDef::lifeStages", 105 }, + { "Verse.BuildableDef::placeWorkers", 104 }, + { "Verse.ThingStyleDef::graphicData", 103 }, + { "Verse.ResearchProjectDef::baseCost", 102 }, + { "Verse.GeneDef::symbolPack", 102 }, + { "Verse.VerbProperties::verbClass", 101 }, + { "Verse.GeneSymbolPack::prefixSymbols", 101 }, + { "Verse.MoteProperties::fadeInTime", 100 }, + { "Verse.VerbProperties::targetParams", 100 }, + { "Verse.ThingDef::apparel", 100 }, + { "Verse.MoteProperties::fadeOutTime", 99 }, + { "Verse.ThingDef::thingSetMakerTags", 99 }, + { "Verse.ResearchProjectDef::techLevel", 99 }, + { "Verse.ResearchProjectDef::prerequisites", 99 }, + { "RimWorld.RitualStage::roleBehaviors", 99 }, + { "Verse.PawnKindDef::initialResistanceRange", 98 }, + { "Verse.BuildableDef::designationHotKey", 98 }, + { "Verse.BodyPartDef::hitPoints", 98 }, + { "RimWorld.BodyTypeDef+WoundAnchor::tag", 98 }, + { "RimWorld.BiomeDiseaseRecord::diseaseInc", 98 }, + { "RimWorld.BiomeDiseaseRecord::commonality", 98 }, + { "Verse.GraphicData::shaderParameters", 97 }, + { "Verse.SubEffecterDef::moteDef", 97 }, + { "RimWorld.IdeoIconDef::iconPath", 94 }, + { "RimWorld.PreceptComp_SelfTookMemoryThought::eventDef", 94 }, + { "RimWorld.StyleItemDef::styleGender", 92 }, + { "Verse.Tool::chanceFactor", 92 }, + { "Verse.HediffDef::hediffClass", 91 }, + { "Verse.RaceProperties::lifeStageAges", 90 }, + { "RimWorld.TaleDef::taleClass", 89 }, + { "RimWorld.TaleDef::type", 89 }, + { "Verse.LifeStageAge::soundDeath", 89 }, + { "Verse.SubEffecterDef::soundDef", 88 }, + { "RimWorld.ScenPart::def", 88 }, + { "RimWorld.ApparelProperties::wornGraphicPath", 86 }, + { "RimWorld.PreceptDef::associatedMemes", 86 }, + { "Verse.DamageGraphicData::rect", 85 }, + { "Verse.BuildableDef::constructionSkillPrerequisite", 85 }, + { "RimWorld.AbilityDef::iconPath", 84 }, + { "Verse.GeneDef::biostatMet", 84 }, + { "RimWorld.AbilityDef::comps", 83 }, + { "RimWorld.TaleDef::baseInterest", 83 }, + { "RimWorld.TaleDef::rulePack", 83 }, + { "Verse.ThingDef::drawerType", 83 }, + { "Verse.AI.DutyDef::thinkNode", 81 }, + { "Verse.PawnKindDef::weaponTags", 81 }, + { "RimWorld.QuestScriptDef::questDescriptionRules", 81 }, + { "Verse.HediffStage::statOffsets", 80 }, + { "RimWorld.StageEndTrigger_DurationPercentage::percentage", 80 }, + { "RimWorld.IdeoSymbolPack::theme", 80 }, + { "Verse.SoundDef::maxVoices", 80 }, + { "RimWorld.AbilityDef::verbProperties", 79 }, + { "Verse.GenStepDef::genStep", 79 }, + { "Verse.ShaderTypeDef::shaderPath", 79 }, + { "Verse.ThingDef::techLevel", 79 }, + { "Verse.ThingDef::ingestible", 79 }, + { "Verse.ThingDef::possessionCount", 78 }, + { "Verse.ThingDef::killedLeavings", 78 }, + { "Verse.ThingDef::plant", 78 }, + { "Verse.GenStepDef::order", 77 }, + { "Verse.RaceProperties::headPosPerRotation", 76 }, + { "RimWorld.ApparelProperties::bodyPartGroups", 75 }, + { "Verse.ThingDef::castEdgeShadows", 75 }, + { "Verse.ThingDef::selectable", 74 }, + { "Verse.ThingDef::minifiedDef", 74 }, + { "RimWorld.ThoughtDef::stackedEffectMultiplier", 74 }, + { "Verse.Sound.SubSoundDef::repeatMode", 74 }, + { "RimWorld.PlantProperties::growDays", 73 }, + { "RimWorld.IdeoSymbolPartDef::memes", 73 }, + { "Verse.RecipeDef::addsHediff", 72 }, + { "RimWorld.BodyTypeDef+WoundAnchor::canMirror", 70 }, + { "Verse.HediffStage::painOffset", 69 }, + { "Verse.RecipeDef::appliedOnFixedBodyParts", 69 }, + { "Verse.SubEffecterDef::ticksBetweenMotes", 69 }, + { "Verse.SubEffecterDef::chancePerTick", 69 }, + { "RimWorld.PawnGroupMaker::kindDef", 68 }, + { "Verse.ThingDef::interactionCellOffset", 68 }, + { "RimWorld.ApparelProperties::layers", 68 }, + { "RimWorld.PawnCapacityFactor::capacity", 68 }, + { "RimWorld.PawnCapacityFactor::weight", 68 }, + { "RimWorld.RecordDef::type", 68 }, + { "Verse.ThingCategoryDef::parent", 68 }, + { "Verse.HediffDef::spawnThingOnRemoved", 67 }, + { "Verse.SubEffecterDef::angle", 67 }, + { "Verse.VerbProperties::soundCast", 67 }, + { "RimWorld.TraitDegreeData::label", 67 }, + { "RimWorld.TraitDegreeData::description", 67 }, + { "RimWorld.QuestScriptDef::questNameRules", 66 }, + { "Verse.ThingDef::useHitPoints", 66 }, + { "Verse.RaceProperties::baseHealthScale", 66 }, + { "RimWorld.BackstoryDef::possessions", 65 }, + { "RimWorld.BackstoryThingDefCountClass::key", 65 }, + { "RimWorld.BackstoryThingDefCountClass::value", 65 }, + { "Verse.Grammar.RulePack::include", 65 }, + { "Verse.Tool::ensureLinkedBodyPartsGroupAlwaysUsable", 65 }, + { "RimWorld.PawnGroupMaker::options", 64 }, + { "Verse.GeneDef::labelShortAdj", 63 }, + { "RimWorld.ColorDef::displayOrder", 63 }, + { "RimWorld.InteractionDef::logRulesInitiator", 62 }, + { "RimWorld.RitualRole::id", 62 }, + { "RimWorld.RitualRole::maxCount", 62 }, + { "Verse.Sound.SubSoundDef::sustainIntervalRange", 62 }, + { "Verse.PawnKindDef::initialWillRange", 61 }, + { "RimWorld.BuildingProperties::paintable", 61 }, + { "RimWorld.AbilityDef::hotKey", 60 }, + { "Verse.RaceProperties::baseBodySize", 60 }, + { "Verse.HediffDef::isBad", 59 }, + { "Verse.PawnKindDef::apparelMoney", 59 }, + { "Verse.JobDef::casualInterruptible", 59 }, + { "RimWorld.IncidentDef::category", 58 }, + { "RimWorld.IncidentDef::targetTags", 58 }, + { "RimWorld.IncidentDef::workerClass", 58 }, + { "Verse.PawnCapacityModifier::postFactor", 58 }, + { "RimWorld.StatDef::showIfUndefined", 58 }, + { "RimWorld.BuildingProperties::destroySound", 58 }, + { "Verse.RaceProperties::body", 58 }, + { "RimWorld.RitualBehaviorDef::roles", 57 }, + { "RimWorld.RitualBehaviorDef::stages", 57 }, + { "RimWorld.PlantProperties::visualSizeRange", 57 }, + { "RimWorld.IssueDef::iconPath", 57 }, + { "Verse.BodyPartDef::tags", 57 }, + { "RimWorld.TerrainThreshold::terrain", 57 }, + { "RimWorld.TerrainThreshold::min", 57 }, + { "RimWorld.TerrainThreshold::max", 57 }, + { "RimWorld.AbilityDef::statBases", 56 }, + { "RimWorld.IncidentDef::baseChance", 56 }, + { "RimWorld.BaseGen.SymbolResolver::minRectSize", 56 }, + { "Verse.RecipeMakerProperties::skillRequirements", 56 }, + { "Verse.VerbProperties::hasStandardCommand", 56 }, + { "RimWorld.StockGenerator_BuyTradeTag::tag", 56 }, + { "RimWorld.PreceptComp_UnwillingToDo::eventDef", 56 }, + { "RimWorld.BodyTypeDef+WoundAnchor::layer", 56 }, + { "Verse.LifeStageAge::soundAngry", 56 }, + { "RimWorld.RitualRole::required", 55 }, + { "Verse.ThingDef::hasInteractionCell", 55 }, + { "Verse.KeyBindingDef::defaultKeyCodeA", 55 }, + { "Verse.PawnKindDef::techHediffsMoney", 54 }, + { "RimWorld.StatDef::hideAtValue", 54 }, + { "Verse.ThingDef::equippedStatOffsets", 54 }, + { "RimWorld.RitualRole::countsAsParticipant", 54 }, + { "Verse.BodyPartDef::permanentInjuryChanceFactor", 54 }, + { "RimWorld.StatDef::parts", 53 }, + { "Verse.ThingDef::projectile", 53 }, + { "RimWorld.StageFailTrigger::desc", 53 }, + { "RimWorld.PlantProperties::harvestYield", 53 }, + { "Verse.RecipeDef::products", 53 }, + { "Verse.RaceProperties::lifeExpectancy", 53 }, + { "Verse.ThingDef::colorGenerator", 52 }, + { "Verse.ThingDef::weaponTags", 52 }, + { "RimWorld.IdeoSymbolPack::prefix", 52 }, + { "Verse.BodyPartDef::bleedRate", 52 }, + { "Verse.GeneSymbolPack::suffixSymbols", 52 }, + { "Verse.RaceProperties::soundMeleeHitPawn", 52 }, + { "Verse.RaceProperties::soundMeleeHitBuilding", 52 }, + { "Verse.RaceProperties::soundMeleeMiss", 52 }, + { "Verse.RaceProperties::wildness", 52 }, + { "RimWorld.TargetingParameters::canTargetAnimals", 51 }, + { "RimWorld.TargetingParameters::canTargetLocations", 51 }, + { "RimWorld.PawnGroupMaker::commonality", 51 }, + { "Verse.ProjectileProperties::speed", 51 }, + { "RimWorld.ConceptDef::priority", 51 }, + { "Verse.FleckDef::growthRate", 50 }, + { "Verse.PawnKindDef::weaponMoney", 50 }, + { "Verse.VerbProperties::defaultProjectile", 50 }, + { "RimWorld.WornGraphicBodyTypeData::scale", 50 }, + { "Verse.SubEffecterDef::positionLerpFactor", 50 }, + { "RimWorld.PlantProperties::topWindExposure", 50 }, + { "Verse.RaceProperties::baseHungerRate", 50 }, + { "RimWorld.AbilityDef::level", 49 }, + { "Verse.GenStepDef::linkWithSite", 49 }, + { "Verse.ProjectileProperties::damageDef", 49 }, + { "RimWorld.ThoughtDef::showBubble", 49 }, + { "Verse.Sound.SoundParameterMapping::inParam", 49 }, + { "Verse.Sound.SoundParameterMapping::outParam", 49 }, + { "Verse.SubEffecterDef::rotation", 48 }, + { "RimWorld.RoomRequirement::labelKey", 48 }, + { "Verse.ThingDef::soundInteract", 48 }, + { "RimWorld.ApparelProperties::tags", 48 }, + { "Verse.RaceProperties::foodType", 48 }, + { "Verse.AbilityCompProperties::compClass", 47 }, + { "Verse.PawnKindDef::ecoSystemWeight", 47 }, + { "RimWorld.IncidentDef::letterLabel", 46 }, + { "RimWorld.QuestScriptDef::rootSelectionWeight", 46 }, + { "RimWorld.RitualBehaviorDef::durationTicks", 46 }, + { "RimWorld.OutcomeChance::label", 46 }, + { "RimWorld.OutcomeChance::chance", 46 }, + { "RimWorld.OutcomeChance::positivityIndex", 46 }, + { "RimWorld.BuildingProperties::ai_chillDestination", 46 }, + { "RimWorld.CompProperties_Explosive::explosiveRadius", 46 }, + { "RimWorld.CompProperties_Explosive::explosiveDamageType", 46 }, + { "Verse.ThingDef::devNote", 46 }, + { "Verse.PawnKindDef::techHediffsTags", 45 }, + { "Verse.Sound.SubSoundDef::tempoAffectedByGameSpeed", 45 }, + { "Verse.RecipeMakerProperties::recipeUsers", 45 }, + { "RimWorld.PawnColumnDef::workerClass", 45 }, + { "Verse.DamageGraphicData::cornerTR", 45 }, + { "RimWorld.TraitDef::degreeDatas", 45 }, + { "Verse.MoteProperties::needsMaintenance", 44 }, + { "Verse.SubEffecterDef::positionOffset", 44 }, + { "Verse.BuildableDef::defaultPlacingRot", 44 }, + { "RimWorld.SkillNeed::skill", 44 }, + { "Verse.HediffGiver::hediff", 43 }, + { "Verse.PawnKindDef::techHediffsChance", 43 }, + { "RimWorld.RitualStage::spectateDistanceOverride", 43 }, + { "Verse.ThingDef::blockWind", 43 }, + { "RimWorld.CompProperties_Glower::glowRadius", 43 }, + { "RimWorld.CompProperties_Glower::glowColor", 43 }, + { "Verse.VerbProperties::muzzleFlashScale", 43 }, + { "Verse.Sound.SubSoundDef::sustainAttack", 43 }, + { "Verse.PawnCapacityModifier::setMax", 42 }, + { "RimWorld.BuildingProperties::buildingTags", 42 }, + { "Verse.ThingDef::weaponClasses", 42 }, + { "RimWorld.PreceptDef::requiredMemes", 42 }, + { "Verse.RaceProperties::trainability", 42 }, + { "Verse.GraphicData::maskPath", 42 }, + { "Verse.RaceProperties::leatherDef", 42 }, + { "Verse.RecipeDef::removesHediff", 41 }, + { "Verse.PawnKindDef::apparelTags", 41 }, + { "RimWorld.ApparelRequirement::bodyPartGroupsMatchAny", 41 }, + { "Verse.VerbProperties::soundCastTail", 41 }, + { "RimWorld.StockGenerator_MarketValue::tradeTag", 41 }, + { "RimWorld.PreceptDef::conflictingMemes", 41 }, + { "RimWorld.PawnCapacityFactor::max", 41 }, + { "RimWorld.WorkGiverDef::prioritizeSustains", 41 }, + { "Verse.ThingDef::stuffProps", 41 }, + { "Verse.StuffProperties::color", 41 }, + { "Verse.RecipeDef::workAmount", 40 }, + { "Verse.TerrainDef::color", 40 }, + { "RimWorld.ApparelProperties::defaultOutfitTags", 40 }, + { "RimWorld.StockGenerator_Tag::tradeTag", 40 }, + { "Verse.DamageGraphicData::cornerTL", 40 }, + { "Verse.DamageGraphicData::cornerBR", 40 }, + { "Verse.ThingDef::drawPlaceWorkersWhileSelected", 39 }, + { "RimWorld.ApparelProperties::countsAsClothingForNudity", 39 }, + { "Verse.SubEffecterDef::fleckUsesAngleForVelocity", 39 }, + { "RimWorld.GoodwillSituationDef::workerClass", 39 }, + { "Verse.DamageGraphicData::cornerBL", 39 }, + { "Verse.GeneDef::displayCategory", 39 }, + { "Verse.RoomStatScoreStage::label", 39 }, + { "Verse.HediffDef::addedPartProps", 38 }, + { "Verse.ThingDef::allowedArchonexusCount", 38 }, + { "Verse.SubEffecterDef::rotationRate", 38 }, + { "RimWorld.OutcomeChance::description", 38 }, + { "RimWorld.StatDef::scenarioRandomizable", 38 }, + { "Verse.HediffDef::defaultLabelColor", 38 }, + { "Verse.RaceProperties::gestationPeriodDays", 38 }, + { "RimWorld.TargetingParameters::canTargetSelf", 37 }, + { "Verse.HediffDef::initialSeverity", 37 }, + { "RimWorld.OutcomeChance::memory", 37 }, + { "RimWorld.TaleDef::firstPawnSymbol", 37 }, + { "RimWorld.PlantProperties::fertilitySensitivity", 37 }, + { "RimWorld.ConceptDef::highlightTags", 37 }, + { "RimWorld.StatDef::skillNeedFactors", 37 }, + { "Verse.ThingDef::inspectorTabs", 37 }, + { "Verse.Sound.SoundParameterMapping::paramUpdateMode", 37 }, + { "Verse.Sound.SoundParameterMapping::curve", 37 }, + { "RimWorld.InstructionDef::text", 37 }, + { "RimWorld.InstructionDef::eventTagInitiate", 37 }, + { "Verse.HediffStage::vomitMtbDays", 36 }, + { "Verse.HediffCompProperties_SeverityPerDay::severityPerDay", 36 }, + { "Verse.PawnKindDef::defaultFactionType", 36 }, + { "Verse.ResearchProjectDef::requiredResearchBuilding", 36 }, + { "RimWorld.RitualBehaviorDef::workerClass", 36 }, + { "RimWorld.TaleDef::secondPawnSymbol", 36 }, + { "RimWorld.GoodwillSituationDef::naturalGoodwillOffset", 36 }, + { "Verse.BuildableDef::uiIconPath", 36 }, + { "RimWorld.PlantProperties::wildClusterWeight", 36 }, + { "Verse.AddedBodyPartProps::solid", 35 }, + { "Verse.ThingDef::leaveResourcesWhenKilled", 35 }, + { "Verse.ThingDef::category", 35 }, + { "Verse.VerbProperties::burstShotCount", 35 }, + { "RimWorld.GoodwillSituationDef::meme", 35 }, + { "RimWorld.StatDef::capacityFactors", 35 }, + { "Verse.TerrainDef::renderPrecedence", 35 }, + { "RimWorld.ThoughtDef::requiredTraits", 35 }, + { "RimWorld.InteractionDef::ignoreTimeSinceLastInteraction", 34 }, + { "RimWorld.CompProperties_Explosive::wickTicks", 34 }, + { "RimWorld.TargetingParameters::canTargetBuildings", 34 }, + { "RimWorld.PlantProperties::harvestWork", 34 }, + { "RimWorld.PreceptDef::defaultSelectionWeight", 34 }, + { "Verse.TerrainDef::texturePath", 34 }, + { "Verse.RoomStatScoreStage::minScore", 34 }, + { "RimWorld.AbilityDef::writeCombatLog", 33 }, + { "Verse.PawnKindDef::gearHealthRange", 33 }, + { "Verse.PawnKindDef::apparelAllowHeadgearChance", 33 }, + { "RimWorld.QuestScriptDef::rootMinPoints", 33 }, + { "RimWorld.RitualRole::allowChild", 33 }, + { "RimWorld.IdeoColorDef::colorDef", 33 }, + { "RimWorld.MemeDef::iconPath", 33 }, + { "RimWorld.MemeDef::generalRules", 33 }, + { "RimWorld.MemeDef::descriptionMaker", 33 }, + { "Verse.ThingDef::staticSunShadowHeight", 33 }, + { "Verse.BodyPartDef::alive", 33 }, + { "Verse.GeneDef::biostatCpx", 33 }, + { "Verse.SkyColorSet::sky", 33 }, + { "Verse.SkyColorSet::shadow", 33 }, + { "Verse.SkyColorSet::overlay", 33 }, + { "Verse.SkyColorSet::saturation", 33 }, + { "Verse.PawnKindDef::xenotypeSet", 32 }, + { "Verse.HediffStage::partEfficiencyOffset", 32 }, + { "RimWorld.DeityNameType::name", 32 }, + { "RimWorld.DeityNameType::type", 32 }, + { "RimWorld.RitualRoleBehavior::speakerInteraction", 32 }, + { "RimWorld.StatCategoryDef::displayOrder", 32 }, + { "Verse.StuffProperties::commonality", 32 }, + { "Verse.StuffProperties::statFactors", 32 }, + { "RimWorld.TraitDegreeData::degree", 32 }, + { "Verse.AI.DutyDef::hook", 31 }, + { "RimWorld.PlantProperties::fertilityMin", 31 }, + { "RimWorld.GoodwillSituationDef::otherMeme", 31 }, + { "Verse.HediffGiver::partsToAffect", 31 }, + { "Verse.MentalStateDef::stateClass", 31 }, + { "RimWorld.TattooDef::tattooType", 31 }, + { "RimWorld.TraitDegreeData::statOffsets", 31 }, + { "RimWorld.InstructionDef::actionTagsAllowed", 31 }, + { "RimWorld.InstructionDef::highlightTags", 31 }, + { "RimWorld.InstructionDef::rejectInputMessage", 31 }, + { "Verse.HediffCompProperties_Disappears::showRemainingTime", 30 }, + { "Verse.HediffDef::maxSeverity", 30 }, + { "Verse.PawnKindDef::apparelRequired", 30 }, + { "Verse.ResearchProjectDef::hiddenPrerequisites", 30 }, + { "RimWorld.RitualBehaviorDef::spectatorsLabel", 30 }, + { "RimWorld.RitualBehaviorDef::spectatorGerund", 30 }, + { "RimWorld.PlantProperties::sowTags", 30 }, + { "RimWorld.StockGenerator::totalPriceRange", 30 }, + { "RimWorld.PlantProperties::wildOrder", 30 }, + { "Verse.MentalStateDef::baseInspectLine", 30 }, + { "RimWorld.PlantProperties::harvestTag", 30 }, + { "Verse.RaceProperties::useMeatFrom", 30 }, + { "RimWorld.WorkGiverDef::canBeDoneByMechs", 30 }, + { "RimWorld.StorytellerCompProperties::allowedTargetTags", 30 } + }; +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs new file mode 100644 index 0000000..b379568 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs @@ -0,0 +1,118 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using JetBrains.Application.Parts; +using JetBrains.ProjectModel; +using JetBrains.ReSharper.Feature.Services.Generate; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.DataContext; +using JetBrains.ReSharper.Psi.Modules; +using JetBrains.ReSharper.Psi.Pointers; +using JetBrains.ReSharper.Psi.Tree; +using JetBrains.ReSharper.Psi.Xml; +using JetBrains.ReSharper.Psi.Xml.Tree; +using JetBrains.Util; + +namespace ReSharperPlugin.RimworldDev.Generator; + +[Language(typeof (XmlLanguage), Instantiation.DemandAnyThread)] +public class XamlGeneratorContextFactory : IGeneratorContextFactory +{ + public IGeneratorContext TryCreate(string kind, IPsiDocumentRangeView psiDocumentRangeView) + { + return XmlGeneratorContext.CreateContext(kind, psiDocumentRangeView); + } + + public IGeneratorContext TryCreate(string kind, IDeclaredElement contextElement) + { + return null; + } + + public IGeneratorContext TryCreate(string kind, ITreeNode targetContext, ITreeNode anchor) + { + return null; + } +} + + + public class XmlGeneratorContext : GeneratorContextBase + { + private XmlGeneratorContext([NotNull] string kind, [NotNull] IXmlFile file, [CanBeNull] ITreeNode anchor) + : base(kind) + { + XmlFile = file; + Anchor = anchor; + if (anchor != null) + CodebehindDeclarations = EmptyList.InstanceList; + } + + [CanBeNull] + public static XmlGeneratorContext CreateContext( + string kind, + [NotNull] IPsiDocumentRangeView psiDocumentRangeView) + { + ITreeNode anchor; + IXmlFile selectedTreeNode = psiDocumentRangeView.View().GetSelectedTreeNode(out anchor); + return selectedTreeNode == null ? null : new XmlGeneratorContext(kind, selectedTreeNode, anchor); + } + + public IXmlFile XmlFile { get; set; } + + public IList CodebehindDeclarations { get; } + + public override sealed ITreeNode Anchor { get; set; } + + public override ITreeNode Root => XmlFile; + + public override ISolution Solution => XmlFile.GetSolution(); + + public override IPsiModule PsiModule => XmlFile.GetPsiModule(); + + public override PsiLanguageType Language => XmlFile.Language; + + public override PsiLanguageType PresentationLanguage + { + get + { + using (IEnumerator enumerator = CodebehindDeclarations.GetEnumerator()) + { + if (enumerator.MoveNext()) + return enumerator.Current.Language; + } + return XmlFile.Language; + } + } + + public override TreeTextRange GetSelectionTreeRange() => TreeTextRange.InvalidRange; + + public override IGeneratorContextPointer CreatePointer() + { + return new XmlGeneratorWorkflowPointer(this); + } + + private sealed class XmlGeneratorWorkflowPointer : GeneratorWorkflowPointer + { + [NotNull] + private readonly ITreeNodePointer myClassPointer; + [CanBeNull] + private readonly ITreeNodePointer myAnchorPointer; + + public XmlGeneratorWorkflowPointer([NotNull] XmlGeneratorContext context) + : base(context) + { + myClassPointer = context.XmlFile.CreateTreeElementPointer(); + ITreeNode anchor = context.Anchor; + if (anchor == null) + return; + myAnchorPointer = anchor.CreateTreeElementPointer(); + } + + protected override GeneratorContextBase CreateWorkflow() + { + IXmlFile treeNode1 = myClassPointer.GetTreeNode(); + if (treeNode1 == null) + return null; + ITreeNode treeNode2 = myAnchorPointer != null ? myAnchorPointer.GetTreeNode() : null; + return new XmlGeneratorContext(Kind, treeNode1, treeNode2); + } + } + } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs new file mode 100644 index 0000000..38c45cd --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using JetBrains.Diagnostics; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.Caches; +using JetBrains.ReSharper.Psi.ExtensionsAPI; +using JetBrains.ReSharper.Psi.ExtensionsAPI.Finder; +using JetBrains.ReSharper.Psi.Files; +using JetBrains.ReSharper.Psi.Search; +using JetBrains.ReSharper.Psi.Tree; + +namespace ReSharperPlugin.RimworldDev.Navigation; + +public class CustomSearcher : IDomainSpecificSearcher where TLanguage : KnownLanguage +{ + private readonly JetHashSet myNames; + private readonly JetHashSet myWordsInText; + private readonly IDeclaredElementsSet myElements; + private readonly ReferenceSearcherParameters myReferenceSearcherParameters; + private readonly bool mySearchForLateBound; + private readonly IWordIndex myWordIndex; + + public CustomSearcher( + IDomainSpecificSearcherFactory factory, + IDeclaredElementsSet elements, + ReferenceSearcherParameters referenceSearcherParameters, + bool searchForLateBound) + { + mySearchForLateBound = searchForLateBound; + myElements = elements; + myReferenceSearcherParameters = referenceSearcherParameters; + myNames = []; + myWordsInText = []; + + // The element shortNames are {defType}/{defName} to make them unique references, however in the text they're + // just {defName} so we split up their shortName for seraching + foreach (var element in myElements) + { + myNames.Add(element.ShortName.Split('/').Last()); + myWordsInText.UnionWith(factory.GetAllPossibleWordsInFile(element)); + } + + myWordIndex = myElements.FirstOrDefault()?.GetPsiServices().WordIndex; + } + + public bool ProcessProjectItem( + IPsiSourceFile sourceFile, + IFindResultConsumer consumer) + { + if (!CanContainWord(sourceFile)) return false; + + foreach (ITreeNode psiFile in sourceFile.GetPsiFiles()) + { + if (ProcessElement(psiFile, consumer)) + return true; + } + + return false; + } + + private bool CanContainWord([NotNull] IPsiSourceFile sourceFile) + { + return myWordsInText.Any(word => myWordIndex?.CanContainWord(sourceFile, word) == true); + } + + public bool ProcessElement(ITreeNode element, IFindResultConsumer consumer) + { + Assertion.Assert(element != null); + JetHashSet referenceNames = []; + foreach (var myElement in myElements) + { + myNames.Add(myElement.ShortName); + } + + return (!mySearchForLateBound + ? new ReferenceSearchSourceFileProcessor(element, myReferenceSearcherParameters, consumer, + myElements, myWordsInText, referenceNames) + : (NamedThingsSearchSourceFileProcessor)new LateBoundReferenceSourceFileProcessor(element, + consumer, myElements, myWordsInText, referenceNames)).Run() == FindExecution.Stop; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs new file mode 100644 index 0000000..af10669 --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.ReSharper.Psi; +using JetBrains.ReSharper.Psi.ExtensionsAPI; +using JetBrains.ReSharper.Psi.Search; +using JetBrains.ReSharper.Psi.Xml; + +namespace ReSharperPlugin.RimworldDev.Navigation; + +[PsiSharedComponent] +public class RimworldSearcherFactory(SearchDomainFactory searchDomainFactory) : DomainSpecificSearcherFactoryBase +{ + public override bool IsCompatibleWithLanguage(PsiLanguageType languageType) => languageType.Is(); + + public override ISearchDomain GetDeclaredElementSearchDomain(IDeclaredElement declaredElement) + { + return searchDomainFactory.CreateSearchDomain(declaredElement.GetSolution(), false); + } + + public override IDomainSpecificSearcher CreateReferenceSearcher( + IDeclaredElementsSet elements, + ReferenceSearcherParameters referenceSearcherParameters) + { + elements = new DeclaredElementsSet(elements.Where(element => + IsCompatibleWithLanguage(element.PresentationLanguage))); + + return new CustomSearcher(this, + elements, referenceSearcherParameters, false); + } + + public override IEnumerable GetAllPossibleWordsInFile(IDeclaredElement element) + { + return new List { element.ShortName.Split('/').Last() }; + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs index 76e56dd..98ee07c 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs @@ -100,6 +100,7 @@ void IRecursiveElementProcessor.ProcessAfterInterior(ITreeNode element) return; case "System.String": break; + case "Verse.CurvePoint": case "UnityEngine.Vector2": ProcessVector2(fullText, range); break; @@ -143,14 +144,14 @@ private void ProcessBoolean(string textValue, DocumentRange range) private void ProcessInt(string textValue, DocumentRange range) { - if (int.TryParse(textValue, out _)) return; + if (IsValidInt(textValue)) return; AddError("Value must be a whole number with no decimal points", range); } private void ProcessFloat(string textValue, DocumentRange range) { - if (float.TryParse(textValue, out _)) return; + if (IsValidFloat(textValue)) return; AddError("Value must be a valid number", range); } @@ -160,7 +161,7 @@ private void ProcessIntRange(string textValue, DocumentRange range) var strArray = textValue.Split('~'); // For some reason IntRange also just accepts a single number instead of a range - if (strArray.Length == 1 && int.TryParse(strArray[0], out _)) return; + if (strArray.Length == 1 && IsValidInt(strArray[0])) return; if (strArray.Length != 2) { @@ -168,13 +169,13 @@ private void ProcessIntRange(string textValue, DocumentRange range) return; } - if (!int.TryParse(strArray[0], out _)) + if (!IsValidInt(strArray[0])) { AddError($"\"{strArray[0]}\" is not a valid integer", range); return; } - if (!int.TryParse(strArray[1], out _)) + if (!IsValidInt(strArray[1])) { AddError($"\"{strArray[1]}\" is not a valid integer", range); return; @@ -185,7 +186,7 @@ private void ProcessFloatRange(string textValue, DocumentRange range) { var strArray = textValue.Split('~'); // For some reason FloatRange also just accepts a single number instead of a range - if (strArray.Length == 1 && float.TryParse(strArray[0], out _)) return; + if (strArray.Length == 1 && IsValidFloat(strArray[0])) return; if (strArray.Length != 2) { @@ -193,13 +194,13 @@ private void ProcessFloatRange(string textValue, DocumentRange range) return; } - if (!float.TryParse(strArray[0], out _)) + if (!IsValidFloat(strArray[0])) { AddError($"\"{strArray[0]}\" is not a valid float", range); return; } - if (!float.TryParse(strArray[1], out _)) + if (!IsValidFloat(strArray[1])) { AddError($"\"{strArray[1]}\" is not a valid float", range); return; @@ -208,11 +209,11 @@ private void ProcessFloatRange(string textValue, DocumentRange range) private void ProcessVector2(string textValue, DocumentRange range) { - var match = Regex.Match(textValue.Trim(), @"^\((.*?),(.*?)\)$"); + var match = Regex.Match(textValue.Trim(), @"^\(?(.*?),(.*?)\)?$"); if (!match.Success) { // For some reason Vector2 allows us to pass in just a single number instead of a vector? - if (float.TryParse(textValue, out _)) return; + if (IsValidFloat(textValue)) return; AddError("Your value must be in a format similar to (1,2)", range); return; @@ -221,13 +222,13 @@ private void ProcessVector2(string textValue, DocumentRange range) var firstValue = match.Groups[1].Value; var secondValue = match.Groups[2].Value; - if (!float.TryParse(firstValue, out _)) + if (!IsValidFloat(firstValue)) { AddError($"\"{firstValue}\" is not a valid number", range); return; } - if (!float.TryParse(secondValue, out _)) + if (!IsValidFloat(secondValue)) { AddError($"\"{secondValue}\" is not a valid number", range); return; @@ -236,7 +237,7 @@ private void ProcessVector2(string textValue, DocumentRange range) private void ProcessVector3(string textValue, DocumentRange range) { - var match = Regex.Match(textValue.Trim(), @"^\((.*?),(.*?),(.*?)\)$"); + var match = Regex.Match(textValue.Trim(), @"^\(?(.*?),(.*?),(.*?)\)?$"); if (!match.Success) { AddError("Your value must be in a format similar to (1,2,3)", range); @@ -247,19 +248,19 @@ private void ProcessVector3(string textValue, DocumentRange range) var secondValue = match.Groups[2].Value; var thirdValue = match.Groups[3].Value; - if (!float.TryParse(firstValue, out _)) + if (!IsValidFloat(firstValue)) { AddError($"\"{firstValue}\" is not a valid number", range); return; } - if (!float.TryParse(secondValue, out _)) + if (!IsValidFloat(secondValue)) { AddError($"\"{secondValue}\" is not a valid number", range); return; } - if (!float.TryParse(thirdValue, out _)) + if (!IsValidFloat(thirdValue)) { AddError($"\"{thirdValue}\" is not a valid number", range); return; @@ -287,6 +288,22 @@ private void ProcessStruct(string textValue, DocumentRange range, ITypeElement c AddError($"\"{textValue}\" is not a valid value for {context.ShortName}", range); } } + + private bool IsValidFloat(string value) + { + if (float.TryParse(value, out _)) return true; + if (value.Trim() == "Infinity") return true; + + return false; + } + + private bool IsValidInt(string value) + { + if (int.TryParse(value, out _)) return true; + if (value.Trim() == "Infinity") return true; + + return false; + } private void AddError(string errorText, DocumentRange range) { diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj index cae39f7..5541a8e 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj @@ -2,7 +2,7 @@ $(MSBuildToolsPath)\Microsoft.CSharp.targets - 10 + latest diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs index 048e135..9350f87 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs @@ -43,6 +43,10 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection { if (!ScopeHelper.UpdateScopes(element.GetSolution())) return new ReferenceCollection(); + if (element.Parent != null && element.NodeType.ToString() == "TEXT" && + element.Parent.GetText().StartsWith("")) + return GetReferenceForDeclaredElement(element, oldReferences); + if (element.NodeType.ToString() == "TEXT") return GetReferencesForText(element, oldReferences); if (element is not XmlIdentifier identifier) return new ReferenceCollection(); if (element.GetSourceFile() is not { } sourceFile) return new ReferenceCollection(); @@ -54,7 +58,16 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection if (hierarchy.Count == 0) { - var @class = rimworldSymbolScope.GetElementsByShortName(identifier.GetText()).FirstOrDefault(); + var className = identifier.GetText(); + + var allWorkingScopes = + ScopeHelper.AllScopes.Where(scope => scope.GetTypeElementByCLRName(className) is not null); + + var @class = className.Contains(".") + ? ScopeHelper.GetScopeForClass(className).GetTypeElementByCLRName(className) + : rimworldSymbolScope.GetElementsByShortName(className).FirstOrDefault(); + + if (@class == null) return new ReferenceCollection(); return new ReferenceCollection(new RimworldXmlReference(@class, identifier)); } @@ -62,7 +75,7 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection RimworldXMLItemProvider.GetContextFromHierachy(hierarchy, rimworldSymbolScope, allSymbolScopes); if (classContext == null) return new ReferenceCollection(); - var field = RimworldXMLItemProvider.GetAllPublicFields(classContext, rimworldSymbolScope) + var field = RimworldXMLItemProvider.GetAllFields(classContext, rimworldSymbolScope) .FirstOrDefault(field => field.ShortName == identifier.GetText()); if (field == null) return new ReferenceCollection(); @@ -92,22 +105,63 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol // return new ReferenceCollection(); } - if (!classContext.GetAllSuperClasses().Any(superClass => superClass.GetClrName().FullName == "Verse.Def") && - !classContext.GetAllSuperTypes().Any(superType => superType.GetClrName().FullName == "Verse.Def")) + if (!ScopeHelper.ExtendsFromVerseDef(classContext.GetClrName().FullName)) return new ReferenceCollection(); var xmlSymbolTable = element.GetSolution().GetComponent(); - var tagId = $"{classContext.ShortName}/{element.GetText()}"; - if (xmlSymbolTable.GetTagByDef(classContext.ShortName, element.GetText()) is not { } tag) + var defType = classContext.ShortName; + var defName = element.GetText(); + + if (xmlSymbolTable.ExtraDefTagNames.ContainsKey($"{defType}/{defName}")) + { + var newDef = xmlSymbolTable.ExtraDefTagNames[$"{defType}/{defName}"]; + defType = newDef.Split('/').First(); + defName = newDef.Split('/').Last(); + } + + if (xmlSymbolTable.GetTagByDef(defType, defName) is not { } tag) + return new ReferenceCollection(); + + return new ReferenceCollection(new RimworldXmlDefReference(element, tag, defType, defName)); + } + + /** + * This is a bit of a hacky workaround. Since we're not constructing our own Custom Language, we don't have control + * over the Psi Tree. Unfortunately, Find Usages expects to be able to find an `IDeclaration` which is a `ITreeNode` + * in the Psi Tree, which would be created on the Tree Construction. + * + * Since we can't do that, in order to be able to invoke Find Usages on the declared element itself, we're going to + * make a hacky workaround and just give it a reference... to itself + */ + private ReferenceCollection GetReferenceForDeclaredElement(ITreeNode element, ReferenceCollection oldReferences) + { + // We're currently in a text node inside a inside another ThingDef node. We want to get that node + var defTypeName = element.Parent?.Parent? + // And then get the TagHeader () of that node + .Children().FirstOrDefault(childElement => childElement is XmlTagHeaderNode)? + // And then get the text that provides the ID of that node (ThingDef) + .Children().FirstOrDefault(childElement => childElement is XmlIdentifier)?.GetText(); + + if (defTypeName is null) new ReferenceCollection(); + + var defName = element.GetText(); + + var xmlSymbolTable = element.GetSolution().GetComponent(); + + var tagId = $"{defTypeName}/{defName}"; + if (!xmlSymbolTable.DefTags.ContainsKey(tagId)) return new ReferenceCollection(); + + if (xmlSymbolTable.GetTagByDef(defTypeName, defName) is not { } tag) return new ReferenceCollection(); - return new ReferenceCollection(new RimworldXmlDefReference(element, - tag.GetNestedTags("defName").FirstOrDefault() ?? tag, tagId)); + return new ReferenceCollection(new RimworldXmlDefReference(element, tag, defTypeName, + element.GetText())); } public bool HasReference(ITreeNode element, IReferenceNameContainer names) { - return true; + 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/RimworldXmlDefReference.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs index 6c5aaa4..8342a37 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs @@ -1,52 +1,51 @@ -using System.Linq; using JetBrains.Annotations; using JetBrains.ProjectModel; using JetBrains.ReSharper.Psi; -using JetBrains.ReSharper.Psi.CSharp.Conversions; using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve; -using JetBrains.ReSharper.Psi.Modules; using JetBrains.ReSharper.Psi.Resolve; using JetBrains.ReSharper.Psi.Tree; -using JetBrains.ReSharper.Psi.Xml.Impl.Tree; -using JetBrains.ReSharper.Psi.Xml.Impl.Tree.References; -using JetBrains.ReSharper.Psi.Xml.Tree; -using JetBrains.Util; -using ReSharperPlugin.RimworldDev.TypeDeclaration; +using ReSharperPlugin.RimworldDev.SymbolScope; namespace ReSharperPlugin.RimworldDev.TypeDeclaration; public class RimworldXmlDefReference : TreeReferenceBase { - private readonly IXmlTag myTypeElement; + private readonly ITreeNode myTypeElement; - private readonly string myName; + private string myName; + private string defName; + private string defType; - public RimworldXmlDefReference([NotNull] ITreeNode owner, IXmlTag typeElement, string name) : base(owner) + public RimworldXmlDefReference([NotNull] ITreeNode owner, ITreeNode typeElement, string defType, string defName) : base(owner) { myTypeElement = typeElement; - myName = name; + myName = defName; + myName = $"{defType}/{defName}"; + this.defName = defName; + this.defType = defType; } public override ISymbolTable GetReferenceSymbolTable(bool useReferenceName) { - var symbolTable = new SymbolTable(myOwner.GetPsiServices()); + var symbolScope = myOwner.GetSolution().GetComponent(); - symbolTable.AddSymbol( - new XMLTagDeclaredElement( - myTypeElement, - myName, - false - ) + symbolScope.AddDeclaredElement( + myOwner.GetSolution(), + myTypeElement, + defType, + defName, + false ); - - return symbolTable; + + return symbolScope.GetSymbolTable(myOwner.GetSolution()); } public override ResolveResultWithInfo ResolveWithoutCache() { - return GetReferenceSymbolTable(true).GetResolveResult(GetName()); + return GetReferenceSymbolTable(true).GetResolveResult(GetName()); } + public override string GetName() => myName; public override IReference BindTo(IDeclaredElement element) => @@ -62,4 +61,4 @@ public override IReference BindTo( 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/References/RimworldXmlReference.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs index 06617f9..f5056bd 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs @@ -1,3 +1,4 @@ +using System.Linq; using JetBrains.Annotations; using JetBrains.Metadata.Reader.API; using JetBrains.ReSharper.Psi; @@ -50,7 +51,8 @@ private static bool FilterToApplicableProperties([NotNull] ISymbolInfo symbolInf } public override string GetName() => myOwner.GetText(); - + private string GetShortName() => GetName().Split('.').Last(); + public override TreeTextRange GetTreeTextRange() => myOwner.GetTreeTextRange(); public override IAccessContext GetAccessContext() => new ElementAccessContext(myOwner); @@ -83,12 +85,7 @@ public override ISymbolTable GetReferenceSymbolTable(bool useReferenceName) if (!useReferenceName) return table; - return table.Filter(GetName(), new AllFilter(myOwner.GetText())); - - // ISymbolTable table = this.myOwner.GetSolution().GetComponent().GetAllTagsSymbolTable(this.myOwner.GetSourceFile()).Distinct(SymbolInfoComparer.OrdinalIgnoreCase); - // if (useReferenceName) - // table = table.Filter(this.GetName()); - // return table; + return table.Filter(GetShortName(), new AllFilter(GetShortName())); } public ISymbolTable GetCompletionSymbolTable() => GetReferenceSymbolTable(false); @@ -104,9 +101,8 @@ public override IReference BindTo( public override ResolveResultWithInfo ResolveWithoutCache() { - ResolveResultWithInfo resolveResult = GetReferenceSymbolTable(true).GetResolveResult(GetName()); + ResolveResultWithInfo resolveResult = GetReferenceSymbolTable(true).GetResolveResult(GetShortName()); return resolveResult; - // return new ResolveResultWithInfo(resolveResult.Result, resolveResult.Info.CheckResolveInfo((ResolveErrorType) HtmlResolveErrorType.UNKNOWN_HTML_TAG)); } public sealed class AllFilter : SimpleSymbolInfoFilter diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs index 1506100..e80d5a0 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs @@ -12,6 +12,7 @@ using JetBrains.ReSharper.Psi.Caches; using JetBrains.ReSharper.Psi.CSharp; using JetBrains.ReSharper.Psi.Impl.reflection2.elements.Compiled; +using JetBrains.ReSharper.Psi.Impl.Types; using JetBrains.ReSharper.Psi.Modules; using JetBrains.ReSharper.Psi.Resolve; using JetBrains.ReSharper.Psi.Tree; @@ -180,16 +181,17 @@ protected void AddTextLookupItems(RimworldXmlCodeCompletionContext context, IIte var xmlSymbolTable = context.TreeNode!.GetSolution().GetSolution().GetComponent(); - var keys = xmlSymbolTable.DefTags.Keys - .Where(key => key.StartsWith($"{className}/")) - .Select(key => key.Substring(className.Length + 1)); - + var keys = xmlSymbolTable.GetDefsByType(className); + foreach (var key in keys) { - var item = xmlSymbolTable.GetTagByDef(className, key); - - var lookup = LookupFactory.CreateDeclaredElementLookupItem(context, key, - new DeclaredElementInstance(new XMLTagDeclaredElement(item, key, false))); + var defType = key.Split('/').First(); + var defName = key.Split('/').Last(); + + var item = xmlSymbolTable.GetTagByDef(defType, defName); + + var lookup = LookupFactory.CreateDeclaredElementLookupItem(context, defName, + new DeclaredElementInstance(new XMLTagDeclaredElement(item, defType, defName, false))); collector.Add(lookup); } @@ -278,11 +280,22 @@ public static List GetHierarchy(ITreeNode treeNode) public static List GetAllPublicFields(ITypeElement desiredClass, ISymbolScope symbolScope) { return desiredClass.GetAllClassMembers() + .Where(field => !field.Member.GetAttributeInstances(AttributesSource.All).Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute")) .Where(member => member.Member.AccessibilityDomain.DomainType == AccessibilityDomain.AccessibilityDomainType.PUBLIC) .Select(member => member.Member) .ToList(); } + // Grabs the fields that we can use for a class by looking at that classes fields as well as the fields for all the + // classes that it inherits from + public static List GetAllFields(ITypeElement desiredClass, ISymbolScope symbolScope) + { + return desiredClass.GetAllClassMembers() + .Where(field => !field.Member.GetAttributeInstances(AttributesSource.All).Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute")) + .Select(member => member.Member) + .ToList(); + } + /** * This is where a lot of our heavy lifting is going to be. We're taking the a list of strings as a hierarchy * (See the docblock for GetHierarchy) and getting the Class for that last item in that list. So for example, if we @@ -310,20 +323,34 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo { // If we know we're in an li but can't pull the expected class from the C# typing for some reason, just // return null so that we don't throw an error - if (previousField == null || - !Regex.Match( - previousField.Type.GetLongPresentableName(CSharpLanguage.Instance), - @"^System.Collections.Generic.List<.*?>$" - ).Success) + if (previousField == null) { return null; } - // Use regex to grab the className and then fetch it from the symbol scope - var classValue = Regex.Match(previousField.Type.GetLongPresentableName(CSharpLanguage.Instance), - @"^System.Collections.Generic.List<(.*?)>$").Groups[1].Value; - - currentContext = symbolScope.GetTypeElementByCLRName(classValue); + string classValue = null; + if (previousField.Type is ISimplifiedIdTypeInfo simpleTypeInfo) + { + classValue = simpleTypeInfo.GetTypeArguments()?.FirstOrDefault()? + .GetLongPresentableName(CSharpLanguage.Instance); + } + + if (classValue == null) + { + if (!Regex.Match( + previousField.Type.GetLongPresentableName(CSharpLanguage.Instance), + @"^System.Collections.Generic.List<.*?>$" + ).Success) + { + return null; + } + + // Use regex to grab the className and then fetch it from the symbol scope + classValue = Regex.Match(previousField.Type.GetLongPresentableName(CSharpLanguage.Instance), + @"^System.Collections.Generic.List<(.*?)>$").Groups[1].Value; + }; + + currentContext = ScopeHelper.GetScopeForClass(classValue).GetTypeElementByCLRName(classValue); continue; } @@ -336,12 +363,13 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo if (classValue == "") return null; + var scopeToUse = ScopeHelper.GetScopeForClass(classValue); // First we try to look it up as a short name from the Rimworld DLL - currentContext = symbolScope.GetElementsByShortName(classValue).FirstOrDefault() as ITypeElement; + currentContext = scopeToUse.GetElementsByShortName(classValue).FirstOrDefault() as ITypeElement; if (currentContext != null) continue; // Then we try to look it up as a fully qualified name from Rimworld - currentContext = symbolScope.GetTypeElementByCLRName(classValue); + currentContext = scopeToUse.GetTypeElementByCLRName(classValue); if (currentContext != null) continue; // If it's not a Rimworld class, let's assume that it's a custom class in our own C#. In that case, let's @@ -362,17 +390,19 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo // current context to be diving into if (!currentContext.IsClass()) { - currentContext = symbolScope.GetElementsByShortName(currentNode).FirstOrDefault() as Class; + currentContext = currentNode.Contains(".") ? ScopeHelper.GetScopeForClass(currentNode)?.GetTypeElementByCLRName(currentNode) : symbolScope?.GetElementsByShortName(currentNode).FirstOrDefault() as Class; + if (currentContext is null) return null; + continue; } - var fields = GetAllPublicFields(currentContext, symbolScope); + var fields = GetAllFields(currentContext, symbolScope); var field = fields.FirstOrDefault(field => field.ShortName == currentNode); if (field == null) return null; previousField = field; var clrName = field.Type.GetLongPresentableName(CSharpLanguage.Instance); - currentContext = symbolScope.GetTypeElementByCLRName(clrName); + currentContext = ScopeHelper.GetScopeForClass(clrName).GetTypeElementByCLRName(clrName); switch (clrName) { diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs index a0b60aa..007cc7f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs @@ -9,6 +9,7 @@ using JetBrains.ProjectModel.model2.Assemblies.Interfaces; using JetBrains.ReSharper.Psi.Caches; using JetBrains.ReSharper.Psi.Modules; +using JetBrains.ReSharper.Psi.Util; using JetBrains.Util; using JetBrains.Util.Threading.Tasks; using ReSharperPlugin.RimworldDev.Settings; @@ -18,6 +19,7 @@ namespace ReSharperPlugin.RimworldDev; public class ScopeHelper { private static List allScopes = new(); + private static List knownCustomScopes = new(); private static ISymbolScope rimworldScope; private static IPsiModule rimworldModule; private static List usedScopes; @@ -56,6 +58,22 @@ public static bool UpdateScopes(ISolution solution) return true; } + public static ISymbolScope GetScopeForClass(string className) + { + if (!className.Contains(".")) return rimworldScope; + if (knownCustomScopes.FirstOrDefault(scope => scope.GetTypeElementByCLRName(className) is not null) is + { } foundScope) return foundScope; + + if (knownCustomScopes.FirstOrDefault(scope => scope.GetTypeElementByCLRName(className) is not null) is + { } newCustomScope) + { + knownCustomScopes.Add(newCustomScope); + return newCustomScope; + } + + return rimworldScope; + } + private static async void AddRef(ISolution solution) { if (adding) return; @@ -85,7 +103,7 @@ public static FileSystemPath FindRimworldDirectory(string currentPath) { return customPath; } - + var locations = new List(); locations.AddRange(GetSteamLocations() @@ -135,7 +153,7 @@ public static FileSystemPath FindRimworldDll(string currentPath) FileSystemPath.ParseRelativelyTo(path, rimworldLocation).ExistsFile); if (location == null) return null; - + var path = FileSystemPath.ParseRelativelyTo(location, rimworldLocation); return path.ExistsFile ? path : null; @@ -154,7 +172,7 @@ public static List FindModDirectories(string currentPath) if (dataDirectory.ExistsDirectory) modDirectories.Add(dataDirectory.FullPath); if (modsDirectory.ExistsDirectory) modDirectories.Add(modsDirectory.FullPath); } - + modDirectories.AddRange(GetSteamLocations() .Select(location => FileSystemPath.TryParse($"{location}/workshop/content/294100/").FullPath) .Where(location => FileSystemPath.TryParse(location).ExistsDirectory) @@ -209,12 +227,12 @@ public static Dictionary GetModLocations(string basePath, List GetModLocations(string basePath, List GetAllSuperClasses(string clrName) + { + if (clrName.StartsWith("System")) return new List(); + + var items = new List(); + var supers = RimworldScope.GetTypeElementByCLRName(clrName)?.GetAllSuperClasses().ToList(); + supers?.ForEach(super => + { + var clr = super.GetClrName().FullName; + items.Add(clr); + items.AddRange(GetAllSuperClasses(clr)); + }); + + return items; + } + + public static List GetAllSuperTypeElements(string clrName) + { + if (clrName.StartsWith("System")) return new List(); + + var items = new List(); + var supers = RimworldScope.GetTypeElementByCLRName(clrName).GetAllSuperTypeElements().ToList(); + supers.ForEach(super => + { + var clr = super.GetClrName().FullName; + items.Add(clr); + items.AddRange(GetAllSuperTypeElements(clr)); + }); + + return items; + } + + public static List GetAllSuperTypes(string clrName) + { + if (clrName.StartsWith("System")) return new List(); + + var items = new List(); + var supers = RimworldScope.GetTypeElementByCLRName(clrName).GetAllSuperTypes().ToList(); + supers.ForEach(super => + { + var clr = super.GetClrName().FullName; + items.Add(clr); + items.AddRange(GetAllSuperTypes(clr)); + }); + + return items; + } + + public static bool ExtendsFromVerseDef(string clrName) + { + if (RimworldScope is null) return false; + + return + GetAllSuperClasses(clrName).Any(super => super == "Verse.Def") || + GetAllSuperTypes(clrName).Any(super => super == "Verse.Def"); + } } \ 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 c0bc0ed..564814a 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs @@ -5,20 +5,31 @@ using JetBrains.Application.Threading; using JetBrains.Collections; 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.Util; using JetBrains.ReSharper.Psi.Xml.Tree; +using ReSharperPlugin.RimworldDev.TypeDeclaration; namespace ReSharperPlugin.RimworldDev.SymbolScope; [PsiComponent] public class RimworldSymbolScope : SimpleICache> { - public Dictionary DefTags = new(); - + public Dictionary DefTags = new(); + + public Dictionary ExtraDefTagNames = new(); + private Dictionary _declaredElements = new(); + private SymbolTable _symbolTable; + public RimworldSymbolScope - (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, long? version = null) + (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, + long? version = null) : base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version) { } @@ -29,13 +40,13 @@ protected override bool IsApplicable(IPsiSourceFile sourceFile) } [CanBeNull] - public IXmlTag GetTagByDef(string defType, string defName) + public ITreeNode GetTagByDef(string defType, string defName) { return GetTagByDef($"{defType}/{defName}"); } - + [CanBeNull] - public IXmlTag GetTagByDef(string defId) + public ITreeNode GetTagByDef(string defId) { if (!DefTags.ContainsKey(defId)) return null; @@ -43,6 +54,20 @@ public IXmlTag GetTagByDef(string defId) return DefTags[defId]; } + public List GetDefsByType(string defType) + { + return DefTags + .Keys + .Where(key => key.StartsWith($"{defType}/")) + .Select(defId => ExtraDefTagNames.ContainsKey(defId) ? ExtraDefTagNames[defId] : defId) + .ToList() + .Concat( + ExtraDefTagNames.Keys + .Where(key => key.StartsWith($"{defType}/")) + .Select(key => ExtraDefTagNames[key]) + ).ToList(); + } + public override object Build(IPsiSourceFile sourceFile, bool isStartup) { if (!IsApplicable(sourceFile)) @@ -61,9 +86,10 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup) foreach (var tag in tags) { var defName = tag.GetNestedTags("defName").FirstOrDefault()?.InnerText; + var defNameTag = tag.GetNestedTags("defName").FirstOrDefault().Children().ElementAt(1); if (defName is null) continue; - defs.Add(new RimworldXmlDefSymbol(tag, defName, tag.GetTagName())); + defs.Add(new RimworldXmlDefSymbol(defNameTag, defName, tag.GetTagName())); } return defs; @@ -75,7 +101,7 @@ public override void Merge(IPsiSourceFile sourceFile, object builtPart) AddToLocalCache(sourceFile, builtPart as List); base.Merge(sourceFile, builtPart); } - + public override void MergeLoaded(object data) { PopulateLocalCache(); @@ -87,32 +113,100 @@ public override void Drop(IPsiSourceFile sourceFile) RemoveFromLocalCache(sourceFile); base.Drop(sourceFile); } - + private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List cacheItem) { + ScopeHelper.UpdateScopes(sourceFile.GetSolution()); 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)); + var matchingDefTag = xmlFile + .GetNestedTags("Defs/*/defName").FirstOrDefault(tag => + tag.Children().ElementAt(1).GetTreeStartOffset().Offset == item.DocumentOffset); + + if (matchingDefTag is null) return; + + var xmlTag = matchingDefTag.Children().ElementAt(1); + + AddDefTagToList(item, xmlTag); }); + + void AddDefTagToList(RimworldXmlDefSymbol item, ITreeNode xmlTag) + { + 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}"; + } + } + } + + if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) + DefTags.Add($"{item.DefType}/{item.DefName}", xmlTag); + else + DefTags[$"{item.DefType}/{item.DefName}"] = xmlTag; + } } private void RemoveFromLocalCache(IPsiSourceFile sourceFile) { var items = Map!.GetValueSafe(sourceFile); - + items?.ForEach(item => { - if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) + 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); } + + public void AddDeclaredElement(ISolution solution, ITreeNode owner, string defType, string defName, + bool caseSensitiveName) + { + if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices()); + + if (_declaredElements.ContainsKey($"{defType}/{defName}")) + { + _declaredElements[$"{defType}/{defName}"].Update(owner); + return; + } + + var declaredElement = new XMLTagDeclaredElement( + owner, + defType, + defName, + caseSensitiveName + ); + + // @TODO: We seem to get "Key Already Exists" errors. Race condition? + _declaredElements.Add($"{defType}/{defName}", 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/RimworldXmlDefSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs index 9bf0b60..fd58ba0 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using JetBrains.ReSharper.Psi.Xml.Tree; +using JetBrains.ReSharper.Psi.Tree; using JetBrains.Serialization; using JetBrains.Util.PersistentMap; @@ -17,7 +17,7 @@ public class RimworldXmlDefSymbol // public IXmlTag Tag { get; } - public RimworldXmlDefSymbol(IXmlTag tag, string defName, string defType) + public RimworldXmlDefSymbol(ITreeNode tag, string defName, string defType) { DefName = defName; DefType = defType; diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs index 8768264..63c9121 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; +using System.Linq; using System.Xml; using JetBrains.ReSharper.Psi; using JetBrains.ReSharper.Psi.Tree; -using JetBrains.ReSharper.Psi.Xml.Tree; using JetBrains.Util; using JetBrains.Util.DataStructures; @@ -11,31 +11,37 @@ namespace ReSharperPlugin.RimworldDev.TypeDeclaration; /** * This class allows us to create an IDeclaredElement out of an IXmlTag, allowing us to use it as an IReference */ -public class XMLTagDeclaredElement: IDeclaredElement +public class XMLTagDeclaredElement : IDeclaredElement { - public XMLTagDeclaredElement(IXmlTag owner, string shortName, bool caseSensitiveName) + public XMLTagDeclaredElement(ITreeNode owner, string defType, string defName, bool caseSensitiveName) { this.owner = owner; myPsiServices = owner.GetPsiServices(); - ShortName = shortName; + ShortName = $"{defType}/{defName}"; CaseSensitiveName = caseSensitiveName; PresentationLanguage = owner.Language; } - private readonly IXmlTag owner; + public void Update(ITreeNode newOwner) + { + owner = newOwner; + myPsiServices = newOwner.GetPsiServices(); + } + + private ITreeNode owner; public string ShortName { get; } public bool CaseSensitiveName { get; } public PsiLanguageType PresentationLanguage { get; } - - private readonly IPsiServices myPsiServices; - + + private IPsiServices myPsiServices; + public DeclaredElementType GetElementType() => XmlTagDeclaredElemntType.Instance; public bool IsValid() => true; - + public bool IsSynthetic() => false; public IList GetDeclarations() @@ -47,7 +53,7 @@ public IList GetDeclarationsIn(IPsiSourceFile sourceFile) { if (!HasDeclarationsIn(sourceFile)) return EmptyList.Instance; - return new List {new XmlTagDeclaration(owner, this, ShortName)}; + return new List { new XmlTagDeclaration(owner, this, ShortName) }; } public HybridCollection GetSourceFiles() @@ -64,8 +70,7 @@ public bool HasDeclarationsIn(IPsiSourceFile sourceFile) public IPsiServices GetPsiServices() => myPsiServices; - public XmlNode GetXMLDoc(bool inherit) => (XmlNode) null; - - public XmlNode GetXMLDescriptionSummary(bool inherit) => (XmlNode) null; + public XmlNode GetXMLDoc(bool inherit) => (XmlNode)null; + public XmlNode GetXMLDescriptionSummary(bool inherit) => (XmlNode)null; } \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs index 9245178..3929b4b 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs @@ -15,13 +15,13 @@ namespace ReSharperPlugin.RimworldDev.TypeDeclaration; /** * In order to have an IDeclaredElement for XMLTags, we need a IDeclaration implementation for it. For the most part this - * is just a wrapper around the original IXmlTag + * is just a wrapper around the original ITreeNode */ -public class XmlTagDeclaration: IXmlTag, IDeclaration +public class XmlTagDeclaration: IDeclaration { - private readonly IXmlTag owner; + private readonly ITreeNode owner; - public XmlTagDeclaration(IXmlTag owner, IDeclaredElement declaredElement, string declaredName) + public XmlTagDeclaration(ITreeNode owner, IDeclaredElement declaredElement, string declaredName) { this.owner = owner; DeclaredElement = declaredElement; @@ -87,57 +87,6 @@ public IReadOnlyCollection FindNodesAt(TreeOffset treeOffset) => public NodeUserData PersistentUserData => owner.PersistentUserData; - public TReturn AcceptVisitor(IXmlTreeVisitor visitor, TContext context) => - owner.AcceptVisitor(visitor, context); - - public XmlTokenTypes XmlTokenTypes => owner.XmlTokenTypes; - - public IXmlTag GetTag(Predicate predicate) => owner.GetTag(predicate); - - public TreeNodeEnumerable GetTags() where T : class, IXmlTag => owner.GetTags(); - - public TreeNodeCollection GetTags2() where T : class, IXmlTag => owner.GetTags2(); - - public IList GetNestedTags(string xpath) where T : class, IXmlTag => owner.GetNestedTags(xpath); - - public TXmlTag AddTagBefore(TXmlTag tag, IXmlTag anchor) where TXmlTag : class, IXmlTag => - owner.AddTagBefore(tag, anchor); - - public TXmlTag AddTagAfter(TXmlTag tag, IXmlTag anchor) where TXmlTag : class, IXmlTag => - owner.AddTagAfter(tag, anchor); - - public void RemoveTag(IXmlTag tag) => owner.RemoveTag(tag); - - public TreeNodeCollection InnerTags => owner.InnerTags; - - public TXmlAttribute AddAttributeBefore(TXmlAttribute attribute, IXmlAttribute anchor) - where TXmlAttribute : class, IXmlAttribute => - owner.AddAttributeBefore(attribute, anchor); - - public TXmlAttribute AddAttributeAfter(TXmlAttribute attribute, IXmlAttribute anchor) - where TXmlAttribute : class, IXmlAttribute => - owner.AddAttributeAfter(attribute, anchor); - - public void RemoveAttribute(IXmlAttribute attribute) => owner.RemoveAttribute(attribute); - - public IXmlTagHeader Header => owner.Header; - - public IXmlTagFooter Footer => owner.Footer; - - public bool IsEmptyTag => owner.IsEmptyTag; - - public ITreeRange InnerXml => owner.InnerXml; - - public TreeNodeCollection InnerTextTokens => owner.InnerTextTokens; - - public string InnerText => owner.InnerText; - - public string InnerValue => owner.InnerValue; - - public IXmlTagHeader HeaderNode => owner.HeaderNode; - - public IXmlTagFooter FooterNode => owner.FooterNode; - // IDeclaration public XmlNode GetXMLDoc(bool inherit) => null; diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml index 54aec81..fb6bed4 100644 --- a/src/rider/main/resources/META-INF/plugin.xml +++ b/src/rider/main/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ com.jetbrains.rider.plugins.rimworlddev Rimworld Development Environment - 2023.3.3 + 2023.4 Garethp com.intellij.modules.rider @@ -18,7 +18,10 @@ in your mods!

      -
    • Fixed some missed API Changes in Rider 2023.3.3
    • +
    • Added a Find Usages for XML Defs
    • +
    • Added support for Custom Def Classes
    • +
    • Add a new "Generate" menu feature to add props to your XML
    • +
    • Improved detection of various properties that weren't being picked up as references before

    ]]>