Skip to content

Protocol Reference

Joël Deffner edited this page Sep 21, 2026 · 18 revisions

px-lsp wire protocol

Mirror of docs/PROTOCOL.md, the canonical copy for the published 0.5.0 release.

The contract between the px-lsp language server (@px-lsp/server) and any client — the bundled VSCode extension, neovim, or an embedding application spawning px-lsp --stdio. TypeScript clients should import the method constants and payload types from @px-lsp/protocol/protocol (the single source of truth); this document mirrors that file for non-TypeScript consumers.

Wiring the server into an application rather than an editor? Embedding next to this file is the guide for that: the process contract, the initialization options worth sending, URI and document-sync conventions, and the in-tree reference clients. This document stays the per-method reference.

Versioning: the protocol is versioned with @px-lsp/protocol (its own version, independent of the extension since extension 0.3.3; first npm release 0.1.0). Treat any change here as an API change: additions are backward-compatible, renames/removals are called out in the package's CHANGELOG.md. Current as of @px-lsp/protocol 0.2.0, which carries the full contract below including serverInfo in the initialize result, the client capability object superseding clientCommands, client.fileLinks, paradox/scopeAt, the paradox/exampleWiki pair, and dataDir (paradox/* has been the method prefix since extension 0.1.2).

Transport and lifecycle

  • Transports: --stdio (what external clients use; also the default when no transport argument is given and the process was not forked over IPC) and node-ipc (VSCode). --socket=<port> and --pipe=<name> are accepted by the underlying library, but nothing ships using them. Standard JSON-RPC 2.0 LSP framing. The process-level contract around this (the processId orphan watchdog, the clean-stdout guarantee, the shutdown/exit sequence) is in Embedding.
  • Standard LSP: the server implements completion (+resolve), signatureHelp, hover, definition, references, rename (+prepare), documentSymbol, workspaceSymbol, codeAction, inlayHint, foldingRange, documentFormatting, documentColor + colorPresentation, semanticTokens (full), publishDiagnostics, and workDoneProgress for the vanilla scan.
  • Document sync: Incremental, with openClose and save. A didChange content change carrying no range (a full-document replacement) is accepted too, and is the simpler choice for a non-editor client. version must increase on every change: the per-document parse cache is keyed by uri + version, so a reused version serves the previous parse.
  • Language ids: paradox (script .txt), paradox-loc (localization .yml), paradox-gui (.gui). The client decides which files get which id; the server keys per-request behavior off it.

Symbol lookup and rename

Symbol identity includes its definition kind. Source priority applies within each kind, so a mod localization key does not hide a vanilla trait with the same name. Definition and reference requests use the schema and grammar at the cursor when the type is known. Definition results retain all same-kind override sites. Scope inference continues to rank and annotate candidates without removing them.

Open script and localization documents are indexed from their current text. Send didChange before requesting completion, navigation or rename; a file watcher does not replace an open document's text. didClose restores the saved content or removes an unsaved-only definition. Watcher notifications also invalidate cached reference searches in dependency roots.

For rename, declare standard capabilities.workspace.workspaceEdit.documentChanges: true and enforce each returned TextDocumentEdit.textDocument.version. Closed files carry a null version. Without this capability, rename returns WorkspaceEdit.changes only when all open sources match disk; otherwise the request fails and asks the user to save first. Rename rejects ambiguous symbol types, stale or unreadable source text, read-only targets and a target name already used by that kind. Graphics and GUI symbol types whose reference forms are not fully indexed cannot be renamed. A declaration with several occurrences of its name on one line must be split across lines before rename.

Initialization

The initialize result carries standard LSP serverInfo: { name: "px-lsp", version }, where version is the @px-lsp/server package version. Clients can use it for compatibility checks.

initializationOptions (all fields optional; the server has fail-soft fallbacks for bare clients):

interface ParadoxInitOptions {
  storageDir?: string;   // server-side cache dir; default: <os tmp>/px-lsp
  dataDir?: string;      // root of the bundled per-game data: <dataDir>/<gameId>/{wikidocs/,freqs.json}
                         // default: data/ next to dist/server.js
  wikidocsDir?: string;  // DEPRECATED narrow override for the wikidocs folder alone, see below
  client?: ParadoxClientCapabilities; // what this client implements; absent = plain LSP client
  clientCommands?: boolean;           // DEPRECATED alias, see below
  settings?: Partial<ParadoxSettings>;
}

interface ParadoxClientCapabilities {
  hoverHtml?: boolean;      // client renders the sanitized <span style="color:var(--vscode-*)"> hover markup
  commands?: string[];      // the px.* command ids this client registers (see "Client command ids")
  ownFileWatcher?: boolean; // client watches the mod tree itself and pushes paradox/modFileChanged
  fileLinks?: boolean;      // client's hover renderer navigates file: links (since @px-lsp/protocol 0.1.1)
  hoverIcons?: boolean;     // client sets supportThemeIcons, so hover badges may use $(codicon) glyphs
}

interface ParadoxSettings {
  gameId?: string;             // game profile: "ck3" | "vic3" | "eu5"; unknown/absent -> default game ("ck3")
  gamePath: string | null;     // the game's data folder ("<install>/game")
  logsPath: string | null;     // folder with script_docs logs
  modPath: string | null;      // default: first workspace folder
  parentPaths: string[];       // dependency mods, load order, base first
  workspaceMods?: string[];    // mods being EDITED (reference indexing + diagnostics)
  locLanguage: string;         // "english", ...
  completionMode?: "minimal" | "examples" | "names"; // default minimal
  texturePreviewBackground?: "checkerboard" | "dark" | "light" | `#${string}`; // default checkerboard
  scopeInlayHints: boolean;
  indexAssets?: boolean;      // default true; index .asset definitions and references
  hoverDetail?: "compact" | "standard" | "full"; // how much a hover shows; default "standard"
  calendar?: CalendarSetting | null;  // FALLBACK custom era calendar for date display (inlay hints + hover); absent = off.
                               //   A mod's own <mod>/.px-toolkit/calendar.json (same shape) wins for files under
                               //   that mod; the server reads it itself and re-reads it when the file changes
                               //   { epoch: number; after: string; before?: string;
                               //     months?: string[] }   // the engine's 12 months under the mod's names
                               //   script year >= epoch displays as (year-epoch+1) <after>,
                               //   year < epoch as (epoch-year) <before>; sanitized on intake
  diagnosticsIgnore: string[];         // diagnostic codes to suppress
  diagnosticsIgnorePatterns: string[]; // workspace-relative globs to suppress
  diagnosticsVanilla: boolean;         // false (default) = never diagnose game files
}

indexAssets defaults to true. Set it to false to skip .asset definition and reference indexing across workspace mods, dependency mods and vanilla, including on-demand reference searches. Changing it through paradox/configChanged rebuilds the index without a restart. Open-file syntax features and on-demand texture previews remain available. Support for .asset indexing comes from the active game profile.

