@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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/guiWidgetInfo backend: the properties of ONE widget, with the
|
|
3
|
+
* template/type chain each value came from. This is the designer inspector's
|
|
4
|
+
* read side.
|
|
5
|
+
*
|
|
6
|
+
* Two rules keep it honest:
|
|
7
|
+
* - It addresses the widget the way the WRITER does (`findWidgetAtLine` over
|
|
8
|
+
* the source model), so what the inspector lists is what a `setProperties`
|
|
9
|
+
* op would rewrite, on the same line, in the same file.
|
|
10
|
+
* - It resolves the widget the way the ENGINE does (`effectiveDefs` +
|
|
11
|
+
* `expandWidgetWithOrigins`), so it cannot list a value the canvas did not
|
|
12
|
+
* lay the widget out with. Last-in-wins per key, exactly as the engine reads
|
|
13
|
+
* an expanded body.
|
|
14
|
+
*
|
|
15
|
+
* Values are rendered from the parser's own tokens rather than sliced out of
|
|
16
|
+
* the document: a property inherited from a type lives in another file, whose
|
|
17
|
+
* text the def store does not keep.
|
|
18
|
+
*
|
|
19
|
+
* Three answers ride on the same expansion, so none of them can disagree with
|
|
20
|
+
* the property list: what a row OVERRIDES (the values the same key had before
|
|
21
|
+
* it), the TEXTURES the widget draws (with their frame-sheet grid), and, when
|
|
22
|
+
* the caller asks for it, the PLACEMENT trace explaining the widget's rect.
|
|
23
|
+
* The placement trace is the only one that costs a layout run, so it is the
|
|
24
|
+
* only one behind a flag.
|
|
25
|
+
*
|
|
26
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
27
|
+
*/
|
|
28
|
+
import type {
|
|
29
|
+
GuiPlacement,
|
|
30
|
+
GuiTextureInfo,
|
|
31
|
+
GuiWidgetInfo,
|
|
32
|
+
GuiWidgetOverride,
|
|
33
|
+
GuiWidgetProperty,
|
|
34
|
+
} from "@px-lsp/protocol/protocol";
|
|
35
|
+
import type { BlockNode, Statement, ValueNode } from "../parser";
|
|
36
|
+
import {
|
|
37
|
+
computeGuiLayout,
|
|
38
|
+
effectiveDefs,
|
|
39
|
+
PROPERTY_BLOCKS,
|
|
40
|
+
resolveFill,
|
|
41
|
+
type Fill,
|
|
42
|
+
type PlacementExplain,
|
|
43
|
+
} from "./layoutEngine";
|
|
44
|
+
import { expandWidgetWithOrigins, typeBaseChain, type GuiDefs } from "./guiDefs";
|
|
45
|
+
import { profileMeasurer } from "./layoutService";
|
|
46
|
+
import { findWidgetAtLine, parseGuiSource, type GuiSourceFile } from "./sourceModel";
|
|
47
|
+
import { describeTexture, type TextureRoots } from "./textureInfo";
|
|
48
|
+
|
|
49
|
+
/** Marker words whose assignment is a named slot, not a property. */
|
|
50
|
+
const SLOT_KEYS = new Set(["block", "blockoverride"]);
|
|
51
|
+
|
|
52
|
+
/** Marker words that turn the FOLLOWING assignment into a declaration. */
|
|
53
|
+
const DECL_MARKERS = new Set(["template", "local_template", "types", "type", "block", "blockoverride"]);
|
|
54
|
+
|
|
55
|
+
export interface WidgetInfoOptions {
|
|
56
|
+
/** Run the layout with an explanation trace and fill `placement`. */
|
|
57
|
+
placement?: boolean;
|
|
58
|
+
/** Roots a texture path resolves against; without them a texture row carries
|
|
59
|
+
* the path alone (no sheet size, so no grid). */
|
|
60
|
+
roots?: TextureRoots;
|
|
61
|
+
/** Viewport the placement trace lays out against; the service's own default. */
|
|
62
|
+
viewport?: { w: number; h: number };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function computeGuiWidgetInfo(
|
|
66
|
+
text: string,
|
|
67
|
+
line: number,
|
|
68
|
+
store?: GuiDefs,
|
|
69
|
+
options?: WidgetInfoOptions
|
|
70
|
+
): GuiWidgetInfo | null {
|
|
71
|
+
const file = parseGuiSource(text);
|
|
72
|
+
const target = findWidgetAtLine(file, line);
|
|
73
|
+
if (!target || !target.block) return null;
|
|
74
|
+
// A template/type DECLARATION is not an instance: expanding its own name
|
|
75
|
+
// would splice the definition into itself. The canvas never selects one.
|
|
76
|
+
if (target.marker) return null;
|
|
77
|
+
|
|
78
|
+
const defs = effectiveDefs(text, store);
|
|
79
|
+
const expanded = expandWidgetWithOrigins(target.key, target.block, defs);
|
|
80
|
+
|
|
81
|
+
// Last-in-wins per key, and the winner keeps the position it was written at:
|
|
82
|
+
// inherited rows stay where the type put them, an override moves down to the
|
|
83
|
+
// instance body, which is where its bytes are.
|
|
84
|
+
const byKey = new Map<string, GuiWidgetProperty>();
|
|
85
|
+
// The same winners as raw nodes, for the readers that need structure rather
|
|
86
|
+
// than a rendering (textures, frame sheets).
|
|
87
|
+
const rawByKey = new Map<string, ValueNode>();
|
|
88
|
+
let marker: string | null = null;
|
|
89
|
+
for (const { stmt, origin } of expanded.statements) {
|
|
90
|
+
if (stmt.kind === "value") {
|
|
91
|
+
marker =
|
|
92
|
+
stmt.value.kind === "scalar" && DECL_MARKERS.has(stmt.value.text.toLowerCase())
|
|
93
|
+
? stmt.value.text.toLowerCase()
|
|
94
|
+
: null;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const wasDecl = marker !== null;
|
|
98
|
+
marker = null;
|
|
99
|
+
if (wasDecl || !stmt.value) continue;
|
|
100
|
+
const keyLower = stmt.key.text.toLowerCase();
|
|
101
|
+
if (SLOT_KEYS.has(keyLower)) continue;
|
|
102
|
+
// The inverse rule the writer and the engine share: a block child is a
|
|
103
|
+
// widget unless its key is a known attribute block.
|
|
104
|
+
if (stmt.value.kind === "block" && !PROPERTY_BLOCKS.has(keyLower)) continue;
|
|
105
|
+
const shadowed = byKey.get(keyLower);
|
|
106
|
+
// The shadowed value is the "overrides X from template Y" note: it is the
|
|
107
|
+
// engine's own discard, recorded where the discard happens rather than
|
|
108
|
+
// reconstructed from a second walk.
|
|
109
|
+
const overrides: GuiWidgetOverride[] | undefined = shadowed
|
|
110
|
+
? [...(shadowed.overrides ?? []), { value: shadowed.value, origin: shadowed.origin }]
|
|
111
|
+
: undefined;
|
|
112
|
+
byKey.delete(keyLower);
|
|
113
|
+
byKey.set(keyLower, {
|
|
114
|
+
key: stmt.key.text,
|
|
115
|
+
value: renderValue(stmt.value),
|
|
116
|
+
origin,
|
|
117
|
+
...(overrides ? { overrides } : {}),
|
|
118
|
+
});
|
|
119
|
+
rawByKey.set(keyLower, stmt.value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const info: GuiWidgetInfo = {
|
|
123
|
+
key: target.key,
|
|
124
|
+
name: nameOf(expanded.statements),
|
|
125
|
+
typeChain: typeBaseChain(target.key, [defs]),
|
|
126
|
+
properties: [...byKey.values()],
|
|
127
|
+
textures: texturesOf(rawByKey, constantsIn(file), defs, options?.roots),
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
if (options?.placement) {
|
|
131
|
+
// The trace is what the flag gates: an ordinary layout never records it,
|
|
132
|
+
// so the default path stays at today's cost.
|
|
133
|
+
const explain: PlacementExplain = { line: target.line };
|
|
134
|
+
computeGuiLayout(text, { defs: store, viewport: options.viewport, explain, measurer: profileMeasurer() });
|
|
135
|
+
// Placement is structurally the wire type, like LayoutNode is.
|
|
136
|
+
if (explain.result) info.placement = explain.result as unknown as GuiPlacement;
|
|
137
|
+
}
|
|
138
|
+
return info;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The widget's effective `name`, last-in-wins like every other scalar. */
|
|
142
|
+
function nameOf(statements: readonly { stmt: Statement }[]): string | undefined {
|
|
143
|
+
let found: string | undefined;
|
|
144
|
+
for (const { stmt } of statements) {
|
|
145
|
+
if (stmt.kind !== "assignment" || stmt.value?.kind !== "scalar") continue;
|
|
146
|
+
if (stmt.key.text.toLowerCase() === "name") found = stmt.value.text;
|
|
147
|
+
}
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Top-level `@name = 42` constants, which a `framesize` or `frame` may use. */
|
|
152
|
+
function constantsIn(file: GuiSourceFile): Map<string, number> {
|
|
153
|
+
const consts = new Map<string, number>();
|
|
154
|
+
for (const entry of file.root.entries) {
|
|
155
|
+
if (!entry.key.startsWith("@") || entry.valueKind !== "scalar" || entry.value === null) continue;
|
|
156
|
+
const v = parseFloat(entry.value);
|
|
157
|
+
if (Number.isFinite(v)) consts.set(entry.key, v);
|
|
158
|
+
}
|
|
159
|
+
return consts;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The textures the widget draws: its own fill first, then its background. Read
|
|
164
|
+
* off the same last-in-wins winners the property list came from, and the
|
|
165
|
+
* background through the engine's own `resolveFill`, so a row here names a
|
|
166
|
+
* texture the canvas drew.
|
|
167
|
+
*/
|
|
168
|
+
function texturesOf(
|
|
169
|
+
raw: Map<string, ValueNode>,
|
|
170
|
+
consts: Map<string, number>,
|
|
171
|
+
defs: GuiDefs,
|
|
172
|
+
roots?: TextureRoots
|
|
173
|
+
): GuiTextureInfo[] {
|
|
174
|
+
const out: GuiTextureInfo[] = [];
|
|
175
|
+
const own = raw.get("texture");
|
|
176
|
+
if (own?.kind === "scalar") {
|
|
177
|
+
const framesize = numberPair(raw.get("framesize"), consts);
|
|
178
|
+
out.push(
|
|
179
|
+
describeTexture(
|
|
180
|
+
{
|
|
181
|
+
texture: own.text,
|
|
182
|
+
framesize,
|
|
183
|
+
frame: framesize ? numberOf(raw.get("frame"), consts) : undefined,
|
|
184
|
+
},
|
|
185
|
+
"fill",
|
|
186
|
+
roots
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
const background = raw.get("background");
|
|
191
|
+
const block: BlockNode | null =
|
|
192
|
+
background?.kind === "block" ? background : background?.kind === "tagged-block" ? background.block : null;
|
|
193
|
+
if (block) {
|
|
194
|
+
const fill: Fill = resolveFill(block, consts, defs);
|
|
195
|
+
if (fill.texture !== undefined) {
|
|
196
|
+
out.push(
|
|
197
|
+
describeTexture(
|
|
198
|
+
{ texture: fill.texture, framesize: fill.framesize, frame: fill.frame },
|
|
199
|
+
"background",
|
|
200
|
+
roots
|
|
201
|
+
)
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function numberOf(value: ValueNode | undefined, consts: Map<string, number>): number | undefined {
|
|
209
|
+
if (value?.kind !== "scalar") return undefined;
|
|
210
|
+
if (value.text.startsWith("@")) return consts.get(value.text);
|
|
211
|
+
const v = parseFloat(value.text);
|
|
212
|
+
return Number.isFinite(v) ? v : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function numberPair(value: ValueNode | undefined, consts: Map<string, number>): [number, number] | undefined {
|
|
216
|
+
if (value?.kind !== "block") return undefined;
|
|
217
|
+
const nums: number[] = [];
|
|
218
|
+
for (const s of value.statements) {
|
|
219
|
+
if (s.kind !== "value" || s.value.kind !== "scalar") continue;
|
|
220
|
+
const n = numberOf(s.value, consts);
|
|
221
|
+
nums.push(n ?? 0);
|
|
222
|
+
}
|
|
223
|
+
return nums.length >= 2 ? [nums[0], nums[1]] : undefined;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A value as authored, from the tokens alone. Quoted scalars keep their quotes
|
|
228
|
+
* (`.gui` distinguishes them and the row should read like the source); a block
|
|
229
|
+
* renders with single spaces, so the rendering is independent of the interior
|
|
230
|
+
* whitespace of a file this reader may not have.
|
|
231
|
+
*/
|
|
232
|
+
function renderValue(value: ValueNode): string {
|
|
233
|
+
if (value.kind === "scalar") return value.quoted ? `"${value.text}"` : value.text;
|
|
234
|
+
if (value.kind === "tagged-block") return `${value.tag.text} ${renderValue(value.block)}`;
|
|
235
|
+
const parts = value.statements.map(renderStatement);
|
|
236
|
+
return parts.length === 0 ? "{}" : `{ ${parts.join(" ")} }`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function renderStatement(stmt: Statement): string {
|
|
240
|
+
if (stmt.kind === "value") return renderValue(stmt.value);
|
|
241
|
+
const key = stmt.key.quoted ? `"${stmt.key.text}"` : stmt.key.text;
|
|
242
|
+
if (!stmt.value) return stmt.op === null ? key : `${key} ${stmt.op}`;
|
|
243
|
+
const value = renderValue(stmt.value);
|
|
244
|
+
return stmt.op === null ? `${key} ${value}` : `${key} ${stmt.op} ${value}`;
|
|
245
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PdxDoc doc comments (§E). A contiguous `#` comment block immediately above a
|
|
3
|
+
* definition (no blank line between) documents it. Plain lines are prose;
|
|
4
|
+
* `@tag` lines are structured.
|
|
5
|
+
*
|
|
6
|
+
* Extraction works off the raw file lines above a definition's line — no CST
|
|
7
|
+
* re-parse, encoding-safe (BOM/CRLF handled by the caller splitting on \n and
|
|
8
|
+
* this module trimming \r). Cost is near zero at index time.
|
|
9
|
+
*
|
|
10
|
+
* No `vscode` imports here: unit-tested in plain Node.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Recognized structured tags (§E1). Unknown `@tags` render as prose. */
|
|
14
|
+
export const KNOWN_DOC_TAGS = new Set(["scope", "param", "saves", "returns", "example", "deprecated"]);
|
|
15
|
+
|
|
16
|
+
/** One structured tag line: `@scope`, `@param NAME desc`, `@example`, … */
|
|
17
|
+
export interface DocTag {
|
|
18
|
+
/** Tag name without the leading `@` (e.g. "param", "scope"). */
|
|
19
|
+
tag: string;
|
|
20
|
+
/** Everything after the tag word (e.g. "DYNASTY_HOUSE the house…"). Empty for bare tags. */
|
|
21
|
+
text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface DocBlock {
|
|
25
|
+
/** Prose lines joined; capped at PROSE_CAP. Empty string when no prose. */
|
|
26
|
+
doc: string;
|
|
27
|
+
tags: DocTag[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Prose is capped so the index stays lean (~338 K defs on AGOT). */
|
|
31
|
+
export const PROSE_CAP = 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* True for a separator line — a comment that is only `#`/punctuation/repeats
|
|
35
|
+
* with no prose (`####`, `#---`, `# ===`, `# ***`). These are section dividers,
|
|
36
|
+
* not documentation, and are skipped (§E2). A line with any letter/digit after
|
|
37
|
+
* the `#` run is real prose, not a separator.
|
|
38
|
+
*/
|
|
39
|
+
function isSeparator(afterHash: string): boolean {
|
|
40
|
+
const t = afterHash.trim();
|
|
41
|
+
if (t === "") return false; // a blank comment line is a spacer, handled separately
|
|
42
|
+
return !/[A-Za-z0-9]/.test(t);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parse a comment block (top-to-bottom, in file order) into prose + tags.
|
|
47
|
+
* Lines must already be stripped of their trailing \r and be `#`-comment lines
|
|
48
|
+
* (leading whitespace allowed). Separator lines are dropped.
|
|
49
|
+
*/
|
|
50
|
+
export function parseDocBlock(commentLines: string[]): DocBlock | null {
|
|
51
|
+
const proseParts: string[] = [];
|
|
52
|
+
const tags: DocTag[] = [];
|
|
53
|
+
|
|
54
|
+
for (const raw of commentLines) {
|
|
55
|
+
const line = raw.replace(/\r$/, "");
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
if (!trimmed.startsWith("#")) continue;
|
|
58
|
+
const afterHash = trimmed.replace(/^#+[ \t]?/, "");
|
|
59
|
+
if (isSeparator(trimmed.slice(1))) continue; // slice off first `#` for the check
|
|
60
|
+
if (afterHash.trim() === "") continue; // blank comment line inside the block: skip
|
|
61
|
+
|
|
62
|
+
const tagMatch = /^@([A-Za-z_][A-Za-z0-9_]*)\b[ \t]*(.*)$/.exec(afterHash);
|
|
63
|
+
if (tagMatch && KNOWN_DOC_TAGS.has(tagMatch[1].toLowerCase())) {
|
|
64
|
+
tags.push({ tag: tagMatch[1].toLowerCase(), text: tagMatch[2].trim() });
|
|
65
|
+
} else {
|
|
66
|
+
proseParts.push(afterHash.trim());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (proseParts.length === 0 && tags.length === 0) return null;
|
|
71
|
+
|
|
72
|
+
let doc = proseParts.join(" ");
|
|
73
|
+
if (doc.length > PROSE_CAP) doc = doc.slice(0, PROSE_CAP);
|
|
74
|
+
return { doc, tags };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Given all file lines (split on `\n`, so entries may retain trailing `\r`) and
|
|
79
|
+
* the 0-based line of a definition, return its attached doc block or null.
|
|
80
|
+
*
|
|
81
|
+
* Walks upward collecting contiguous `#` comment lines. Stops at the first
|
|
82
|
+
* blank/non-comment line — a blank line detaches the block (§E2). Separator
|
|
83
|
+
* lines interrupt attachment too: a divider above the def is not documentation.
|
|
84
|
+
*/
|
|
85
|
+
export function docForDefinition(lines: string[], defLine: number): DocBlock | null {
|
|
86
|
+
const collected: string[] = [];
|
|
87
|
+
for (let i = defLine - 1; i >= 0; i--) {
|
|
88
|
+
const line = lines[i] ?? "";
|
|
89
|
+
const trimmed = line.replace(/\r$/, "").trim();
|
|
90
|
+
if (trimmed === "") break; // blank line detaches
|
|
91
|
+
if (!trimmed.startsWith("#")) break; // code line detaches
|
|
92
|
+
// A pure separator directly adjacent detaches the block (a divider is not doc);
|
|
93
|
+
// but a separator further up simply bounds an already-collected block.
|
|
94
|
+
if (isSeparator(trimmed.slice(1))) {
|
|
95
|
+
if (collected.length === 0) return null;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
collected.push(line);
|
|
99
|
+
}
|
|
100
|
+
if (collected.length === 0) return null;
|
|
101
|
+
collected.reverse();
|
|
102
|
+
return parseDocBlock(collected);
|
|
103
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema-driven definition extraction: given a file's content and its schema
|
|
3
|
+
* entry, produce the definitions it contains. This is what the per-entry
|
|
4
|
+
* schema fixture tests exercise.
|
|
5
|
+
*
|
|
6
|
+
* No `vscode` imports here: unit-tested in plain Node.
|
|
7
|
+
*/
|
|
8
|
+
import type { Definition, DefSource } from "@px-lsp/protocol/types";
|
|
9
|
+
import type { SchemaEntry } from "../schema/types";
|
|
10
|
+
import { LineIndex, parseLoc, parseScript, walkStatements, type Statement } from "../parser";
|
|
11
|
+
import { docForDefinition } from "./docComments";
|
|
12
|
+
import { shareDefinitionStrings } from "./intern";
|
|
13
|
+
import { activeProfile } from "../games/active";
|
|
14
|
+
|
|
15
|
+
/** Max loc value length kept in memory; the edit flow re-reads the yml from disk. */
|
|
16
|
+
export const LOC_VALUE_LIMIT = 200;
|
|
17
|
+
|
|
18
|
+
const DEF_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
|
|
19
|
+
export const EVENT_ID = /^[A-Za-z0-9_-]+\.\d+$/;
|
|
20
|
+
const TITLE_KEY = /^[ekdcb]_[A-Za-z0-9_-]+$/;
|
|
21
|
+
|
|
22
|
+
export function extractDefinitions(
|
|
23
|
+
content: string,
|
|
24
|
+
entry: SchemaEntry,
|
|
25
|
+
file: string,
|
|
26
|
+
source: DefSource
|
|
27
|
+
): Definition[] {
|
|
28
|
+
const extraction = entry.extraction ?? "top-level-key";
|
|
29
|
+
if (extraction === "loc-key") return extractLocDefinitions(content, file, source);
|
|
30
|
+
|
|
31
|
+
const defs: Definition[] = [];
|
|
32
|
+
const { root } = parseScript(content);
|
|
33
|
+
const lines = new LineIndex(content);
|
|
34
|
+
// Raw lines (split on \n; entries may keep a trailing \r) for encoding-safe
|
|
35
|
+
// leading-comment capture (§E). Computed once per file, near-zero cost.
|
|
36
|
+
const rawLines = content.split("\n");
|
|
37
|
+
const push = (name: string, offset: number, container?: string) => {
|
|
38
|
+
const line = lines.positionAt(offset).line;
|
|
39
|
+
const def: Definition = { name, kind: entry.kind, file, line, source };
|
|
40
|
+
if (container !== undefined) def.container = container;
|
|
41
|
+
const block = docForDefinition(rawLines, line);
|
|
42
|
+
if (block) {
|
|
43
|
+
if (block.doc) def.doc = block.doc;
|
|
44
|
+
if (block.tags.length > 0) def.tags = block.tags;
|
|
45
|
+
}
|
|
46
|
+
defs.push(def);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Scripted effects/triggers/modifiers can declare $PARAM$ parameters in their body.
|
|
50
|
+
const harvestParams =
|
|
51
|
+
entry.kind === "scripted_effect" ||
|
|
52
|
+
entry.kind === "scripted_trigger" ||
|
|
53
|
+
entry.kind === "scripted_modifier";
|
|
54
|
+
const PARAM = /\$([A-Za-z0-9_]+)(?:\|[^$\n]*)?\$/g;
|
|
55
|
+
|
|
56
|
+
// Names declared INSIDE a definition body but referenced like definitions
|
|
57
|
+
// elsewhere: trait `group = X` (has_trait accepts the group) and game-concept
|
|
58
|
+
// `alias = { a b }` (loc [Concept] links). Deduped per file.
|
|
59
|
+
const seenInnerNames = new Set<string>();
|
|
60
|
+
|
|
61
|
+
// Database entry modes (`REPLACE:key = { ... }`), for games whose profile
|
|
62
|
+
// declares them: index under the bare name, keep the mode on the Definition.
|
|
63
|
+
const entryModes = activeProfile().entryModes;
|
|
64
|
+
const modePrefix = entryModes?.length ? new RegExp(`^(${entryModes.join("|")}):`) : null;
|
|
65
|
+
|
|
66
|
+
switch (extraction) {
|
|
67
|
+
case "top-level-key":
|
|
68
|
+
for (const stmt of root.statements) {
|
|
69
|
+
if (stmt.kind !== "assignment" || stmt.key.quoted) continue;
|
|
70
|
+
if (stmt.op !== "=" && stmt.op !== "?=") continue;
|
|
71
|
+
let name = stmt.key.text;
|
|
72
|
+
let entryMode: string | undefined;
|
|
73
|
+
const mode = modePrefix?.exec(name);
|
|
74
|
+
if (mode) {
|
|
75
|
+
entryMode = mode[1];
|
|
76
|
+
name = name.slice(mode[0].length);
|
|
77
|
+
}
|
|
78
|
+
if (!DEF_NAME.test(name) || name === "namespace") continue;
|
|
79
|
+
push(name, stmt.key.range.start);
|
|
80
|
+
if (entryMode) defs[defs.length - 1].entryMode = entryMode;
|
|
81
|
+
if (harvestParams) {
|
|
82
|
+
const body = content.slice(stmt.range.start, stmt.range.end);
|
|
83
|
+
const params: string[] = [];
|
|
84
|
+
PARAM.lastIndex = 0;
|
|
85
|
+
let m: RegExpExecArray | null;
|
|
86
|
+
while ((m = PARAM.exec(body)) !== null) {
|
|
87
|
+
if (!params.includes(m[1])) params.push(m[1]);
|
|
88
|
+
}
|
|
89
|
+
if (params.length > 0) defs[defs.length - 1].params = params;
|
|
90
|
+
}
|
|
91
|
+
// Scripted lists generate every_/any_/random_/ordered_<name> iterators;
|
|
92
|
+
// the `base` link decides the iterated scope (scopes/model.ts consumes it).
|
|
93
|
+
if (entry.kind === "scripted_list" && stmt.value?.kind === "block") {
|
|
94
|
+
for (const s of stmt.value.statements) {
|
|
95
|
+
if (s.kind !== "assignment" || s.key.quoted || s.key.text !== "base") continue;
|
|
96
|
+
if (s.value?.kind !== "scalar" || s.value.quoted) continue;
|
|
97
|
+
defs[defs.length - 1].value = s.value.text;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (entry.kind === "trait" && stmt.value?.kind === "block") {
|
|
102
|
+
for (const s of stmt.value.statements) {
|
|
103
|
+
if (s.kind !== "assignment" || s.key.quoted || s.key.text !== "group") continue;
|
|
104
|
+
if (s.value?.kind !== "scalar" || s.value.quoted || !DEF_NAME.test(s.value.text)) continue;
|
|
105
|
+
if (seenInnerNames.has(s.value.text)) continue;
|
|
106
|
+
seenInnerNames.add(s.value.text);
|
|
107
|
+
defs.push({
|
|
108
|
+
name: s.value.text,
|
|
109
|
+
kind: "trait_group",
|
|
110
|
+
file,
|
|
111
|
+
line: lines.positionAt(s.value.range.start).line,
|
|
112
|
+
source,
|
|
113
|
+
container: name,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (entry.kind === "game_concept" && stmt.value?.kind === "block") {
|
|
118
|
+
for (const s of stmt.value.statements) {
|
|
119
|
+
if (
|
|
120
|
+
s.kind !== "assignment" ||
|
|
121
|
+
s.key.quoted ||
|
|
122
|
+
s.key.text !== "alias" ||
|
|
123
|
+
s.value?.kind !== "block"
|
|
124
|
+
)
|
|
125
|
+
continue;
|
|
126
|
+
for (const el of s.value.statements) {
|
|
127
|
+
if (el.kind !== "value" || el.value.kind !== "scalar" || el.value.quoted) continue;
|
|
128
|
+
if (!DEF_NAME.test(el.value.text) || seenInnerNames.has(el.value.text)) continue;
|
|
129
|
+
seenInnerNames.add(el.value.text);
|
|
130
|
+
defs.push({
|
|
131
|
+
name: el.value.text,
|
|
132
|
+
kind: "game_concept",
|
|
133
|
+
file,
|
|
134
|
+
line: lines.positionAt(el.value.range.start).line,
|
|
135
|
+
source,
|
|
136
|
+
container: name,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
|
|
144
|
+
case "event-id": {
|
|
145
|
+
// Event files may also declare `scripted_trigger NAME = { ... }` /
|
|
146
|
+
// `scripted_effect NAME = { ... }` inline (file-local, but referenced
|
|
147
|
+
// like their common/ counterparts). The tolerant parser reads the
|
|
148
|
+
// keyword as a bare marker scalar followed by the NAME assignment.
|
|
149
|
+
// Scanned at EVERY depth: one tolerant-parse hiccup earlier in a
|
|
150
|
+
// thousand-line vanilla file nests everything after it inside a block,
|
|
151
|
+
// and the declarations must survive that (#5). Event ids stay
|
|
152
|
+
// top-level only — nested ids are option/trigger content, not events.
|
|
153
|
+
const scanList = (statements: Statement[], depth: number) => {
|
|
154
|
+
let inlineKind: "scripted_trigger" | "scripted_effect" | null = null;
|
|
155
|
+
for (const stmt of statements) {
|
|
156
|
+
if (stmt.kind === "value") {
|
|
157
|
+
const text = stmt.value.kind === "scalar" && !stmt.value.quoted ? stmt.value.text : "";
|
|
158
|
+
inlineKind = text === "scripted_trigger" || text === "scripted_effect" ? text : null;
|
|
159
|
+
if (stmt.value.kind === "block") scanList(stmt.value.statements, depth + 1);
|
|
160
|
+
else if (stmt.value.kind === "tagged-block") scanList(stmt.value.block.statements, depth + 1);
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const marker = inlineKind;
|
|
164
|
+
inlineKind = null;
|
|
165
|
+
if (stmt.key.quoted) continue;
|
|
166
|
+
if (stmt.op === "=" || stmt.op === "?=") {
|
|
167
|
+
if (marker && DEF_NAME.test(stmt.key.text)) {
|
|
168
|
+
const line = lines.positionAt(stmt.key.range.start).line;
|
|
169
|
+
const def: Definition = { name: stmt.key.text, kind: marker, file, line, source };
|
|
170
|
+
const block = docForDefinition(rawLines, line);
|
|
171
|
+
if (block) {
|
|
172
|
+
if (block.doc) def.doc = block.doc;
|
|
173
|
+
if (block.tags.length > 0) def.tags = block.tags;
|
|
174
|
+
}
|
|
175
|
+
const body = content.slice(stmt.range.start, stmt.range.end);
|
|
176
|
+
const params: string[] = [];
|
|
177
|
+
PARAM.lastIndex = 0;
|
|
178
|
+
let m: RegExpExecArray | null;
|
|
179
|
+
while ((m = PARAM.exec(body)) !== null) {
|
|
180
|
+
if (!params.includes(m[1])) params.push(m[1]);
|
|
181
|
+
}
|
|
182
|
+
if (params.length > 0) def.params = params;
|
|
183
|
+
defs.push(def);
|
|
184
|
+
} else if (depth === 0 && EVENT_ID.test(stmt.key.text)) {
|
|
185
|
+
push(stmt.key.text, stmt.key.range.start);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (stmt.value?.kind === "block") scanList(stmt.value.statements, depth + 1);
|
|
189
|
+
else if (stmt.value?.kind === "tagged-block") scanList(stmt.value.block.statements, depth + 1);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
scanList(root.statements, 0);
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
case "nested-title":
|
|
197
|
+
// Landed titles nest: e_empire { k_kingdom { d_duchy { c_county { b_barony } } } }
|
|
198
|
+
walkStatements(root, (stmt: Statement, ancestors) => {
|
|
199
|
+
if (stmt.kind !== "assignment" || stmt.key.quoted) return;
|
|
200
|
+
if (!TITLE_KEY.test(stmt.key.text)) return;
|
|
201
|
+
if (stmt.value?.kind !== "block") return;
|
|
202
|
+
// Container: the nearest ancestor assignment that is itself a title.
|
|
203
|
+
let container: string | undefined;
|
|
204
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
205
|
+
const a = ancestors[i];
|
|
206
|
+
if (a.kind === "assignment" && TITLE_KEY.test(a.key.text)) {
|
|
207
|
+
container = a.key.text;
|
|
208
|
+
break;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
push(stmt.key.text, stmt.key.range.start, container);
|
|
212
|
+
});
|
|
213
|
+
break;
|
|
214
|
+
|
|
215
|
+
case "gui-type": {
|
|
216
|
+
// GUI: `type NAME = base { ... }`, `template NAME { ... }` and
|
|
217
|
+
// `local_template NAME { ... }`. The tolerant parser reads these as a bare
|
|
218
|
+
// marker scalar statement followed by an assignment whose key is the NAME.
|
|
219
|
+
// `types Group { ... }` wraps the `type` pattern one level down.
|
|
220
|
+
const scan = (statements: Statement[]) => {
|
|
221
|
+
for (let i = 0; i < statements.length - 1; i++) {
|
|
222
|
+
const marker = statements[i];
|
|
223
|
+
if (marker.kind !== "value" || marker.value.kind !== "scalar" || marker.value.quoted) continue;
|
|
224
|
+
const kw = marker.value.text.toLowerCase();
|
|
225
|
+
if (kw !== "type" && kw !== "template" && kw !== "local_template" && kw !== "types") continue;
|
|
226
|
+
const named = statements[i + 1];
|
|
227
|
+
if (named.kind !== "assignment" || named.key.quoted) continue;
|
|
228
|
+
if (kw === "types") {
|
|
229
|
+
if (named.value?.kind === "block") scan(named.value.statements);
|
|
230
|
+
} else if (DEF_NAME.test(named.key.text)) {
|
|
231
|
+
push(named.key.text, named.key.range.start);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
scan(root.statements);
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Every string above is a slice of `content` and would pin the whole file for
|
|
240
|
+
// as long as the index holds the definition (§C2).
|
|
241
|
+
return shareDefinitionStrings(defs);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function extractLocDefinitions(content: string, file: string, source: DefSource): Definition[] {
|
|
245
|
+
const defs: Definition[] = [];
|
|
246
|
+
for (const entry of parseLoc(content).entries) {
|
|
247
|
+
let value = entry.value;
|
|
248
|
+
if (value.length > LOC_VALUE_LIMIT) value = value.slice(0, LOC_VALUE_LIMIT);
|
|
249
|
+
defs.push({ name: entry.key, kind: "loc_key", file, line: entry.line, source, value });
|
|
250
|
+
}
|
|
251
|
+
return shareDefinitionStrings(defs);
|
|
252
|
+
}
|