@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.
- package/LICENSE +674 -0
- package/README.md +334 -0
- package/THIRD-PARTY-NOTICES.md +83 -0
- package/data/ck3/dataTypes.json +2195 -0
- package/data/ck3/data_types/data_types_common.txt +2040 -0
- package/data/ck3/data_types/data_types_gui.txt +5264 -0
- package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
- package/data/ck3/data_types/data_types_script.txt +4251 -0
- package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
- package/data/ck3/freqs.json +1 -0
- package/data/ck3/guiSchema.json +6344 -0
- package/data/ck3/script_docs/effects.log +16059 -0
- package/data/ck3/script_docs/event_targets.log +2098 -0
- package/data/ck3/script_docs/modifiers.log +2228 -0
- package/data/ck3/script_docs/on_actions.log +5275 -0
- package/data/ck3/script_docs/triggers.log +11991 -0
- package/data/ck3/structures.json +9743 -0
- package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
- package/data/ck3/wikidocs/Data_types.md +2568 -0
- package/data/ck3/wikidocs/Effects_list.md +1176 -0
- package/data/ck3/wikidocs/Scopes_list.md +341 -0
- package/data/ck3/wikidocs/Triggers_list.md +1097 -0
- package/data/eu5/data_types/data_types_common.txt +2087 -0
- package/data/eu5/data_types/data_types_gui.txt +6732 -0
- package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
- package/data/eu5/data_types/data_types_script.txt +5688 -0
- package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
- package/data/vic3/data_types/data_types_common.txt +2021 -0
- package/data/vic3/data_types/data_types_gui.txt +5592 -0
- package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
- package/data/vic3/data_types/data_types_script.txt +2817 -0
- package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
- package/data/vic3/freqs.json +1 -0
- package/data/vic3/guiSchema.json +5578 -0
- package/data/vic3/script_docs/effects.log +38135 -0
- package/data/vic3/script_docs/event_targets.log +2028 -0
- package/data/vic3/script_docs/modifiers.log +18954 -0
- package/data/vic3/script_docs/on_actions.log +1561 -0
- package/data/vic3/script_docs/triggers.log +15738 -0
- package/data/vic3/structures.json +10189 -0
- package/dist/server.js +63668 -0
- package/media/px-lsp.svg +12 -0
- package/package.json +50 -0
- package/src/clientMode.ts +60 -0
- package/src/coa/coa.ts +184 -0
- package/src/coa/coaParse.ts +267 -0
- package/src/context.ts +78 -0
- package/src/contextKeywords.ts +224 -0
- package/src/data/dataBindingMacros.ts +82 -0
- package/src/data/dataFnDocs.ts +152 -0
- package/src/data/dataFnUsage.ts +431 -0
- package/src/data/dataTypes.ts +279 -0
- package/src/data/defines.ts +123 -0
- package/src/data/docsParser.ts +453 -0
- package/src/data/keywordDocs.ts +98 -0
- package/src/data/modifierTemplates.ts +143 -0
- package/src/data/textFormatting.ts +165 -0
- package/src/data/wikiDocs.ts +187 -0
- package/src/dds/decoder.ts +1007 -0
- package/src/dds/encode.ts +235 -0
- package/src/dds/index.ts +58 -0
- package/src/dds/png.ts +96 -0
- package/src/dds/tga.ts +62 -0
- package/src/documents.ts +35 -0
- package/src/features/assetPaths.ts +169 -0
- package/src/features/codeActions.ts +148 -0
- package/src/features/colors.ts +244 -0
- package/src/features/completion.ts +961 -0
- package/src/features/datafunction.ts +729 -0
- package/src/features/definition.ts +84 -0
- package/src/features/diagnostics.ts +244 -0
- package/src/features/folding.ts +106 -0
- package/src/features/formatting.ts +60 -0
- package/src/features/guiLanguage.ts +366 -0
- package/src/features/guiNavigation.ts +140 -0
- package/src/features/guiTree.ts +97 -0
- package/src/features/hover.ts +817 -0
- package/src/features/hoverRender.ts +222 -0
- package/src/features/inlayHints.ts +147 -0
- package/src/features/locFormatting.ts +127 -0
- package/src/features/references.ts +70 -0
- package/src/features/rename.ts +135 -0
- package/src/features/scopeAt.ts +65 -0
- package/src/features/semanticTokens.ts +188 -0
- package/src/features/signatureHelp.ts +72 -0
- package/src/features/symbols.ts +241 -0
- package/src/features/textureHover.ts +143 -0
- package/src/features/workspaceSymbols.ts +69 -0
- package/src/games/active.ts +19 -0
- package/src/games/ck3/ambientScopes.ts +273 -0
- package/src/games/ck3/index.ts +38 -0
- package/src/games/ck3/meta.ts +28 -0
- package/src/games/ck3/modifierPlaceholders.ts +61 -0
- package/src/games/ck3/saveSchema.ts +134 -0
- package/src/games/ck3/scaffolds.ts +197 -0
- package/src/games/ck3/schema.ts +422 -0
- package/src/games/ck3/structures.ts +887 -0
- package/src/games/eu5/index.ts +75 -0
- package/src/games/eu5/meta.ts +44 -0
- package/src/games/eu5/scaffolds.ts +49 -0
- package/src/games/eu5/schema.generated.ts +1043 -0
- package/src/games/jomini/variables.ts +134 -0
- package/src/games/profile.ts +205 -0
- package/src/games/registry.ts +27 -0
- package/src/games/vic3/index.ts +52 -0
- package/src/games/vic3/meta.ts +55 -0
- package/src/games/vic3/saveSchema.ts +77 -0
- package/src/games/vic3/scaffolds.ts +135 -0
- package/src/games/vic3/schema.ts +650 -0
- package/src/games/vic3/structures.ts +33 -0
- package/src/gui/anchorSpec.ts +66 -0
- package/src/gui/declMarkers.ts +30 -0
- package/src/gui/fillGeometry.ts +101 -0
- package/src/gui/guiDefs.ts +386 -0
- package/src/gui/guiDependencies.ts +352 -0
- package/src/gui/guiLinks.ts +64 -0
- package/src/gui/layoutEngine.ts +1998 -0
- package/src/gui/layoutService.ts +221 -0
- package/src/gui/measuredMetrics.ts +21 -0
- package/src/gui/previewService.ts +89 -0
- package/src/gui/saveSchema.ts +220 -0
- package/src/gui/saveValues.ts +399 -0
- package/src/gui/saveZip.ts +60 -0
- package/src/gui/sourceEdit.ts +535 -0
- package/src/gui/sourceEditService.ts +439 -0
- package/src/gui/sourceModel.ts +603 -0
- package/src/gui/textResolve.ts +145 -0
- package/src/gui/textureInfo.ts +106 -0
- package/src/gui/vocabulary.ts +149 -0
- package/src/gui/widgetEdit.ts +52 -0
- package/src/gui/widgetInfo.ts +245 -0
- package/src/index/docComments.ts +103 -0
- package/src/index/extract.ts +252 -0
- package/src/index/indexer.ts +369 -0
- package/src/index/intern.ts +101 -0
- package/src/index/lazyRefs.ts +145 -0
- package/src/index/modOrigin.ts +69 -0
- package/src/index/references.ts +534 -0
- package/src/overview/dependencies.ts +240 -0
- package/src/overview/eventBanner.ts +95 -0
- package/src/overview/eventDetail.ts +482 -0
- package/src/overview/eventGraph.ts +617 -0
- package/src/overview/eventVocabulary.ts +214 -0
- package/src/overview/locCoverage.ts +138 -0
- package/src/overview/modOverview.ts +29 -0
- package/src/overview/overrides.ts +89 -0
- package/src/parseCache.ts +81 -0
- package/src/parser/cst.ts +257 -0
- package/src/parser/encoding.ts +106 -0
- package/src/parser/index.ts +7 -0
- package/src/parser/lexer.ts +245 -0
- package/src/parser/locParser.ts +276 -0
- package/src/parser/parser.ts +360 -0
- package/src/schema/freqs.ts +70 -0
- package/src/schema/loader.ts +113 -0
- package/src/schema/types.ts +142 -0
- package/src/scopes/inference.ts +478 -0
- package/src/scopes/model.ts +148 -0
- package/src/scopes/varTypes.ts +290 -0
- package/src/server.ts +1894 -0
- package/src/serverData.ts +98 -0
- package/src/structure.ts +56 -0
- 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
|
+
? `\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
|
+
}
|