@px-lsp/server 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +1 -1
  2. package/data/ck3/skeletons.json +1 -0
  3. package/data/eu5/skeletons.json +1 -0
  4. package/data/vic3/skeletons.json +1 -0
  5. package/dist/browser-data/ck3/docs.json +1 -1
  6. package/dist/browser-data/ck3/tokens.json +1 -1
  7. package/dist/browser-data/vic3/tokens.json +1 -1
  8. package/dist/browser.js +50 -37
  9. package/dist/server.js +2000 -352
  10. package/dist/types/packages/server/src/features/blockSnippets.d.ts +20 -4
  11. package/dist/types/packages/server/src/features/completion.d.ts +11 -0
  12. package/dist/types/packages/server/src/features/definitionSkeletons.d.ts +24 -0
  13. package/dist/types/packages/server/src/games/ck3/schema.d.ts +1 -1
  14. package/dist/types/packages/server/src/games/eu5/index.d.ts +1 -1
  15. package/dist/types/packages/server/src/games/profile.d.ts +85 -1
  16. package/dist/types/packages/server/src/schema/skeletons.d.ts +102 -0
  17. package/dist/types/packages/server/src/schema/types.d.ts +30 -0
  18. package/package.json +3 -3
  19. package/src/coa/coa.ts +31 -0
  20. package/src/coa/coaDesigner.ts +131 -0
  21. package/src/coa/coaParse.ts +3 -0
  22. package/src/creators/definitionEdit.ts +198 -0
  23. package/src/creators/definitionForm.ts +479 -0
  24. package/src/creators/modifierFormats.ts +0 -0
  25. package/src/data/docsParser.ts +21 -4
  26. package/src/features/blockSnippets.ts +151 -20
  27. package/src/features/calendarDates.ts +8 -5
  28. package/src/features/completion.ts +38 -4
  29. package/src/features/definitionSkeletons.ts +109 -0
  30. package/src/features/inlayHints.ts +5 -2
  31. package/src/features/locText.ts +223 -0
  32. package/src/features/snippetList.ts +83 -0
  33. package/src/games/ck3/index.ts +16 -0
  34. package/src/games/ck3/meta.ts +81 -1
  35. package/src/games/ck3/schema.ts +83 -7
  36. package/src/games/ck3/structures.ts +37 -0
  37. package/src/games/eu5/index.ts +7 -1
  38. package/src/games/eu5/meta.ts +5 -1
  39. package/src/games/profile.ts +77 -1
  40. package/src/games/vic3/index.ts +4 -0
  41. package/src/games/vic3/meta.ts +5 -1
  42. package/src/gui/sourceModel.ts +51 -8
  43. package/src/gui/textResolve.ts +3 -3
  44. package/src/overview/dynastyTree.ts +493 -0
  45. package/src/overview/eventGraph.ts +21 -1
  46. package/src/overview/eventVocabulary.ts +33 -33
  47. package/src/overview/exampleWiki.ts +4 -1
  48. package/src/schema/loader.ts +2 -1
  49. package/src/schema/skeletons.ts +182 -0
  50. package/src/schema/types.ts +30 -0
  51. package/src/server.ts +147 -6
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import type { CalendarLocSpec } from "@px-lsp/protocol/calendarLoc";
12
12
  import type { DefRootKey, RefField, SchemaEntry, StructureSpec } from "../schema/types";
13
+ import type { KindSkeleton } from "../schema/skeletons";
13
14
  import type { PlaceholderSpec } from "../data/modifierTemplates";
14
15
  import type { GuiLayoutQuirks, GuiTextMetrics } from "../gui/layoutEngine";
15
16
  import type { SaveSchema } from "../gui/saveSchema";
