@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,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The GUI-to-script dependency surface, both directions over one link.
|
|
3
|
+
*
|
|
4
|
+
* PdxGui reaches script through exactly one door, `GetScriptedGui('name')`
|
|
5
|
+
* (see guiLinks.ts), so both questions the designer asks are the same walk read
|
|
6
|
+
* from opposite ends:
|
|
7
|
+
*
|
|
8
|
+
* forward — this widget calls scripted_gui X, which fires event E directly
|
|
9
|
+
* or via effect_a -> effect_b (`paradox/guiDependencies`)
|
|
10
|
+
* reverse — event E is reached from these .gui files, through X
|
|
11
|
+
* (`paradox/dependencies` with `guiUses`)
|
|
12
|
+
*
|
|
13
|
+
* `reachFrom` is that shared walk. It is driven by the active profile's
|
|
14
|
+
* event/on_action reference fields, never a hard-coded key list, and it follows
|
|
15
|
+
* scripted-effect calls a bounded number of hops, which is what turns a flat
|
|
16
|
+
* reference list into a chain a human can read.
|
|
17
|
+
*
|
|
18
|
+
* The loc side is a plain index question: the `text` / `tooltip` keys the
|
|
19
|
+
* document names, each flagged against the loc index.
|
|
20
|
+
*
|
|
21
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
22
|
+
*/
|
|
23
|
+
import * as fs from "fs";
|
|
24
|
+
import type {
|
|
25
|
+
GuiDependenciesResult,
|
|
26
|
+
GuiEventChain,
|
|
27
|
+
GuiLocRow,
|
|
28
|
+
GuiScriptedGuiRow,
|
|
29
|
+
GuiUseSite,
|
|
30
|
+
} from "@px-lsp/protocol/protocol";
|
|
31
|
+
import type { Definition } from "@px-lsp/protocol/types";
|
|
32
|
+
import type { ServerData } from "../serverData";
|
|
33
|
+
import type { SchemaData } from "../schema/loader";
|
|
34
|
+
import { decode, LineIndex, parseScript, type BlockNode, type Statement } from "../parser";
|
|
35
|
+
import { findScriptedGuiCalls, type GuiScriptLinks } from "./guiLinks";
|
|
36
|
+
import { findWidgetAtLine, parseGuiSource } from "./sourceModel";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How many scripted-effect hops a chain follows. Three is what the readout
|
|
40
|
+
* ("via effect_a -> effect_b") stays legible at, and it bounds the file reads
|
|
41
|
+
* a single request can trigger.
|
|
42
|
+
*/
|
|
43
|
+
const MAX_HOPS = 3;
|
|
44
|
+
|
|
45
|
+
/** Caps, so one pathological definition cannot return an unbounded payload. */
|
|
46
|
+
const MAX_REACHED = 60;
|
|
47
|
+
const MAX_LOC_ROWS = 100;
|
|
48
|
+
const MAX_GUI_USES = 100;
|
|
49
|
+
|
|
50
|
+
/** Guard against a malformed document nesting without end. */
|
|
51
|
+
const MAX_BLOCK_DEPTH = 32;
|
|
52
|
+
|
|
53
|
+
/** `.gui` properties whose bare value is a localization key. `raw_text` /
|
|
54
|
+
* `raw_tooltip` are literal strings by definition and are NOT keys. */
|
|
55
|
+
const LOC_PROPS = new Set(["text", "tooltip"]);
|
|
56
|
+
|
|
57
|
+
/** The charset a loc key uses; anything else (a `[datafunction]`) is not one. */
|
|
58
|
+
const LOC_KEY = /^[A-Za-z_][A-Za-z0-9_.-]*$/;
|
|
59
|
+
|
|
60
|
+
// ---- forward: a .gui document -> script ------------------------------------
|
|
61
|
+
|
|
62
|
+
export function computeGuiDependencies(
|
|
63
|
+
data: ServerData,
|
|
64
|
+
schema: SchemaData,
|
|
65
|
+
text: string,
|
|
66
|
+
line: number | undefined,
|
|
67
|
+
links: GuiScriptLinks
|
|
68
|
+
): GuiDependenciesResult {
|
|
69
|
+
const file = parseGuiSource(text);
|
|
70
|
+
let from = 0;
|
|
71
|
+
let to = Number.POSITIVE_INFINITY;
|
|
72
|
+
let widget: GuiDependenciesResult["widget"];
|
|
73
|
+
|
|
74
|
+
if (line !== undefined) {
|
|
75
|
+
const target = findWidgetAtLine(file, line);
|
|
76
|
+
if (!target || target.kind !== "widget") return { scriptedGuis: [], locKeys: [] };
|
|
77
|
+
// The widget's SOURCE subtree: what THIS document says it does. A property
|
|
78
|
+
// inherited from a template lives in another file and is that file's row.
|
|
79
|
+
from = target.line;
|
|
80
|
+
to = target.endLine;
|
|
81
|
+
widget = { key: target.key, name: nameOfWidget(target.block), line: target.line };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const inScope = (at: number): boolean => at >= from && at <= to;
|
|
85
|
+
const byName = new Map<string, number[]>();
|
|
86
|
+
for (const call of findScriptedGuiCalls(text)) {
|
|
87
|
+
if (!inScope(call.line)) continue;
|
|
88
|
+
const lines = byName.get(call.name);
|
|
89
|
+
if (lines) lines.push(call.line);
|
|
90
|
+
else byName.set(call.name, [call.line]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const reads = new Map<string, ParsedFile | null>();
|
|
94
|
+
const scriptedGuis: GuiScriptedGuiRow[] = [];
|
|
95
|
+
for (const [name, callLines] of [...byName.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
96
|
+
const def = scriptedGuiDef(data, name);
|
|
97
|
+
scriptedGuis.push({
|
|
98
|
+
name,
|
|
99
|
+
file: def?.file,
|
|
100
|
+
line: def?.line,
|
|
101
|
+
callLines,
|
|
102
|
+
// The store's count is the whole gui tree's; without a store (no game
|
|
103
|
+
// path configured, an unsaved buffer) this document is all there is.
|
|
104
|
+
uses: Math.max(links.calls.get(name)?.length ?? 0, callLines.length),
|
|
105
|
+
chains: def ? chainsOf(data, schema, def, reads) : [],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return { widget, scriptedGuis, locKeys: locRowsIn(data, file, inScope) };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The `name = "..."` a widget block carries, without expanding anything. */
|
|
113
|
+
function nameOfWidget(block: BlockNode | null): string | undefined {
|
|
114
|
+
if (!block) return undefined;
|
|
115
|
+
let found: string | undefined;
|
|
116
|
+
for (const stmt of block.statements) {
|
|
117
|
+
if (stmt.kind !== "assignment" || stmt.value?.kind !== "scalar") continue;
|
|
118
|
+
if (stmt.key.text.toLowerCase() === "name") found = stmt.value.text;
|
|
119
|
+
}
|
|
120
|
+
return found;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Loc keys the document names, deduped by key, first site winning. */
|
|
124
|
+
function locRowsIn(
|
|
125
|
+
data: ServerData,
|
|
126
|
+
file: ReturnType<typeof parseGuiSource>,
|
|
127
|
+
inScope: (line: number) => boolean
|
|
128
|
+
): GuiLocRow[] {
|
|
129
|
+
const out: GuiLocRow[] = [];
|
|
130
|
+
const seen = new Set<string>();
|
|
131
|
+
for (const entry of file.entries) {
|
|
132
|
+
if (out.length >= MAX_LOC_ROWS) break;
|
|
133
|
+
if (entry.valueKind !== "scalar" || entry.value === null) continue;
|
|
134
|
+
if (!LOC_PROPS.has(entry.keyLower) || !inScope(entry.line)) continue;
|
|
135
|
+
const key = entry.value;
|
|
136
|
+
if (!LOC_KEY.test(key) || seen.has(key)) continue;
|
|
137
|
+
seen.add(key);
|
|
138
|
+
const def = data.index.lookup(key).find((d) => d.kind === "loc_key");
|
|
139
|
+
out.push({ key, prop: entry.key, line: entry.line, missing: !def, value: def?.value });
|
|
140
|
+
}
|
|
141
|
+
return out;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ---- reverse: a script definition -> the .gui files reaching it -------------
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Every `.gui` call site that reaches `name`. Only the scripted_guis some .gui
|
|
148
|
+
* file actually calls are walked (the link index is the filter), so the cost is
|
|
149
|
+
* set by the gui tree's real script surface, not by the definition count.
|
|
150
|
+
*/
|
|
151
|
+
export function computeGuiUses(
|
|
152
|
+
data: ServerData,
|
|
153
|
+
schema: SchemaData,
|
|
154
|
+
links: GuiScriptLinks,
|
|
155
|
+
name: string
|
|
156
|
+
): GuiUseSite[] {
|
|
157
|
+
const reads = new Map<string, ParsedFile | null>();
|
|
158
|
+
const out: GuiUseSite[] = [];
|
|
159
|
+
for (const [scriptedGui, sites] of links.calls) {
|
|
160
|
+
let via: string[] | null = null;
|
|
161
|
+
if (scriptedGui === name) {
|
|
162
|
+
via = [];
|
|
163
|
+
} else {
|
|
164
|
+
const def = scriptedGuiDef(data, scriptedGui);
|
|
165
|
+
if (!def) continue;
|
|
166
|
+
via = reachFrom(data, schema, def, reads).get(name)?.via ?? null;
|
|
167
|
+
}
|
|
168
|
+
if (via === null) continue;
|
|
169
|
+
for (const site of sites) out.push({ file: site.file, line: site.line, scriptedGui, via });
|
|
170
|
+
}
|
|
171
|
+
out.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line);
|
|
172
|
+
return out.slice(0, MAX_GUI_USES);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- the shared walk --------------------------------------------------------
|
|
176
|
+
|
|
177
|
+
/** What kind of thing a walk step found, and how the walk got to it. */
|
|
178
|
+
interface Reached {
|
|
179
|
+
kind: "event" | "on_action" | "scripted_effect";
|
|
180
|
+
/** The scripted effects traversed to get here, outermost first. */
|
|
181
|
+
via: string[];
|
|
182
|
+
file?: string;
|
|
183
|
+
line?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
interface ParsedFile {
|
|
187
|
+
root: ReturnType<typeof parseScript>["root"];
|
|
188
|
+
lineOf: (offset: number) => number;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function chainsOf(
|
|
192
|
+
data: ServerData,
|
|
193
|
+
schema: SchemaData,
|
|
194
|
+
def: Definition,
|
|
195
|
+
reads: Map<string, ParsedFile | null>
|
|
196
|
+
): GuiEventChain[] {
|
|
197
|
+
const out: GuiEventChain[] = [];
|
|
198
|
+
for (const [name, hit] of reachFrom(data, schema, def, reads)) {
|
|
199
|
+
if (hit.kind === "scripted_effect") continue;
|
|
200
|
+
out.push({ name, kind: hit.kind, file: hit.file, line: hit.line, via: hit.via });
|
|
201
|
+
}
|
|
202
|
+
// Directly-reached first, then by name: "directly" is the row a reader wants
|
|
203
|
+
// at the top of the list.
|
|
204
|
+
out.sort((a, b) => a.via.length - b.via.length || a.name.localeCompare(b.name));
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Every event, on_action and scripted effect a definition's block reaches, each
|
|
210
|
+
* mapped to the scripted effects traversed to get there. One walk serves both
|
|
211
|
+
* directions; it widens hop by hop, so the first path recorded to a name is the
|
|
212
|
+
* shortest one.
|
|
213
|
+
*/
|
|
214
|
+
export function reachFrom(
|
|
215
|
+
data: ServerData,
|
|
216
|
+
schema: SchemaData,
|
|
217
|
+
def: Definition,
|
|
218
|
+
reads: Map<string, ParsedFile | null>
|
|
219
|
+
): Map<string, Reached> {
|
|
220
|
+
const out = new Map<string, Reached>();
|
|
221
|
+
const visitedEffects = new Set<string>([def.name]);
|
|
222
|
+
const refKinds = (key: string): string[] => schema.refFields.get(key)?.kinds ?? [];
|
|
223
|
+
let frontier: { def: Definition; via: string[] }[] = [{ def, via: [] }];
|
|
224
|
+
|
|
225
|
+
for (let hop = 0; hop <= MAX_HOPS && frontier.length > 0; hop++) {
|
|
226
|
+
const next: { def: Definition; via: string[] }[] = [];
|
|
227
|
+
for (const step of frontier) {
|
|
228
|
+
const parsed = blockOfDefinition(step.def, reads);
|
|
229
|
+
if (!parsed) continue;
|
|
230
|
+
walkScriptBlock(parsed.block, refKinds, (kind, name, offset) => {
|
|
231
|
+
if (out.size >= MAX_REACHED) return;
|
|
232
|
+
if (kind === "call") {
|
|
233
|
+
if (visitedEffects.has(name)) return;
|
|
234
|
+
const target = data.index.lookup(name).find((d) => d.kind === "scripted_effect");
|
|
235
|
+
if (!target) return;
|
|
236
|
+
visitedEffects.add(name);
|
|
237
|
+
out.set(name, { kind: "scripted_effect", via: step.via, file: target.file, line: target.line });
|
|
238
|
+
if (hop < MAX_HOPS) next.push({ def: target, via: [...step.via, name] });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (out.has(name)) return;
|
|
242
|
+
const target = data.index.lookup(name).find((d) => d.kind === kind);
|
|
243
|
+
out.set(name, {
|
|
244
|
+
kind,
|
|
245
|
+
via: step.via,
|
|
246
|
+
file: target?.file,
|
|
247
|
+
// Unindexed target (a vanilla event a mod fires without defining):
|
|
248
|
+
// the reference's own line is the only site there is.
|
|
249
|
+
line: target?.line ?? parsed.lineOf(offset),
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
frontier = next;
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The parsed block of a top-level definition, file reads memoized per request. */
|
|
259
|
+
function blockOfDefinition(
|
|
260
|
+
def: Definition,
|
|
261
|
+
reads: Map<string, ParsedFile | null>
|
|
262
|
+
): { block: BlockNode; lineOf: (offset: number) => number } | null {
|
|
263
|
+
let parsed: ParsedFile | null | undefined = reads.get(def.file);
|
|
264
|
+
if (parsed === undefined) {
|
|
265
|
+
parsed = null;
|
|
266
|
+
try {
|
|
267
|
+
const text = decode(fs.readFileSync(def.file)).text;
|
|
268
|
+
const li = new LineIndex(text);
|
|
269
|
+
parsed = { root: parseScript(text).root, lineOf: (o) => li.positionAt(o).line };
|
|
270
|
+
} catch {
|
|
271
|
+
/* unreadable: memoized as a miss so it is attempted once */
|
|
272
|
+
}
|
|
273
|
+
reads.set(def.file, parsed);
|
|
274
|
+
}
|
|
275
|
+
if (!parsed) return null;
|
|
276
|
+
const lineOf = parsed.lineOf;
|
|
277
|
+
const stmt = parsed.root.statements.find(
|
|
278
|
+
(s): s is Statement & { kind: "assignment" } =>
|
|
279
|
+
s.kind === "assignment" && s.key.text === def.name && lineOf(s.key.range.start) === def.line
|
|
280
|
+
);
|
|
281
|
+
const block = stmt ? childBlock(stmt) : null;
|
|
282
|
+
return block ? { block, lineOf } : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Ref-field kinds that mean "control moves here" (eventDetail's own rule). */
|
|
286
|
+
function stepKind(kinds: string[]): "event" | "on_action" | null {
|
|
287
|
+
if (kinds.includes("event")) return "event";
|
|
288
|
+
if (kinds.includes("on_action")) return "on_action";
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Walk one block, reporting every event/on_action reference and every
|
|
294
|
+
* key-position call. Same shape as eventDetail's target walk: the profile's
|
|
295
|
+
* reference fields drive it, and `trigger_event = { id = X }` /
|
|
296
|
+
* `random_events = { 100 = X }` inherit the enclosing field. A key in call
|
|
297
|
+
* position is reported as `"call"`; only the caller's index decides whether it
|
|
298
|
+
* really names a scripted effect.
|
|
299
|
+
*/
|
|
300
|
+
function walkScriptBlock(
|
|
301
|
+
block: BlockNode,
|
|
302
|
+
refKinds: (key: string) => string[],
|
|
303
|
+
emit: (kind: "event" | "on_action" | "call", name: string, offset: number) => void,
|
|
304
|
+
inherited: { via: string; kind: "event" | "on_action" } | null = null,
|
|
305
|
+
depth = 0
|
|
306
|
+
): void {
|
|
307
|
+
if (depth > MAX_BLOCK_DEPTH) return;
|
|
308
|
+
for (const s of block.statements) {
|
|
309
|
+
if (s.kind === "value") {
|
|
310
|
+
// A bare list entry inherits the enclosing field (`events = { a b }`).
|
|
311
|
+
if (inherited && s.value.kind === "scalar" && !s.value.quoted) {
|
|
312
|
+
emit(inherited.kind, s.value.text, s.value.range.start);
|
|
313
|
+
} else if (s.value.kind === "block") {
|
|
314
|
+
walkScriptBlock(s.value, refKinds, emit, null, depth + 1);
|
|
315
|
+
} else if (s.value.kind === "tagged-block") {
|
|
316
|
+
walkScriptBlock(s.value.block, refKinds, emit, null, depth + 1);
|
|
317
|
+
}
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const key = s.key.quoted ? "" : s.key.text.toLowerCase();
|
|
321
|
+
const own = stepKind(refKinds(key));
|
|
322
|
+
const field = own ? { via: key, kind: own } : null;
|
|
323
|
+
const v = s.value;
|
|
324
|
+
if (field && v?.kind === "scalar" && !v.quoted) {
|
|
325
|
+
emit(field.kind, v.text, v.range.start);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
if (inherited && v?.kind === "scalar" && !v.quoted && (/^\d+(\.\d+)?$/.test(key) || key === "id")) {
|
|
329
|
+
emit(inherited.kind, v.text, v.range.start);
|
|
330
|
+
}
|
|
331
|
+
// A scripted effect is invoked as a bare key, either `name = yes` or
|
|
332
|
+
// `name = { PARAM = … }`; both spellings reach here.
|
|
333
|
+
if (!field && key !== "" && (v === null || v.kind !== "scalar" || v.text.toLowerCase() === "yes")) {
|
|
334
|
+
emit("call", s.key.text, s.key.range.start);
|
|
335
|
+
}
|
|
336
|
+
const sub = childBlock(s);
|
|
337
|
+
if (sub) walkScriptBlock(sub, refKinds, emit, field, depth + 1);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function childBlock(stmt: Statement): BlockNode | null {
|
|
342
|
+
if (stmt.kind !== "assignment") return null;
|
|
343
|
+
const v = stmt.value;
|
|
344
|
+
if (!v) return null;
|
|
345
|
+
if (v.kind === "block") return v;
|
|
346
|
+
if (v.kind === "tagged-block") return v.block;
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function scriptedGuiDef(data: ServerData, name: string): Definition | null {
|
|
351
|
+
return data.index.lookup(name).find((d) => d.kind === "scripted_gui") ?? null;
|
|
352
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one link a `.gui` file has to script: `GetScriptedGui('name')`.
|
|
3
|
+
*
|
|
4
|
+
* PdxGui cannot call an event or an effect. It calls a scripted_gui, whose own
|
|
5
|
+
* blocks then reach the rest of the script database, so every GUI-to-script
|
|
6
|
+
* path starts here. Verified over both supported titles' vanilla trees (the
|
|
7
|
+
* only spelling in use is the datafunction, with either quote style);
|
|
8
|
+
* `window_name`, the Imperator-era binding, appears in neither tree.
|
|
9
|
+
*
|
|
10
|
+
* The index is built by the layout service's file walk, which already reads
|
|
11
|
+
* every `.gui` file for the template/type store, so the marginal cost is a
|
|
12
|
+
* substring test per file plus a regex over the handful that match.
|
|
13
|
+
*
|
|
14
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
15
|
+
*/
|
|
16
|
+
import { LineIndex } from "../parser";
|
|
17
|
+
|
|
18
|
+
/** One `GetScriptedGui(...)` call site. */
|
|
19
|
+
export interface GuiScriptedGuiCall {
|
|
20
|
+
/** Absolute path of the `.gui` file, or whatever label the caller passed. */
|
|
21
|
+
file: string;
|
|
22
|
+
/** 0-based line of the call. */
|
|
23
|
+
line: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface GuiScriptLinks {
|
|
27
|
+
/** scripted_gui name -> every call site naming it, file order. */
|
|
28
|
+
calls: Map<string, GuiScriptedGuiCall[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function emptyGuiScriptLinks(): GuiScriptLinks {
|
|
32
|
+
return { calls: new Map() };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The cheap test that lets a non-matching file skip the regex and the LineIndex. */
|
|
36
|
+
const MARKER = "GetScriptedGui";
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* `GetScriptedGui('name')` / `GetScriptedGui("name")`, whitespace tolerated
|
|
40
|
+
* around the argument. The name charset is the script-definition one.
|
|
41
|
+
*/
|
|
42
|
+
const CALL = /GetScriptedGui\s*\(\s*(['"])([A-Za-z0-9_][A-Za-z0-9_.-]*)\1\s*\)/g;
|
|
43
|
+
|
|
44
|
+
/** Every scripted_gui `text` calls, with the 0-based line of each call. */
|
|
45
|
+
export function findScriptedGuiCalls(text: string): { name: string; line: number }[] {
|
|
46
|
+
if (!text.includes(MARKER)) return [];
|
|
47
|
+
const lines = new LineIndex(text);
|
|
48
|
+
const out: { name: string; line: number }[] = [];
|
|
49
|
+
CALL.lastIndex = 0;
|
|
50
|
+
let m: RegExpExecArray | null;
|
|
51
|
+
while ((m = CALL.exec(text)) !== null) {
|
|
52
|
+
out.push({ name: m[2], line: lines.positionAt(m.index).line });
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Merge one file's calls into a links index. */
|
|
58
|
+
export function collectScriptedGuiCalls(text: string, file: string, into: GuiScriptLinks): void {
|
|
59
|
+
for (const call of findScriptedGuiCalls(text)) {
|
|
60
|
+
const list = into.calls.get(call.name);
|
|
61
|
+
if (list) list.push({ file, line: call.line });
|
|
62
|
+
else into.calls.set(call.name, [{ file, line: call.line }]);
|
|
63
|
+
}
|
|
64
|
+
}
|