texturePreviewBackground sets the display background for texture hover thumbnails: checkerboard (default), dark, light, or a six-digit hex color such as #376694. Invalid values use checkerboard. The server composites the background into its bounded PNG thumbnail, so clients need no custom hover styling. Changing it through paradox/configChanged affects subsequent hovers without rebuilding the index. Source textures are unchanged. VS Code shares this value with its DDS editors through px.texturePreview.background; the editor palette updates one workspace preference, and exported PNGs retain their original transparency.

Completion documentation uses the first supported format in textDocument.completion.completionItem.documentationFormat, falling back to plain text when none is declared. Plain-text clients receive insertion previews and value hints without Markdown tables or code fences. paradox/snippetCatalogue always returns Markdown in its explicitly typed preview field, independently of the editor's completion capabilities.

Standard event value completions include proposed localization keys for the current event's title, description and option names where the game profile has a verified convention (CK3 and Victoria 3, including Victoria 3 flavor text). New keys have a detail identifying them as new localization keys, with no file edits or client commands. Existing keys retain their definition metadata. Proposals use the current document's event ID and option order, skip names assigned to another option, and do not add entries to the definition index or localization files.

completionMode controls ordinary script keyword insertion. minimal (the default) inserts the documented operator and blank values and the fields from valid documented examples or scripted parameters, omitting fields marked optional, with no example values. examples restores full documented blocks and scripted-call parameter snippets. names inserts only keyword names. Unknown syntax stays a name in Minimal mode. Explicit definition/child-block snippet items and paradox/snippets keep their full templates. Snippet-capable clients receive CompletionItemKind.Snippet for generated templates, so the editor shows its snippet icon. Reference/value completions are unchanged. Clients without standard LSP snippet support receive the same punctuation as plain text. Send paradox/configChanged to change the mode without restarting or rebuilding the index. Resolving a completion adds an insertion preview and expected-value descriptions from the token documentation or the definition's @param tags. Example values are not treated as confirmed datatypes. These hints are documentation only; insertText is unchanged.

Every capability in client is independent and defaults to off, so a client declares exactly what it implements and the server tailors its output per capability (see "Degraded modes"). A client can take the hover markup without registering any command, or register one command and not the others.

One capability an embedder should declare is NOT in client, because LSP already has it: ${1:…} completion snippets are gated on the standard capabilities.textDocument.completion.completionItem.snippetSupport of the initialize params. Without it every completion insert is plain text (see "Degraded modes").

clientCommands: true is a deprecated alias kept for older clients: it resolves to { hoverHtml: true, commands: <every id below>, ownFileWatcher: true, fileLinks: true } plus snippet support, which is what the VSCode extension used to declare. false or absent resolves to all-off. It is ignored when client is present. New clients should send client.

One server instance serves one game at a time, and choosing it is the client's job: there is no server-side auto-detection. The VSCode extension detects it per workspace (descriptor shape, else configuration); any other client sends what it knows. It travels as settings.gameId at initialize and in paradox/configChanged; changing it triggers a full reload.

Bundled data is per-game: everything the server loads from disk lives under <root>/<gameId>/, where the root is dataDir when the client sends one and otherwise data/ next to dist/server.js. wikidocs/ and freqs.json are resolved independently under that folder, and the root is re-resolved against the new gameId when the game changes. CK3 and Victoria 3 also ship script_docs/ snapshots; all three games ship data-type dump snapshots. User-generated dumps take priority. A missing per-game bundle is supported and can leave a category without definitions.

wikidocsDir is a deprecated narrow override kept for older clients: it replaces the wikidocs/ folder alone, leaves freqs.json on the dataDir/ bundle root, and, being one fixed folder, does not follow a gameId change. Clients that ship the data apart from the server bundle should send dataDir instead.

Settings updates and workspace files

Initialization accepts partial ParadoxSettings and fills omitted fields with defaults. paradox/configChanged replaces the configured settings: omitted fields return to defaults. Standard workspace/didChangeConfiguration instead accepts { "settings": { "pxLsp": { ... } } }, where pxLsp is a partial ParadoxSettings update. Supplied fields replace their previous values, including whole arrays; omitted fields retain their configured values. Other sections, including the VS Code extension's native px section, are ignored. A malformed section or invalid known field leaves the previous configuration intact. gamePath, logsPath, and modPath accept null, which clears that configured path. calendar: null clears the configured calendar. texturePreviewBackground uses checkerboard for invalid values. Other fields do not accept null.

Root resolution is the same at initialization and after every update. A nonempty modPath and the roots in workspaceMods are editable mods. If both are empty, the first initialization workspace folder (or rootUri when no folder is supplied) becomes modPath. This fallback is session context, so a later workspaceMods update can replace it. Sending modPath: null with workspaceMods: [] restores the workspace fallback. Dependency roots in parentPaths remain read-only and keep their supplied load order.

Clients with capabilities.workspace.configuration: true receive a workspace/configuration request after initialized, before the first index build. Its single item has section: "pxLsp" and the initialization workspace root as scopeUri, when available. Return [partialSettings]; this patches the initialization settings. A failed request or absent/null section preserves the current settings. An empty or null didChangeConfiguration.settings triggers another pull when supported. A response is ignored if a newer push, custom update, or pull started while it was pending. Dynamic registration of workspace/didChangeConfiguration occurs only when the client advertises its dynamicRegistration capability. Clients without configuration capabilities can use initialization options and either push transport.

For example, this standard notification changes hover detail while retaining the game, roots, and other settings. Display-only settings do not rebuild the index; paths, game, localization language, and asset indexing changes do.

{ "jsonrpc": "2.0", "method": "workspace/didChangeConfiguration", "params": { "settings": { "pxLsp": { "hoverDetail": "compact" } } } }

Creation, changes, and deletion of schema.json or playset.json in an editable mod's profile config directory trigger a debounced full rebuild. The profile's legacy directory is also watched where supported. Standard workspace/didChangeWatchedFiles and custom paradox/modFileChanged events share the same debounce, so a burst across both transports rebuilds once. Custom watcher clients must include these JSON files; dynamically registered server watchers include them. Other file changes keep their existing single-file behavior.

Reloading a playset does not extend the client's watched roots. A host must watch external dependency folders if it needs later edits in those files to update the index.

Custom methods: client → server

