@px-lsp/server 0.1.0

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 (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. package/src/wordAt.ts +49 -0
@@ -0,0 +1,366 @@
1
+ /**
2
+ * PdxGui language features (paradox-gui): completion, hover and template
3
+ * navigation for .gui files, grounded in a build-time harvest of the vanilla
4
+ * gui/ tree (packages/server/data/<game>/guiSchema.json: 600+ widget types with per-type
5
+ * property usage counts) plus the definition index's gui_type entries
6
+ * (mod + vanilla `template X { }` / `type x = base { }` declarations).
7
+ */
8
+ import {
9
+ CompletionItemKind,
10
+ MarkupKind,
11
+ type CompletionItem,
12
+ type Hover,
13
+ type Position,
14
+ } from "vscode-languageserver/node";
15
+ import type { TextDocument } from "vscode-languageserver-textdocument";
16
+ import { activeProfile } from "../games/active";
17
+ import type { ServerData } from "../serverData";
18
+ import { blockStackFromParse } from "../context";
19
+ import { getParse } from "../parseCache";
20
+ import { finalize, MAX_ITEMS, type CompletionResult } from "./completion";
21
+ import { provideDataFnCompletion, provideDataFnHover } from "./datafunction";
22
+ import { guiDefSources, type GuiPaths } from "./guiNavigation";
23
+ import { collectOverridableBlocks, resolveGuiDef, typeBaseChain, type GuiTypeDef } from "../gui/guiDefs";
24
+ import { wordRangeAt } from "../wordAt";
25
+ import { getLineText } from "../documents";
26
+ import { renderCard, renderHover } from "./hoverRender";
27
+ import { assetDirContext, provideAssetDirCompletion } from "./assetPaths";
28
+ import type { ParadoxSettings } from "@px-lsp/protocol/protocol";
29
+ import * as path from "path";
30
+ import { URI } from "vscode-uri";
31
+
32
+ interface GuiTypeInfo {
33
+ count: number;
34
+ props: Record<string, number>;
35
+ }
36
+ interface GuiSchemaShape {
37
+ types: Record<string, GuiTypeInfo>;
38
+ globalProps: Record<string, number>;
39
+ /** Property key → its bounded set of enum-like scalar values (harvested). */
40
+ enums: Record<string, string[]>;
41
+ /** Enum keys observed with `|`-combined values (top|left, alphamask|…). */
42
+ enumCombinable: string[];
43
+ }
44
+ const EMPTY_GUI: GuiSchemaShape = { types: {}, globalProps: {}, enums: {}, enumCombinable: [] };
45
+ // Memoized view of the active profile's bundled gui schema (absent for games
46
+ // without a harvest: gui completion/hover degrade to index-only, fail-soft).
47
+ let guiCacheKey: unknown = Symbol("unset");
48
+ let guiCache: GuiSchemaShape = EMPTY_GUI;
49
+ let combinableCache = new Set<string>();
50
+ function guiSchema(): GuiSchemaShape {
51
+ const raw = activeProfile().guiSchema;
52
+ if (raw !== guiCacheKey) {
53
+ guiCacheKey = raw;
54
+ guiCache = (raw as GuiSchemaShape | undefined) ?? EMPTY_GUI;
55
+ combinableCache = new Set(guiCache.enumCombinable ?? []);
56
+ }
57
+ return guiCache;
58
+ }
59
+ function enumCombinable(): Set<string> {
60
+ guiSchema();
61
+ return combinableCache;
62
+ }
63
+
64
+ /** `key = a|b|par` in a value position: key + the value text typed so far. */
65
+ const ENUM_VALUE_POSITION = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*([A-Za-z0-9_|]*)$/;
66
+
67
+ const TIER_PROP = "0";
68
+ const TIER_TYPE = "2";
69
+
70
+ function rank2(rank: number): string {
71
+ return String(Math.min(99, rank)).padStart(2, "0");
72
+ }
73
+
74
+ const VALUE_POSITION = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"?([A-Za-z0-9_.-]*)$/;
75
+ const WORD_AT_END = /[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
76
+
77
+ /** Innermost enclosing widget-block key at the offset, lowercased. */
78
+ function enclosingType(document: TextDocument, offset: number): string | null {
79
+ const { result } = getParse(document);
80
+ const named = blockStackFromParse(result, offset).filter((s) => s !== "<anon>");
81
+ return named.length > 0 ? named[named.length - 1].toLowerCase() : null;
82
+ }
83
+
84
+ /**
85
+ * Value-side completion for an enum-valued gui property. Returns null when the
86
+ * line is not `enumKey = …`. After a `|` only the segment being typed is
87
+ * completed and flags already in the combination are dropped from the list.
88
+ */
89
+ function provideEnumCompletion(linePrefix: string): CompletionResult | null {
90
+ const m = ENUM_VALUE_POSITION.exec(linePrefix);
91
+ if (!m) return null;
92
+ const key = m[1].toLowerCase();
93
+ const values = guiSchema().enums?.[key];
94
+ if (!values) return null;
95
+ const segments = m[2].split("|");
96
+ const partial = segments[segments.length - 1];
97
+ const used = new Set(segments.slice(0, -1).map((s) => s.toLowerCase()));
98
+ const combinable = enumCombinable().has(key);
99
+ const items: CompletionItem[] = [];
100
+ values.forEach((v, i) => {
101
+ if (used.has(v)) return;
102
+ items.push({
103
+ label: v,
104
+ kind: CompletionItemKind.EnumMember,
105
+ detail: `${key} value${combinable ? " · combine with |" : ""}`,
106
+ sortText: TIER_PROP + rank2(i) + v,
107
+ });
108
+ });
109
+ return finalize(items, partial, MAX_ITEMS);
110
+ }
111
+
112
+ export function provideGuiCompletion(
113
+ data: ServerData,
114
+ document: TextDocument,
115
+ offset: number,
116
+ settings?: ParadoxSettings
117
+ ): CompletionResult {
118
+ const { lineIndex } = getParse(document);
119
+ const pos = lineIndex.positionAt(offset);
120
+ const linePrefix = document.getText({
121
+ start: { line: pos.line, character: 0 },
122
+ end: { line: pos.line, character: pos.character },
123
+ });
124
+
125
+ // Inside a [ ... ] datafunction expression → data types / promotes / functions.
126
+ const dataFn = provideDataFnCompletion(data.dataTypes, data.dataFnUsage, linePrefix, data.index, pos);
127
+ if (dataFn !== null) return dataFn;
128
+
129
+ // Quoted asset path (`texture = "gfx/interface/ico`) → directory-segment drill-down.
130
+ if (settings) {
131
+ const assetPath = assetDirContext(linePrefix);
132
+ if (assetPath !== null) return provideAssetDirCompletion(settings, assetPath);
133
+ }
134
+ // A stray "/" that is not a path context must not spam the widget list.
135
+ if (linePrefix.endsWith("/")) return { isIncomplete: false, items: [] };
136
+
137
+ // `using = |` → templates (mod first, then vanilla).
138
+ const valueMatch = VALUE_POSITION.exec(linePrefix);
139
+ if (valueMatch) {
140
+ const key = valueMatch[1].toLowerCase();
141
+ if (key === "using" || key === "template") {
142
+ const items: CompletionItem[] = [];
143
+ for (const d of data.index.entries((def) => def.kind === "gui_type")) {
144
+ items.push({
145
+ label: d.name,
146
+ kind: CompletionItemKind.Class,
147
+ detail: `gui template/type (${d.source})`,
148
+ sortText: TIER_PROP + (d.source === "mod" ? "0" : "1") + d.name,
149
+ data: { t: "def", k: "gui_type", n: d.name },
150
+ });
151
+ }
152
+ return finalize(items, valueMatch[2], MAX_ITEMS);
153
+ }
154
+ }
155
+
156
+ // Enum-valued property (`parentanchor = top|hc`): complete the segment after
157
+ // the last `|`, excluding flags already present in the combination. Runs even
158
+ // when VALUE_POSITION misses (its regex stops at `|`).
159
+ const enumResult = provideEnumCompletion(linePrefix);
160
+ if (enumResult !== null) return enumResult;
161
+
162
+ if (valueMatch) {
163
+ // Literal values (numbers, "[data functions]", texture paths): nothing to offer.
164
+ return { isIncomplete: false, items: [] };
165
+ }
166
+
167
+ const typedWord = WORD_AT_END.exec(linePrefix)?.[0] ?? "";
168
+ const enclosing = enclosingType(document, offset);
169
+ const items: CompletionItem[] = [];
170
+ const seen = new Set<string>();
171
+
172
+ // Properties/children of the enclosing widget type (global stats when the
173
+ // type is mod-defined and unknown to the vanilla harvest).
174
+ const typeInfo = enclosing ? guiSchema().types[enclosing] : null;
175
+ const props = typeInfo?.props ?? (enclosing ? guiSchema().globalProps : {});
176
+ const ranked = Object.entries(props).sort((a, b) => b[1] - a[1]);
177
+ ranked.forEach(([key, count], i) => {
178
+ seen.add(key);
179
+ const alsoType = key in guiSchema().types;
180
+ items.push({
181
+ label: key,
182
+ kind: alsoType ? CompletionItemKind.Class : CompletionItemKind.Property,
183
+ detail: typeInfo
184
+ ? `${alsoType ? "child widget" : "property"} of ${enclosing} · ${count}× in vanilla`
185
+ : `gui ${alsoType ? "widget" : "property"} · ${count}× in vanilla`,
186
+ sortText: TIER_PROP + rank2(i) + key,
187
+ });
188
+ });
189
+
190
+ // Remaining widget types (and everything at top level).
191
+ const types = Object.entries(guiSchema().types).sort((a, b) => b[1].count - a[1].count);
192
+ types.forEach(([name, info], i) => {
193
+ if (seen.has(name)) return;
194
+ items.push({
195
+ label: name,
196
+ kind: CompletionItemKind.Class,
197
+ detail: `widget type · ${info.count}× in vanilla`,
198
+ sortText: TIER_TYPE + rank2(Math.min(99, i)) + name,
199
+ });
200
+ });
201
+
202
+ return finalize(items, typedWord, MAX_ITEMS);
203
+ }
204
+
205
+ /** Cap for the overridable-blocks list in a template/type hover card. */
206
+ const MAX_BLOCKS_SHOWN = 24;
207
+
208
+ export function provideGuiHover(
209
+ data: ServerData,
210
+ document: TextDocument,
211
+ position: Position,
212
+ paths?: GuiPaths
213
+ ): Hover | null {
214
+ const lineText = getLineText(document, position.line);
215
+
216
+ // A chain segment inside [ ... ] → datafunction card (own range: word
217
+ // boundaries differ, dots split segments).
218
+ const dataFn = provideDataFnHover(
219
+ data.dataTypes,
220
+ data.dataFnUsage,
221
+ lineText,
222
+ position.character,
223
+ paths?.gamePath ?? null
224
+ );
225
+ if (dataFn) {
226
+ return {
227
+ contents: { kind: MarkupKind.Markdown, value: dataFn.markdown },
228
+ range: {
229
+ start: { line: position.line, character: dataFn.start },
230
+ end: { line: position.line, character: dataFn.end },
231
+ },
232
+ };
233
+ }
234
+
235
+ const range = wordRangeAt(lineText, position.character);
236
+ if (!range) return null;
237
+ const word = range.word;
238
+ const lower = word.toLowerCase();
239
+ const { lineIndex } = getParse(document);
240
+ const offset = lineIndex.offsetAt(position);
241
+
242
+ const cards: string[] = [];
243
+
244
+ const typeInfo = guiSchema().types[lower];
245
+ if (typeInfo) {
246
+ const top = Object.entries(typeInfo.props)
247
+ .sort((a, b) => b[1] - a[1])
248
+ .slice(0, 8)
249
+ .map(([k]) => `\`${k}\``)
250
+ .join(" ");
251
+ cards.push(
252
+ renderCard({
253
+ kind: "gui_type",
254
+ badgeLabel: "widget type",
255
+ name: lower,
256
+ headTail: `· ${typeInfo.count}× in vanilla gui`,
257
+ doc: top ? `Common properties: ${top}` : undefined,
258
+ })
259
+ );
260
+ }
261
+
262
+ // Enum value token (`parentanchor = top|left`): the hovered word is a value
263
+ // belonging to an enum property named on the same line.
264
+ const enumKey = /^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(lineText)?.[1]?.toLowerCase();
265
+ if (enumKey && enumKey !== lower) {
266
+ const values = guiSchema().enums?.[enumKey];
267
+ if (values && values.includes(lower)) {
268
+ const combinable = enumCombinable().has(enumKey);
269
+ cards.push(
270
+ renderCard({
271
+ kind: "keyword",
272
+ badgeLabel: "gui enum value",
273
+ name: lower,
274
+ headTail: `of \`${enumKey}\``,
275
+ doc:
276
+ `Allowed: ${values.map((v) => `\`${v}\``).join(" ")}` +
277
+ (combinable ? "\n\nCombine flags with `|` (e.g. `top|left`)." : "."),
278
+ })
279
+ );
280
+ }
281
+ }
282
+
283
+ const enclosing = enclosingType(document, offset);
284
+ const encInfo = enclosing ? guiSchema().types[enclosing] : null;
285
+ const propCount = encInfo?.props[lower] ?? guiSchema().globalProps[lower];
286
+ if (!typeInfo && propCount) {
287
+ cards.push(
288
+ renderCard({
289
+ kind: "gui_property",
290
+ badgeLabel: "gui property",
291
+ name: lower,
292
+ headTail: encInfo ? `on ${enclosing}` : undefined,
293
+ doc: `${propCount.toLocaleString("en-US")} uses in the vanilla gui tree.`,
294
+ })
295
+ );
296
+ }
297
+
298
+ // Template/type card from the cross-file store: base chain, overridable
299
+ // blocks (what a blockoverride can target), definition link.
300
+ let resolvedCard = false;
301
+ if (paths) {
302
+ const sources = guiDefSources(document, paths);
303
+ const resolved = resolveGuiDef(word, sources);
304
+ if (resolved) {
305
+ resolvedCard = true;
306
+ const docParts: string[] = [];
307
+ if (resolved.kind === "type") {
308
+ const chain = typeBaseChain(resolved.name, sources);
309
+ if (chain.length > 0) docParts.push(`Extends: \`${chain.join("` → `")}\``);
310
+ }
311
+ const blocks = [...collectOverridableBlocks(resolved, sources).keys()].sort();
312
+ if (blocks.length > 0) {
313
+ const shown = blocks.slice(0, MAX_BLOCKS_SHOWN).map((b) => `\`${b}\``);
314
+ if (blocks.length > MAX_BLOCKS_SHOWN) shown.push(`… +${blocks.length - MAX_BLOCKS_SHOWN} more`);
315
+ docParts.push(`Overridable blocks: ${shown.join(" ")}`);
316
+ }
317
+ const def = resolved.def;
318
+ const footer: string[] = [];
319
+ if (def.file !== undefined && def.line !== undefined) {
320
+ footer.push(
321
+ `[${path.basename(def.file)}:${def.line + 1}](${URI.file(def.file)
322
+ .with({ fragment: String(def.line + 1) })
323
+ .toString()})`
324
+ );
325
+ }
326
+ cards.push(
327
+ renderCard({
328
+ kind: "gui_type",
329
+ badgeLabel: resolved.kind,
330
+ name: resolved.kind === "template" ? word : resolved.name,
331
+ headTail: resolved.kind === "type" ? `= ${(def as GuiTypeDef).base}` : undefined,
332
+ doc: docParts.length > 0 ? docParts.join("\n\n") : undefined,
333
+ footer: footer.length > 0 ? footer : undefined,
334
+ })
335
+ );
336
+ }
337
+ }
338
+
339
+ // Index-based fallback card (kept for gui_type defs the store cannot see).
340
+ if (!resolvedCard) {
341
+ for (const def of data.index.lookup(word)) {
342
+ if (def.kind !== "gui_type") continue;
343
+ const link = `[${path.basename(def.file)}:${def.line + 1}](${URI.file(def.file)
344
+ .with({ fragment: String(def.line + 1) })
345
+ .toString()})`;
346
+ cards.push(
347
+ renderCard({
348
+ kind: "gui_type",
349
+ badgeLabel: "template / type",
350
+ name: def.name,
351
+ headTail: `· ${data.originLabel(def)}`,
352
+ footer: [link],
353
+ })
354
+ );
355
+ }
356
+ }
357
+
358
+ if (cards.length === 0) return null;
359
+ return {
360
+ contents: { kind: MarkupKind.Markdown, value: renderHover(cards, null) },
361
+ range: {
362
+ start: { line: position.line, character: range.start },
363
+ end: { line: position.line, character: range.end },
364
+ },
365
+ };
366
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Go-to-definition for .gui files: widget type usages, `using = Template`
3
+ * references, base types in `type X = base` declarations, and
4
+ * `blockoverride "name"` → the `block "name"` site in the widget's
5
+ * template/type chain. Resolution order: the current document's own
6
+ * declarations (including local_templates) first, then the cross-file FIOS
7
+ * store (mod + parents + vanilla, first definition wins — what the game uses).
8
+ */
9
+ import * as fs from "fs";
10
+ import type { Location, Position } from "vscode-languageserver/node";
11
+ import type { TextDocument } from "vscode-languageserver-textdocument";
12
+ import { URI } from "vscode-uri";
13
+ import {
14
+ collectGuiDefsParsed,
15
+ collectOverridableBlocks,
16
+ resolveGuiDef,
17
+ type GuiBlockSite,
18
+ type GuiDefs,
19
+ } from "../gui/guiDefs";
20
+ import { getGuiDefs } from "../gui/layoutService";
21
+ import { LineIndex, nodeAtOffset, type BlockNode, type Statement } from "../parser";
22
+ import { getParse } from "../parseCache";
23
+ import { getLineText } from "../documents";
24
+ import { wordRangeAt } from "../wordAt";
25
+
26
+ export interface GuiPaths {
27
+ gamePath: string | null;
28
+ modPath: string | null;
29
+ parentPaths: string[];
30
+ /** Engine-layer roots (jomini), lowest FIOS priority. */
31
+ engineRoots?: string[];
32
+ }
33
+
34
+ /** Current-document defs (live text, cached parse) + the cached cross-file store. */
35
+ export function guiDefSources(document: TextDocument, paths: GuiPaths): GuiDefs[] {
36
+ const fsPath = URI.parse(document.uri).fsPath;
37
+ const { result, lineIndex } = getParse(document);
38
+ const docDefs = collectGuiDefsParsed(result.root.statements, undefined, fsPath, lineIndex);
39
+ return [docDefs, getGuiDefs(paths.gamePath, paths.modPath, paths.parentPaths, paths.engineRoots)];
40
+ }
41
+
42
+ function locationAt(file: string, line: number): Location {
43
+ return {
44
+ uri: URI.file(file).toString(),
45
+ range: { start: { line, character: 0 }, end: { line, character: 0 } },
46
+ };
47
+ }
48
+
49
+ /** Convert a block site (file + offset) to a Location, using the live document for its own file. */
50
+ function siteLocation(site: GuiBlockSite, document: TextDocument, docFsPath: string): Location | null {
51
+ if (site.file === undefined) return null;
52
+ if (site.file === docFsPath) {
53
+ const { lineIndex } = getParse(document);
54
+ return locationAt(site.file, lineIndex.positionAt(site.offset).line);
55
+ }
56
+ try {
57
+ const text = fs.readFileSync(site.file, "utf8");
58
+ return locationAt(site.file, new LineIndex(text).positionAt(site.offset).line);
59
+ } catch {
60
+ return null;
61
+ }
62
+ }
63
+
64
+ /** `using = X` values among a block's direct statements, in order. */
65
+ function usingRefs(block: BlockNode): string[] {
66
+ const refs: string[] = [];
67
+ for (const stmt of block.statements) {
68
+ if (stmt.kind !== "assignment") continue;
69
+ if (stmt.key.text.toLowerCase() !== "using") continue;
70
+ if (stmt.value?.kind === "scalar") refs.push(stmt.value.text);
71
+ }
72
+ return refs;
73
+ }
74
+
75
+ function childBlock(stmt: Statement): BlockNode | null {
76
+ if (stmt.kind !== "assignment" || !stmt.value) return null;
77
+ if (stmt.value.kind === "block") return stmt.value;
78
+ if (stmt.value.kind === "tagged-block") return stmt.value.block;
79
+ return null;
80
+ }
81
+
82
+ /**
83
+ * Candidate template/type names whose chain may define the block a
84
+ * `blockoverride "name"` targets, innermost enclosing widget first: for each
85
+ * ancestor block enclosing the offset, its `using =` templates, then the
86
+ * ancestor's own key (a widget type usage).
87
+ */
88
+ function blockOverrideCandidates(document: TextDocument, offset: number): string[] {
89
+ const { result } = getParse(document);
90
+ const hit = nodeAtOffset(result.root, offset);
91
+ if (!hit) return [];
92
+ const candidates: string[] = [];
93
+ for (let i = hit.path.length - 1; i >= 0; i--) {
94
+ const stmt = hit.path[i];
95
+ const block = childBlock(stmt);
96
+ if (!block) continue;
97
+ if (offset <= block.openBrace) continue;
98
+ if (block.closeBrace !== null && offset > block.closeBrace) continue;
99
+ candidates.push(...usingRefs(block));
100
+ if (stmt.kind === "assignment") candidates.push(stmt.key.text);
101
+ }
102
+ return candidates;
103
+ }
104
+
105
+ export function provideGuiDefinition(
106
+ document: TextDocument,
107
+ position: Position,
108
+ paths: GuiPaths
109
+ ): Location[] | null {
110
+ const lineText = getLineText(document, position.line);
111
+ const range = wordRangeAt(lineText, position.character);
112
+ if (!range) return null;
113
+ const word = range.word;
114
+ const { lineIndex } = getParse(document);
115
+ const offset = lineIndex.offsetAt(position);
116
+ const docFsPath = URI.parse(document.uri).fsPath;
117
+ const sources = guiDefSources(document, paths);
118
+
119
+ // blockoverride "name" → the block "name" site in the enclosing chain.
120
+ const before = lineText.slice(0, range.start);
121
+ if (/\bblockoverride\s+"?$/i.test(before)) {
122
+ for (const candidate of blockOverrideCandidates(document, offset)) {
123
+ const resolved = resolveGuiDef(candidate, sources);
124
+ if (!resolved) continue;
125
+ const site = collectOverridableBlocks(resolved, sources).get(word);
126
+ if (site) {
127
+ const loc = siteLocation(site, document, docFsPath);
128
+ if (loc) return [loc];
129
+ }
130
+ }
131
+ return null;
132
+ }
133
+
134
+ // Template/type name: widget key, `using =` value, or a base in `type X = base`.
135
+ const resolved = resolveGuiDef(word, sources);
136
+ if (resolved && resolved.def.file !== undefined && resolved.def.line !== undefined) {
137
+ return [locationAt(resolved.def.file, resolved.def.line)];
138
+ }
139
+ return null;
140
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * GUI widget tree: turn a PdxGui document into the hierarchy a modder thinks
3
+ * in — windows > containers > widgets, with template/type declarations and
4
+ * animation states — for the Show GUI Widget Tree webview.
5
+ *
6
+ * PdxGui reuses the jomini syntax, so the tolerant script parser handles it.
7
+ * Detection is INVERSE: every block child is a node unless its key is a known
8
+ * attribute block (size, position…), because widget vocabularies are open
9
+ * (mods derive custom types like icon_observer) while attribute blocks are a
10
+ * small closed set.
11
+ *
12
+ * No `vscode` imports: unit-tested in plain Node.
13
+ */
14
+ import type { GuiTree, GuiTreeNode } from "@px-lsp/protocol/protocol";
15
+ import { LineIndex, parseScript, type BlockNode, type Statement } from "../parser";
16
+
17
+ /** Value-form headers that mark the NEXT assignment as a declaration. */
18
+ const DECL_MARKERS = new Set(["template", "types", "type", "blockoverride", "block", "local_template"]);
19
+
20
+ /** Attribute blocks — data, not children. Everything else is a node. */
21
+ const PROPERTY_BLOCKS = new Set([
22
+ "size",
23
+ "minimumsize",
24
+ "position",
25
+ "framesize",
26
+ "spriteborder",
27
+ "color",
28
+ "disabledcolor",
29
+ "uv_scale",
30
+ "margin",
31
+ "padding",
32
+ "mipmaplodbias",
33
+ "modify_texture",
34
+ "resizeparent",
35
+ "soundeffect",
36
+ "cursor_properties",
37
+ ]);
38
+
39
+ /** Animation-ish blocks rendered as dimmed nodes rather than widgets. */
40
+ const STATE_BLOCKS = new Set(["state", "animation", "attachanimation", "onpressed", "onreleased"]);
41
+
42
+ export function buildGuiTree(text: string): GuiTree {
43
+ const result = parseScript(text);
44
+ const lineIndex = new LineIndex(text);
45
+ let count = 0;
46
+
47
+ const walk = (statements: Statement[]): GuiTreeNode[] => {
48
+ const nodes: GuiTreeNode[] = [];
49
+ let pendingDecl: string | null = null;
50
+ for (const stmt of statements) {
51
+ if (stmt.kind !== "assignment") {
52
+ // A bare `template` / `types` word labels the following assignment.
53
+ if (stmt.value.kind === "scalar" && DECL_MARKERS.has(stmt.value.text.toLowerCase())) {
54
+ pendingDecl = stmt.value.text.toLowerCase();
55
+ } else if (stmt.value.kind === "block" || stmt.value.kind === "tagged-block") {
56
+ // Anonymous block: descend, keep any children it holds.
57
+ const block = stmt.value.kind === "block" ? stmt.value : stmt.value.block;
58
+ nodes.push(...walk(block.statements));
59
+ pendingDecl = null;
60
+ }
61
+ continue;
62
+ }
63
+ const value = stmt.value;
64
+ const block: BlockNode | null =
65
+ value?.kind === "block" ? value : value?.kind === "tagged-block" ? value.block : null;
66
+ if (!block) {
67
+ pendingDecl = null;
68
+ continue;
69
+ }
70
+ const rawKey = stmt.key.text;
71
+ const lower = rawKey.toLowerCase();
72
+ if (!pendingDecl && PROPERTY_BLOCKS.has(lower)) continue;
73
+
74
+ const node: GuiTreeNode = {
75
+ key: pendingDecl ? `${pendingDecl} ${rawKey}` : rawKey,
76
+ kind: pendingDecl ? "decl" : STATE_BLOCKS.has(lower) ? "state" : "widget",
77
+ line: lineIndex.positionAt(stmt.key.range.start).line,
78
+ children: [],
79
+ };
80
+ if (value?.kind === "tagged-block") node.base = value.tag.text;
81
+ for (const child of block.statements) {
82
+ if (child.kind !== "assignment" || child.value?.kind !== "scalar") continue;
83
+ const ck = child.key.text.toLowerCase();
84
+ if (ck === "name" && node.name === undefined) node.name = child.value.text;
85
+ else if (ck === "using") (node.using ??= []).push(child.value.text);
86
+ }
87
+ node.children = walk(block.statements);
88
+ count++;
89
+ nodes.push(node);
90
+ pendingDecl = null;
91
+ }
92
+ return nodes;
93
+ };
94
+
95
+ const nodes = walk(result.root.statements);
96
+ return { nodes, count };
97
+ }