Series: Building IntelliJ Plugins for Complex XML Systems
There is a class of problem that appears constantly in large configuration-driven systems: the structure you see is not the structure that matters.
You see a tree of XML files. The system behaves as a graph of interdependent entities. The IDE knows nothing about that graph — it only knows about tags and attributes. So you navigate it manually: file by file, search by search, cross-reference by cross-reference.
This article is about that problem, why existing tools do not solve it, and how I approached building an IntelliJ plugin that does — using RimWorld modding as the concrete domain.
The Context: Game Modding as an Engineering Problem
RimWorld is a colony management game with an unusually open modding system. Almost all game content — items, research, factions, recipes, pawns, events — is defined in XML. Mods extend or override that content by adding their own XML definitions.
This makes it accessible. At small scale, modding feels straightforward: add a file, define a thing, run the game.
At larger scale, that feeling disappears fast.
A serious mod (like the X-Rim:Enemy Unknown project) can contain dozens of files and hundreds of definitions. Community modpacks combine the outputs of multiple independent mods. At that point, the XML stops being “data files” and starts being a system — with its own dependency structure, inheritance hierarchy, and cross-cutting references that can fail in non-obvious ways.
The engineering problem is real, but it is underexplored from a tooling perspective. Most mod developers use VS Code or a text editor. They rely on search, intuition, and manually maintained mental models of how their definitions relate to each other.
That is not sustainable past a certain scale.
Surface Symptoms: What Actually Goes Wrong
The problems that appear in large RimWorld mods are not random. They follow patterns.
Broken cross-references. A definition references another by name — a research prerequisite, a parent definition, a recipe ingredient. That reference is a plain string. If the target doesn’t exist, or has been renamed, or lives in a mod that loads in the wrong order, nothing in the IDE flags it. The game silently fails at runtime, or throws an error in the log that requires reading to connect to a source.
Inheritance confusion. RimWorld uses XML inheritance: a definition can declare ParentName="WeaponBase" and inherit properties from that base. Base definitions can be abstract (meaning they don’t appear in-game directly). The inheritance chain can be several levels deep and spread across multiple files, including vanilla game data. To understand what properties a given definition actually has, you need to manually trace the chain upward.
Cross-mod dependency failures. When your mod references definitions from another mod — which is common — those references are invisible in the IDE. There is no index of the external mod’s definitions. You cannot navigate to them. Whether they exist is something you find out by running the game.
Patch operations on unknown targets. RimWorld supports XML patching: modifying existing definitions without replacing them. But a patch operates on a definition you cannot navigate to. You write the path, run the game, and find out if it worked.
The common thread: the IDE treats XML as text. The system treats it as a semantic graph. That mismatch generates friction at every step.
Why the Standard Tools Don’t Solve This
Before building anything, it’s worth being honest about what already exists.
VS Code with RimWorld XML extensions provides some syntax highlighting and schema validation. It does not build a semantic index, does not resolve cross-references, and does not model inheritance. It makes the XML more readable — it does not make the system navigable.
RimWorld LSP (Language Server Protocol for VS Code) is the most capable existing option. It provides definition lookup and some reference resolution. It is VS Code-native and targets the VS Code ecosystem. It is not particularly relevant to engineers already working in IntelliJ-based IDEs — the tooling ecosystem, shortcut model, and integration points are different.
JetBrains XML tools give you good XML editing and XSD-based validation. RimWorld XML does not have a maintained XSD schema that maps its actual semantics. The built-in tools do not know that <researchPrerequisite> contains a reference to a ResearchProjectDef — they see a string value.
Manual search is what most people use. It works. It scales poorly and becomes the dominant time cost in large projects.
The gap is specific: there is no tool that builds a semantic model of a RimWorld project inside IntelliJ. That is the gap I am building into.
The Core Idea: Reconstruct the Graph
The key reframe is this:
XML is a syntax for encoding data. In a RimWorld mod project, that data encodes a domain model — a graph of definitions connected by typed relationships. The tooling problem is not about XML; it’s about recovering that domain model and making it navigable.
Once stated that way, the approach follows:
Extract entities. Parse XML files and pull out definition objects with identity (defName), type (ThingDef, ResearchProjectDef, etc.), and parent reference (ParentName). Stop treating these as XML tags. Treat them as domain entities.
Build an index. Map each entity by its logical key — DefType::defName — so resolution is O(1) regardless of which file the definition lives in.
Recover edges. Define, via configuration, which XML paths carry references to which definition types. A field like recipeMaker/researchPrerequisite inside a ThingDef is not a string — it’s a reference to a ResearchProjectDef. Encode that rule. Now the graph has typed edges.
Expose the graph to the IDE. Wire navigation into IntelliJ’s reference resolution system (PSI). When a developer clicks on a value that is actually a reference, the IDE resolves it and jumps to the target — exactly like navigating between code symbols.
This is not a novel idea in the IDE plugin space. It is exactly what language servers do for programming languages. The gap is that no one built it for this domain.
Phase 1: What the Plugin Does Now
The first phase establishes the foundation: parsing, indexing, reference resolution, and incremental updates.
Parsing uses IntelliJ’s PSI (Program Structure Interface) to traverse XML files and extract definitions. Each parsed definition becomes a RimDef — a lightweight domain object carrying type, defName, parent, and a reference to its PSI node.
Indexing is project-scoped and held in memory. The index maps DefType::defName to RimDef. The key format is deliberate — it disambiguates definitions with the same name but different types. The index builds on project startup in a background thread, avoiding UI blocking.
Reference resolution is driven by a JSON configuration file that ships with the plugin. It maps definition types to the XML paths within their structure that carry references. When the reference provider encounters a tag at one of those paths, it creates a PsiReference that resolves through the index.
Incremental updates listen to PSI change events. When a file changes, only that file is re-parsed and its definitions re-indexed. There is no full reindex on every edit.
Validation is currently limited to detecting missing <defName> nodes — definitions that cannot participate in the index. This is caught at the annotator level with an inline error highlight.
The result: within a project, you can navigate from a reference value directly to its definition. Ctrl+Click works across files, across definition types, without search.
What Phase 1 Does Not Handle (And Why That’s Intentional)
The current system is correct within a bounded domain. It is not yet correct for the full RimWorld environment.
Inheritance is not resolved. The ParentName attribute is parsed and stored, but the inheritance chain is not walked. This means the effective property set of a definition — what it actually inherits — is not knowable from the model. Navigation to parent definitions exists for explicit references, but not for implicit property inheritance.
Vanilla data is not indexed. Most RimWorld mods depend on base-game definitions. Those definitions live in the game installation, not in the mod project. The current plugin has no mechanism to index them. Cross-references to vanilla definitions will not resolve.
Patch files are not processed. XML patches modify existing definitions. They are not definitions themselves. The current model does not track what patches do, so the effective state of a patched definition is unknown.
Cross-mod references do not resolve. Same problem as vanilla data — external mod definitions are outside the project scope.
These are not omissions from carelessness. They are the next phases of the model, in the order that makes the model incrementally more correct. The principle is deliberate: correctness of the existing scope before expansion into new scope.
Risks Worth Naming
Three engineering risks are real here and worth stating plainly:
IntelliJ PSI API instability across IDE versions. PSI is not a stable public API in the same sense that a library contract is. Deprecations and behavioral changes happen across major IDE releases. A plugin that works on IDEA 2023.2 may require adaptation for 2024.x. The mitigation is CI testing against a version matrix — not assuming compatibility without verification.
RimWorld XML structure changes between game versions. The game’s definition schema evolves. New definition types appear, existing paths change. The config-driven approach to reference mapping is the mitigation here: updating the JSON configuration file handles schema changes without touching the resolution code. This is a low-friction update path.
Ecosystem overlap with RimWorld LSP. The VS Code LSP covers some of the same ground. The risk is low because the audiences are different — IntelliJ users are not going to switch editors for a mod project, and VS Code users are not the target. But it’s worth monitoring what the LSP implements, particularly if it adds features that define user expectations for this class of tool.
Why IntelliJ Specifically
The plugin is built for IntelliJ-family IDEs for a reason that goes beyond personal preference.
IntelliJ’s plugin architecture is mature, well-documented, and specifically designed for the kind of deep IDE integration this requires. PSI gives you a structured, queryable representation of any file type — not just programming languages. The extension point system (reference providers, annotators, indexers, startup activities) maps directly to the problem structure.
More importantly, IntelliJ is where the engineers who build complex mod systems tend to work. If you’re writing a large mod with custom Java components, a build system, and hundreds of XML configuration files, you’re probably already in IntelliJ. Having the XML tooling in the same IDE — with the same navigation model, the same key bindings, the same project context — is a significant workflow improvement over switching editors.
The argument for IntelliJ is not that VS Code is insufficient. It’s that IntelliJ is already where this work is happening, and a plugin that integrates with the IDE runtime is more useful than a separate tool.
Key Takeaways
XML-based configuration systems at scale create semantic graphs that text editors cannot navigate. This is not a RimWorld-specific problem. It appears anywhere domain relationships are encoded in XML or JSON: enterprise configurations, integration schemas, security policies, legacy platform setups.
The right framing is domain modeling, not text processing. A plugin that provides “XML navigation” is describing its mechanism. A plugin that reconstructs a semantic graph of definition dependencies is describing its purpose. The purpose matters for how you design the architecture.
Config-driven behavior is not a convenience feature — it’s a correctness strategy. Hardcoding reference paths makes the plugin brittle to schema changes and impossible to extend without code changes. Externalizing the mapping keeps the resolution logic independent of the domain specifics.
Build correctness in layers, not features in parallel. The order of phases — core indexing, inheritance, vanilla data, patch system, graph visualization — is determined by dependency, not by user demand. A visualization built on an incorrect model is worse than no visualization.
The staging/production gap exists in plugin development too. What works in a small test project with clean XML may break in a 200-file mod project with circular inheritance, malformed patches, and definitions that override vanilla entries. That’s where the real failure modes surface.
What’s Next
The next article in this series covers Phase 2: inheritance resolution.
The central problem is this: once you have an index of definitions, you discover that most definitions are not self-contained. Their effective property set is the result of walking a parent chain that may cross files, reference abstract nodes, and terminate in vanilla data that is not in the index.
Resolving that requires more than a lookup table. It requires walking a dependency graph — and handling the cases where that graph has gaps, cycles, or nodes that live outside the project scope.
That’s where the model gets interesting.
Project: RimWorld XML Toolkit