Method Kind Params → Result
paradox/configChanged notification ParadoxSettings
paradox/modFileChanged notification { fsPath: string } — a mod file changed on disk (client-side watcher); re-indexes the file, or rebuilds for workspace schema/playset changes
paradox/reloadDocs request { force: boolean } → { tokens: number, status?: StatusPayload }, reload both script docs and data types; return and notify their loaded sources
paradox/indexStats request null → IndexStats (definition counts by kind/source)
paradox/lookupLoc request { key: string, language?: string } → LocEntryInfo[] — localization entries for a key, mod first; omitted language uses the configured language
paradox/locText request LocTextParams → LocTextResult — the same values as the PLAYER reads them: { raw, text, resolved } per key, with the game's markup stripped and its [ … ] datafunctions resolved. A key the loc index cannot find is absent from values
paradox/modOverview request ModScopedParams → ModOverview — content inventory by kind
paradox/locCoverage request ModScopedParams → LocCoverage[] — per-language missing/orphaned/untranslated keys
paradox/overrides request ModScopedParams → OverrideInfo[] — mod definitions shadowing vanilla/parents, with LIOS/FIOS winner
paradox/eventDetail request { id: string, file?: string } → EventDetail | null — full event structure for an inspector UI; optional file selects one exact source
paradox/eventGraph request EventGraphParams → EventGraph — event/on_action reference graph, plus the suggestions catalog a query box completes against
paradox/eventVocabulary request EventVocabularyParams → EventVocabularyResult — the keys, value sets, effect and trigger tokens an event editor may offer, each with its own documentation
paradox/eventValueOptions request EventValueOptionsParams → EventValueOptionsResult | null — the value set one VALUE belongs to, resolved through the definition index (secret_cultivator is a secret, so the answer is every indexed secret, mod entries first); null when the value resolves to nothing enumerable
paradox/eventBanner request { theme } → EventBannerResult — the illustration an event theme puts behind its window, as a mod-relative texture path, or a reason when it resolves to nothing
paradox/dynastyTree request DynastyTreeParams → DynastyTreeResult — without dynasty, every dynasty the index knows as a picker list (mod entries first, each with its member and house counts); with dynasty, that dynasty's houses and members read out of history/characters, plus the parents and spouses they name from other dynasties, marked external
paradox/exampleWiki request null → ExampleWikiIndex — one compact row (name, kind, shortDoc, count) per trigger, effect, event target, modifier, datafunction, data type, keyword, scope word, and indexed variable or list the server knows, most used first, plus the sentences naming where the rows came from
paradox/exampleWikiEntry request ExampleWikiEntryParams → ExampleWikiDetail | null — everything known about one row: documentation, scopes, the usage: block, datafunction signature, observed literal arguments, members and producers, a variable's valueType and containers, the triggers, effects and targets usable from each scope the token outputs (fromScope), and example sites as absolute paths with inline context; null when the name is not in the catalog
paradox/dependencies request DependenciesParams → DependenciesResult — dependents/dependencies of a definition (by cursor or name), plus the .gui paths reaching it when guiUses is set
paradox/scopeAt request ScopeAtParams → ScopeAtResult | null — inferred scope chain (outermost first) and visible saved scopes at a position; null when the document is not an open script document
paradox/snippets request SnippetsParams → SnippetsResult — the code snippets a host can offer at a cursor: the measured skeleton of the document folder's definition kind (form: "definition", header line included when the document declares none), that kind's child blocks (form: "block"), then the engine triggers/effects legal in the cursor's block whose script_docs entry ships a usable usage: example (form: "token", frequency-ordered, capped at 60). A token whose example marks fields # optional yields a second entry right after it, <token>.full, labelled " (all fields)", carrying every field the example shows while the first carries only the required ones. Every entry carries both a ${1:…} snippet and a plain form. A document the server does not know answers with an empty list
paradox/definitionForm request DefinitionFormParams → DefinitionForm | null — everything a visual creator needs to draw a form for one definition kind: the schema entry's folder, the full set of loc key patterns the game reads for the kind (not the conservative requiredLoc subset a diagnostic demands) and the icon folder, the harvested body keys (with the game's own docs, value hints and vanilla usage counts), the option list per referenced kind (each option labelled with its group where the kind has families), the values the game itself writes for keys no index can answer (sampled), the value list per trigger a condition builder may offer rows for (conditions), the modifier vocabulary, every indexed definition of the kind with its source, and (with name) that definition's block verbatim. null when the active game's schema has no such kind
paradox/modifierFormats request ModifierFormatsParams → ModifierFormatsResult | null — how the game PRINTS each modifier it knows: the player-facing label, decimals, color (which direction is good for the player), percent / alreadyPercent / noSign / hidden, and prefix / suffix / negativeSuffix as parts that are either a word or a texticon (texture plus an optional uv rectangle). With lines (loc keys), lines in the result holds each resolved key's value as the same parts, so a client can print a line of the game's own UI (a cost) with the game's icons. null when the active profile names no formats source or no game folder is configured
paradox/definitionEdit request DefinitionEditParams → DefinitionEditResult — text edits that write a definition into a script file: setProperties changes or removes keys of one top-level block, upsertBlock replaces or appends a whole name = { … }. Offsets into the request's text, one verdict per op
paradox/guiTree request { uri, text } → GuiTree — widget tree of a .gui document
paradox/guiLayout request { uri, text, visibility?, loc?, previewValues? } → GuiLayoutResult — measured layout rectangles for a .gui document, with stage timings, the conditional-visibility checks it met, and each textbox's text resolved through the loc index unless loc: "raw"
paradox/guiWidgetInfo request GuiWidgetInfoParams → GuiWidgetInfo | null — one widget's effective properties with the template/type each came from, its textures, and (on request) why its rect is where it is
paradox/guiDependencies request GuiDependenciesParams → GuiDependenciesResult — the scripted_guis and loc keys a .gui document (or one widget in it) reaches
paradox/guiPreview request { uri, text, entries: [{ name, kind: builtin|type|template|raw, fragment? }] } → GuiPreviewResult — one laid-out instance per palette entry for a library tile (the document's own declarations kept, the store shared with guiLayout); node: null + reason when nothing stands up; at most GUI_PREVIEW_MAX (48) entries per request
paradox/guiSaveValues request { path } → GuiSaveValuesResult — preview values read out of a save game: values keyed by datafunction chain without brackets (the shape guiLayout's previewValues takes), plus the source save's name, date and game. A chain the save has no field for is absent; an ironman or binary save comes back with error and no values. Victoria 3 (the played country, its ruler and heir) and Crusader Kings III (the played character's name, titles, currencies, age, house and character variables) both have an entity mapping; any other game answers the meta-only rows. A save whose script is zip-packed, which is what CK3 writes unless the game runs with -debug_mode, is unpacked while streaming
paradox/guiVocabulary request { uri, text } → GuiVocabularyResult — the widget names a designer palette may offer, plus the property names an inspector may offer per widget type: the bundled per-game harvest plus this document's own templates and types
paradox/guiSourceEdit request GuiSourceEditParams → GuiSourceEditResult | null — source edits for a designer gesture (one op, or a batch of ops answered as one edit set with a verdict each), or a refusal with a reason
paradox/guiWidgetEdit request GuiWidgetEditParams → GuiWidgetEditResult | null — DEPRECATED, the position/size half of guiSourceEdit

ModScopedParams is { modRoot?: string | null }: restrict a mod-scoped request to one workspace mod (absolute root path); absent = all workspace mods.

paradox/scopeAt reports scopes as string ARRAYS, never a single name: a link or iterator with several documented outputs stays ambiguous, and an empty array means unknown. That is the honest answer, not an error — the server annotates and ranks, it never hides or diagnoses on scope grounds. Render several as a|b and none as "unknown".

paradox/snippets answers only from what the game states. A definition skeleton carries a key because at least half of the game's own definitions of that kind carry it, in the median position it holds there; a value is pre-filled only where the key's whole measured vocabulary is small enough to be a choice, and is the key's own name as placeholder text otherwise. A kind whose definitions share no key at all (a scripted effect's body is whatever the effect does) still answers with the wrapper, the measured name shape and the file header, and an empty body — that is the honest shape, not an invented one. The same skeletons also reach clients that never send this request: they are completion items at a file's top level and on a blank line inside a definition body, ranked after the block's structure keys.

paradox/dynastyTree is one method with two answers, because a family tree needs the whole picker before it needs one family. Both come from the game's own files: the folders are the ones the active profile's schema maps to the dynasty, dynasty_house and character kinds, the members come from the character blocks (name, female, dynasty or dynasty_house, father, mother, culture, religion, trait, dna, the six skills of DYNASTY_SKILLS as a skills map, and the dated blocks whose KEY is the date of the birth, death or add_spouse inside them), and the display names come from the loc index, falling back to the loc key itself rather than inventing one. A character reaches its dynasty through its house when it names one. dna is the portrait name without the quotes the file may put around it, and a skill whose value is not a number (a script value) is left out of skills rather than guessed at, so a client rewriting the block keeps that line as written. nextCharacterId and nextDynastyId are the largest numeric id seen across game and mods plus one, so a client can offer a free id without searching. A profile whose schema has no dynasty kind answers supported: false with empty lists, which a client says out loud instead of drawing an empty tree.

Answering costs one full read of the character corpus, because the link points from a character to its dynasty and never back. The server does that read once per index revision: measured on a vanilla CK3 install (71 142 characters in 17.4 MB), 0.8 s for the first request, 12 ms for the next, and 1 ms for one dynasty; the list of 10 338 dynasties is a 2.7 MB answer.

The Examples Wiki is two requests because the shapes differ by orders of magnitude. paradox/exampleWiki answers the whole catalog as thousands of tiny rows, so a client filters and ranks locally instead of asking again per keystroke; paradox/exampleWikiEntry answers ONE row with its prose, its usage block and its example sites. Nothing in either answer is hand written: the rows come from the user's script_docs dump (or the bundled snapshot / wiki tables behind it), the datafunction tables (DumpDataTypes output or the bundled tables), the vanilla usage harvest, and the definition and reference indexes, and each answer says which of those it came from in its provenance / sources text. Example sites are searched in the game files when the entry is asked for, and come back as absolute paths with 1-based lines, so a client opens them without resolving anything. Every capped list carries its own total (literalsTotal, membersTotal, producersTotal, containersTotal), and an example list that is short for a reason carries examplesNote saying why.

An engine token whose scopes declare one or more output: <scope> entries (event targets, mostly) also carries fromScope, one ExampleWikiFromScope per produced scope: scope (the produced scope word), plus triggers, effects and targets with a triggersTotal / effectsTotal / targetsTotal beside each. The lists answer "what can I write once I am here": every trigger and effect whose own declared scopes contain that word, and every event target that declares it as an input:. They are ordered by vanilla usage count, most used first, and capped at 2000 names each with the true count in the total. The matching is word for word against the game's own docs, so a token that declares no scopes is simply absent from every list and nothing is inferred from a scope model. The field is optional and absent when the token produces no scope, so a client that ignores it is unaffected.

The two grammar ExampleWikiKinds (keyword, scope_word, exported as exampleWikiVocabularyKinds) are the script glue the game documents nowhere: limit, NOT, base, days and the scope words root, this, prev, from (chained forms included). They are the one part of the catalog whose prose is the toolkit's own rather than a dump's, they read the SAME table the hover card reads so the two cannot disagree, and every such article says so in its provenance. Their example sites are searched in the game files like an engine token's. A logic word has ONE article under its uppercase spelling, so not and NOT both resolve to NOT.

The seven variable ExampleWikiKinds (variable, local_variable, global_variable, variable_list, local_variable_list, global_variable_list, list, exported as exampleWikiVariableKinds) are the rows that come from the indexed script rather than from the engine: names the user's own files created with set_variable, add_to_variable_list, add_to_list and their relatives. Their articles carry valueType (what the set sites resolve to, or the word "unknown" — a value set from a runtime scope is not guessed at), containers (the top-level definitions the set sites sit in) and example sites labeled set or read. The rows follow the index, so they change with a save.

Every example site may carry context, the lines around it as written and dedented, with contextStart giving the 1-based line number of context[0]; line stays the site's own line, so a client can pick it out of the block. Both fields are optional and absent when the file could not be read, so a client that ignores them still has text.

paradox/eventGraph answers suggestions alongside the graph: the mod-side ids (event / on_action / decision, sorted, capped at 2000) and the namespaces those ids imply. It is the VOCABULARY, not the selection — the same list whatever root or namespace the request asked for — so a client completes a query box from the answer it already has instead of asking again. modRoot scopes it like the graph. The field is optional: a server that predates it simply omits it.

paradox/eventGraph reports a namespace (or the whole mod) as its DEFINITIONS, not as the endpoints of the edges between them, and then applies connectedOnly (on unless the request says false): definitions with no edge are dropped before their cards are read, root always stays, and a graph that lost every node says so in emptyReason. A client that wants the event the author just wrote, edges or not, sends connectedOnly: false. Edges that pass through a scripted effect are followed transitively (visited-guarded, three hops) and answered as a direct from -> to edge whose label reads via effect_a -> effect_b, so an event whose trigger_event sits inside a scripted effect is not reported as firing nothing.

An EventGraphNode also carries what a card says about itself without a second request: options (its option blocks) and triggerSummary (the first keys of its trigger block) for this mod's own definitions, read from the file the answer already parses, and fires (how many nodes of this graph it fires), counted from the edges; all three are optional and absent where unknown.

paradox/eventVocabulary is what an event editor is allowed to offer. Every list in it is derived, never hand-written: eventKeys / optionKeys from the active profile's structure table ordered by its usage counts, values from a key's declared enum: spec or from the schema's reference field resolved through the definition index (theme gives every indexed event_theme, this mod's entries first), effects / triggers from the parsed script_docs (or the bundled wiki fallback) with the log's own description, and savedScopes from the mod's own save_scope_as sites. Docs are capped to one line for a menu row. A key whose value is free text is simply absent from values; that is the signal to render an input instead of a dropdown. modRoot scopes the definition-backed sets like the graph.

paradox/eventDetail accepts an optional file selector as an absolute path or file URI. With it, only the event in that indexed file can match; a missing event or unreadable file returns null. Without it, the existing ID lookup order applies.

paradox/eventDetail carries an event's blocks twice over: keys / effectKeys summarize them for an inspector, and lines / totalLines / targets render them for a walkthrough. lines is the block flattened back into pseudo-script ({ depth, text, line } per statement) capped at 60 lines, with totalLines giving the real count so a UI states what it hid instead of truncating silently. An option's lines drop name / trigger / ai_chance / ai_value: those gate or label the option, they are not its effect.

Blocks and keys that only one game has are carried by the same shapes rather than a per-game payload, since the names do not collide: sections may hold a cancellation_trigger alongside trigger / immediate / after / on_trigger_fail, and flavor is the event's third displayed string where the game has one (absent otherwise). An option's effectKeys summary drops that game's option markers (default_option, highlighted_option) the same way it drops custom_tooltip; lines still renders them.

fields (on the detail and on each option) is the scalar key = value statements written directly in that block, each with its line, so an editor can rewrite one in place instead of re-parsing the file; bodyLine is where a new statement may be inserted. Blocks are not fields: they are sections and options. A key written twice keeps the LAST site, because that is the one the game reads.

targets are the references that hand control on: the step-into edges of an event chain. They come from the active profile's event/on_action reference fields (trigger_event and its { id = X } block form, on_action, on_actions, events, random_events, first_valid, plus whatever a game profile names), never from a hard-coded key list. The list is capped at 40 and targetsTotal gives the real count, the same honesty rule totalLines follows. Each target says what its name resolved to: kind is "event", "on_action" or "unknown", and "unknown" means the index has no such definition, so render it as unresolvable and do not guess. An on_action target additionally carries fires, what that on_action's own definition fires, resolved exactly one level deep: absent when there was nothing to read (including a target that is itself already one level deep), [] when the definition names no events, capped at 24 with firesTotal giving the true count.

paradox/guiWidgetInfo is the designer inspector's read side: { uri, text, line } in, the widget's effective properties out, or null when that line carries no widget of its own (the same answer guiSourceEdit refuses with). It addresses the widget the way the WRITER does and resolves it the way the ENGINE does, so a row it lists is a value the canvas laid the widget out with and a line a setProperties op would rewrite. Properties are last-in-wins per key in expansion order, and each carries an origin: the chain of definitions it was spliced through, innermost first ([template PxDeco, type px_card]). An EMPTY origin means the property is authored in the widget's own body, which is the only case a write rewrites in place. Values are rendered from the parser's tokens, not sliced from a file: an inherited block lives in a document this request was not handed.

It is a per-selection request rather than a field on GuiLayoutNode on purpose: a vanilla window lays out 500+ widgets, and every layout push would carry every widget's expanded property list for rows one widget at a time is ever shown.

A property assigned more than once also carries overrides: the values it shadowed, in expansion order, base-most first, each with its own origin. That is the "this overrides { 100 50 } from type px_card" note, and it is the engine's own discard recorded where the discard happens, not a second walk. A key assigned once has no overrides field at all.

textures lists what the widget draws, its own fill first and then its background, with the frame-sheet geometry of each: framesize and frame as authored, plus width / height / columns / rows / cell once the file resolves. Sizes come from the DDS header (128 bytes read, never a decode) so an inspector row cannot cost a 4096x4096 BC7 decode; the path resolves the way the game loads an asset, mod first, then parent mods from the last loaded back, then the game. The grid is driven by framesize alone: neither the CK3 nor the Victoria 3 gui tree, nor either harvested guiSchema.json, contains a noofframes, so no second spelling is guessed at. Frames are 1-based and row-major over floor(width/cellW) columns, and an out-of-range frame clamps into the grid rather than reading off the sheet.

placement answers "why is it here", and only when the request sets placement: true — it costs a full layout of the document, and the trace it records is gated so an ordinary paradox/guiLayout never pays for it. Two shapes, never both:

  • an anchored widget carries terms, the contributions of the engine's own formula in order (parentOrigin, parentanchor, widgetanchor, position). Their dx/dy sum exactly to the rect's x/y, which is the invariant that keeps the readout from drifting from the placement it explains. A widgetanchor that was never written still appears, sourced from the parentanchor it mirrors;
  • a widget inside a layout container carries placedBy and an EMPTY terms: the container computed the slot. droppedPosition is the position the engine discarded there (it logs "Widget cannot have a position in a layout"), which is the single most common "why is my widget not where I put it".

clippedBy rides along either way, naming the innermost scrollarea viewport or scissor = yes ancestor and its clip rect. The rect is the clipper's, not an intersection: the widget's own geometry stays true and the renderer clips. placement is absent for a widget the layout never reaches, such as one inside a tooltipwidget (created lazily in-engine, skipped in a static preview).

paradox/guiLayout takes an optional visibility mode for the widgets a static preview cannot resolve, those whose visible holds an expression. visible = no and visible = yes are deterministic and unaffected by the mode; only an expression is a check.

  • showAll (the default, and what the server did before this field existed): a conditional widget is KEPT. Showing it is the non-destructive default, and the same unknown is what makes a container's content unmeasurable;
  • hideAll: every conditional widget collapses, exactly as ignoreinvisible collapses a visible = no one — its slot disappears and its siblings shift up;
  • evaluate: the widgets whose check the caller assigned false collapse. A check with no assignment behaves as shown, so a partial map cannot hide something the caller never decided about.

The check KEY is the visible value exactly as authored, minus its quotes ([GetPlayer.IsAI]). A static preview has no widget-independent identity for a condition, and the source string is the one thing that is stable across edits that do not touch the condition; two widgets written with the same condition therefore share one toggle, which is what a toggle UI wants. visibilityChecks reports every check met — in ALL modes, showAll included, so the UI can be built before the user switches mode — each with the number of widgets carrying it and whether THIS run resolved it to hidden.

Each GuiLayoutNode also carries its onclick and tooltip values verbatim (minus quotes) when the widget has them. Neither is evaluated: a client's interact mode reads the GetVariableSystem.Set/Clear/Toggle calls out of onclick and turns them into evaluate assignments of the checks above, and shows the rest as what the running game would do; tooltip is a loc key or a datafunction the client resolves the same way it resolves text.

paradox/guiLayout resolves what a textbox SHOWS, as far as a static preview can know it, and loc chooses between the two honest answers. resolve (the default) looks a text = value up: a localization key becomes the configured language's text, a [datafunction] becomes its Localize('key') / Concept('key', 'text') value or the modder's own preview text, and a #bold/§Y/@icon! formatting is stripped for measurement. What cannot be known is shown as is and flagged, never invented: a key the index lacks shows the key, a chain like [GetPlayer.GetName] shows its last segment (Name). raw is the file's own value verbatim, which is what a layout measured before this field existed. Sizes follow the shown text either way, so an autoresizing label is as wide as what the player would read.

previewValues is the modder's table of preview text per expression, keyed [GetPlayer.GetName] (the brackets are accepted either way) and kept by the client with the mod (the VS Code host reads and writes <mod>/<configDirName>/gui-preview-values.json). A value there wins over every other resolution of that expression, and a value is text, never a number the server would format.

The answer is on GuiLayoutText: text is what was measured and drawn, raw is the text = value when it differs, and segments explains the pieces when there is something to explain (absent for a plain literal). Each GuiTextSegment has a kind (literal, loc, datafn), its source (the key, or the expression without brackets) and resolved, false for a key the index lacks or a datafunction only the running game evaluates. A key whose value itself holds datafunctions resolves one level deep and yields one loc segment per literal piece plus a datafn segment per expression, so a client can style the unresolved chips inside an otherwise localized line.

timings is that request's own wall clock, split into parseMs (the document's CST plus its own declarations), defsMs (the cross-file template/type store, 0 on a cache hit), layoutMs (widget tree and rects) and totalMs. Four clock reads per request; there is no flag because there is nothing to switch off.