@@ -70,8 +71,10 @@ export interface GameMeta {
70
71
  engine: "jomini" | "clausewitz-classic";
71
72
  /** Mod descriptor convention: Paradox-launcher `.mod` file vs `.metadata/metadata.json`. */
72
73
  descriptor: "mod" | "metadata";
73
- /** Per-workspace config dir holding schema.json / playset.json overlays. */
74
+ /** Per-mod config dir holding schema.json / playset.json overlays, workshop.json, the listing folder. */
74
75
  configDirName: string;
76
+ /** The pre-0.4.0 per-game name of that dir, still read as a fallback and renamed on first write. */
77
+ legacyConfigDirName?: string;
75
78
  /** Game folder under `Documents/Paradox Interactive/` (script_docs logs live in its logs/). */
76
79
  docsFolderName: string;
77
80
  /**
@@ -87,6 +90,12 @@ export interface GameMeta {
87
90
  */
88
91
  dataTypesCommand?: string;
89
92
  steamAppId: number;
93
+ /**
94
+ * Folder under the game data dir holding one icon per DLC, named after the
95
+ * DLC folder's number (`dlc_003.dds` or `dlc003.dds`). Absent = the install
96
+ * ships no such folder and the DLC's own `thumbnail.png` is used instead.
97
+ */
98
+ dlcIconDir?: string;
90
99
  /** Whether event files declare `namespace = x` and use `ns.N` event ids. */
91
100
  eventNamespaces: boolean;
92
101
  /**
@@ -128,6 +137,39 @@ export interface GameMeta {
128
137
  * not open for this game.
129
138
  */
130
139
  flagBuilder?: boolean;
140
+ /**
141
+ * The game ships its own in-game Coat of Arms designer, and the files that
142
+ * drive it (`gfx/coat_of_arms/{patterns,colored_emblems,color_palettes,
143
+ * emblem_layouts}/50_coa_designer_*.txt` and the `99_coa_designer_templates`
144
+ * coat-of-arms templates). Present = the toolkit offers the designer
145
+ * instead of the raw Flag Builder, when those files are actually on disk.
146
+ * Absent = the Flag Builder, as before.
147
+ */
148
+ coaDesigner?: boolean;
149
+ /**
150
+ * Visual content creators offered for this game, in Create-group order. A
151
+ * row is a definition kind the schema knows (the panel's command opens that
152
+ * creator), except `dynasty_tree`, which is a view over history files rather
153
+ * than one definition. Absent = no creator has been built against this
154
+ * game's own files, and the Create group shows only the scaffolds.
155
+ */
156
+ creators?: {
157
+ /** Schema kind, or the id of a bespoke creator that owns no single kind. */
158
+ kind: string;
159
+ label: string;
160
+ /** A Lucide icon name present in the client's webviews/shared/icons.ts. */
161
+ icon: string;
162
+ /** One-line hint for the panel row. */
163
+ tip?: string;
164
+ }[];
165
+ /**
166
+ * Where the game states how a modifier is PRINTED: the folder of format
167
+ * definitions (one block per modifier), and the `.gui` file whose `texticon`
168
+ * blocks map an icon name to its sprite. Both paths are relative to the game
169
+ * data dir. Absent = this game's print rules have not been read out of its
170
+ * own files, and `paradox/modifierFormats` answers null rather than guessing.
171
+ */
172
+ modifierFormats?: { folder: string; textIcons: string };
131
173
  /**
132
174
  * Database entry-mode prefixes legal on top-level definition keys
133
175
  * (EU5's `REPLACE:key`). The indexer strips a leading `<MODE>:` before
@@ -166,6 +208,25 @@ export interface GameMeta {
166
208
  cacheSuffix: string;
167
209
  }
168
210
 
211
+ /**
212
+ * Where a creator's condition builder gets the values of one trigger
213
+ * (`DefinitionForm.conditions`). Each source is something the server already
214
+ * holds, so a game patch changes the list without a release:
215
+ *
216
+ * docList the trigger's own script_docs entry enumerates them on its
217
+ * metadata line ("has_dlc_feature … Traits: Valid Features: a,
218
+ * b, and c", CK3 triggers.log 1.19)
219
+ * kind every indexed definition of a definition kind
220
+ * innerKeys the inner block keys of every definition of a kind, minus
221
+ * `except` (a CK3 game rule's settings ARE its inner blocks;
222
+ * the other two keys are `categories` and `default`, per
223
+ * common/game_rules/_game_rules.info)
224
+ */
225
+ export type ConditionValueSource =
226
+ | { from: "docList" }
227
+ | { from: "kind"; kind: string }
228
+ | { from: "innerKeys"; kind: string; except?: string[] };
229
+
169
230
  /** A game's full knowledge bundle: meta plus the tables the engine consumes. */
170
231
  export interface GameProfile extends GameMeta {
171
232
  /** Folder→definition-kind table (see schema/types.ts). */
@@ -185,6 +246,14 @@ export interface GameProfile extends GameMeta {
185
246
  * those entries always win.
186
247
  */
187
248
  structures?: Record<string, StructureSpec>;
249
+ /**
250
+ * Definition skeletons per kind (data/<id>/skeletons.json, harvested by
251
+ * scripts/build-skeletons.ts): the shape a new definition of the kind takes,
252
+ * measured over the game's own files. Absent or empty = nobody has measured
253
+ * this game's vanilla tree, and completion offers no skeleton for it rather
254
+ * than a shape written from memory.
255
+ */
256
+ skeletons?: Record<string, KindSkeleton>;
188
257
  /**
189
258
  * Definition kinds that declare their own root scope in their body, keyed by
190
259
  * kind (scopes/inference.ts). Absent = every kind's root scope comes from the
@@ -193,6 +262,13 @@ export interface GameProfile extends GameMeta {
193
262
  defRootKeys?: Record<string, DefRootKey>;
194
263
  /** Templated-modifier placeholder table (data/modifierTemplates.ts). */
195
264
  modifierPlaceholders: Record<string, PlaceholderSpec>;
265
+ /**
266
+ * Triggers a creator's condition builder may offer a value list for, keyed
267
+ * by trigger name (`paradox/definitionForm`'s `conditions`). Absent = the
268
+ * creators offer no picker for this game and fall back to free input, which
269
+ * is the honest answer for a game whose sources have not been measured.
270
+ */
271
+ conditionValues?: Record<string, ConditionValueSource>;
196
272
  /**
197
273
  * Bundled `[ ... ]` data-function tables (data/<id>/dataTypes.json), when the
198
274
  * game ships one. Shape is data/dataTypes.ts's bundled-JSON shape; typed
@@ -5,11 +5,14 @@
5
5
  * (markdown format). No wiki fallback and no structures layer, see below.
6
6
  */
7
7
  import type { GameProfile } from "../profile";
8
+ import type { SkeletonData } from "../../schema/skeletons";
8
9
  import { vic3Meta } from "./meta";
9
10
  import { VIC3_BLOCK_REF_FIELDS, VIC3_PREFIX_REFS, VIC3_REF_FIELDS, VIC3_SCHEMA } from "./schema";
10
11
  import { VIC3_STRUCTURES, VIC3_STRUCTURE_SOURCES } from "./structures";
11
12
  import { VIC3_SAVE_SCHEMA } from "./saveSchema";
12
13
  import GUI_SCHEMA from "../../../data/vic3/guiSchema.json";
14
+ // Definition skeletons measured over the vanilla tree (scripts/build-skeletons.ts).
15
+ import SKELETONS from "../../../data/vic3/skeletons.json";
13
16
 
14
17
  export const vic3Profile: GameProfile = {
15
18
  ...vic3Meta,
@@ -18,6 +21,7 @@ export const vic3Profile: GameProfile = {
18
21
  prefixRefs: VIC3_PREFIX_REFS,
19
22
  blockRefFields: VIC3_BLOCK_REF_FIELDS,
20
23
  guiSchema: GUI_SCHEMA,
24
+ skeletons: (SKELETONS as unknown as SkeletonData).kinds,
21
25
  // guiTextMetrics live on the meta (the client reads them too, for the GUI
22
26
  // editor gate and its canvas line height).
23
27
  // Vic3 keeps an authored size on an EMPTY container (px_probe_c C5: the
@@ -12,13 +12,17 @@ export const vic3Meta: GameMeta = {
12
12
  engine: "jomini",
13
13
  // Vic3 mods carry .metadata/metadata.json instead of a launcher .mod file.
14
14
  descriptor: "metadata",
15
- configDirName: ".vic3modding",
15
+ configDirName: ".px-toolkit",
16
+ legacyConfigDirName: ".vic3modding",
16
17
  docsFolderName: "Victoria 3",
17
18
  // Vic3's `script_docs` writes to Documents/.../docs (like EU5), while
18
19
  // `dump_data_types` writes to logs/data_types — verified on a live install.
19
20
  scriptDocsSubdir: "docs",
20
21
  dataTypesCommand: "dump_data_types",
21
22
  steamAppId: 529340,
23
+ // Verified on the live install: a different folder name from CK3's, and only
24
+ // some DLC have a file there (the rest fall back to their thumbnail.png).
25
+ dlcIconDir: "gfx/interface/icons/dlc_icons",
22
26
  eventNamespaces: true,
23
27
  scaffolds: VIC3_SCAFFOLDS,
24
28
  // uiFont deliberately absent: this game ships no Gitan (its fonts/ holds
@@ -57,6 +57,40 @@ export type GuiEntryKind = "widget" | "decl" | "property";
57
57
  * block alone.
58
58
  */
59
59
 
60
+ /**
61
+ * The three .gui-specific tables the model reads, lifted into a parameter so
62
+ * the same span model can serve plain jomini script (`common/traits/*.txt`),
63
+ * which has none of them.
64
+ *
65
+ * `propertyBlocks` is NOT a set for script: the gui rule is the INVERSE one (a
66
+ * block child is a child widget unless its key is a known attribute block), so
67
+ * an empty set would classify every block in a trait as a widget and the
68
+ * writer would refuse to rewrite it. `null` says "this dialect has no child
69
+ * declarations at all", which is what script is.
70
+ */
71
+ export interface SourceDialect {
72
+ /** Bare words that label the next assignment as a declaration. */
73
+ declMarkers: ReadonlySet<string>;
74
+ /** Keys taking the `key = "name" { ... }` slot spelling. */
75
+ slotKeys: ReadonlySet<string>;
76
+ /** Block-valued keys that are attributes; `null` = every block is an attribute. */
77
+ propertyBlocks: ReadonlySet<string> | null;
78
+ }
79
+
80
+ /** The default: the .gui tables, so every existing caller is unchanged. */
81
+ export const GUI_DIALECT: SourceDialect = {
82
+ declMarkers: DECL_MARKERS,
83
+ slotKeys: SLOT_KEYS,
84
+ propertyBlocks: PROPERTY_BLOCKS,
85
+ };
86
+
87
+ /** Plain jomini script: no markers, no slot form, no child declarations. */
88
+ export const SCRIPT_DIALECT: SourceDialect = {
89
+ declMarkers: new Set(),
90
+ slotKeys: new Set(),
91
+ propertyBlocks: null,
92
+ };
93
+
60
94
  /** One `key [op] value` statement, with every raw span the writer addresses it by. */
61
95
  export interface GuiEntry {
62
96
  kind: GuiEntryKind;
@@ -177,13 +211,14 @@ interface BuildCtx {
177
211
  lineKind: Uint8Array;
178
212
  commentsByLine: Map<number, CommentNode[]>;
179
213
  entries: GuiEntry[];
214
+ dialect: SourceDialect;
180
215
  }
181
216
 
182
217
  const LINE_BLANK = 0;
183
218
  const LINE_COMMENT = 1;
184
219
  const LINE_CODE = 2;
185
220
 
186
- export function parseGuiSource(text: string): GuiSourceFile {
221
+ export function parseGuiSource(text: string, dialect: SourceDialect = GUI_DIALECT): GuiSourceFile {
187
222
  const parsed = parseScript(text);
188
223
  const lines = new LineIndex(text);
189
224
  const commentsByLine = new Map<number, CommentNode[]>();
@@ -198,6 +233,7 @@ export function parseGuiSource(text: string): GuiSourceFile {
198
233
  lineKind: classifyLines(text, lines, commentsByLine),
199
234
  commentsByLine,
200
235
  entries: [],
236
+ dialect,
201
237
  };
202
238
  const root = buildBody(parsed.root.statements, -1, null, { start: 0, end: text.length }, null, ctx);
203
239
  return {
@@ -269,7 +305,7 @@ function buildBody(
269
305
  // A bare `template` / `type` / `blockoverride` word labels the next
270
306
  // assignment; anything else clears the label.
271
307
  marker =
272
- stmt.value.kind === "scalar" && DECL_MARKERS.has(stmt.value.text.toLowerCase())
308
+ stmt.value.kind === "scalar" && ctx.dialect.declMarkers.has(stmt.value.text.toLowerCase())
273
309
  ? { text: stmt.value.text.toLowerCase(), span: stmt.value.range }
274
310
  : null;
275
311
  continue;
@@ -353,7 +389,7 @@ function buildEntry(
353
389
  ): GuiEntry {
354
390
  const { text, lines } = ctx;
355
391
  const opSpan = operatorSpan(text, stmt);
356
- const slot = slotForm(stmt, declMarker);
392
+ const slot = slotForm(stmt, declMarker, ctx.dialect);
357
393
  const marker = slot?.marker ?? declMarker;
358
394
  const key = slot?.key ?? stmt.key;
359
395
  const value = slot?.value ?? stmt.value;
@@ -384,7 +420,7 @@ function buildEntry(
384
420
  const ownLine = startsLine && endsLine;
385
421
 
386
422
  const entry: GuiEntry = {
387
- kind: classify(key, marker, block),
423
+ kind: classify(key, marker, block, ctx.dialect),
388
424
  key: key.text,
389
425
  keyLower: key.text.toLowerCase(),
390
426
  keySpan: key.range,
@@ -444,22 +480,29 @@ function buildEntry(
444
480
  * the writer and the preview can never disagree about what is a widget: a
445
481
  * block child is a widget unless its key is a known attribute block.
446
482
  */
447
- function classify(key: ScalarNode, marker: { text: string } | null, block: BlockNode | null): GuiEntryKind {
483
+ function classify(
484
+ key: ScalarNode,
485
+ marker: { text: string } | null,
486
+ block: BlockNode | null,
487
+ dialect: SourceDialect
488
+ ): GuiEntryKind {
448
489
  if (marker) return "decl";
449
490
  if (!block) return "property";
491
+ if (!dialect.propertyBlocks) return "property";
450
492
  const lower = key.text.toLowerCase();
451
- if (PROPERTY_BLOCKS.has(lower) || lower.startsWith("@")) return "property";
493
+ if (dialect.propertyBlocks.has(lower) || lower.startsWith("@")) return "property";
452
494
  return "widget";
453
495
  }
454
496
 
455
497
  /** The `blockoverride = "name" { ... }` spelling, re-read as marker + name + body. */
456
498
  function slotForm(
457
499
  stmt: AssignmentNode,
458
- marker: { text: string; span: GuiSpan } | null
500
+ marker: { text: string; span: GuiSpan } | null,
501
+ dialect: SourceDialect
459
502
  ): { marker: { text: string; span: GuiSpan }; key: ScalarNode; value: BlockNode } | null {
460
503
  if (marker || stmt.value?.kind !== "tagged-block") return null;
461
504
  const lower = stmt.key.text.toLowerCase();
462
- if (!SLOT_KEYS.has(lower)) return null;
505
+ if (!dialect.slotKeys.has(lower)) return null;
463
506
  return {
464
507
  marker: { text: lower, span: stmt.key.range },
465
508
  key: stmt.value.tag,
@@ -34,7 +34,7 @@ const LOC_KEY = /^[A-Za-z0-9_][A-Za-z0-9_.\-']*$/;
34
34
  * Loc formatting the game does not draw: `#bold text#!`, `#R text#!`, `§Ytext§!`,
35
35
  * `@icon!` icon references. Removed for measurement; the preview has no glyphs for them.
36
36
  */
37
- function stripFormatting(s: string): string {
37
+ export function stripFormatting(s: string): string {
38
38
  return s
39
39
  .replace(/#!/g, "")
40
40
  .replace(/#[A-Za-z_][A-Za-z0-9_;:]*\s?/g, "")
@@ -45,7 +45,7 @@ function stripFormatting(s: string): string {
45
45
  }
46
46
 
47
47
  /** Split `a [Fn] b [[literal]` into literal and datafunction pieces. */
48
- function tokenize(s: string): { literal?: string; fn?: string }[] {
48
+ export function tokenize(s: string): { literal?: string; fn?: string }[] {
49
49
  const out: { literal?: string; fn?: string }[] = [];
50
50
  let lit = "";
51
51
  for (let i = 0; i < s.length; i++) {
@@ -87,7 +87,7 @@ function locOfCall(fn: string, loc: TextResolvers["loc"]): string | undefined {
87
87
  }
88
88
 
89
89
  /** `GetPlayer.GetName` -> `GetName`; `Concept('x','y')` -> `Concept`; strips a `Get` prefix and arguments. */
90
- function chipFor(fn: string): string {
90
+ export function chipFor(fn: string): string {
91
91
  // `|0`, `|%`: format specifiers after the chain, not part of the name.
92
92
  const chain = fn.split("|")[0];
93
93
  const last = chain.split(".").pop() ?? chain;