@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,534 @@
1
+ /**
2
+ * Schema-driven reference extraction over the CST: which names a script file
3
+ * *uses* — cross-reference fields (trigger_event = ..., has_trait = ...),
4
+ * prefixed values (culture:czech, scope:target), loc-key-valued properties —
5
+ * plus the definitions that only exist as usage sites (save_scope_as,
6
+ * set_variable) and the event namespaces a file declares.
7
+ *
8
+ * References power find-all-references, rename, unused detection, coverage
9
+ * and the event graph. Extracted for mod (and parent) files only; vanilla
10
+ * references are computed lazily on demand (rework plan AD-4).
11
+ *
12
+ * No `vscode` imports here: unit-tested in plain Node.
13
+ */
14
+ import type { Definition, DefSource, Reference } from "@px-lsp/protocol/types";
15
+ import {
16
+ dynamicRefKinds,
17
+ VARIABLE_SET_KINDS,
18
+ VARIABLE_LIST_SET_KINDS,
19
+ VARIABLE_READ_KINDS,
20
+ VAR_PREFIX_KINDS,
21
+ } from "../games/jomini/variables";
22
+ import { activeProfile } from "../games/active";
23
+ import { isLocProperty } from "@px-lsp/protocol/locProperties";
24
+ import { isStructuralKeyword } from "../contextKeywords";
25
+ import { intern, shareDefinitionStrings, shareReferenceStrings } from "./intern";
26
+ import type { SchemaData } from "../schema/loader";
27
+ import {
28
+ LineIndex,
29
+ parseScript,
30
+ walkStatements,
31
+ type AssignmentNode,
32
+ type BlockNode,
33
+ type ScalarNode,
34
+ type Statement,
35
+ } from "../parser";
36
+
37
+ const NAME_OK = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
38
+ /** Words that are never references even in name position. */
39
+ const NOT_A_NAME = new Set(["yes", "no", "this", "root", "prev", "from"]);
40
+
41
+ const PREFIX_VALUE = /^([a-z_]+):([A-Za-z0-9_\-.']+)/;
42
+
43
+ /** `save_temporary_value_as` (script-value math) is read back as scope:x too. */
44
+ const SAVE_SCOPE_KEYS = new Set(["save_scope_as", "save_temporary_scope_as", "save_temporary_value_as"]);
45
+ const SAVE_SCOPE_VALUE_KEYS = new Set(["save_scope_value_as", "save_temporary_scope_value_as"]);
46
+ /** `add_character_flag = X` / `add_house_flag = { flag = X … }` — flag declarations. */
47
+ const FLAG_SET_KEY = /^(?:add|set)_[a-z_]*flag$/;
48
+ const LIST_SET_KEYS = new Set(["add_to_list", "add_to_temporary_list"]);
49
+
50
+ const VAR_PREFIXES = new Set(Object.keys(VAR_PREFIX_KINDS));
51
+
52
+ /** What a key-position call (`my_effect = yes`) may refer to. */
53
+ const CALL_KINDS = ["scripted_effect", "scripted_trigger", "scripted_modifier"];
54
+
55
+ export interface ExtractedRefs {
56
+ references: Reference[];
57
+ /** save_scope_as / set_variable sites, indexed as definitions. */
58
+ implicitDefs: Definition[];
59
+ /** `namespace = x` declarations in this file. */
60
+ namespaces: string[];
61
+ }
62
+
63
+ export function extractReferences(
64
+ content: string,
65
+ file: string,
66
+ source: DefSource,
67
+ schema: SchemaData,
68
+ /** Engine-token test (script_docs names): call sites of engine effects/
69
+ * triggers are NOT indexed — they dominate large mods (AGOT: ~800k sites)
70
+ * and their docs come from tokens, not the index. Absent = index all. */
71
+ isEngineToken?: (name: string) => boolean
72
+ ): ExtractedRefs {
73
+ const { root } = parseScript(content);
74
+ const lines = new LineIndex(content);
75
+ const references: Reference[] = [];
76
+ const implicitDefs: Definition[] = [];
77
+ const namespaces: string[] = [];
78
+
79
+ const pushRef = (name: string, kinds: string[], startOffset: number, call?: boolean, chain?: string) => {
80
+ if (!NAME_OK.test(name) || NOT_A_NAME.has(name)) return;
81
+ const pos = lines.positionAt(startOffset);
82
+ const ref: Reference = {
83
+ name,
84
+ kinds,
85
+ file,
86
+ line: pos.line,
87
+ startChar: pos.character,
88
+ endChar: pos.character + name.length,
89
+ };
90
+ if (call) ref.call = true;
91
+ if (chain !== undefined) ref.chain = chain;
92
+ references.push(ref);
93
+ };
94
+
95
+ /** A scalar in value position: prefixed references and dotted chains. */
96
+ const scanValueScalar = (scalar: ScalarNode) => {
97
+ if (scalar.quoted) return;
98
+ const text = scalar.text;
99
+ const m = PREFIX_VALUE.exec(text);
100
+ if (!m) return;
101
+ const prefix = m[1];
102
+ let name = m[2];
103
+ // `culture:czech.some_link` — the reference is the segment before the dot.
104
+ const dot = name.indexOf(".");
105
+ if (dot >= 0) name = name.slice(0, dot);
106
+ const offset = scalar.range.start + prefix.length + 1;
107
+ if (VAR_PREFIXES.has(prefix)) {
108
+ pushRef(name, VAR_PREFIX_KINDS[prefix], offset);
109
+ } else if (prefix === "scope") {
110
+ pushRef(name, ["saved_scope"], offset);
111
+ } else {
112
+ const kinds = schema.prefixRefs[prefix];
113
+ if (kinds) pushRef(name, kinds, offset);
114
+ }
115
+ };
116
+
117
+ const topLevelName = (ancestors: readonly (AssignmentNode | BlockNode)[]): string | undefined => {
118
+ for (const a of ancestors) {
119
+ if (a.kind === "assignment" && !a.key.quoted) return a.key.text;
120
+ }
121
+ return undefined;
122
+ };
123
+
124
+ /** Enclosing key chain below the top-level definition, dotted, outermost
125
+ * first — the static-resolution input for resolveKeyChainScopes. */
126
+ const enclosingKeyChain = (ancestors: readonly (AssignmentNode | BlockNode)[]): string => {
127
+ const chainKeys: string[] = [];
128
+ let sawTopLevel = false;
129
+ for (const a of ancestors) {
130
+ if (a.kind !== "assignment") continue;
131
+ if (!sawTopLevel) {
132
+ sawTopLevel = true;
133
+ continue;
134
+ }
135
+ if (!a.key.quoted) chainKeys.push(a.key.text);
136
+ }
137
+ return chainKeys.join(".");
138
+ };
139
+
140
+ walkStatements(root, (stmt: Statement, ancestors) => {
141
+ if (stmt.kind === "value") {
142
+ if (stmt.value.kind === "scalar") scanValueScalar(stmt.value);
143
+ return;
144
+ }
145
+ // Assignment: the key itself may be a scope/var change (scope:x = { ... }).
146
+ if (!stmt.key.quoted) {
147
+ const keyText = stmt.key.text;
148
+ const km = PREFIX_VALUE.exec(keyText);
149
+ if (km) {
150
+ const prefix = km[1];
151
+ let name = km[2];
152
+ const dot = name.indexOf(".");
153
+ if (dot >= 0) name = name.slice(0, dot);
154
+ const offset = stmt.key.range.start + prefix.length + 1;
155
+ if (VAR_PREFIXES.has(prefix)) pushRef(name, VAR_PREFIX_KINDS[prefix], offset);
156
+ else if (prefix === "scope") pushRef(name, ["saved_scope"], offset);
157
+ }
158
+ }
159
+
160
+ const key = stmt.key.quoted ? null : stmt.key.text;
161
+ const value = stmt.value;
162
+
163
+ // namespace declarations
164
+ if (key === "namespace" && value?.kind === "scalar" && !value.quoted && ancestors.length === 0) {
165
+ namespaces.push(value.text);
166
+ return;
167
+ }
168
+
169
+ // save_scope_as = my_target → implicit definition. def.value carries a
170
+ // discriminated type hint for mod-wide saved-scope typing (varTypes.ts):
171
+ // "chain:<keys>" = resolve the save site's enclosing key chain statically;
172
+ // "type:value" = script-value math save (always a value scope).
173
+ if (key !== null && SAVE_SCOPE_KEYS.has(key) && value?.kind === "scalar" && !value.quoted) {
174
+ if (NAME_OK.test(value.text)) {
175
+ const def: Definition = {
176
+ name: value.text,
177
+ kind: "saved_scope",
178
+ file,
179
+ line: lines.positionAt(value.range.start).line,
180
+ source,
181
+ };
182
+ def.value =
183
+ key === "save_temporary_value_as" ? "type:value" : `chain:${enclosingKeyChain(ancestors)}`;
184
+ const container = topLevelName(ancestors);
185
+ if (container !== undefined) def.container = container;
186
+ implicitDefs.push(def);
187
+ }
188
+ return;
189
+ }
190
+
191
+ // save_scope_value_as = { name = X value = … } → implicit saved_scope
192
+ // definition (a value/boolean/flag scope, but referenced via scope:X all the same).
193
+ if (key !== null && SAVE_SCOPE_VALUE_KEYS.has(key) && value?.kind === "block") {
194
+ let nameScalar: ScalarNode | null = null;
195
+ let exprScalar: ScalarNode | null = null;
196
+ for (const s of value.statements) {
197
+ if (s.kind !== "assignment" || s.key.quoted || s.value?.kind !== "scalar") continue;
198
+ if (s.key.text === "name") nameScalar = s.value;
199
+ else if (s.key.text === "value") exprScalar = s.value;
200
+ }
201
+ if (nameScalar && !nameScalar.quoted && NAME_OK.test(nameScalar.text)) {
202
+ const def: Definition = {
203
+ name: nameScalar.text,
204
+ kind: "saved_scope",
205
+ file,
206
+ line: lines.positionAt(nameScalar.range.start).line,
207
+ source,
208
+ };
209
+ // "expr:<expr>" — typed by resolving the value expression statically.
210
+ if (exprScalar && !exprScalar.quoted) def.value = `expr:${exprScalar.text}`;
211
+ const container = topLevelName(ancestors);
212
+ if (container !== undefined) def.container = container;
213
+ implicitDefs.push(def);
214
+ }
215
+ return;
216
+ }
217
+
218
+ // add_character_flag = x / add_*_flag = { flag = x } and add_to_list = x
219
+ // → implicit definitions, referenced by has_*_flag / is_in_list.
220
+ if (key !== null && value && (FLAG_SET_KEY.test(key) || LIST_SET_KEYS.has(key))) {
221
+ const kind = LIST_SET_KEYS.has(key) ? "list" : "flag";
222
+ let nameScalar: ScalarNode | null = null;
223
+ if (value.kind === "scalar" && !value.quoted) nameScalar = value;
224
+ else if (value.kind === "block" && kind === "flag") {
225
+ for (const s of value.statements) {
226
+ if (
227
+ s.kind === "assignment" &&
228
+ !s.key.quoted &&
229
+ s.key.text === "flag" &&
230
+ s.value?.kind === "scalar"
231
+ ) {
232
+ nameScalar = s.value;
233
+ break;
234
+ }
235
+ }
236
+ }
237
+ if (
238
+ nameScalar &&
239
+ !nameScalar.quoted &&
240
+ NAME_OK.test(nameScalar.text) &&
241
+ !nameScalar.text.includes(":")
242
+ ) {
243
+ const def: Definition = {
244
+ name: nameScalar.text,
245
+ kind,
246
+ file,
247
+ line: lines.positionAt(nameScalar.range.start).line,
248
+ source,
249
+ };
250
+ // List set-sites carry the enclosing key chain (below the top-level
251
+ // definition, outermost first) so the item scope can be resolved
252
+ // statically mod-wide (resolveKeyChainScopes in scopes/inference.ts).
253
+ if (kind === "list") def.value = enclosingKeyChain(ancestors);
254
+ const container = topLevelName(ancestors);
255
+ if (container !== undefined) def.container = container;
256
+ implicitDefs.push(def);
257
+ }
258
+ return;
259
+ }
260
+
261
+ // set_variable = { name = x value = y } / set_variable = x → implicit
262
+ // definition(s) in the right namespace; list set-sites dual-index (see
263
+ // VARIABLE_SET_KINDS docs in the shared schema).
264
+ const setKind = key !== null ? (VARIABLE_SET_KINDS[key] ?? VARIABLE_LIST_SET_KINDS[key]) : undefined;
265
+ if (key !== null && setKind && value) {
266
+ let nameScalar: ScalarNode | null = null;
267
+ let exprScalar: ScalarNode | null = null;
268
+ if (value.kind === "scalar" && !value.quoted) nameScalar = value;
269
+ else if (value.kind === "block") {
270
+ for (const s of value.statements) {
271
+ if (s.kind !== "assignment" || s.key.quoted || s.value?.kind !== "scalar") continue;
272
+ if (s.key.text === "name") nameScalar = s.value;
273
+ // set_variable stores `value = …`; add_to_*_variable_list stores `target = …`.
274
+ else if (s.key.text === "value" || s.key.text === "target") exprScalar = s.value;
275
+ }
276
+ }
277
+ if (
278
+ nameScalar &&
279
+ NAME_OK.test(nameScalar.text) &&
280
+ !nameScalar.quoted &&
281
+ !nameScalar.text.includes(":")
282
+ ) {
283
+ const line = lines.positionAt(nameScalar.range.start).line;
284
+ const container = topLevelName(ancestors);
285
+ // Only true set-sites carry the value expression (change_* holds a delta).
286
+ const expr = key.startsWith("set_") || key.startsWith("add_to_") ? exprScalar : null;
287
+ const kinds = VARIABLE_LIST_SET_KINDS[key]
288
+ ? [VARIABLE_LIST_SET_KINDS[key], VARIABLE_LIST_SET_KINDS[key].replace("_list", "")]
289
+ : [setKind];
290
+ for (let i = 0; i < kinds.length; i++) {
291
+ const def: Definition = { name: nameScalar.text, kind: kinds[i], file, line, source };
292
+ // Expr only on the primary kind: the dual-indexed base-kind def of a
293
+ // LIST set-site must not type `var:x` with the list's ITEM type.
294
+ if (i === 0 && expr && !expr.quoted) def.value = expr.text;
295
+ if (container !== undefined) def.container = container;
296
+ implicitDefs.push(def);
297
+ }
298
+ }
299
+ return;
300
+ }
301
+
302
+ // has_variable = x / is_target_in_variable_list = { name = x … } → references.
303
+ const readKind = key !== null ? VARIABLE_READ_KINDS[key] : undefined;
304
+ if (key !== null && readKind && value) {
305
+ let nameScalar: ScalarNode | null = null;
306
+ if (value.kind === "scalar" && !value.quoted) nameScalar = value;
307
+ else if (value.kind === "block") {
308
+ for (const s of value.statements) {
309
+ if (
310
+ s.kind === "assignment" &&
311
+ !s.key.quoted &&
312
+ s.key.text === "name" &&
313
+ s.value?.kind === "scalar"
314
+ ) {
315
+ nameScalar = s.value;
316
+ break;
317
+ }
318
+ }
319
+ }
320
+ if (
321
+ nameScalar &&
322
+ !nameScalar.quoted &&
323
+ !nameScalar.text.includes(":") &&
324
+ !nameScalar.text.includes("$")
325
+ ) {
326
+ // List reads accept only lists; scalar reads accept both (has_variable
327
+ // is true for a list variable).
328
+ const kinds = readKind.endsWith("_list") ? [readKind] : [readKind, `${readKind}_list`];
329
+ pushRef(nameScalar.text, kinds, nameScalar.range.start);
330
+ }
331
+ return;
332
+ }
333
+
334
+ // Key-position calls (`my_effect = yes`, `my_trigger = { ... }`): indexed
335
+ // as call references so find-references and rename cover call sites, which
336
+ // is where scripted effects/triggers/modifiers are actually used. Grammar
337
+ // keywords and iterator-shaped keys are excluded via isStructuralKeyword —
338
+ // NOT classifyKeyword, whose X_trigger/on_X convention buckets would drop
339
+ // the call sites of conventionally named scripted triggers (#5); engine
340
+ // tokens are filtered by the caller (memory: they dominate large mods).
341
+ // Top-level keys are definitions, not calls. Excluded from the usage-count
342
+ // ranking signal via the `call` flag.
343
+ if (
344
+ key !== null &&
345
+ value != null &&
346
+ ancestors.length > 0 &&
347
+ !key.includes(".") &&
348
+ !key.includes(":") &&
349
+ !key.includes("$") &&
350
+ !isStructuralKeyword(key) &&
351
+ !schema.refFields.has(key) &&
352
+ !isEngineToken?.(key)
353
+ ) {
354
+ // The chain lets call-site scope aggregation type the CALLED definition.
355
+ pushRef(key, CALL_KINDS, stmt.key.range.start, true, enclosingKeyChain(ancestors));
356
+ }
357
+
358
+ if (value?.kind === "scalar") {
359
+ scanValueScalar(value);
360
+ if (key !== null && !value.quoted) {
361
+ // Schema ref fields (scalar form), plus pattern families (flags, lists).
362
+ const field = schema.refFields.get(key);
363
+ if (field && field.form !== "list" && !value.text.includes(":")) {
364
+ pushRef(value.text, field.kinds, value.range.start);
365
+ } else if (!field && !value.text.includes(":")) {
366
+ const kinds = dynamicRefKinds(key);
367
+ if (kinds) {
368
+ pushRef(value.text, kinds, value.range.start);
369
+ } else if (!value.text.includes("$")) {
370
+ // Keys too generic for a global ref field, resolved by their
371
+ // enclosing block (BLOCK_REF_FIELDS): `variable` / `list` in an
372
+ // in-list iterator, `id` in `trigger_event = { … }` (the delayed
373
+ // form the event graph needs), `reference` in override_background.
374
+ for (let i = ancestors.length - 1; i >= 0; i--) {
375
+ const a = ancestors[i];
376
+ if (a.kind === "assignment" && !a.key.quoted) {
377
+ const blockKinds = activeProfile().blockRefFields[a.key.text.toLowerCase()]?.[key];
378
+ if (blockKinds) pushRef(value.text, blockKinds, value.range.start);
379
+ break;
380
+ }
381
+ }
382
+ }
383
+ }
384
+ }
385
+ // Loc-key-valued properties (both `desc = key` and `desc = "key"`).
386
+ if (key !== null && isLocProperty(key) && NAME_OK.test(value.text)) {
387
+ pushRef(value.text, ["loc_key"], value.range.start + (value.quoted ? 1 : 0));
388
+ }
389
+ } else if (value?.kind === "block" && key !== null) {
390
+ const field = schema.refFields.get(key);
391
+ if (field && field.form !== "scalar") {
392
+ for (const s of value.statements) {
393
+ if (
394
+ s.kind === "value" &&
395
+ s.value.kind === "scalar" &&
396
+ !s.value.quoted &&
397
+ !s.value.text.includes(":")
398
+ ) {
399
+ pushRef(s.value.text, field.kinds, s.value.range.start);
400
+ } else if (
401
+ // Weighted entries: `random_events = { 100 = my.event }`. Only on
402
+ // fields the schema marks weighted; elsewhere a numeric key is a
403
+ // map entry, not a reference.
404
+ field.weighted === true &&
405
+ s.kind === "assignment" &&
406
+ /^\d+$/.test(s.key.text) &&
407
+ s.value?.kind === "scalar" &&
408
+ !s.value.quoted &&
409
+ !s.value.text.includes(":")
410
+ ) {
411
+ pushRef(s.value.text, field.kinds, s.value.range.start);
412
+ }
413
+ }
414
+ }
415
+ }
416
+ });
417
+
418
+ // Every string above is a slice of `content`, and a SlicedString pins its
419
+ // parent: without this the index retains a copy of every file it read (§C2).
420
+ shareReferenceStrings(references);
421
+ shareDefinitionStrings(implicitDefs);
422
+ for (let i = 0; i < namespaces.length; i++) namespaces[i] = intern(namespaces[i]);
423
+ return { references, implicitDefs, namespaces };
424
+ }
425
+
426
+ // ---------------------------------------------------------------------------
427
+
428
+ export class ReferenceIndex {
429
+ private byName = new Map<string, Reference[]>();
430
+ private byFile = new Map<string, Reference[]>();
431
+ /** Per-name count of non-call references (the §C2 ranking signal). */
432
+ private rankCounts = new Map<string, number>();
433
+ revision = 0;
434
+
435
+ addAll(refs: Reference[]): void {
436
+ for (const ref of refs) {
437
+ let list = this.byName.get(ref.name);
438
+ if (!list) this.byName.set(ref.name, (list = []));
439
+ list.push(ref);
440
+ if (!ref.call) this.rankCounts.set(ref.name, (this.rankCounts.get(ref.name) ?? 0) + 1);
441
+ const fkey = normFile(ref.file);
442
+ let flist = this.byFile.get(fkey);
443
+ if (!flist) this.byFile.set(fkey, (flist = []));
444
+ flist.push(ref);
445
+ }
446
+ if (refs.length > 0) this.revision++;
447
+ }
448
+
449
+ /**
450
+ * Drop every reference a file contributed.
451
+ *
452
+ * Grouped by name first (perf campaign §B2): a file that names the same
453
+ * token 25 times used to rebuild that token's whole reference list 25 times.
454
+ * On the AGOT-sized workspace one save of a small event file spent 490-510 ms
455
+ * here — the entire remaining cost of the save — copying and discarding
456
+ * megabytes of arrays for names with six-figure usage counts.
457
+ */
458
+ removeFile(file: string): void {
459
+ const fkey = normFile(file);
460
+ const refs = this.byFile.get(fkey);
461
+ if (!refs) return;
462
+ this.byFile.delete(fkey);
463
+ const dropped = new Map<string, Set<Reference>>();
464
+ for (const ref of refs) {
465
+ let set = dropped.get(ref.name);
466
+ if (!set) dropped.set(ref.name, (set = new Set()));
467
+ set.add(ref);
468
+ if (!ref.call) {
469
+ const n = (this.rankCounts.get(ref.name) ?? 0) - 1;
470
+ if (n <= 0) this.rankCounts.delete(ref.name);
471
+ else this.rankCounts.set(ref.name, n);
472
+ }
473
+ }
474
+ for (const [name, set] of dropped) {
475
+ const list = this.byName.get(name);
476
+ if (!list) continue;
477
+ const filtered = list.filter((r) => !set.has(r));
478
+ if (filtered.length === 0) this.byName.delete(name);
479
+ else this.byName.set(name, filtered);
480
+ }
481
+ this.revision++;
482
+ }
483
+
484
+ lookup(name: string): Reference[] {
485
+ return this.byName.get(name) ?? [];
486
+ }
487
+
488
+ /**
489
+ * How many times `name` is used across mod files (workspace usage count for
490
+ * ranking, §C2). O(1) map hit. Call sites are excluded so the ranking signal
491
+ * is unchanged by call-reference indexing; use `lookup(name).length` for the
492
+ * user-facing "N references" count.
493
+ */
494
+ usageCount(name: string): number {
495
+ return this.rankCounts.get(name) ?? 0;
496
+ }
497
+
498
+ /** All references in a file. */
499
+ inFile(file: string): Reference[] {
500
+ return this.byFile.get(normFile(file)) ?? [];
501
+ }
502
+
503
+ /** Iterate every reference (used by coverage/graph computations). */
504
+ *all(): IterableIterator<Reference> {
505
+ for (const list of this.byFile.values()) yield* list;
506
+ }
507
+
508
+ /** All references that may target the given kind. */
509
+ allOfKind(kind: string): Reference[] {
510
+ const out: Reference[] = [];
511
+ for (const list of this.byFile.values()) {
512
+ for (const ref of list) if (ref.kinds.includes(kind)) out.push(ref);
513
+ }
514
+ return out;
515
+ }
516
+
517
+ get size(): number {
518
+ let n = 0;
519
+ for (const list of this.byFile.values()) n += list.length;
520
+ return n;
521
+ }
522
+
523
+ clear(): void {
524
+ this.byName.clear();
525
+ this.byFile.clear();
526
+ this.rankCounts.clear();
527
+ this.revision++;
528
+ }
529
+ }
530
+
531
+ function normFile(file: string): string {
532
+ const n = file.replace(/\//g, "\\");
533
+ return process.platform === "win32" ? n.toLowerCase() : file;
534
+ }