paradox/guiDependencies is the GUI-to-script surface, forward. PdxGui reaches script through exactly one door — GetScriptedGui('name'), the only spelling the CK3 and Victoria 3 trees use — so a widget's script dependencies are the scripted_guis its own SOURCE subtree calls (line scopes the answer to one widget; absent means the whole document). Each row carries the definition site, callLines in the requested document, uses across every .gui file the layout store scanned, and chains: the events and on_actions that scripted_gui hands control to. A chain's via is empty for "directly" and lists the scripted effects otherwise, outermost first, so ["effect_a", "effect_b"] renders as "via effect_a -> effect_b". The walk follows the active profile's event/on_action reference fields (never a hard-coded key list), goes at most three scripted-effect hops, records the shortest path to each name, and terminates on cycles. locKeys are the text / tooltip values the subtree names, deduped, each flagged against the loc index (raw_text / raw_tooltip are literal strings by definition and are not keys, and a [datafunction] value is not one either).

The REVERSE direction is paradox/dependencies with guiUses: true, so a client asking "what depends on this event" gets the GUI answer in the same place. guiUses is off by default because it walks the scripted_gui definitions that some .gui file calls; when set, the response carries guiUses ([] is a real "none found", absent means it was not requested). Each site is one file:line of a GetScriptedGui(...) call plus the scriptedGui it names and the same via hop list, so the whole path reads file:line -> scripted_gui -> effects -> definition.

