@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,729 @@
1
+ /**
2
+ * Completion, hover and signature help for [ ... ] data-function expressions
3
+ * in .gui and localization files. Three knowledge layers, best-first:
4
+ *
5
+ * - the user's DumpDataTypes output (version-exact names, args, returns);
6
+ * - the bundled wiki baseline (packages/server/data/<game>/dataTypes.json);
7
+ * - vanilla usage harvested from the user's own game files (dataFnUsage.ts):
8
+ * names newer than both tables, usage counts for ranking, observed literal
9
+ * arguments, formatting suffixes, and real example sites.
10
+ *
11
+ * AD-5 applies: unknown names never produce diagnostics; unresolved chains
12
+ * fall back to the vanilla member pool instead of going silent.
13
+ */
14
+ import { CompletionItemKind, type CompletionItem, type SignatureHelp } from "vscode-languageserver/node";
15
+ import { describeDataFn } from "../data/dataFnDocs";
16
+ import { membersOf, resolveChainType, type DataTypeMember, type DataTypesData } from "../data/dataTypes";
17
+ import type { DataFnUsage } from "../data/dataFnUsage";
18
+ import type { DefinitionIndex } from "../index/indexer";
19
+ import { finalize, MAX_ITEMS, type CompletionResult } from "./completion";
20
+ import { URI } from "vscode-uri";
21
+ import * as path from "path";
22
+
23
+ /**
24
+ * Functions whose single quoted argument names a script definition, so the
25
+ * literal completes from the definition index (the mod's own defs plus vanilla)
26
+ * rather than only from harvested vanilla literals. Keyed by the function name
27
+ * (the last chain segment), so it fires for any owner: `Scope.ScriptValue`,
28
+ * `TopScope.ScriptValue`, `GetPlayer.MakeScope.ScriptValue`, … all map alike.
29
+ *
30
+ * Add an entry only when the dump signature or a vanilla example proves the
31
+ * argument's kind. Verified against the 1.19 DumpDataTypes output:
32
+ * - ScriptValue( Arg0 ) → CFixedPoint, macro "Calculates the named script
33
+ * value" — Arg0 is a script_value key;
34
+ * - GetTrait( Arg0 ) → Trait, "Get the Trait object with the provided key" —
35
+ * Arg0 is a trait key.
36
+ */
37
+ const ARG_INDEX_KIND: Record<string, string> = {
38
+ ScriptValue: "script_value",
39
+ GetTrait: "trait",
40
+ };
41
+
42
+ /**
43
+ * The expression text when `linePrefix` ends inside an unclosed [ ... ], else
44
+ * null. Datafunction expressions never span lines, so the line prefix is
45
+ * enough context.
46
+ */
47
+ export function datafunctionExprAt(linePrefix: string): string | null {
48
+ let open = -1;
49
+ for (let i = 0; i < linePrefix.length; i++) {
50
+ const ch = linePrefix[i];
51
+ if (ch === "[") open = i;
52
+ else if (ch === "]") open = -1;
53
+ }
54
+ return open >= 0 ? linePrefix.slice(open + 1) : null;
55
+ }
56
+
57
+ /**
58
+ * The dotted chain being typed at the end of an expression: for
59
+ * `Concat( 'x', Character.GetFather.` → ["Character","GetFather",""].
60
+ * The last element is the (possibly empty) segment under the cursor.
61
+ */
62
+ export function chainAtEnd(expr: string): string[] {
63
+ // Cut at the last argument/string/formatting boundary; dots stay.
64
+ let start = 0;
65
+ for (let i = expr.length - 1; i >= 0; i--) {
66
+ if ("('\", |".includes(expr[i]) || expr[i] === ")") {
67
+ start = i + 1;
68
+ break;
69
+ }
70
+ }
71
+ const tail = expr.slice(start).trim();
72
+ if (tail.length === 0) return [""];
73
+ if (!/^[A-Za-z0-9_.]*$/.test(tail)) return [""];
74
+ return tail.split(".");
75
+ }
76
+
77
+ /** The innermost function call still open at the end of `expr`, if any. */
78
+ export interface OpenCall {
79
+ /** Dotted chain of the called name, e.g. ["Character","GetHouseAspiration"]. */
80
+ chain: string[];
81
+ /** 0-based index of the argument the cursor is in. */
82
+ argIndex: number;
83
+ /** Text typed inside an unclosed '...' literal, or null when not in one. */
84
+ literalPrefix: string | null;
85
+ }
86
+
87
+ export function openCallAt(expr: string): OpenCall | null {
88
+ interface Frame {
89
+ chain: string[];
90
+ argIndex: number;
91
+ }
92
+ const stack: Frame[] = [];
93
+ let chain: string[] = [];
94
+ let word = "";
95
+ const flushWord = () => {
96
+ if (word.length > 0) {
97
+ chain.push(word);
98
+ word = "";
99
+ }
100
+ };
101
+ let i = 0;
102
+ while (i < expr.length) {
103
+ const ch = expr[i];
104
+ if (/[A-Za-z0-9_]/.test(ch)) {
105
+ word += ch;
106
+ i++;
107
+ } else if (ch === ".") {
108
+ flushWord();
109
+ i++;
110
+ } else if (ch === "(") {
111
+ flushWord();
112
+ stack.push({ chain, argIndex: 0 });
113
+ chain = [];
114
+ i++;
115
+ } else if (ch === ")") {
116
+ stack.pop();
117
+ chain = [];
118
+ word = "";
119
+ i++;
120
+ } else if (ch === ",") {
121
+ if (stack.length > 0) stack[stack.length - 1].argIndex++;
122
+ chain = [];
123
+ word = "";
124
+ i++;
125
+ } else if (ch === "'") {
126
+ const close = expr.indexOf("'", i + 1);
127
+ if (close < 0) {
128
+ const top = stack[stack.length - 1];
129
+ if (!top || top.chain.length === 0) return null;
130
+ return { chain: top.chain, argIndex: top.argIndex, literalPrefix: expr.slice(i + 1) };
131
+ }
132
+ i = close + 1;
133
+ } else if (ch === " " || ch === "\t") {
134
+ flushWord();
135
+ i++;
136
+ } else {
137
+ flushWord();
138
+ chain = [];
139
+ i++;
140
+ }
141
+ }
142
+ const top = stack[stack.length - 1];
143
+ if (!top || top.chain.length === 0) return null;
144
+ return { chain: top.chain, argIndex: top.argIndex, literalPrefix: null };
145
+ }
146
+
147
+ /**
148
+ * Text of the unclosed `'...'` literal at the end of `expr`, or null when the
149
+ * cursor is not inside one. Pairs quotes the same way `openCallAt` does, but
150
+ * needs no enclosing call — so a cast literal like `'(CFixedPoint` is seen even
151
+ * when it is not a function argument.
152
+ */
153
+ export function openLiteralPrefix(expr: string): string | null {
154
+ let open = -1;
155
+ for (let i = 0; i < expr.length; i++) {
156
+ if (expr[i] === "'") open = open === -1 ? i : -1;
157
+ }
158
+ return open === -1 ? null : expr.slice(open + 1);
159
+ }
160
+
161
+ // ---- ranking / rendering helpers ----------------------------------------------
162
+
163
+ /** Log-bucketed frequency rank: higher vanilla usage sorts first. */
164
+ function freq3(count: number): string {
165
+ return String(999 - Math.min(998, Math.round(Math.log2(1 + count) * 55))).padStart(3, "0");
166
+ }
167
+
168
+ function memberDetail(member: DataTypeMember, owner?: string): string {
169
+ const args = member.args && member.args.length > 0 ? `( ${member.args.join(", ")} )` : "";
170
+ const ret = member.ret ? ` → ${member.ret}` : "";
171
+ return `${owner ? owner + " " : ""}${member.kind}${args}${ret}`;
172
+ }
173
+
174
+ function usesSuffix(count: number): string {
175
+ return count > 0 ? ` · ${count.toLocaleString("en-US")}× in vanilla` : "";
176
+ }
177
+
178
+ const SOURCE_LABEL: Record<string, string> = {
179
+ dump: "your DumpDataTypes log",
180
+ wiki: "bundled wiki tables",
181
+ macro: "game data_binding macro",
182
+ };
183
+
184
+ /** Total vanilla-usage count of a name in any role. */
185
+ function usageCount(usage: DataFnUsage, name: string): number {
186
+ return (usage.starts.get(name) ?? 0) + (usage.memberPool.get(name) ?? 0);
187
+ }
188
+
189
+ /** Member rank: type-specific pairs weigh more than the global pool. */
190
+ function memberRank(usage: DataFnUsage, owner: string | null, name: string): number {
191
+ const pair = owner ? (usage.pairs.get(owner)?.get(name) ?? 0) : 0;
192
+ return pair * 3 + (usage.memberPool.get(name) ?? 0);
193
+ }
194
+
195
+ // ---- completion ----------------------------------------------------------------
196
+
197
+ /** Cursor position for explicit replace ranges (line of `linePrefix`). */
198
+ export interface DataFnCursor {
199
+ line: number;
200
+ character: number;
201
+ }
202
+
203
+ /**
204
+ * Completion inside a [ ... ] expression, or null when the cursor is not in
205
+ * one (caller falls through to its normal provider).
206
+ *
207
+ * When `cursor` is given, every item carries a textEdit replacing exactly the
208
+ * typed tail segment. Without it, the client derives the replace range from
209
+ * its word pattern — which includes "." for gui/loc files, so after
210
+ * `[GetPlayer.` the word is the whole dotted chain: member items neither match
211
+ * the filter nor insert correctly (#2).
212
+ */
213
+ export function provideDataFnCompletion(
214
+ data: DataTypesData,
215
+ usage: DataFnUsage,
216
+ linePrefix: string,
217
+ index?: DefinitionIndex,
218
+ cursor?: DataFnCursor
219
+ ): CompletionResult | null {
220
+ const expr = datafunctionExprAt(linePrefix);
221
+ if (expr === null) return null;
222
+
223
+ /** finalize + anchor each survivor to replace exactly the typed `partial`. */
224
+ const finish = (items: CompletionItem[], partial: string): CompletionResult => {
225
+ const result = finalize(items, partial, MAX_ITEMS);
226
+ if (cursor) {
227
+ const range = {
228
+ start: { line: cursor.line, character: cursor.character - partial.length },
229
+ end: { line: cursor.line, character: cursor.character },
230
+ };
231
+ for (const item of result.items) {
232
+ item.textEdit = { range, newText: item.insertText ?? item.label };
233
+ }
234
+ }
235
+ return result;
236
+ };
237
+
238
+ // Inside a cast literal `'(CFixedPoint)…'`: complete the datatype name. Comes
239
+ // before the function-argument branch, since the cast sits inside a call too
240
+ // (`GreaterThan_CFixedPoint( x, '(CFixed` ).
241
+ const literal = openLiteralPrefix(expr);
242
+ if (literal !== null && literal.startsWith("(") && !literal.includes(")")) {
243
+ const partial = literal.slice(1);
244
+ const items: CompletionItem[] = [...data.types.keys()].map((type) => ({
245
+ label: type,
246
+ kind: CompletionItemKind.Class,
247
+ detail: "datatype (cast)",
248
+ sortText: type,
249
+ data: { t: "dfn" },
250
+ }));
251
+ return finish(items, partial);
252
+ }
253
+
254
+ // Inside a '...' literal argument: the definition index for functions whose
255
+ // argument names a script definition (ScriptValue → the mod's own script
256
+ // values, first), merged with the observed vanilla literals.
257
+ const call = openCallAt(expr);
258
+ if (call && call.literalPrefix !== null) {
259
+ const fn = call.chain[call.chain.length - 1];
260
+ const items: CompletionItem[] = [];
261
+ const seen = new Set<string>();
262
+ const indexKind = ARG_INDEX_KIND[fn];
263
+ if (indexKind && index) {
264
+ const kindLabel = indexKind.replace(/_/g, " ");
265
+ for (const def of index.entries((d) => d.kind === indexKind)) {
266
+ if (seen.has(def.name)) continue;
267
+ seen.add(def.name);
268
+ const isMod = def.source === "mod";
269
+ items.push({
270
+ label: def.name,
271
+ kind: CompletionItemKind.Value,
272
+ detail: `${kindLabel} (${def.source})`,
273
+ // Mod defs first, then vanilla literals (tier 1, below), then the
274
+ // rest of the index defs (tier 2).
275
+ sortText: (isMod ? "0" : "2") + def.name,
276
+ data: { t: "dfn" },
277
+ });
278
+ }
279
+ }
280
+ const lits = usage.literals.get(fn);
281
+ if (lits) {
282
+ for (const [value, count] of lits) {
283
+ if (seen.has(value)) continue;
284
+ seen.add(value);
285
+ items.push({
286
+ label: value,
287
+ kind: CompletionItemKind.Value,
288
+ detail: `argument of ${fn}${usesSuffix(count)}`,
289
+ sortText: "1" + freq3(count) + value,
290
+ data: { t: "dfn" },
291
+ });
292
+ }
293
+ }
294
+ return finish(items, call.literalPrefix);
295
+ }
296
+
297
+ // After `|`: formatting suffixes observed in vanilla ( |E, |U, |V0, … ).
298
+ const fmt = /\|([A-Za-z0-9+\-=*%.]*)$/.exec(expr);
299
+ if (fmt) {
300
+ const items: CompletionItem[] = [...usage.formats.entries()]
301
+ .filter(([, count]) => count >= 3)
302
+ .map(([suffix, count]) => ({
303
+ label: suffix,
304
+ kind: CompletionItemKind.EnumMember,
305
+ detail: `format suffix${usesSuffix(count)}`,
306
+ sortText: freq3(count) + suffix,
307
+ data: { t: "dfn" },
308
+ }));
309
+ return finish(items, fmt[1]);
310
+ }
311
+
312
+ const chain = chainAtEnd(expr);
313
+ const typed = chain[chain.length - 1];
314
+ const items: CompletionItem[] = [];
315
+
316
+ if (chain.length === 1) {
317
+ // Chain start: data types, global promotes/functions, and names vanilla
318
+ // uses that neither table knows yet. Ranked by vanilla frequency.
319
+ const seen = new Set<string>();
320
+ for (const type of data.types.keys()) {
321
+ seen.add(type);
322
+ const count = usage.starts.get(type) ?? 0;
323
+ items.push({
324
+ label: type,
325
+ kind: CompletionItemKind.Class,
326
+ detail: `data type${usesSuffix(count)}`,
327
+ sortText: freq3(count) + type,
328
+ data: { t: "dfn" },
329
+ });
330
+ }
331
+ for (const [name, member] of data.globals) {
332
+ if (seen.has(name)) continue;
333
+ seen.add(name);
334
+ const count = usage.starts.get(name) ?? 0;
335
+ const doc = member.desc ?? describeDataFn(name, member);
336
+ items.push({
337
+ label: name,
338
+ kind: member.kind === "promote" ? CompletionItemKind.Variable : CompletionItemKind.Function,
339
+ detail: `global ${memberDetail(member)}${usesSuffix(count)}`,
340
+ ...(doc ? { documentation: doc } : {}),
341
+ sortText: freq3(count) + name,
342
+ data: { t: "dfn" },
343
+ });
344
+ }
345
+ for (const [name, count] of usage.starts) {
346
+ if (seen.has(name)) continue;
347
+ const called = usage.argCounts.has(name);
348
+ const hasMembers = usage.pairs.has(name);
349
+ const doc = describeDataFn(name, null);
350
+ items.push({
351
+ label: name,
352
+ kind: hasMembers && !called ? CompletionItemKind.Class : CompletionItemKind.Function,
353
+ detail: `vanilla usage${usesSuffix(count)} (not in the data-type tables)`,
354
+ ...(doc ? { documentation: doc } : {}),
355
+ sortText: freq3(count) + name,
356
+ data: { t: "dfn" },
357
+ });
358
+ }
359
+ return finish(items, typed);
360
+ }
361
+
362
+ // Member position: resolve the chain to a type when the tables allow it.
363
+ const ownerSegments = chain.slice(0, -1);
364
+ const ownerType = resolveChainType(data, ownerSegments);
365
+ const pairOwner = ownerSegments.length === 1 ? ownerSegments[0] : null;
366
+ if (ownerType) {
367
+ const members = membersOf(data, ownerType) ?? new Map<string, DataTypeMember>();
368
+ const seen = new Set<string>();
369
+ for (const [name, member] of members) {
370
+ seen.add(name);
371
+ const doc = member.desc ?? describeDataFn(name, member) ?? undefined;
372
+ items.push({
373
+ label: name,
374
+ kind: member.kind === "promote" ? CompletionItemKind.Property : CompletionItemKind.Method,
375
+ detail: memberDetail(member, ownerType + ".") + usesSuffix(memberRank(usage, ownerType, name)),
376
+ ...(doc ? { documentation: doc } : {}),
377
+ sortText: freq3(memberRank(usage, ownerType, name)) + name,
378
+ data: { t: "dfn" },
379
+ });
380
+ }
381
+ // Members vanilla chains off this same start but the tables don't list.
382
+ const harvested = usage.pairs.get(ownerType) ?? (pairOwner ? usage.pairs.get(pairOwner) : undefined);
383
+ for (const [name, count] of harvested ?? []) {
384
+ if (seen.has(name)) continue;
385
+ const doc = describeDataFn(name, null);
386
+ items.push({
387
+ label: name,
388
+ kind: CompletionItemKind.Method,
389
+ detail: `vanilla usage on ${pairOwner ?? ownerType}${usesSuffix(count)}`,
390
+ ...(doc ? { documentation: doc } : {}),
391
+ sortText: freq3(count) + name,
392
+ data: { t: "dfn" },
393
+ });
394
+ }
395
+ return finish(items, typed);
396
+ }
397
+
398
+ // Chain the tables cannot resolve (unknown start, missing return type…):
399
+ // offer the vanilla member pool rather than nothing — AD-5, annotate not hide.
400
+ const harvestedPairs = pairOwner ? usage.pairs.get(pairOwner) : undefined;
401
+ if (harvestedPairs && harvestedPairs.size > 0) {
402
+ for (const [name, count] of harvestedPairs) {
403
+ const doc = describeDataFn(name, null);
404
+ items.push({
405
+ label: name,
406
+ kind: CompletionItemKind.Method,
407
+ detail: `vanilla usage on ${pairOwner}${usesSuffix(count)}`,
408
+ ...(doc ? { documentation: doc } : {}),
409
+ sortText: freq3(count) + name,
410
+ data: { t: "dfn" },
411
+ });
412
+ }
413
+ return finish(items, typed);
414
+ }
415
+ for (const [name, count] of usage.memberPool) {
416
+ items.push({
417
+ label: name,
418
+ kind: CompletionItemKind.Method,
419
+ detail: `vanilla usage${usesSuffix(count)} (chain not resolved)`,
420
+ sortText: freq3(count) + name,
421
+ data: { t: "dfn" },
422
+ });
423
+ }
424
+ return finish(items, typed);
425
+ }
426
+
427
+ // ---- hover ----------------------------------------------------------------------
428
+
429
+ export interface DataFnHoverInfo {
430
+ markdown: string;
431
+ start: number;
432
+ end: number;
433
+ }
434
+
435
+ function exampleLines(usage: DataFnUsage, name: string, gameRoot: string | null): string[] {
436
+ const examples = usage.examples.get(name);
437
+ if (!examples || examples.length === 0) return [];
438
+ const lines = ["", "Vanilla examples:"];
439
+ for (const ex of examples) {
440
+ const site = `${ex.file}:${ex.line}`;
441
+ const link = gameRoot
442
+ ? `[${site}](${URI.file(path.join(gameRoot, ex.file))
443
+ .with({ fragment: String(ex.line) })
444
+ .toString()})`
445
+ : site;
446
+ lines.push(`- \`${ex.text}\` — ${link}`);
447
+ }
448
+ return lines;
449
+ }
450
+
451
+ function literalLines(usage: DataFnUsage, name: string): string[] {
452
+ const lits = usage.literals.get(name);
453
+ if (!lits || lits.size === 0) return [];
454
+ const top = [...lits.entries()]
455
+ .sort((a, b) => b[1] - a[1])
456
+ .slice(0, 8)
457
+ .map(([v]) => `\`'${v}'\``);
458
+ return ["", `Observed arguments: ${top.join(", ")}${lits.size > 8 ? ", …" : ""}`];
459
+ }
460
+
461
+ function provenance(member: DataTypeMember | null): string {
462
+ if (member?.src && SOURCE_LABEL[member.src]) return SOURCE_LABEL[member.src];
463
+ return "deduced from vanilla usage";
464
+ }
465
+
466
+ /** Top-8 `.member` names observed after `name` in vanilla, as one hover line. */
467
+ function topMemberLines(usage: DataFnUsage, name: string, label: string): string[] {
468
+ const pairs = usage.pairs.get(name);
469
+ if (!pairs || pairs.size === 0) return [];
470
+ const top = [...pairs.entries()]
471
+ .sort((a, b) => b[1] - a[1])
472
+ .slice(0, 8)
473
+ .map(([n]) => `\`${n}\``);
474
+ return ["", `${label}: ${top.join(" ")}`];
475
+ }
476
+
477
+ /**
478
+ * Footer for names the loaded tables do not resolve. Which advice applies
479
+ * depends on what is loaded: without a dump, running DumpDataTypes is the fix;
480
+ * with one loaded, saying so again would read as "your logs were not found".
481
+ */
482
+ function dumpHintFor(data: DataTypesData): string {
483
+ return data.source === "data_types.log"
484
+ ? "*Not in your `DumpDataTypes` dump (which is loaded) — shown from vanilla usage instead.*"
485
+ : "*Using the bundled wiki tables. Run `DumpDataTypes` in the game console to load the complete, version-exact definitions.*";
486
+ }
487
+
488
+ /** Shared hover tail: description, observed literals, vanilla example sites. */
489
+ function usageDetailLines(
490
+ usage: DataFnUsage,
491
+ name: string,
492
+ desc: string | null | undefined,
493
+ gameRoot: string | null,
494
+ dumpHint: string | null = null
495
+ ): string[] {
496
+ const lines: string[] = [];
497
+ if (desc) lines.push("", desc);
498
+ lines.push(...literalLines(usage, name));
499
+ lines.push(...exampleLines(usage, name, gameRoot));
500
+ if (dumpHint) lines.push("", dumpHint);
501
+ return lines;
502
+ }
503
+
504
+ /**
505
+ * Hover info for the chain segment at `character` when it sits inside a
506
+ * [ ... ] expression and is known to any layer; null lets the caller fall
507
+ * through to its normal hover.
508
+ */
509
+ export function provideDataFnHover(
510
+ data: DataTypesData,
511
+ usage: DataFnUsage,
512
+ lineText: string,
513
+ character: number,
514
+ gameRoot: string | null = null
515
+ ): DataFnHoverInfo | null {
516
+ // Inside an expression? The [ must be open where the cursor sits.
517
+ const prefix = lineText.slice(0, character);
518
+ if (datafunctionExprAt(prefix) === null && lineText[character] !== "[") return null;
519
+
520
+ // The dotted chain around the cursor.
521
+ const isWord = (ch: string) => /[A-Za-z0-9_.]/.test(ch);
522
+ let start = character;
523
+ while (start > 0 && isWord(lineText[start - 1])) start--;
524
+ let end = character;
525
+ while (end < lineText.length && isWord(lineText[end])) end++;
526
+ const dotted = lineText.slice(start, end);
527
+ if (!/^[A-Za-z0-9_.]+$/.test(dotted)) return null;
528
+
529
+ // Which segment is under the cursor?
530
+ const segments = dotted.split(".");
531
+ let segStart = start;
532
+ let index = 0;
533
+ for (; index < segments.length; index++) {
534
+ const segEnd = segStart + segments[index].length;
535
+ if (character <= segEnd) break;
536
+ segStart = segEnd + 1; // skip the dot
537
+ }
538
+ if (index >= segments.length) return null;
539
+ const segment = segments[index];
540
+ if (segment.length === 0) return null;
541
+
542
+ const lines: string[] = [];
543
+ const uses = usageCount(usage, segment);
544
+
545
+ if (index === 0) {
546
+ const typeMembers = membersOf(data, segment);
547
+ if (typeMembers) {
548
+ lines.push(`\`${segment}\` — data type (${typeMembers.size} known members)`);
549
+ lines.push(...topMemberLines(usage, segment, "Common members"));
550
+ lines.push(...exampleLines(usage, segment, gameRoot));
551
+ } else {
552
+ const global = data.globals.get(segment);
553
+ if (global) {
554
+ lines.push(
555
+ `\`${segment}${global.args?.length ? `( ${global.args.join(", ")} )` : ""}\`${global.ret ? ` → \`${global.ret}\`` : ""}`
556
+ );
557
+ lines.push("", `global ${global.kind} — ${provenance(global)}`);
558
+ lines.push(
559
+ ...usageDetailLines(usage, segment, global.desc ?? describeDataFn(segment, global), gameRoot)
560
+ );
561
+ } else if (uses > 0) {
562
+ lines.push(`\`${segment}\``);
563
+ lines.push("", `not in the data-type tables — ${provenance(null)}`);
564
+ const desc = describeDataFn(segment, null);
565
+ if (desc) lines.push("", desc);
566
+ lines.push(...topMemberLines(usage, segment, "Members seen after it"));
567
+ lines.push(...literalLines(usage, segment));
568
+ lines.push(...exampleLines(usage, segment, gameRoot));
569
+ lines.push("", dumpHintFor(data));
570
+ } else {
571
+ return null;
572
+ }
573
+ }
574
+ } else {
575
+ const ownerType = resolveChainType(data, segments.slice(0, index));
576
+ const member = ownerType ? membersOf(data, ownerType)?.get(segment) : undefined;
577
+ // The chain's owner type may not resolve (unknown start, missing return
578
+ // type) while the member name is still in the tables — match by name so a
579
+ // loaded dump keeps answering (dump names are near-unique per type).
580
+ const byName: Array<{ owner: string; member: DataTypeMember }> = [];
581
+ if (!member) {
582
+ for (const [typeName, members] of data.types) {
583
+ const m = members.get(segment);
584
+ if (m) byName.push({ owner: typeName, member: m });
585
+ if (byName.length >= 6) break;
586
+ }
587
+ }
588
+ if (member && ownerType) {
589
+ lines.push(
590
+ `\`${ownerType}.${segment}${member.args?.length ? `( ${member.args.join(", ")} )` : ""}\`${member.ret ? ` → \`${member.ret}\`` : ""}`
591
+ );
592
+ lines.push("", `${member.kind} on \`${ownerType}\` — ${provenance(member)}`);
593
+ lines.push(
594
+ ...usageDetailLines(usage, segment, member.desc ?? describeDataFn(segment, member), gameRoot)
595
+ );
596
+ } else if (byName.length > 0) {
597
+ const hit = byName[0];
598
+ lines.push(
599
+ `\`${hit.owner}.${segment}${hit.member.args?.length ? `( ${hit.member.args.join(", ")} )` : ""}\`${hit.member.ret ? ` → \`${hit.member.ret}\`` : ""}`
600
+ );
601
+ lines.push(
602
+ "",
603
+ `${hit.member.kind} on \`${hit.owner}\` — ${provenance(hit.member)}, matched by name (the chain before it did not resolve to a type)`
604
+ );
605
+ if (byName.length > 1) {
606
+ lines.push(
607
+ "",
608
+ `Also defined on: ${byName
609
+ .slice(1)
610
+ .map((o) => `\`${o.owner}\``)
611
+ .join(" ")}${byName.length >= 6 ? " …" : ""}`
612
+ );
613
+ }
614
+ lines.push(
615
+ ...usageDetailLines(usage, segment, hit.member.desc ?? describeDataFn(segment, hit.member), gameRoot)
616
+ );
617
+ } else if (uses > 0) {
618
+ lines.push(`\`${segment}\``);
619
+ lines.push("", `member — ${provenance(null)}`);
620
+ lines.push(
621
+ ...usageDetailLines(usage, segment, describeDataFn(segment, null), gameRoot, dumpHintFor(data))
622
+ );
623
+ } else {
624
+ return null;
625
+ }
626
+ }
627
+ return { markdown: lines.join("\n"), start: segStart, end: segStart + segment.length };
628
+ }
629
+
630
+ // ---- signature help ----------------------------------------------------------------
631
+
632
+ /**
633
+ * Signature help for the innermost open call in a [ ... ] expression:
634
+ * `ObjectsEqual( a, | )` shows the argument list with the active one
635
+ * highlighted. Argument types come from the dump when known, else the
636
+ * most-observed vanilla arity.
637
+ */
638
+ export function provideDataFnSignature(
639
+ data: DataTypesData,
640
+ usage: DataFnUsage,
641
+ lineText: string,
642
+ character: number
643
+ ): SignatureHelp | null {
644
+ const prefix = lineText.slice(0, character);
645
+ const expr = datafunctionExprAt(prefix);
646
+ if (expr === null) return null;
647
+ const call = openCallAt(expr);
648
+ if (!call) return null;
649
+
650
+ const fn = call.chain[call.chain.length - 1];
651
+ let member: DataTypeMember | undefined;
652
+ let owner: string | null = null;
653
+ if (call.chain.length > 1) {
654
+ owner = resolveChainType(data, call.chain.slice(0, -1));
655
+ if (owner) member = membersOf(data, owner)?.get(fn);
656
+ } else {
657
+ member = data.globals.get(fn);
658
+ }
659
+ if (!member) {
660
+ // Any type carrying this member (dump data makes this near-unique).
661
+ for (const [typeName, members] of data.types) {
662
+ const m = members.get(fn);
663
+ if (m?.args && m.args.length > 0) {
664
+ member = m;
665
+ owner = typeName;
666
+ break;
667
+ }
668
+ }
669
+ }
670
+
671
+ let argNames: string[];
672
+ if (member?.args && member.args.length > 0) {
673
+ argNames = member.args;
674
+ } else {
675
+ const arities = usage.argCounts.get(fn);
676
+ if (!arities || arities.size === 0) return null;
677
+ let best = 0;
678
+ let bestCount = -1;
679
+ for (const [arity, count] of arities) {
680
+ if (count > bestCount) {
681
+ best = arity;
682
+ bestCount = count;
683
+ }
684
+ }
685
+ if (best === 0) return null;
686
+ argNames = Array.from({ length: best }, (_, i) => `arg${i + 1}`);
687
+ }
688
+
689
+ // Build the label with per-parameter offsets so duplicate type names
690
+ // (CString, CString) still highlight the right one.
691
+ let label = `${fn}( `;
692
+ const params: Array<{ label: [number, number]; documentation?: string }> = [];
693
+ argNames.forEach((arg, i) => {
694
+ if (i > 0) label += ", ";
695
+ const from = label.length;
696
+ label += arg;
697
+ params.push({ label: [from, label.length] });
698
+ });
699
+ label += " )";
700
+ if (member?.ret) label += ` → ${member.ret}`;
701
+
702
+ const docParts: string[] = [];
703
+ const desc = member?.desc ?? describeDataFn(fn, member ?? null);
704
+ if (desc) docParts.push(desc);
705
+ const lits = usage.literals.get(fn);
706
+ if (lits && lits.size > 0) {
707
+ const top = [...lits.entries()]
708
+ .sort((a, b) => b[1] - a[1])
709
+ .slice(0, 6)
710
+ .map(([v]) => `'${v}'`);
711
+ docParts.push(`Observed arguments: ${top.join(", ")}${lits.size > 6 ? ", …" : ""}`);
712
+ }
713
+ if (member === undefined)
714
+ docParts.push(
715
+ `Arity observed in vanilla usage (${usage.argCounts.get(fn)?.get(argNames.length) ?? 0} sites).`
716
+ );
717
+
718
+ return {
719
+ signatures: [
720
+ {
721
+ label,
722
+ ...(docParts.length > 0 ? { documentation: docParts.join("\n\n") } : {}),
723
+ parameters: params,
724
+ },
725
+ ],
726
+ activeSignature: 0,
727
+ activeParameter: Math.min(call.argIndex, argNames.length - 1),
728
+ };
729
+ }