@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,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Victoria 3 block-schema `structure` layer (update plan v1.1 §B2): the
|
|
3
|
+
* document shape of each definition kind, the structural keys every modder
|
|
4
|
+
* types most, which come from neither script_docs tokens nor the definition
|
|
5
|
+
* index. Without this layer, typing at a definition's top level offered only
|
|
6
|
+
* the flat engine-token soup, which is why Vic3 completion ranked far below
|
|
7
|
+
* CK3's before 0.3.2.
|
|
8
|
+
*
|
|
9
|
+
* SOURCE: fully harvested, no hand curation. scripts/build-structures-json.ts
|
|
10
|
+
* reads the 91 `*.md` schema docs Victoria 3 ships next to its data
|
|
11
|
+
* (common/buildings/buildings.md and friends, the same `key = value # doc`
|
|
12
|
+
* shape as CK3's `_*.info`) and cross-checks every key against real depth-1
|
|
13
|
+
* usage in the vanilla corpus, which also supplies the `freq` counts that drive
|
|
14
|
+
* completion ranking and the vocabulary for the folders that ship no doc at all
|
|
15
|
+
* (events and decisions among them).
|
|
16
|
+
*
|
|
17
|
+
* Static bundled JSON on purpose: users may not have gamePath set, so nothing is
|
|
18
|
+
* harvested at runtime. Regenerate per game patch with:
|
|
19
|
+
* node dist/build-structures-json.cjs --game vic3
|
|
20
|
+
*/
|
|
21
|
+
import type { KeySpec, StructureSpec } from "../../schema/types";
|
|
22
|
+
import HARVESTED_JSON from "../../../data/vic3/structures.json";
|
|
23
|
+
|
|
24
|
+
interface HarvestedShape {
|
|
25
|
+
sources: Record<string, string>;
|
|
26
|
+
kinds: Record<string, { topLevel: KeySpec[]; blocks?: Record<string, KeySpec[]> }>;
|
|
27
|
+
}
|
|
28
|
+
const HARVESTED = HARVESTED_JSON as unknown as HarvestedShape;
|
|
29
|
+
|
|
30
|
+
/** Provenance shown in hover, keyed by schema kind (the doc folder's name). */
|
|
31
|
+
export const VIC3_STRUCTURE_SOURCES: Record<string, string> = HARVESTED.sources;
|
|
32
|
+
|
|
33
|
+
export const VIC3_STRUCTURES: Record<string, StructureSpec> = HARVESTED.kinds;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `parentanchor` / `widgetanchor` vocabulary, and the fractions the layout
|
|
3
|
+
* engine turns it into.
|
|
4
|
+
*
|
|
5
|
+
* A dependency-free leaf like `fillGeometry.ts`, and for the same reason: the
|
|
6
|
+
* engine imports it to place widgets and the webview's anchor picker imports it
|
|
7
|
+
* to OFFER anchors. One table means the picker cannot offer a word the engine
|
|
8
|
+
* does not parse, which would write a value the game silently ignores.
|
|
9
|
+
*
|
|
10
|
+
* The engine reads a spec as `|`-separated words, in either order, and ignores
|
|
11
|
+
* anything it does not know (B1-B/C). Every word here is one the engine reads.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** The horizontal words, left to right: fraction = index / 2. */
|
|
15
|
+
export const ANCHOR_X = ["left", "hcenter", "right"] as const;
|
|
16
|
+
/** The vertical words, top to bottom: fraction = index / 2. */
|
|
17
|
+
export const ANCHOR_Y = ["top", "vcenter", "bottom"] as const;
|
|
18
|
+
/** The one word that sets BOTH axes to the middle. */
|
|
19
|
+
export const ANCHOR_CENTER = "center";
|
|
20
|
+
|
|
21
|
+
export type AnchorX = (typeof ANCHOR_X)[number];
|
|
22
|
+
export type AnchorY = (typeof ANCHOR_Y)[number];
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `parentanchor`/`widgetanchor` -> fractional point (0 = left/top, 1 =
|
|
26
|
+
* right/bottom). An empty or unknown spec anchors at the origin, which is the
|
|
27
|
+
* engine's own default.
|
|
28
|
+
*/
|
|
29
|
+
export function anchorFractions(spec: string | undefined): [number, number] {
|
|
30
|
+
let fx = 0;
|
|
31
|
+
let fy = 0;
|
|
32
|
+
if (!spec) return [0, 0];
|
|
33
|
+
for (const raw of spec.toLowerCase().split("|")) {
|
|
34
|
+
const part = raw.trim();
|
|
35
|
+
if (part === ANCHOR_CENTER) {
|
|
36
|
+
fx = 0.5;
|
|
37
|
+
fy = 0.5;
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const x = ANCHOR_X.indexOf(part as AnchorX);
|
|
41
|
+
if (x >= 0) {
|
|
42
|
+
fx = x / 2;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const y = ANCHOR_Y.indexOf(part as AnchorY);
|
|
46
|
+
if (y >= 0) fy = y / 2;
|
|
47
|
+
}
|
|
48
|
+
return [fx, fy];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The spec a 9-point picker writes for one cell. `center` for the middle,
|
|
53
|
+
* because the engine has that exact word and vanilla writes it; everything else
|
|
54
|
+
* is `<vertical>|<horizontal>`, the order vanilla uses (`bottom|right`), though
|
|
55
|
+
* the engine reads either.
|
|
56
|
+
*/
|
|
57
|
+
export function anchorSpec(x: AnchorX, y: AnchorY): string {
|
|
58
|
+
if (x === "hcenter" && y === "vcenter") return ANCHOR_CENTER;
|
|
59
|
+
return `${y}|${x}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The cell a written spec lands on, so a picker can show what the file says. */
|
|
63
|
+
export function anchorCell(spec: string | undefined): { x: AnchorX; y: AnchorY } {
|
|
64
|
+
const [fx, fy] = anchorFractions(spec);
|
|
65
|
+
return { x: ANCHOR_X[Math.round(fx * 2)], y: ANCHOR_Y[Math.round(fy * 2)] };
|
|
66
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The words that make a `.gui` statement a DECLARATION rather than a widget or
|
|
3
|
+
* a property, shared by the source model and the layout engine.
|
|
4
|
+
*
|
|
5
|
+
* A dependency-free leaf, like `fillGeometry.ts`, and for the same reason: the
|
|
6
|
+
* writer's reorder/insert/delete sibling list (`GuiBody.children`) and the
|
|
7
|
+
* engine's `srcIndex` for a laid-out node have to count the SAME entries. Two
|
|
8
|
+
* copies of this set would drift, and the drift would show up as a reorder
|
|
9
|
+
* moving the wrong block.
|
|
10
|
+
*
|
|
11
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/** Marker words that turn the FOLLOWING assignment into a declaration. */
|
|
15
|
+
export const DECL_MARKERS = new Set([
|
|
16
|
+
"template",
|
|
17
|
+
"local_template",
|
|
18
|
+
"types",
|
|
19
|
+
"type",
|
|
20
|
+
"block",
|
|
21
|
+
"blockoverride",
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Named slots, which vanilla spells two ways: `blockoverride "name" { ... }`
|
|
26
|
+
* (272 uses in the game tree, the marker form) and `blockoverride = "name"
|
|
27
|
+
* { ... }` (29 uses, one assignment whose value is a tagged block). Both are
|
|
28
|
+
* declarations, so both count as one sibling.
|
|
29
|
+
*/
|
|
30
|
+
export const SLOT_KEYS = new Set(["block", "blockoverride"]);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sprite fill geometry: nine-slice regions and frame-sheet cells. Deterministic
|
|
3
|
+
* arithmetic over a rect plus the source texture's pixel size, with no engine
|
|
4
|
+
* state behind it, which is why it lives in its own dependency-free leaf module:
|
|
5
|
+
* the layout engine re-exports it, and the webview GUI editor bundles it
|
|
6
|
+
* DIRECTLY into the canvas renderer. One implementation means the drawn pixels
|
|
7
|
+
* cannot drift from the measured rules (parity-checklist.md L21a-d, L22).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The destination rect a fill covers; structurally LayoutRect. */
|
|
11
|
+
export interface FillRect {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Nine-slice (corneredtiled/corneredstretched) region layout: corners drawn
|
|
20
|
+
* 1:1, edges stretched on one axis, center on both. Pure deterministic
|
|
21
|
+
* geometry — border widths come from the .gui `spriteborder` attributes and
|
|
22
|
+
* the source texture's own pixel size. Returns 9 src->dst blits in row-major
|
|
23
|
+
* order (TL, T, TR, L, C, R, BL, B, BR); zero-area slices are dropped.
|
|
24
|
+
*/
|
|
25
|
+
export interface NineSliceRegion {
|
|
26
|
+
sx: number;
|
|
27
|
+
sy: number;
|
|
28
|
+
sw: number;
|
|
29
|
+
sh: number;
|
|
30
|
+
dx: number;
|
|
31
|
+
dy: number;
|
|
32
|
+
dw: number;
|
|
33
|
+
dh: number;
|
|
34
|
+
}
|
|
35
|
+
export function computeNineSlice(
|
|
36
|
+
rect: FillRect,
|
|
37
|
+
border: [number, number, number, number],
|
|
38
|
+
texW: number,
|
|
39
|
+
texH: number
|
|
40
|
+
): NineSliceRegion[] {
|
|
41
|
+
// Clamp borders so opposite sides never overlap on a small texture or rect.
|
|
42
|
+
const bl = Math.max(0, Math.min(border[0], texW, rect.w));
|
|
43
|
+
const bt = Math.max(0, Math.min(border[1], texH, rect.h));
|
|
44
|
+
const br = Math.max(0, Math.min(border[2], texW - bl, rect.w - bl));
|
|
45
|
+
const bb = Math.max(0, Math.min(border[3], texH - bt, rect.h - bt));
|
|
46
|
+
// Source and destination column/row spans: [start, size] triples.
|
|
47
|
+
const sCols: [number, number][] = [
|
|
48
|
+
[0, bl],
|
|
49
|
+
[bl, texW - bl - br],
|
|
50
|
+
[texW - br, br],
|
|
51
|
+
];
|
|
52
|
+
const sRows: [number, number][] = [
|
|
53
|
+
[0, bt],
|
|
54
|
+
[bt, texH - bt - bb],
|
|
55
|
+
[texH - bb, bb],
|
|
56
|
+
];
|
|
57
|
+
const dCols: [number, number][] = [
|
|
58
|
+
[rect.x, bl],
|
|
59
|
+
[rect.x + bl, rect.w - bl - br],
|
|
60
|
+
[rect.x + rect.w - br, br],
|
|
61
|
+
];
|
|
62
|
+
const dRows: [number, number][] = [
|
|
63
|
+
[rect.y, bt],
|
|
64
|
+
[rect.y + bt, rect.h - bt - bb],
|
|
65
|
+
[rect.y + rect.h - bb, bb],
|
|
66
|
+
];
|
|
67
|
+
const out: NineSliceRegion[] = [];
|
|
68
|
+
for (let r = 0; r < 3; r++) {
|
|
69
|
+
for (let c = 0; c < 3; c++) {
|
|
70
|
+
const [sx, sw] = sCols[c];
|
|
71
|
+
const [sy, sh] = sRows[r];
|
|
72
|
+
const [dx, dw] = dCols[c];
|
|
73
|
+
const [dy, dh] = dRows[r];
|
|
74
|
+
if (sw <= 0 || sh <= 0 || dw <= 0 || dh <= 0) continue;
|
|
75
|
+
out.push({ sx, sy, sw, sh, dx, dy, dw, dh });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Frame-sheet cell for `framesize = { w h }` + `frame = N`: the texture is a
|
|
83
|
+
* cols x rows GRID indexed ROW-MAJOR and 1-based, so frame 4 on a 3-wide sheet
|
|
84
|
+
* is the first cell of the SECOND row, not a fourth column. `frame <= 0`
|
|
85
|
+
* clamps to the first cell, a frame past the last clamps to the last.
|
|
86
|
+
* (Studio §L, in-game 2026-07-17; L22.) Deterministic geometry like
|
|
87
|
+
* computeNineSlice: the texture's pixel size comes from the renderer.
|
|
88
|
+
*/
|
|
89
|
+
export function computeFrameCell(
|
|
90
|
+
framesize: [number, number],
|
|
91
|
+
frame: number,
|
|
92
|
+
texW: number,
|
|
93
|
+
texH: number
|
|
94
|
+
): { sx: number; sy: number; sw: number; sh: number } {
|
|
95
|
+
const [fw, fh] = framesize;
|
|
96
|
+
if (fw <= 0 || fh <= 0) return { sx: 0, sy: 0, sw: texW, sh: texH };
|
|
97
|
+
const cols = Math.max(1, Math.floor(texW / fw));
|
|
98
|
+
const rows = Math.max(1, Math.floor(texH / fh));
|
|
99
|
+
const index = Math.min(Math.max(Math.floor(frame) - 1, 0), cols * rows - 1);
|
|
100
|
+
return { sx: (index % cols) * fw, sy: Math.floor(index / cols) * fh, sw: fw, sh: fh };
|
|
101
|
+
}
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdxGui reuse-vocabulary store: `template` / `local_template` property
|
|
3
|
+
* snippets (spliced via `using = Name`) and `types Group { type name = base
|
|
4
|
+
* {...} }` widget classes, collected across files with the gui/ override
|
|
5
|
+
* rule: FIOS, the FIRST definition wins (opposite of script's LIOS; see
|
|
6
|
+
* AGENTS.md).
|
|
7
|
+
*
|
|
8
|
+
* The layout engine consumes this store to expand instances; collection is
|
|
9
|
+
* separate so callers control the file set and order (vanilla + mod).
|
|
10
|
+
*
|
|
11
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
12
|
+
*/
|
|
13
|
+
import { LineIndex, parseScript, type BlockNode, type Statement } from "../parser";
|
|
14
|
+
|
|
15
|
+
export interface GuiTypeDef {
|
|
16
|
+
/** The base widget/type key after `=`, e.g. `textbox` in `type text_single = textbox {}`. */
|
|
17
|
+
base: string;
|
|
18
|
+
block: BlockNode;
|
|
19
|
+
/**
|
|
20
|
+
* Offset of the declared NAME token in the text this def was collected from.
|
|
21
|
+
* Always present (unlike `line`, which needs a file path), so a caller
|
|
22
|
+
* holding the same text can turn a declaration back into a position.
|
|
23
|
+
*/
|
|
24
|
+
keyOffset: number;
|
|
25
|
+
/** Declaration site, present when the collector was given a file path. */
|
|
26
|
+
file?: string;
|
|
27
|
+
line?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface GuiTemplateDef {
|
|
31
|
+
block: BlockNode;
|
|
32
|
+
/** local_template: valid only inside its defining file; mergeGuiDefs skips it. */
|
|
33
|
+
local: boolean;
|
|
34
|
+
/** Declaration site, present when the collector was given a file path. */
|
|
35
|
+
file?: string;
|
|
36
|
+
line?: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface GuiDefs {
|
|
40
|
+
types: Map<string, GuiTypeDef>;
|
|
41
|
+
templates: Map<string, GuiTemplateDef>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function emptyGuiDefs(): GuiDefs {
|
|
45
|
+
return { types: new Map(), templates: new Map() };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DECL_MARKERS = new Set(["template", "local_template", "types", "type"]);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collect one file's declarations (including its local_templates). Pass `file`
|
|
52
|
+
* to record each declaration's site (file + line) for navigation features.
|
|
53
|
+
*/
|
|
54
|
+
export function collectGuiDefs(text: string, into?: GuiDefs, file?: string): GuiDefs {
|
|
55
|
+
const lines = file !== undefined ? new LineIndex(text) : null;
|
|
56
|
+
return collectGuiDefsParsed(parseScript(text).root.statements, into, file, lines);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Same as collectGuiDefs but over an already-parsed tree (parseCache reuse). */
|
|
60
|
+
export function collectGuiDefsParsed(
|
|
61
|
+
statements: Statement[],
|
|
62
|
+
into?: GuiDefs,
|
|
63
|
+
file?: string,
|
|
64
|
+
lines?: LineIndex | null
|
|
65
|
+
): GuiDefs {
|
|
66
|
+
const defs = into ?? emptyGuiDefs();
|
|
67
|
+
collectFrom(statements, defs, false, file, lines);
|
|
68
|
+
return defs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function collectFrom(
|
|
72
|
+
statements: Statement[],
|
|
73
|
+
defs: GuiDefs,
|
|
74
|
+
insideTypes: boolean,
|
|
75
|
+
file?: string,
|
|
76
|
+
lines?: LineIndex | null
|
|
77
|
+
): void {
|
|
78
|
+
let pending: string | null = null;
|
|
79
|
+
const siteOf = (offset: number): { file?: string; line?: number } =>
|
|
80
|
+
file !== undefined && lines ? { file, line: lines.positionAt(offset).line } : {};
|
|
81
|
+
for (const stmt of statements) {
|
|
82
|
+
if (stmt.kind === "value") {
|
|
83
|
+
if (stmt.value.kind === "scalar" && DECL_MARKERS.has(stmt.value.text.toLowerCase())) {
|
|
84
|
+
pending = stmt.value.text.toLowerCase();
|
|
85
|
+
} else {
|
|
86
|
+
pending = null;
|
|
87
|
+
}
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
// assignment
|
|
91
|
+
const marker = pending;
|
|
92
|
+
pending = null;
|
|
93
|
+
if (!marker && !insideTypes) continue;
|
|
94
|
+
const name = stmt.key.text;
|
|
95
|
+
if (marker === "template" || marker === "local_template") {
|
|
96
|
+
if (stmt.value?.kind === "block" && !defs.templates.has(name)) {
|
|
97
|
+
defs.templates.set(name, {
|
|
98
|
+
block: stmt.value,
|
|
99
|
+
local: marker === "local_template",
|
|
100
|
+
...siteOf(stmt.key.range.start),
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
} else if (marker === "types") {
|
|
104
|
+
if (stmt.value?.kind === "block") {
|
|
105
|
+
collectFrom(stmt.value.statements, defs, true, file, lines);
|
|
106
|
+
}
|
|
107
|
+
} else if (marker === "type" || insideTypes) {
|
|
108
|
+
// `type name = base { ... }` parses as name = tagged-block(base).
|
|
109
|
+
// Type names are stored lowercase (widget keys are matched
|
|
110
|
+
// case-insensitively); template names stay exact-case like `using` refs.
|
|
111
|
+
const lower = name.toLowerCase();
|
|
112
|
+
if (stmt.value?.kind === "tagged-block" && !defs.types.has(lower)) {
|
|
113
|
+
defs.types.set(lower, {
|
|
114
|
+
base: stmt.value.tag.text,
|
|
115
|
+
block: stmt.value.block,
|
|
116
|
+
keyOffset: stmt.key.range.start,
|
|
117
|
+
...siteOf(stmt.key.range.start),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Merge `source` into `target` under FIOS: existing (earlier-file) entries
|
|
126
|
+
* win, and local_templates never cross files. Call in game load order
|
|
127
|
+
* (path-sorted file list).
|
|
128
|
+
*/
|
|
129
|
+
export function mergeGuiDefs(target: GuiDefs, source: GuiDefs): void {
|
|
130
|
+
for (const [name, def] of source.types) {
|
|
131
|
+
if (!target.types.has(name)) target.types.set(name, def);
|
|
132
|
+
}
|
|
133
|
+
for (const [name, def] of source.templates) {
|
|
134
|
+
if (!def.local && !target.templates.has(name)) target.templates.set(name, def);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
// Expansion
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
/** Guards recursive templates/types (a self-referencing type hard-crashes the game). */
|
|
143
|
+
const MAX_DEPTH = 32;
|
|
144
|
+
|
|
145
|
+
/** One step of the chain a statement was spliced through (innermost first). */
|
|
146
|
+
export interface GuiOriginStep {
|
|
147
|
+
kind: "type" | "template";
|
|
148
|
+
name: string;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** An expanded statement plus where it came from ([] = the instance's own body). */
|
|
152
|
+
export interface OriginedStatement {
|
|
153
|
+
stmt: Statement;
|
|
154
|
+
origin: GuiOriginStep[];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Resolve a widget key's type chain and produce the effective statement list:
|
|
159
|
+
* base-most type first, derived types after, instance statements last (so
|
|
160
|
+
* later statements override scalar props and draw on top). `using =`
|
|
161
|
+
* template references are spliced inline at their position.
|
|
162
|
+
*/
|
|
163
|
+
export function expandWidget(
|
|
164
|
+
key: string,
|
|
165
|
+
instance: BlockNode,
|
|
166
|
+
defs: GuiDefs,
|
|
167
|
+
/** Skip the type chain (recursion guard) but still splice templates. */
|
|
168
|
+
skipChain = false
|
|
169
|
+
): { baseKey: string; statements: Statement[] } {
|
|
170
|
+
const expanded = expandWidgetWithOrigins(key, instance, defs, skipChain);
|
|
171
|
+
return { baseKey: expanded.baseKey, statements: expanded.statements.map((s) => s.stmt) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* {@link expandWidget} with provenance: the same expansion, each statement
|
|
176
|
+
* labelled with the definitions it was spliced through. The inspector reads
|
|
177
|
+
* this so a property row can say where its value came from, and it goes through
|
|
178
|
+
* the one expansion the engine itself uses rather than a second walk that could
|
|
179
|
+
* disagree with the rendering.
|
|
180
|
+
*/
|
|
181
|
+
export function expandWidgetWithOrigins(
|
|
182
|
+
key: string,
|
|
183
|
+
instance: BlockNode,
|
|
184
|
+
defs: GuiDefs,
|
|
185
|
+
skipChain = false
|
|
186
|
+
): { baseKey: string; statements: OriginedStatement[] } {
|
|
187
|
+
const chain: { block: BlockNode; origin: GuiOriginStep[] }[] = [];
|
|
188
|
+
let base = key.toLowerCase();
|
|
189
|
+
const seen = new Set<string>();
|
|
190
|
+
while (!skipChain && defs.types.has(base) && !seen.has(base)) {
|
|
191
|
+
seen.add(base);
|
|
192
|
+
const def = defs.types.get(base)!;
|
|
193
|
+
chain.unshift({ block: def.block, origin: [{ kind: "type", name: base }] });
|
|
194
|
+
base = def.base.toLowerCase();
|
|
195
|
+
}
|
|
196
|
+
const statements: OriginedStatement[] = [];
|
|
197
|
+
for (const entry of chain) spliceTemplates(entry.block.statements, defs, statements, 0, entry.origin);
|
|
198
|
+
spliceTemplates(instance.statements, defs, statements, 0, []);
|
|
199
|
+
return { baseKey: base, statements };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function spliceTemplates(
|
|
203
|
+
statements: Statement[],
|
|
204
|
+
defs: GuiDefs,
|
|
205
|
+
out: OriginedStatement[],
|
|
206
|
+
depth: number,
|
|
207
|
+
origin: GuiOriginStep[]
|
|
208
|
+
): void {
|
|
209
|
+
if (depth > MAX_DEPTH) return;
|
|
210
|
+
for (const stmt of statements) {
|
|
211
|
+
if (
|
|
212
|
+
stmt.kind === "assignment" &&
|
|
213
|
+
stmt.key.text.toLowerCase() === "using" &&
|
|
214
|
+
stmt.value?.kind === "scalar"
|
|
215
|
+
) {
|
|
216
|
+
const name = stmt.value.text;
|
|
217
|
+
const tpl = defs.templates.get(name);
|
|
218
|
+
if (tpl) {
|
|
219
|
+
spliceTemplates(tpl.block.statements, defs, out, depth + 1, [{ kind: "template", name }, ...origin]);
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
// Unknown template: keep the statement (the game logs one error and
|
|
223
|
+
// renders nothing for it; downstream treats it as an inert property).
|
|
224
|
+
}
|
|
225
|
+
out.push({ stmt, origin });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
// Navigation helpers (go-to-definition, hover)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
/** A resolved template/type declaration plus what it is. */
|
|
234
|
+
export interface ResolvedGuiDef {
|
|
235
|
+
kind: "template" | "type";
|
|
236
|
+
name: string;
|
|
237
|
+
def: GuiTypeDef | GuiTemplateDef;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolve a name against an ordered list of def stores (current document
|
|
242
|
+
* first, then the cross-file FIOS store). Templates match exact-case (like
|
|
243
|
+
* `using =` references); types match lowercased (like widget keys).
|
|
244
|
+
*/
|
|
245
|
+
export function resolveGuiDef(name: string, sources: GuiDefs[]): ResolvedGuiDef | null {
|
|
246
|
+
for (const defs of sources) {
|
|
247
|
+
const tpl = defs.templates.get(name);
|
|
248
|
+
if (tpl) return { kind: "template", name, def: tpl };
|
|
249
|
+
}
|
|
250
|
+
const lower = name.toLowerCase();
|
|
251
|
+
for (const defs of sources) {
|
|
252
|
+
const type = defs.types.get(lower);
|
|
253
|
+
if (type) return { kind: "type", name: lower, def: type };
|
|
254
|
+
}
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A `block "name"` site inside a template/type body (offset is into `file`'s text). */
|
|
259
|
+
export interface GuiBlockSite {
|
|
260
|
+
name: string;
|
|
261
|
+
file?: string;
|
|
262
|
+
offset: number;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* All block names a widget built from `root` can override with
|
|
267
|
+
* `blockoverride "name"`: `block` declarations in the body itself, in
|
|
268
|
+
* templates spliced via `using =` (recursively), and, for types, up the
|
|
269
|
+
* base-type chain. First site found per name wins (derived-most, matching
|
|
270
|
+
* FIOS spirit); `blockoverride` declarations inside templates re-expose the
|
|
271
|
+
* same name and count too.
|
|
272
|
+
*/
|
|
273
|
+
export function collectOverridableBlocks(
|
|
274
|
+
root: ResolvedGuiDef,
|
|
275
|
+
sources: GuiDefs[]
|
|
276
|
+
): Map<string, GuiBlockSite> {
|
|
277
|
+
const out = new Map<string, GuiBlockSite>();
|
|
278
|
+
const seen = new Set<string>();
|
|
279
|
+
const walk = (statements: Statement[], file: string | undefined, depth: number): void => {
|
|
280
|
+
if (depth > MAX_DEPTH) return;
|
|
281
|
+
let pending = false;
|
|
282
|
+
for (const stmt of statements) {
|
|
283
|
+
if (stmt.kind === "value") {
|
|
284
|
+
pending =
|
|
285
|
+
stmt.value.kind === "scalar" &&
|
|
286
|
+
(stmt.value.text.toLowerCase() === "block" || stmt.value.text.toLowerCase() === "blockoverride");
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
const wasBlockDecl = pending;
|
|
290
|
+
pending = false;
|
|
291
|
+
if (wasBlockDecl && stmt.value?.kind === "block") {
|
|
292
|
+
if (!out.has(stmt.key.text)) {
|
|
293
|
+
out.set(stmt.key.text, { name: stmt.key.text, file, offset: stmt.key.range.start });
|
|
294
|
+
}
|
|
295
|
+
// Blocks nest (a block's body can declare further blocks).
|
|
296
|
+
walk(stmt.value.statements, file, depth + 1);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (
|
|
300
|
+
stmt.kind === "assignment" &&
|
|
301
|
+
stmt.key.text.toLowerCase() === "using" &&
|
|
302
|
+
stmt.value?.kind === "scalar"
|
|
303
|
+
) {
|
|
304
|
+
const tplName = stmt.value.text;
|
|
305
|
+
if (!seen.has(`t:${tplName}`)) {
|
|
306
|
+
seen.add(`t:${tplName}`);
|
|
307
|
+
for (const defs of sources) {
|
|
308
|
+
const tpl = defs.templates.get(tplName);
|
|
309
|
+
if (tpl) {
|
|
310
|
+
walk(tpl.block.statements, tpl.file, depth + 1);
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
const inner = stmt.value;
|
|
318
|
+
if (inner?.kind === "block") walk(inner.statements, file, depth + 1);
|
|
319
|
+
else if (inner?.kind === "tagged-block") walk(inner.block.statements, file, depth + 1);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
walk(root.def.block.statements, root.def.file, 0);
|
|
324
|
+
if (root.kind === "type") {
|
|
325
|
+
let base = (root.def as GuiTypeDef).base.toLowerCase();
|
|
326
|
+
let depth = 0;
|
|
327
|
+
while (depth++ < MAX_DEPTH && !seen.has(`y:${base}`)) {
|
|
328
|
+
seen.add(`y:${base}`);
|
|
329
|
+
let next: GuiTypeDef | null = null;
|
|
330
|
+
for (const defs of sources) {
|
|
331
|
+
const t = defs.types.get(base);
|
|
332
|
+
if (t) {
|
|
333
|
+
next = t;
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (!next) break;
|
|
338
|
+
walk(next.block.statements, next.file, 0);
|
|
339
|
+
base = next.base.toLowerCase();
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/** The base-type chain of a type, derived-first (for hover display). */
|
|
346
|
+
export function typeBaseChain(name: string, sources: GuiDefs[]): string[] {
|
|
347
|
+
const chain: string[] = [];
|
|
348
|
+
let base = name.toLowerCase();
|
|
349
|
+
const seen = new Set<string>();
|
|
350
|
+
while (!seen.has(base) && chain.length <= MAX_DEPTH) {
|
|
351
|
+
seen.add(base);
|
|
352
|
+
let def: GuiTypeDef | null = null;
|
|
353
|
+
for (const defs of sources) {
|
|
354
|
+
const t = defs.types.get(base);
|
|
355
|
+
if (t) {
|
|
356
|
+
def = t;
|
|
357
|
+
break;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
if (!def) break;
|
|
361
|
+
chain.push(def.base);
|
|
362
|
+
base = def.base.toLowerCase();
|
|
363
|
+
}
|
|
364
|
+
return chain;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Scan an expanded statement list for `blockoverride "name" { ... }` pairs.
|
|
369
|
+
* Later declarations win (instance statements come after type statements in
|
|
370
|
+
* expandWidget order, so instance overrides beat type-internal ones).
|
|
371
|
+
*/
|
|
372
|
+
export function collectBlockOverrides(statements: Statement[]): Map<string, BlockNode> {
|
|
373
|
+
const overrides = new Map<string, BlockNode>();
|
|
374
|
+
let pending = false;
|
|
375
|
+
for (const stmt of statements) {
|
|
376
|
+
if (stmt.kind === "value") {
|
|
377
|
+
pending = stmt.value.kind === "scalar" && stmt.value.text.toLowerCase() === "blockoverride";
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (pending && stmt.value?.kind === "block") {
|
|
381
|
+
overrides.set(stmt.key.text, stmt.value);
|
|
382
|
+
}
|
|
383
|
+
pending = false;
|
|
384
|
+
}
|
|
385
|
+
return overrides;
|
|
386
|
+
}
|