paradox/guiVocabulary is what a designer palette is allowed to offer. Every name is harvested rather than listed: the active game's bundled guiSchema.json (built from the vanilla gui/ tree) plus the requested document's own template and type declarations, which come first and are never capped. container says the vanilla tree writes widgets inside that type, which is what a "wrap in a container" menu should show; it is derived from the harvest's own child counts, with the engine's attribute blocks excluded, not from a list of container names. The harvested tail is capped and total gives the real count.

The same answer carries what an inspector's add-property row may offer. properties maps a widget type to its harvested property names, most used first: only the types the requested document NAMES are in it (the keys it writes blocks under, plus the bases of its own type X = base declarations), because the harvest holds hundreds of types and a designer re-asks after every layout. commonProperties is the vanilla tree's overall ranking, the fallback for a widget whose type the harvest has never seen. Both are capped, both are empty for a game with no harvest, and neither ever contains a name the vanilla gui/ tree does not write.

paradox/guiSourceEdit takes { uri, text, op } and answers { edits } or { refused }, never both, and null only for an op it does not know. The server never writes: edits are { start, end, newText } offsets into the text of the REQUEST, computed against that one text and applied end-first, so the host keeps undo, dirty state and the live preview (host-owns-text). Every edit is surgical, over the exact span of the entry it changes, so comments, CRLF, tabs-vs-spaces and single-line bodies survive a write byte for byte.

