@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,241 @@
1
+ /**
2
+ * Document symbols (outline / breadcrumbs / sticky scroll), free with the CST:
3
+ * the full nested block tree of a script or `.gui` file, and loc entries
4
+ * grouped under the language header.
5
+ */
6
+ import { SymbolKind, type DocumentSymbol, type Range as LspRange } from "vscode-languageserver/node";
7
+ import type { TextDocument } from "vscode-languageserver-textdocument";
8
+ import type { AssignmentNode, LineIndex, Range, Statement } from "../parser";
9
+ import { EVENT_ID } from "../index/indexer";
10
+ import { DECL_MARKERS, SLOT_KEYS } from "../gui/declMarkers";
11
+ import { PROPERTY_BLOCKS } from "../gui/layoutEngine";
12
+ import { getLocParse, getParse } from "../parseCache";
13
+
14
+ function toLspRange(lines: LineIndex, range: Range): LspRange {
15
+ return { start: lines.positionAt(range.start), end: lines.positionAt(range.end) };
16
+ }
17
+
18
+ function childBlockStatements(stmt: AssignmentNode): Statement[] {
19
+ if (stmt.value?.kind === "block") return stmt.value.statements;
20
+ if (stmt.value?.kind === "tagged-block") return stmt.value.block.statements;
21
+ return [];
22
+ }
23
+
24
+ /** The scalar value of a direct child assignment named `key`, if any. */
25
+ function childScalar(statements: Statement[], key: string): string | null {
26
+ for (const s of statements) {
27
+ if (s.kind === "assignment" && !s.key.quoted && s.key.text === key && s.value?.kind === "scalar") {
28
+ return s.value.text;
29
+ }
30
+ }
31
+ return null;
32
+ }
33
+
34
+ export function provideDocumentSymbols(document: TextDocument): DocumentSymbol[] {
35
+ if (document.languageId === "paradox-loc") return locSymbols(document);
36
+ if (document.languageId === "paradox-gui") return guiSymbols(document);
37
+ return scriptSymbols(document);
38
+ }
39
+
40
+ /**
41
+ * PdxGui outline: the declaration markers (`types Group`, `template Name`,
42
+ * `type name = base`) plus the FULL nested widget-block tree. Nesting is what
43
+ * sticky scroll pins headers from and breadcrumbs navigate by. Property
44
+ * blocks (`size = {...}`, `state`, `modify_texture`, ... — the engine's
45
+ * PROPERTY_BLOCKS, the same "data, not children" split guiTree draws) emit no
46
+ * symbol: measured over vanilla, they were 33% of hud.gui's and 14% of
47
+ * custom_tooltip.gui's entries, pure noise between the widget headers. The
48
+ * cap guards degenerate files; with the property filter the largest measured
49
+ * vanilla file (right_click_menu.gui, ~3.7k widget blocks) fits under it.
50
+ */
51
+ const GUI_SYMBOL_CAP = 6000;
52
+
53
+ /** A quoted scalar's text without its quotes (widget `name = "x"` details). */
54
+ function unquote(text: string): string {
55
+ return text.length >= 2 && text.startsWith('"') && text.endsWith('"') ? text.slice(1, -1) : text;
56
+ }
57
+
58
+ function guiSymbols(document: TextDocument): DocumentSymbol[] {
59
+ const { result, lineIndex } = getParse(document);
60
+ const budget = { left: GUI_SYMBOL_CAP };
61
+ return guiBlockSymbols(result.root.statements, lineIndex, budget);
62
+ }
63
+
64
+ function guiBlockSymbols(
65
+ statements: Statement[],
66
+ lineIndex: LineIndex,
67
+ budget: { left: number }
68
+ ): DocumentSymbol[] {
69
+ const symbols: DocumentSymbol[] = [];
70
+ let marker: { word: string; start: number } | null = null;
71
+ for (const stmt of statements) {
72
+ if (budget.left <= 0) break;
73
+ // `types X { }` / `template X { }` / `type x = base { }` /
74
+ // `blockoverride "slot" { }` lex as a loose scalar marker followed by the
75
+ // named assignment (guiDefs and the source model read them the same way).
76
+ if (stmt.kind === "value") {
77
+ if (stmt.value.kind === "scalar" && DECL_MARKERS.has(stmt.value.text.toLowerCase())) {
78
+ marker = { word: stmt.value.text.toLowerCase(), start: stmt.range.start };
79
+ } else if (stmt.value.kind === "block") {
80
+ // An anonymous list block: no symbol of its own, but the widgets
81
+ // inside still reach the outline.
82
+ marker = null;
83
+ symbols.push(...guiBlockSymbols(stmt.value.statements, lineIndex, budget));
84
+ } else {
85
+ marker = null;
86
+ }
87
+ continue;
88
+ }
89
+ const declared = marker?.word ?? null;
90
+ const declaredStart = marker?.start;
91
+ marker = null;
92
+ if (stmt.kind !== "assignment") continue;
93
+ const value = stmt.value;
94
+ const block = value?.kind === "block" ? value : value?.kind === "tagged-block" ? value.block : null;
95
+ if (!block) continue;
96
+ const base = value?.kind === "tagged-block" ? value.tag.text : null;
97
+ const lower = stmt.key.text.toLowerCase();
98
+ // Property blocks are data, not structure (guiTree's split): no symbol,
99
+ // no recursion — unless the key is a declared slot or the assignment
100
+ // spelling of one (`blockoverride = "name" { ... }`).
101
+ if (!declared && PROPERTY_BLOCKS.has(lower) && !(SLOT_KEYS.has(lower) && base)) continue;
102
+ budget.left--;
103
+ const children = guiBlockSymbols(block.statements, lineIndex, budget);
104
+ let name = stmt.key.text;
105
+ let detail: string | undefined;
106
+ let kind: SymbolKind = SymbolKind.Object;
107
+ if (declared === "types") {
108
+ name = `types ${stmt.key.text}`;
109
+ kind = SymbolKind.Namespace;
110
+ } else if (declared === "template" || declared === "local_template") {
111
+ name = `${declared} ${stmt.key.text}`;
112
+ kind = SymbolKind.Class;
113
+ } else if (declared === "block" || declared === "blockoverride") {
114
+ name = `${declared} ${unquote(stmt.key.text)}`;
115
+ kind = SymbolKind.Field;
116
+ } else if (SLOT_KEYS.has(lower) && base) {
117
+ // The assignment spelling of a slot: `blockoverride = "name" { ... }`.
118
+ name = `${stmt.key.text} ${unquote(base)}`;
119
+ kind = SymbolKind.Field;
120
+ } else if (declared === "type" || base) {
121
+ detail = base ? `= ${base}` : undefined;
122
+ kind = SymbolKind.Class;
123
+ } else {
124
+ // A widget instance: its `name = "..."` property is how modders and the
125
+ // game's error log identify it.
126
+ const widgetName = childScalar(block.statements, "name");
127
+ detail = widgetName ? unquote(widgetName) : undefined;
128
+ }
129
+ symbols.push({
130
+ name,
131
+ detail,
132
+ kind,
133
+ // A declaration's range starts at its marker word (`types`, `template`,
134
+ // `blockoverride`...) so cursor-on-the-keyword still resolves to the
135
+ // symbol; the marker is a sibling statement, so containment holds.
136
+ range: toLspRange(lineIndex, { start: declaredStart ?? stmt.range.start, end: stmt.range.end }),
137
+ selectionRange: toLspRange(lineIndex, stmt.key.range),
138
+ children,
139
+ });
140
+ }
141
+ return symbols;
142
+ }
143
+
144
+ /**
145
+ * Script outline: every top-level definition and, under it, the FULL nested
146
+ * block tree. The nesting is the point: sticky scroll pins its headers and
147
+ * breadcrumbs walk it, so a `limit` eight levels down inside an event needs
148
+ * every block above it named, not just the event.
149
+ *
150
+ * The cap guards degenerate files. It is spent on nesting only: top-level
151
+ * definitions are always emitted, so a file past the budget degrades to the
152
+ * old flat outline instead of going blank halfway down. Measured over the
153
+ * vanilla corpus (3785 script files), only four exceed it (the giant history
154
+ * and landed_titles files, worst case 21,677 blocks in
155
+ * history/characters/japanese.txt, 103k lines).
156
+ */
157
+ const SCRIPT_SYMBOL_CAP = 12000;
158
+
159
+ function scriptSymbols(document: TextDocument): DocumentSymbol[] {
160
+ const { result, lineIndex } = getParse(document);
161
+ const budget = { left: SCRIPT_SYMBOL_CAP };
162
+ const symbols: DocumentSymbol[] = [];
163
+ for (const stmt of result.root.statements) {
164
+ if (stmt.kind !== "assignment" || stmt.key.quoted) continue;
165
+ const name = stmt.key.text;
166
+ if (name === "namespace") continue;
167
+ const isEvent = EVENT_ID.test(name);
168
+ const stmts = childBlockStatements(stmt);
169
+ const detail = isEvent ? childScalar(stmts, "type") : childScalar(stmts, "name");
170
+ symbols.push({
171
+ name,
172
+ detail: detail ? unquote(detail) : undefined,
173
+ kind: isEvent ? SymbolKind.Event : SymbolKind.Function,
174
+ range: toLspRange(lineIndex, stmt.range),
175
+ selectionRange: toLspRange(lineIndex, stmt.key.range),
176
+ children: scriptChildSymbols(stmts, lineIndex, budget),
177
+ });
178
+ }
179
+ return symbols;
180
+ }
181
+
182
+ function scriptChildSymbols(
183
+ statements: Statement[],
184
+ lineIndex: LineIndex,
185
+ budget: { left: number }
186
+ ): DocumentSymbol[] {
187
+ const symbols: DocumentSymbol[] = [];
188
+ for (const stmt of statements) {
189
+ if (budget.left <= 0) break;
190
+ if (stmt.kind !== "assignment") continue;
191
+ const value = stmt.value;
192
+ const block = value?.kind === "block" ? value : value?.kind === "tagged-block" ? value.block : null;
193
+ if (!block) continue;
194
+ // Two kinds of block earn no row, the same "data, not structure" split
195
+ // guiSymbols draws with PROPERTY_BLOCKS: one holding nothing but bare
196
+ // values (`traits = { brave shy }`, `color = { 0.5 0.5 0.5 }`), and one
197
+ // that opens and closes on a single line, which can never be a sticky
198
+ // header and only pads the outline.
199
+ if (block.statements.length === 0 || block.statements.every((s) => s.kind === "value")) continue;
200
+ const openLine = lineIndex.positionAt(block.openBrace).line;
201
+ const closeLine = lineIndex.positionAt(block.closeBrace ?? block.range.end).line;
202
+ if (closeLine <= openLine) continue;
203
+ budget.left--;
204
+ const named = childScalar(block.statements, "name");
205
+ symbols.push({
206
+ name: stmt.key.text,
207
+ detail: named ? unquote(named) : undefined,
208
+ kind: stmt.key.text === "option" ? SymbolKind.EnumMember : SymbolKind.Field,
209
+ range: toLspRange(lineIndex, stmt.range),
210
+ selectionRange: toLspRange(lineIndex, stmt.key.range),
211
+ children: scriptChildSymbols(block.statements, lineIndex, budget),
212
+ });
213
+ }
214
+ return symbols;
215
+ }
216
+
217
+ function locSymbols(document: TextDocument): DocumentSymbol[] {
218
+ const { result, lineIndex } = getLocParse(document);
219
+ const entries: DocumentSymbol[] = result.entries.map((e) => ({
220
+ name: e.key,
221
+ detail: e.value.length > 60 ? e.value.slice(0, 59) + "…" : e.value,
222
+ kind: SymbolKind.String,
223
+ range: toLspRange(lineIndex, { start: e.keyRange.start, end: e.valueRange.end + 1 }),
224
+ selectionRange: toLspRange(lineIndex, e.keyRange),
225
+ }));
226
+ if (result.language !== null && result.headerRange) {
227
+ return [
228
+ {
229
+ name: `l_${result.language}`,
230
+ kind: SymbolKind.Namespace,
231
+ range: {
232
+ start: lineIndex.positionAt(result.headerRange.start),
233
+ end: lineIndex.positionAt(Number.MAX_SAFE_INTEGER),
234
+ },
235
+ selectionRange: toLspRange(lineIndex, result.headerRange),
236
+ children: entries,
237
+ },
238
+ ];
239
+ }
240
+ return entries;
241
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Texture preview on hover (rework plan Phase 3, adopted from the Sublime
3
+ * JominiTools texture popups): hovering a `gfx/...*.dds` path shows the image
4
+ * inline, resolved mod-first then vanilla, decoded to PNG by the pure-TS DDS
5
+ * decoder — no external tools.
6
+ */
7
+ import { MarkupKind, type Hover, type Position } from "vscode-languageserver/node";
8
+ import type { TextDocument } from "vscode-languageserver-textdocument";
9
+ import { URI } from "vscode-uri";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+ import { ddsFormatInfo, ddsToPngDataUri } from "../dds";
13
+ import { getLineText } from "../documents";
14
+ import type { ParadoxSettings } from "@px-lsp/protocol/protocol";
15
+ import { assetRoots, bareNameBaseDirs } from "./assetPaths";
16
+
17
+ const DDS_PATH = /[A-Za-z0-9_\-./\\]+\.dds/gi;
18
+ const CACHE_MAX = 100;
19
+
20
+ interface TexturePreview {
21
+ /** PNG data URI, or null when the format can't be decoded. */
22
+ uri: string | null;
23
+ format: string;
24
+ width: number;
25
+ height: number;
26
+ fileBytes: number;
27
+ }
28
+
29
+ /** Preview cache keyed by fsPath:mtime. */
30
+ const cache = new Map<string, TexturePreview | null>();
31
+
32
+ function cachedPreview(fsPath: string): TexturePreview | null {
33
+ let stat: fs.Stats;
34
+ try {
35
+ stat = fs.statSync(fsPath);
36
+ } catch {
37
+ return null;
38
+ }
39
+ const key = `${fsPath}:${stat.mtimeMs}`;
40
+ if (cache.has(key)) return cache.get(key) ?? null;
41
+ let entry: TexturePreview | null = null;
42
+ try {
43
+ const buf = fs.readFileSync(fsPath);
44
+ const info = ddsFormatInfo(buf);
45
+ if (info) {
46
+ let uri: string | null;
47
+ try {
48
+ uri = ddsToPngDataUri(buf, 256);
49
+ } catch {
50
+ uri = null; // unsupported format → caller degrades to a file link
51
+ }
52
+ entry = { uri, format: info.format, width: info.width, height: info.height, fileBytes: stat.size };
53
+ }
54
+ } catch {
55
+ entry = null;
56
+ }
57
+ if (cache.size >= CACHE_MAX) {
58
+ const first = cache.keys().next().value;
59
+ if (first !== undefined) cache.delete(first);
60
+ }
61
+ cache.set(key, entry);
62
+ return entry;
63
+ }
64
+
65
+ function formatBytes(n: number): string {
66
+ return n >= 1024 * 1024 ? `${(n / (1024 * 1024)).toFixed(1)} MB` : `${(n / 1024).toFixed(1)} KB`;
67
+ }
68
+
69
+ export function provideTextureHover(
70
+ settings: ParadoxSettings,
71
+ document: TextDocument,
72
+ position: Position,
73
+ entryKind?: string | null
74
+ ): Hover | null {
75
+ const lineText = getLineText(document, position.line);
76
+ DDS_PATH.lastIndex = 0;
77
+ let m: RegExpExecArray | null;
78
+ let hit: { text: string; start: number; end: number } | null = null;
79
+ while ((m = DDS_PATH.exec(lineText)) !== null) {
80
+ if (position.character >= m.index && position.character <= m.index + m[0].length) {
81
+ hit = { text: m[0], start: m.index, end: m.index + m[0].length };
82
+ break;
83
+ }
84
+ }
85
+ if (!hit) return null;
86
+
87
+ const rel = hit.text.replace(/\\/g, "/").replace(/^\/+/, "");
88
+ // Mod shadows parents shadow vanilla, like every other asset.
89
+ const candidates: Array<{ root: string | null; label: string }> = [
90
+ { root: settings.modPath, label: "mod" },
91
+ ...(settings.parentPaths ?? []).map((p) => ({ root: p as string | null, label: "parent" })),
92
+ { root: settings.gamePath, label: "vanilla" },
93
+ ];
94
+ let resolved: { fsPath: string; label: string } | null = null;
95
+ for (const { root, label } of candidates) {
96
+ if (!root) continue;
97
+ const full = path.join(root, ...rel.split("/"));
98
+ if (fs.existsSync(full)) {
99
+ resolved = { fsPath: full, label };
100
+ break;
101
+ }
102
+ }
103
+ // Also try relative to the hovered file's own folder (portrait/coa fragments).
104
+ if (!resolved) {
105
+ const docDir = path.dirname(URI.parse(document.uri).fsPath);
106
+ const full = path.join(docDir, ...rel.split("/"));
107
+ if (fs.existsSync(full)) resolved = { fsPath: full, label: "relative" };
108
+ }
109
+ // Bare filename (`icon = cultivation_realm_2.dds`): resolve against the
110
+ // engine-fixed base dir for this field (trait icon → gfx/interface/icons/traits/).
111
+ if (!resolved && !rel.includes("/")) {
112
+ const keyMatch = /([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"?$/.exec(lineText.slice(0, hit.start));
113
+ const dirs = keyMatch ? bareNameBaseDirs(entryKind, keyMatch[1]) : null;
114
+ if (dirs) {
115
+ outer: for (const { root, label } of assetRoots(settings)) {
116
+ for (const dir of dirs) {
117
+ const full = path.join(root, ...dir.split("/"), rel);
118
+ if (fs.existsSync(full)) {
119
+ resolved = { fsPath: full, label };
120
+ break outer;
121
+ }
122
+ }
123
+ }
124
+ }
125
+ }
126
+ if (!resolved) return null;
127
+
128
+ const range = {
129
+ start: { line: position.line, character: hit.start },
130
+ end: { line: position.line, character: hit.end },
131
+ };
132
+ const fileLink = URI.file(resolved.fsPath).toString();
133
+ const preview = cachedPreview(resolved.fsPath);
134
+ // The path is what's hovered, so the caption carries what the file itself
135
+ // knows: dimensions, encoding, size on disk, and where it resolved.
136
+ const meta = preview
137
+ ? `${preview.width}×${preview.height} · ${preview.format} · ${formatBytes(preview.fileBytes)} · ${resolved.label}`
138
+ : `texture (${resolved.label})`;
139
+ const body = preview?.uri
140
+ ? `![texture](${preview.uri})\n\n*${meta}* — [open file](${fileLink})`
141
+ : `*${meta}* — not previewable, [open file](${fileLink})`;
142
+ return { contents: { kind: MarkupKind.Markdown, value: body }, range };
143
+ }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Fuzzy workspace symbols (Ctrl+T): jump to any indexed definition in the mod
3
+ * or vanilla. Subsequence matching, mod content and better matches first.
4
+ */
5
+ import { SymbolKind, type WorkspaceSymbol } from "vscode-languageserver/node";
6
+ import { URI } from "vscode-uri";
7
+ import type { Definition } from "@px-lsp/protocol/types";
8
+ import type { ServerData } from "../serverData";
9
+
10
+ const MAX_RESULTS = 512;
11
+
12
+ const KIND_MAP: Record<string, SymbolKind> = {
13
+ event: SymbolKind.Event,
14
+ on_action: SymbolKind.Event,
15
+ scripted_effect: SymbolKind.Function,
16
+ scripted_trigger: SymbolKind.Interface,
17
+ scripted_modifier: SymbolKind.Property,
18
+ script_value: SymbolKind.Constant,
19
+ loc_key: SymbolKind.String,
20
+ trait: SymbolKind.EnumMember,
21
+ landed_title: SymbolKind.Namespace,
22
+ character: SymbolKind.Object,
23
+ saved_scope: SymbolKind.Variable,
24
+ variable: SymbolKind.Variable,
25
+ local_variable: SymbolKind.Variable,
26
+ global_variable: SymbolKind.Variable,
27
+ variable_list: SymbolKind.Variable,
28
+ local_variable_list: SymbolKind.Variable,
29
+ global_variable_list: SymbolKind.Variable,
30
+ gui_type: SymbolKind.Class,
31
+ };
32
+
33
+ /** Case-insensitive subsequence match with a crude quality score (lower = better). */
34
+ function fuzzyScore(query: string, candidate: string): number | null {
35
+ if (query.length === 0) return 1000;
36
+ const q = query.toLowerCase();
37
+ const c = candidate.toLowerCase();
38
+ if (c === q) return 0;
39
+ if (c.startsWith(q)) return 1;
40
+ const sub = c.indexOf(q);
41
+ if (sub >= 0) return 2 + Math.min(sub, 50);
42
+ let qi = 0;
43
+ for (let ci = 0; ci < c.length && qi < q.length; ci++) {
44
+ if (c[ci] === q[qi]) qi++;
45
+ }
46
+ if (qi < q.length) return null;
47
+ return 100 + (c.length - q.length);
48
+ }
49
+
50
+ export function provideWorkspaceSymbols(data: ServerData, query: string): WorkspaceSymbol[] {
51
+ const scored: Array<{ score: number; def: Definition }> = [];
52
+ for (const def of data.index.entries()) {
53
+ // Vanilla loc keys are too many to be useful in symbol search.
54
+ if (def.kind === "loc_key" && def.source === "vanilla") continue;
55
+ const score = fuzzyScore(query, def.name);
56
+ if (score === null) continue;
57
+ scored.push({ score: score + (def.source === "mod" ? 0 : 5), def });
58
+ }
59
+ scored.sort((a, b) => a.score - b.score || a.def.name.localeCompare(b.def.name));
60
+ return scored.slice(0, MAX_RESULTS).map(({ def }) => ({
61
+ name: def.name,
62
+ kind: KIND_MAP[def.kind] ?? SymbolKind.Object,
63
+ containerName: `${def.kind.replace(/_/g, " ")} (${def.source})`,
64
+ location: {
65
+ uri: URI.file(def.file).toString(),
66
+ range: { start: { line: def.line, character: 0 }, end: { line: def.line, character: 0 } },
67
+ },
68
+ }));
69
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The active game profile. One server instance serves one game at a time
3
+ * (PLAN.md non-goals: no multi-game workspaces), so features and engine
4
+ * modules read the profile through this accessor instead of threading it
5
+ * through every signature. server.ts sets it from settings.gameId at
6
+ * initialize and on config changes; tests may set it directly.
7
+ */
8
+ import type { GameProfile } from "./profile";
9
+ import { defaultProfile } from "./registry";
10
+
11
+ let current: GameProfile = defaultProfile;
12
+
13
+ export function activeProfile(): GameProfile {
14
+ return current;
15
+ }
16
+
17
+ export function setActiveProfile(profile: GameProfile): void {
18
+ current = profile;
19
+ }