@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,214 @@
1
+ /**
2
+ * paradox/eventVocabulary: what an event editor is allowed to offer.
3
+ *
4
+ * The graph inspector guides a new modder with dropdowns, and a dropdown is
5
+ * only as honest as its source. So every list here is derived from data the
6
+ * server already holds:
7
+ *
8
+ * keys the active profile's structure table (harvested `_*.info`),
9
+ * ordered by the usage counts it carries
10
+ * value sets a `values: "enum:a|b|c"` spec, or the schema's reference
11
+ * fields resolved through the definition index (`theme` ->
12
+ * every indexed event_theme, mod entries first)
13
+ * effects script_docs tokens (the user's own logs, or the bundled
14
+ * triggers wiki fallback), with the log's own description
15
+ * saved scopes the mod's own `save_scope_as` sites, from the index
16
+ *
17
+ * Nothing is hand-written, so a game patch that adds a theme or renames an
18
+ * effect changes the dropdown without a release. No `vscode` imports.
19
+ */
20
+ import {
21
+ EVENT_VOCABULARY_MAX_TOKENS,
22
+ EVENT_VOCABULARY_MAX_VALUES,
23
+ type EventValueOptionsResult,
24
+ type EventVocabularyItem,
25
+ type EventVocabularyResult,
26
+ } from "@px-lsp/protocol/protocol";
27
+ import type { ServerData } from "../serverData";
28
+ import type { SchemaData } from "../schema/loader";
29
+ import type { KeySpec } from "../schema/types";
30
+
31
+ /** One line is what a menu row shows; the rest is noise at that size. */
32
+ const MAX_DOC = 220;
33
+
34
+ function short(doc: string | undefined): string | undefined {
35
+ if (!doc) return undefined;
36
+ const oneLine = doc.replace(/\s+/g, " ").trim();
37
+ if (oneLine === "") return undefined;
38
+ return oneLine.length > MAX_DOC ? oneLine.slice(0, MAX_DOC - 1) + "…" : oneLine;
39
+ }
40
+
41
+ /** Structure keys of one block, most used first (freq is the harvest's count). */
42
+ function keyItems(specs: Map<string, KeySpec> | undefined): EventVocabularyItem[] {
43
+ if (!specs) return [];
44
+ const out = [...specs.values()].sort((a, b) => (b.freq ?? 0) - (a.freq ?? 0));
45
+ return out.map((spec) => ({
46
+ value: spec.key,
47
+ doc: short(spec.doc),
48
+ hint:
49
+ spec.values === "block"
50
+ ? "block"
51
+ : spec.values === "bool"
52
+ ? "yes/no"
53
+ : spec.values === "loc"
54
+ ? "loc"
55
+ : undefined,
56
+ }));
57
+ }
58
+
59
+ export function computeEventVocabulary(
60
+ data: ServerData,
61
+ schema: SchemaData,
62
+ inFocus: (file: string) => boolean = () => true
63
+ ): EventVocabularyResult {
64
+ const blocks = schema.structures.keysByKindBlock.get("event");
65
+ const eventKeys = keyItems(blocks?.get(""));
66
+ const optionKeys = keyItems(blocks?.get("option"));
67
+
68
+ // Value sets, for the keys that have one. Two sources, in this order: a
69
+ // declared enumeration wins (it is the exact vocabulary), otherwise the
70
+ // schema's reference field names the definition kinds the index can list.
71
+ const values: Record<string, EventVocabularyItem[]> = {};
72
+ const byKind = new Map<string, EventVocabularyItem[]>();
73
+ const definitionsOf = (kind: string): EventVocabularyItem[] => {
74
+ let cached = byKind.get(kind);
75
+ if (cached) return cached;
76
+ const seen = new Set<string>();
77
+ const mod: EventVocabularyItem[] = [];
78
+ const rest: EventVocabularyItem[] = [];
79
+ for (const def of data.index.allDefinitions()) {
80
+ if (def.kind !== kind || seen.has(def.name)) continue;
81
+ if (def.source === "mod" && !inFocus(def.file)) continue;
82
+ seen.add(def.name);
83
+ (def.source === "mod" ? mod : rest).push({
84
+ value: def.name,
85
+ doc: short(def.doc),
86
+ hint: def.source === "mod" ? "this mod" : def.source,
87
+ });
88
+ }
89
+ mod.sort((a, b) => a.value.localeCompare(b.value));
90
+ rest.sort((a, b) => a.value.localeCompare(b.value));
91
+ cached = [...mod, ...rest].slice(0, EVENT_VOCABULARY_MAX_VALUES);
92
+ byKind.set(kind, cached);
93
+ return cached;
94
+ };
95
+
96
+ for (const specs of [blocks?.get(""), blocks?.get("option")]) {
97
+ for (const spec of specs?.values() ?? []) {
98
+ if (values[spec.key]) continue;
99
+ if (spec.values?.startsWith("enum:")) {
100
+ values[spec.key] = spec.values
101
+ .slice(5)
102
+ .split("|")
103
+ .filter((v) => v !== "")
104
+ .map((v) => ({ value: v }));
105
+ continue;
106
+ }
107
+ if (spec.values === "bool") {
108
+ values[spec.key] = [{ value: "yes" }, { value: "no" }];
109
+ continue;
110
+ }
111
+ const field = schema.refFields.get(spec.key);
112
+ if (!field) continue;
113
+ const items: EventVocabularyItem[] = [];
114
+ for (const kind of field.kinds) items.push(...definitionsOf(kind));
115
+ if (items.length > 0) values[spec.key] = items.slice(0, EVENT_VOCABULARY_MAX_VALUES);
116
+ }
117
+ }
118
+
119
+ // Engine tokens. `data.tokens` is the parsed script_docs (or the bundled wiki
120
+ // baseline); the reference index's non-call usage counts order them, so the
121
+ // effects a real corpus writes come first instead of the alphabet.
122
+ const effects: EventVocabularyItem[] = [];
123
+ const triggers: EventVocabularyItem[] = [];
124
+ const seenToken = new Set<string>();
125
+ const ranked = [...data.tokens].sort(
126
+ (a, b) => data.refIndex.usageCount(b.name) - data.refIndex.usageCount(a.name)
127
+ );
128
+ for (const token of ranked) {
129
+ const bucket = token.kind === "effect" ? effects : token.kind === "trigger" ? triggers : null;
130
+ if (!bucket || bucket.length >= EVENT_VOCABULARY_MAX_TOKENS) continue;
131
+ const key = `${token.kind}:${token.name}`;
132
+ if (seenToken.has(key)) continue;
133
+ seenToken.add(key);
134
+ bucket.push({
135
+ value: token.name,
136
+ doc: short(token.doc),
137
+ hint: token.scopes.length > 0 ? token.scopes.slice(0, 3).join(", ") : undefined,
138
+ });
139
+ }
140
+
141
+ const savedScopes: EventVocabularyItem[] = [];
142
+ const seenScope = new Set<string>();
143
+ for (const def of data.index.allDefinitions()) {
144
+ if (def.kind !== "saved_scope" || seenScope.has(def.name)) continue;
145
+ if (!inFocus(def.file)) continue;
146
+ seenScope.add(def.name);
147
+ savedScopes.push({ value: def.name, hint: def.container });
148
+ }
149
+ savedScopes.sort((a, b) => a.value.localeCompare(b.value));
150
+
151
+ return {
152
+ eventKeys,
153
+ optionKeys,
154
+ values,
155
+ effects,
156
+ triggers,
157
+ savedScopes: savedScopes.slice(0, EVENT_VOCABULARY_MAX_VALUES),
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Kinds whose full listing is not a useful dropdown: sites rather than
163
+ * choices (loc keys, saved scopes, variables), bodies rather than values
164
+ * (scripted effects/triggers), and the id spaces so large a capped list
165
+ * would silently lie (events, on_actions, decisions — those get inline
166
+ * completions instead).
167
+ */
168
+ const VALUE_KIND_SKIP = new Set([
169
+ "event",
170
+ "on_action",
171
+ "decision",
172
+ "loc_key",
173
+ "saved_scope",
174
+ "variable",
175
+ "scripted_effect",
176
+ "scripted_trigger",
177
+ "script_value",
178
+ ]);
179
+
180
+ /**
181
+ * The value set `value` belongs to: resolve it through the definition index
182
+ * (`secret_cultivator` is a `secret`), then list every definition of that
183
+ * kind, mod entries first. Null when the value resolves to nothing
184
+ * enumerable — a number, yes/no, a scope chain, an unindexed name.
185
+ */
186
+ export function computeValueOptions(
187
+ data: ServerData,
188
+ value: string,
189
+ inFocus: (file: string) => boolean = () => true
190
+ ): EventValueOptionsResult | null {
191
+ const name = value.trim();
192
+ if (name === "" || name === "yes" || name === "no") return null;
193
+ if (/^-?[\d.]+$/.test(name) || name.includes(":") || name.includes(".")) return null;
194
+ const def = data.index.lookup(name).find((d) => !VALUE_KIND_SKIP.has(d.kind));
195
+ if (!def) return null;
196
+ const seen = new Set<string>();
197
+ const mod: EventVocabularyItem[] = [];
198
+ const rest: EventVocabularyItem[] = [];
199
+ for (const d of data.index.allDefinitions()) {
200
+ if (d.kind !== def.kind || seen.has(d.name)) continue;
201
+ if (d.source === "mod" && !inFocus(d.file)) continue;
202
+ seen.add(d.name);
203
+ (d.source === "mod" ? mod : rest).push({
204
+ value: d.name,
205
+ doc: short(d.doc),
206
+ hint: d.source === "mod" ? "this mod" : d.source,
207
+ });
208
+ }
209
+ mod.sort((a, b) => a.value.localeCompare(b.value));
210
+ rest.sort((a, b) => a.value.localeCompare(b.value));
211
+ const items = [...mod, ...rest].slice(0, EVENT_VOCABULARY_MAX_VALUES);
212
+ // A single entry (the value itself) enumerates nothing worth a menu.
213
+ return items.length > 1 ? { kind: def.kind, items } : null;
214
+ }
@@ -0,0 +1,138 @@
1
+ /**
2
+ * paradox/locCoverage: per-language localization health for the mod —
3
+ * referenced-but-missing keys, defined-but-orphaned keys, and untranslated
4
+ * keys (value identical to the source language). Feeds the coverage tree and
5
+ * the translation workflow.
6
+ */
7
+ import * as path from "path";
8
+ import * as fs from "fs";
9
+ import type { LocCoverage, LocIssue } from "@px-lsp/protocol/protocol";
10
+ import type { SchemaEntry } from "../schema/types";
11
+ import { listFiles } from "@px-lsp/protocol/fsWalk";
12
+ import { detectLocFileLanguage } from "@px-lsp/protocol/translationCore";
13
+ import { parseLoc } from "../parser";
14
+ import type { ServerData } from "../serverData";
15
+
16
+ const ISSUE_CAP = 500;
17
+
18
+ interface LocEntrySite {
19
+ key: string;
20
+ value: string;
21
+ file: string;
22
+ line: number;
23
+ }
24
+
25
+ /** The mod-relative localization roots, from the profile's loc_key schema entries
26
+ * (some games nest localization under a load-stage folder rather than the top level). */
27
+ function locRoots(schemaEntries: SchemaEntry[]): string[] {
28
+ const roots = schemaEntries.filter((e) => e.kind === "loc_key").map((e) => e.path);
29
+ return roots.length > 0 ? [...new Set(roots)] : ["localization"];
30
+ }
31
+
32
+ /** All loc entries in the mod, grouped per language (read fresh — mods are small). */
33
+ function modLocByLanguage(
34
+ modPath: string,
35
+ schemaEntries: SchemaEntry[]
36
+ ): Map<string, Map<string, LocEntrySite>> {
37
+ const byLang = new Map<string, Map<string, LocEntrySite>>();
38
+ for (const root of locRoots(schemaEntries)) {
39
+ const locDir = path.join(modPath, root);
40
+ for (const file of listFiles(locDir, ".yml")) {
41
+ const lang = detectLocFileLanguage(file);
42
+ if (!lang) continue;
43
+ let content: string;
44
+ try {
45
+ content = fs.readFileSync(file, "utf8");
46
+ } catch {
47
+ continue;
48
+ }
49
+ let entries = byLang.get(lang);
50
+ if (!entries) byLang.set(lang, (entries = new Map()));
51
+ for (const e of parseLoc(content).entries) {
52
+ entries.set(e.key, { key: e.key, value: e.value, file, line: e.line });
53
+ }
54
+ }
55
+ }
56
+ return byLang;
57
+ }
58
+
59
+ export function computeLocCoverage(
60
+ data: ServerData,
61
+ modPath: string | null,
62
+ sourceLanguage: string,
63
+ schemaEntries: SchemaEntry[],
64
+ inFocus: (file: string) => boolean = () => true
65
+ ): LocCoverage[] {
66
+ if (!modPath) return [];
67
+ const byLang = modLocByLanguage(modPath, schemaEntries);
68
+ if (byLang.size === 0) return [];
69
+
70
+ // Keys this mod's script uses (recorded loc references), plus schema-required
71
+ // keys — both restricted to the focus mod's own files in multi-mod workspaces.
72
+ const referenced = new Map<string, { file: string; line: number }>();
73
+ for (const ref of data.refIndex.allOfKind("loc_key")) {
74
+ if (!inFocus(ref.file)) continue;
75
+ if (!referenced.has(ref.name)) referenced.set(ref.name, { file: ref.file, line: ref.line });
76
+ }
77
+ const requiredByKind = new Map<string, string[]>();
78
+ for (const e of schemaEntries) {
79
+ if (e.requiredLoc && e.requiredLoc.length > 0) requiredByKind.set(e.kind, e.requiredLoc);
80
+ }
81
+ for (const def of data.index.allDefinitions()) {
82
+ if (def.source !== "mod" || !inFocus(def.file)) continue;
83
+ const patterns = requiredByKind.get(def.kind);
84
+ if (!patterns) continue;
85
+ for (const p of patterns) {
86
+ const key = p.replace(/\$/g, def.name);
87
+ if (!referenced.has(key)) referenced.set(key, { file: def.file, line: def.line });
88
+ }
89
+ }
90
+
91
+ /** Key defined outside this mod (vanilla, parents, other workspace mods, in
92
+ * the configured language index)? Then it's an override/covered. */
93
+ const inheritedLoc = (key: string): boolean =>
94
+ data.index.lookupAll(key).some((d) => d.kind === "loc_key" && (d.source !== "mod" || !inFocus(d.file)));
95
+
96
+ const source = byLang.get(sourceLanguage) ?? new Map<string, LocEntrySite>();
97
+ const result: LocCoverage[] = [];
98
+
99
+ for (const [language, entries] of [...byLang.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
100
+ const missing: LocIssue[] = [];
101
+ for (const [key, site] of referenced) {
102
+ if (entries.has(key)) continue;
103
+ if (inheritedLoc(key)) continue;
104
+ if (missing.length >= ISSUE_CAP) break;
105
+ missing.push({ key, file: site.file, line: site.line });
106
+ }
107
+
108
+ const orphaned: LocIssue[] = [];
109
+ for (const [key, site] of entries) {
110
+ if (referenced.has(key)) continue;
111
+ if (inheritedLoc(key)) continue; // overriding vanilla text is intentional
112
+ if (data.refIndex.lookup(key).length > 0) continue; // referenced under a non-loc kind
113
+ if (orphaned.length >= ISSUE_CAP) break;
114
+ orphaned.push({ key, file: site.file, line: site.line });
115
+ }
116
+
117
+ const untranslated: LocIssue[] = [];
118
+ if (language !== sourceLanguage) {
119
+ for (const [key, site] of entries) {
120
+ const src = source.get(key);
121
+ if (!src) continue;
122
+ // Untranslated = still the source text verbatim, OR a blank value
123
+ // (the translation scaffold blanks values so nothing fake ships).
124
+ const isCopy = src.value === site.value && site.value.trim() !== "";
125
+ const isBlank = site.value.trim() === "";
126
+ if (!isCopy && !isBlank) continue;
127
+ if (untranslated.length >= ISSUE_CAP) break;
128
+ untranslated.push({ key, file: site.file, line: site.line, value: src.value });
129
+ }
130
+ }
131
+
132
+ missing.sort((a, b) => a.key.localeCompare(b.key));
133
+ orphaned.sort((a, b) => a.key.localeCompare(b.key));
134
+ untranslated.sort((a, b) => a.key.localeCompare(b.key));
135
+ result.push({ language, defined: entries.size, missing, orphaned, untranslated });
136
+ }
137
+ return result;
138
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * paradox/modOverview: the mod's content inventory by kind, for the Mod Overview
3
+ * tree. Definition lists are capped per kind; counts stay exact.
4
+ */
5
+ import type { ModOverview, OverviewKind } from "@px-lsp/protocol/protocol";
6
+ import type { ServerData } from "../serverData";
7
+
8
+ const DEFS_CAP = 500;
9
+
10
+ export function computeModOverview(
11
+ data: ServerData,
12
+ inFocus: (file: string) => boolean = () => true
13
+ ): ModOverview {
14
+ const byKind = new Map<string, OverviewKind>();
15
+ let total = 0;
16
+ for (const def of data.index.allDefinitions()) {
17
+ if (def.source !== "mod" || !inFocus(def.file)) continue;
18
+ total++;
19
+ let bucket = byKind.get(def.kind);
20
+ if (!bucket) byKind.set(def.kind, (bucket = { kind: def.kind, count: 0, defs: [] }));
21
+ bucket.count++;
22
+ if (bucket.defs.length < DEFS_CAP) {
23
+ bucket.defs.push({ name: def.name, file: def.file, line: def.line });
24
+ }
25
+ }
26
+ const kinds = [...byKind.values()].sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind));
27
+ for (const k of kinds) k.defs.sort((a, b) => a.name.localeCompare(b.name));
28
+ return { kinds, totalDefs: total, totalRefs: data.refIndex.size };
29
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * paradox/overrides: mod definitions that shadow vanilla or parent-mod content,
3
+ * annotated with the folder's load rule — script folders are last-in-or-same
4
+ * ("LIOS", the mod wins), GUI is first-in-only ("FIOS", vanilla wins unless
5
+ * the mod replaces the whole file at the same relative path). This is
6
+ * database_conflicts.log brought into the editor, before launch.
7
+ *
8
+ * Multi-mod workspaces: definitions of OTHER workspace mods count as shadowed
9
+ * sites too (labeled with their mod's name); between two enabled mods the
10
+ * launcher's load order decides, which we cannot know — the note says so.
11
+ */
12
+ import type { OverrideInfo } from "@px-lsp/protocol/protocol";
13
+ import type { ServerData } from "../serverData";
14
+
15
+ const CAP = 2000;
16
+
17
+ /** Kinds whose files load first-in-only. */
18
+ const FIOS_KINDS = new Set(["gui_type"]);
19
+
20
+ function relUnder(root: string, file: string): string | null {
21
+ const lower = file.toLowerCase().replace(/\\/g, "/");
22
+ const rootLower = root.toLowerCase().replace(/\\/g, "/").replace(/\/+$/, "");
23
+ return lower.startsWith(rootLower + "/") ? lower.slice(rootLower.length + 1) : null;
24
+ }
25
+
26
+ export function computeOverrides(
27
+ data: ServerData,
28
+ gamePath: string | null,
29
+ inFocus: (file: string) => boolean = () => true
30
+ ): OverrideInfo[] {
31
+ const out: OverrideInfo[] = [];
32
+ const seen = new Set<string>();
33
+
34
+ for (const def of data.index.allDefinitions()) {
35
+ if (def.source !== "mod" || !inFocus(def.file)) continue;
36
+ const ownRoot = data.modRootOf(def.file);
37
+ const dedupe = `${ownRoot ?? ""}:${def.kind}:${def.name}`;
38
+ if (seen.has(dedupe)) continue;
39
+ const shadowed = data.index
40
+ .lookupAll(def.name)
41
+ .filter(
42
+ (d) => d.kind === def.kind && d !== def && (d.source !== "mod" || data.modRootOf(d.file) !== ownRoot)
43
+ );
44
+ if (shadowed.length === 0) continue;
45
+ seen.add(dedupe);
46
+
47
+ const rule = FIOS_KINDS.has(def.kind) ? "FIOS" : "LIOS";
48
+ let winner: "mod" | "other" = rule === "LIOS" ? "mod" : "other";
49
+ let note: string | undefined;
50
+ if (rule === "FIOS") {
51
+ // Whole-file replacement at the same relative path still wins.
52
+ const modRel = ownRoot ? relUnder(ownRoot, def.file) : null;
53
+ const replacesFile =
54
+ modRel !== null && gamePath !== null && shadowed.some((s) => relUnder(gamePath, s.file) === modRel);
55
+ if (replacesFile) {
56
+ winner = "mod";
57
+ note = "whole-file replacement (same relative path)";
58
+ } else {
59
+ note = "GUI loads first-in-only: the vanilla definition wins unless the mod replaces the whole file";
60
+ }
61
+ } else if (shadowed.some((s) => s.source === "mod")) {
62
+ note = "another enabled mod defines this too; the launcher load order decides which wins";
63
+ }
64
+
65
+ out.push({
66
+ name: def.name,
67
+ kind: def.kind,
68
+ mod: { source: "mod", label: data.originLabel(def), file: def.file, line: def.line },
69
+ shadowed: shadowed.map((s) => ({
70
+ source:
71
+ s.source === "mod"
72
+ ? ("mod" as const)
73
+ : s.source === "parent"
74
+ ? ("parent" as const)
75
+ : ("vanilla" as const),
76
+ label: data.originLabel(s),
77
+ file: s.file,
78
+ line: s.line,
79
+ })),
80
+ rule,
81
+ winner,
82
+ note,
83
+ });
84
+ if (out.length >= CAP) break;
85
+ }
86
+
87
+ out.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
88
+ return out;
89
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Per-document parse cache: one CST (or loc parse) per open document version,
3
+ * so completion, symbols, folding, diagnostics and semantic tokens share a
4
+ * single parse per edit instead of each re-scanning the text.
5
+ *
6
+ * Keyed by document version (equivalent to a content hash for open documents,
7
+ * and cheaper). Entries are evicted when the document closes.
8
+ */
9
+ import type { TextDocument } from "vscode-languageserver-textdocument";
10
+ import { LineIndex, parseLoc, parseScript, type LocParseResult, type ParseResult } from "./parser";
11
+ import { collectSavedScopeTypes, type InferenceContext } from "./scopes/inference";
12
+ import type { Scope, ScopeModel } from "./scopes/model";
13
+
14
+ export interface CachedParse {
15
+ version: number;
16
+ result: ParseResult;
17
+ lineIndex: LineIndex;
18
+ /** Lazily computed save_scope_as → scope types for this document version. */
19
+ savedScopes?: Map<string, Set<Scope> | null>;
20
+ }
21
+
22
+ export interface CachedLocParse {
23
+ version: number;
24
+ result: LocParseResult;
25
+ lineIndex: LineIndex;
26
+ }
27
+
28
+ const scriptCache = new Map<string, CachedParse>();
29
+ const locCache = new Map<string, CachedLocParse>();
30
+
31
+ export function getParse(document: TextDocument): CachedParse {
32
+ const cached = scriptCache.get(document.uri);
33
+ if (cached && cached.version === document.version) return cached;
34
+ const text = document.getText();
35
+ const entry: CachedParse = {
36
+ version: document.version,
37
+ result: parseScript(text),
38
+ lineIndex: new LineIndex(text),
39
+ };
40
+ scriptCache.set(document.uri, entry);
41
+ return entry;
42
+ }
43
+
44
+ export function getLocParse(document: TextDocument): CachedLocParse {
45
+ const cached = locCache.get(document.uri);
46
+ if (cached && cached.version === document.version) return cached;
47
+ const text = document.getText();
48
+ const entry: CachedLocParse = {
49
+ version: document.version,
50
+ result: parseLoc(text),
51
+ lineIndex: new LineIndex(text),
52
+ };
53
+ locCache.set(document.uri, entry);
54
+ return entry;
55
+ }
56
+
57
+ export function evictParse(uri: string): void {
58
+ scriptCache.delete(uri);
59
+ locCache.delete(uri);
60
+ }
61
+
62
+ /** Saved-scope types for a document. The save-site scan is cached per version;
63
+ * `ambient` (engine-provided scopes from the file's schema entry, §B3) seeds
64
+ * the collection so saves INSIDE `scope:ambient = { … }` blocks type correctly,
65
+ * and `ctx` carries the per-definition root/structure/variable context.
66
+ * The FIRST caller per document version bakes `ambient`/`ctx` into the cache,
67
+ * so every caller must pass the file's `entry?.ambientScopes` and
68
+ * `inferenceContextFor(data, entry)` (scopes/varTypes.ts) — never a subset. */
69
+ export function getSavedScopes(
70
+ document: TextDocument,
71
+ model: ScopeModel,
72
+ rootScopes: Set<Scope> | null,
73
+ ambient?: ReadonlyArray<{ name: string; type: string }>,
74
+ ctx?: InferenceContext
75
+ ): Map<string, Set<Scope> | null> {
76
+ const entry = getParse(document);
77
+ if (!entry.savedScopes) {
78
+ entry.savedScopes = collectSavedScopeTypes(entry.result, model, rootScopes, ambient, ctx);
79
+ }
80
+ return entry.savedScopes;
81
+ }