ops: GuiSourceOp[] replaces op for a BATCH: several ops against the one text, answered as one edit set, which is what makes a gesture over a multi-selection one document change and one undo step. Sending both op and ops answers null — a request carrying two shapes cannot say which it meant. A batch answers with results, one GuiSourceOpResult per op in request order:

  • edits (top level) is every applied op's edits together, already checked for overlap. Apply the whole set as ONE change.
  • results[i].refused is that op's own answer and skips only that op; the rest still applied. A client shows the reason verbatim, per member.
  • results[i].edits is what that op contributed (empty for a refused op, and empty for an op whose bytes were already what it would write).
  • Ops are computed in the order given, and a later one whose bytes an earlier one already rewrites is refused rather than dropped: applyAll discards an overlapping edit silently, and an op reported as applied must have been.
  • Top-level refused on a batch names a WHOLE-REQUEST failure only (a document that does not parse, an empty ops); it is never a per-op refusal.
  • warning joins the distinct per-op warnings; each op also carries its own.

The op is a discriminated union on kind: setProperties (a batch; a null value removes), reorder, insert, insertRaw (paste), delete, duplicate, wrap and the read-only blockText. A widget is addressed by the 0-based line its own statement starts on, the same line GuiLayoutNode reports; a node spliced in from a template or a type has no line of its own and resolves to nothing. An index counts SOURCE children, not the template-expanded ones the preview shows, and out of range appends.

refused is an ANSWER, not an error: it is what the server says when a gesture would not do what it looks like it does. A layout container places its children itself, so a position on one of its children is dropped by the game; an hbox/vbox/flowcontainer takes its size from its children, so an explicit size does nothing; a child expanding on both axes inside a container has both taken from it (one axis writes and sets warning naming the other); a type definition is not restructured through one instance's preview; the only root widget is not deleted; and a document that does not parse is not edited at all. Render the string.

paradox/definitionForm and paradox/definitionEdit are the creators' pair: the read that lets a client draw a form for a definition kind, and the write that puts one into a file. Nothing in the form is written for the creator. The folder, the locPatterns (every loc key the game reads for the kind, $ being the definition name, so trait_$_desc is trait_brave_desc; the schema's requiredLoc is the narrower subset a diagnostic may demand, and the form answers the full set) and the iconFolder are the schema table's row for the kind; keys and blocks are the harvest of the game's own _*.info docs, in its own order (curated keys first, then by vanilla usage count), each with the game's one-line documentation, a coarse values hint and its freq; options lists every indexed definition of each kind a key names, mod entries first, through the same resolver paradox/eventValueOptions answers with; modifiers is the script_docs modifier vocabulary hover already reads; existing is every indexed definition of the kind, the mod's own first and each side name-sorted (capped at 500), each carrying its source (mod / vanilla / parent) so a client's Open menu can say where one comes from: a creator opens a game definition to duplicate or override it, so a list of the mod's own could not answer "start from the game's"; and current is the block's own bytes read off disk. A key with no widget in a client is still in keys, so a form can show it rather than hide it (AD-5).

When loading name, an optional file (absolute path or file URI) selects its exact indexed source, including a source outside modRoot. This selector affects current only; modRoot still filters the form's mod-side option lists. If that source is missing or unreadable, current is absent. The server never substitutes another file with the same name. Without file, the existing mod-first lookup applies.

Three more fields answer questions the flat lists cannot. An option (and an existing entry) carries label when the loc index resolves the kind's first loc pattern with $ replaced by the definition name (trait_$ → trait_brave → "Brave"); a kind whose schema entry names no pattern is tried as $_name then $, and a name nothing resolves for has no label at all, so a client shows the key rather than an invented word. A key carries example with the scalar literal the indexed definitions of the kind write most often for it: a real value for a form to show as its input's placeholder. Unlike sampled it counts numbers and quoted text (quotes stripped) and it survives the cap, because a key whose value differs in every definition is exactly the one that needs an example. A key whose value is a BLOCK has no scalar literal at all, so it carries the body the definitions write most often instead, collapsed onto one line, comments dropped, cut with an ellipsis past DEFINITION_FORM_MAX_EXAMPLE (120): a script field gets a placeholder like every other field. A key whose value set is already stated (bool, enum:) carries example too, and no sampled: measuring a list the schema or the doc already gives would only be less complete, but a dropdown showing the value the game itself writes still says more than one reading "not set". A key answered by the definition index (refKinds) has neither: its options are the index's.

Two fields answer questions one flat list cannot. An option carries group when the schema entry for its kind names a groupKey: one folder can hold several families of the same kind (CK3 keeps all five culture pillars in common/culture/pillars and tells them apart with type = ethos inside each block), and group is that value read out of the definition, so a client can draw one picker per family. A key carries sampled when it names values no index can answer (a culture's clothing_gfx names an art set, its ethnicities a portrait ethnicity): the distinct values the indexed definitions of the kind actually write for it, most used first, measured from the files at request time rather than stored. A key whose value differs in every definition has no value SET, so past DEFINITION_FORM_MAX_SAMPLED (80) the field is absent instead of listing everything; a key with refKinds never carries it, because options already answers it.

conditions answers a question options cannot: a key whose value is a whole TRIGGER (is_shown, can_be_picked) names no kind, so a client that wants to offer condition rows instead of a text area has nothing to fill them from. It maps a trigger name to the values that trigger accepts, for the handful of triggers the active game's profile names. Each list comes from what the server already holds: the trigger's own script_docs entry where the game enumerates the values on it (CK3's has_dlc_feature carries Valid Features: … in triggers.log), every indexed definition of a kind (scripted_trigger), or the inner block keys of every definition of a kind (a CK3 game rule's settings ARE its inner blocks, so has_game_rule is answered from common/game_rules). A trigger nothing resolves for in this workspace (no script_docs dump, no game folder) is ABSENT rather than empty, so a client offers a free input instead of a picker with nothing in it, and the whole field is absent for a game whose profile names no triggers.

paradox/modifierFormats completes the pair for a client that lets a modder add modifiers: paradox/definitionForm says which modifiers exist, this says what the player will READ. A modifier row is not its script. The game prints monthly_income = 0.5 as a gold icon, +0.50 in green and the words "Monthly Income", and every part of that is stated in the game's own files: the flags in the profile's format folder (one block per modifier, documented by the _definitions.info sitting beside them; decimals defaults to 2, and color defaults to good even though the doc says bad: the file writes color = bad 113 times and color = good twice across its 667 blocks, and the game prints an unmarked diplomacy = 1 green), the words in the loc index (the modifier's own key, else MOD_<NAME>, else the name made readable), and the pictures in the profile's texticon file, which a [gold_i] in a loc value reaches through the concept entry game_concept_gold_i = "@gold_icon!". A bracket that resolves to nothing stays as text rather than disappearing. Every modifier token the server knows gets an entry, with those defaults when no block names it, so a client never has to decide what an unformatted modifier looks like. color says which direction is GOOD FOR THE PLAYER, not which sign is positive: stress_gain_mult is bad, so a negative value is the green one. negativeSuffix is why noSign is safe — the game hides the sign exactly where the suffix carries the direction in words ("5 days faster"). lines runs any loc key through the same chain: CK3 prints a tradition's cost as PRESTIGE_COST = "[prestige_i] $VALUE|0$", and a client that asks for that key gets the prestige icon and the $VALUE|0$ slot back as parts, with the number's decimals in the slot.

paradox/lookupLoc accepts an optional language identifier containing lowercase letters and underscores, including custom languages. An explicit language searches only that language in the active profile's localization folders and includes unsaved open text. Invalid identifiers or missing translations return an empty list, never an entry from another language. The configured completion language and index stay unchanged.

paradox/locText is the reading half of paradox/lookupLoc. lookupLoc answers a loc value VERBATIM, which is what an editor needs; a panel that shows the value needs the sentence. CK3 words a culture parameter as "The [GetTrait('rough_terrain_expert').GetName( GetNullCharacter )] Commander Trait is more common" (145 of the 280 culture_parameter_* values that carry a real call take that one shape, measured over the 609 english values), and a form that prints it verbatim shows the modder brackets. Each answered key carries raw (byte for byte what lookupLoc gives), text, and resolved, which is false when any part of the value stayed a word for something the server could not finish. A key the loc index does not have is ABSENT from values, so a client shows the key itself rather than a defined blank.

The rules text follows, in order: the game's #tag … #!, §Y … §! and @icon! markup is dropped; a nested $key$ is substituted one level from the loc index (an unfilled slot such as $VALUE|0$ stays verbatim and sets resolved: false); [culture|E] concept links and Concept('x', … ) become that concept's own word; [prestige_i] icon tags are dropped with the space they leave; Localize('k') becomes its value; SelectLocalization( cond, 'a', 'b' ) becomes the value of 'a', the branch a player with the DLC reads; and any Get<Something>('name') chain ending in GetName, GetTypeName or GetNameNoTooltip becomes that definition's own name. Every other expression falls back to the chain's last word (ScriptValue) with resolved: false, never an invented value. What the value a resolved call lands on holds is rendered one level further, so a key that IS a call (court_physician = "[GetCourtPositionType('court_physician_court_position').GetName()]") reads as words.

Nothing in that chain is written per game. The words come from the loc index (mod entries shadow the game's), the KIND a Get<Something>('name') names comes from the definition index, and the loc key that kind's names take comes from the active profile's schema entry (requiredLoc first, then locPatterns, then the bare name): CK3's trait states trait_$, so GetTrait('brave') reads trait_brave, while men_at_arms states $ and GetMaA('bowmen') reads bowmen. Where a name carries several kinds, the one whose PascalCase spelling the function name contains wins (GetCourtPositionType → court_position). A game whose schema states no pattern for a kind still resolves whatever the bare name localizes, and answers the name itself when nothing does.

paradox/definitionEdit is the script sibling of paradox/guiSourceEdit and follows the same contract: the server never writes, edits are { start, end, newText } offsets into the text of the REQUEST (the document text, BOM excluded, the way an editor delivers it), computed against that one text and applied end-first as ONE change and one undo step. Every edit is surgical, so other definitions, comments, CRLF and indentation survive byte for byte. ops answers one verdict per requested op in request order: a refused string names why that op wrote nothing and skips only it, and an op the file already satisfies has neither refused nor edits. setProperties rewrites the LAST entry for a key (the engine's last-in-wins order) and adds the keys the block lacks in one insert; a null value removes a key; a file with no top-level block of that name is refused with a reason instead of being appended to. upsertBlock replaces the top-level block of that name, or appends it after one blank separator line in the file's own newline style; an empty file gets the block alone. A file that does not parse refuses every op: no offset in it can be trusted.

Full payload shapes: see packages/protocol/src/protocol.ts — every interface there is part of this contract.

Custom methods: server → client

Method Kind Payload
paradox/status notification StatusPayload, token counts, loaded dump sources and index health
paradox/indexChanged notification none — definition index changed (debounced); overview views should re-query
paradox/progress notification { phase, state: "start" | "done", detail? } — one coarse loading phase (index, engine, guiStore); detail carries the label, sent with start

StatusPayload.tokensFromScriptDocs and tokensFromBundledDumps distinguish a generated script dump from a bundled snapshot; with neither set, any tokens come from bundled wiki data. The optional dataTypesSource is generated, bundled or none. Generated data types can supplement bundled entries. These fields describe successfully loaded definitions, not just files found on disk, and do not certify that a dump matches the installed patch. Older servers can omit dataTypesSource and the status field of paradox/reloadDocs; clients should show an unknown source in that case.

Client command ids

Code actions and hover markdown reference these client-side commands. A client lists the ones it registers in client.commands; the server emits command: links and command-carrying code actions only for listed ids, so an unlisted command is never shipped dead (the affordance degrades, nothing else breaks). The ids carry the px. prefix. They were renamed from ck3. in the Paradox Toolkit rebrand; clients registering the old ids get no fallback:

  • px.editLocalization (args: [locKey])
  • px.openLocalizationSideBySide (args: [locKey])
  • px.showReferences (args: [uri, line, character], via a command: markdown link in hover — requires the client to trust it)
  • px.showExamplesWiki (args: [] for the catalog, or [{ name, kind }] for one article, via a command: markdown link on the hover card of a variable or list — requires the client to trust it)

Degraded modes (bare LSP clients)

Documented behavior without the VSCode client: no tiger diagnostics (tiger runs client-side), no overview webview UIs (the data behind every one of them is on the wire; only the drawing is VSCode's), and no .dds rendering (a texture hover carries a data: URI, which a client that does not render images in markdown shows as link text). Completion, hover, definition, references, rename, symbols, formatting, folding, inlay hints, semantic tokens and structural diagnostics all work over plain LSP.

The missing-bom diagnostic is an Error for localization files and a Warning for editable mod script .txt files. Hosts fix the encoding through their own save controls, then send textDocument/didSave so the server checks the disk BOM again. The VS Code encoding quick fix is local to that client and is not emitted as an LSP command.

The client capabilities switch the remaining surface automatically, one capability at a time. A client declaring nothing (every field off) gets:

  • hoverIcons off — hover kind badges use a ■ square instead of a $(codicon) glyph. The default matters: a client that does not render theme icons prints the literal text $(symbol-method), which is worse than the square. The <details> disclosure that caps long examples is also omitted, since it needs hoverHtml;
  • hoverHtml off — hover markdown is plain: no sanitized HTML spans. The span content is always self-sufficient plain text ("■ trigger", a scope name), so the cards read the same either way;
  • px.editLocalization not listed — the "create localization key" quick fix carries a real WorkspaceEdit instead (appending to <locRoot>/<lang>/zzz_px_lsp_edits_l_<lang>.yml, creating it BOM-first when absent), and the "edit localization" action is omitted;
  • px.openLocalizationSideBySide not listed — that action is omitted;
  • px.showReferences not listed — the hover reference line is dropped entirely. A count the user cannot click answers no question, so the card ends with the provenance instead (changed in server 0.2.0: it used to render the count as plain text);
  • px.showExamplesWiki not listed — the "Examples Wiki" link the shared hover footer carries (engine tokens, datafunctions and data types, keywords and scope words, variables and lists) is dropped, exactly like the reference count;
  • fileLinks off — every file: link any hover would carry (provenance, variable and saved-scope set sites, define sources, gui template/type definitions, #format sources, [ ... ] datafunction examples) renders as the same file.txt:12 label without the link, so a renderer that cannot navigate file: targets never shows a dead link. A texture hover's "open file" becomes the resolved path as text (since server 0.2.0);
  • standard snippetSupport off — no completion item carries ${…} or insertTextFormat: Snippet. Inserts that would be snippets (parameter skeletons for scripted effects, key = { } for schema keys that open a block, engine block templates from the usage: dumps) arrive as plain-text skeletons instead: same shape, no tabstops (since server 0.2.0; before that, snippet syntax could reach clients that never declared it);
  • ownFileWatcher off — the server dynamically registers workspace/didChangeWatchedFiles when the client supports dynamic registration, so external edits re-index without a restart. Declare ownFileWatcher only if you push paradox/modFileChanged yourself;
  • index health is mirrored to window/logMessage regardless: a startup line naming the resolved bundled-data folders (or their absence) and status: lines with token/definition counts on indexing transitions.

paradox/snippetCatalogue

Request with {} to export every generated script template for the active game. No open document, cursor filter or item cap applies. The response is { gameId, gameName, generatedAt, indexing, entries }; generatedAt is an ISO timestamp. While indexing, indexing is true and entries is empty; request again after the index finishes.

Each entry has id, label, category, detail, and variants. Categories are Engine, Definitions, Child blocks, and Scripted calls. Each variant carries label (Minimal, Examples, All fields, or Template), snippet, plain, and preview (completion-details Markdown). Engine templates use loaded documentation, skeletons use the active profile, and scripted calls use effective indexed definitions. Unknown syntax contributes no template. Fixed JSON snippets and GUI completions are outside this script catalogue.

This request is independent of completionMode and client snippet support: every available variant includes both insertion formats. Hosts can build searchable or printable exports from the response. The VS Code command is px.exportSnippets (Paradox: Export Generated Snippets).

Clone this wiki locally