@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,478 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort scope inference at a cursor position (rework plan AD-5): walk
|
|
3
|
+
* the CST key stack from the schema-declared root scope through iterators,
|
|
4
|
+
* links, dot-chains and scope:x references.
|
|
5
|
+
*
|
|
6
|
+
* The result RANKS and ANNOTATES, it never hides a completion and never
|
|
7
|
+
* produces a diagnostic. "unknown" is a first-class outcome.
|
|
8
|
+
*
|
|
9
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
10
|
+
*/
|
|
11
|
+
import { classifyKeyword, isExplicitKeyword } from "../contextKeywords";
|
|
12
|
+
import { walkStatements, type ParseResult, type Statement } from "../parser";
|
|
13
|
+
import { blockPathFromParse } from "../context";
|
|
14
|
+
import type { SchemaEntry, KeySpec } from "../schema/types";
|
|
15
|
+
import { activeProfile } from "../games/active";
|
|
16
|
+
import type { Scope, ScopeModel } from "./model";
|
|
17
|
+
|
|
18
|
+
export interface ScopeInference {
|
|
19
|
+
/** Possible scopes at the cursor; null = unknown. */
|
|
20
|
+
scopes: Set<Scope> | null;
|
|
21
|
+
/** Human-readable chain, e.g. ["character", "every_held_title → landed_title"]. */
|
|
22
|
+
chain: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Extra context for inference (all optional; absence degrades to the old behavior). */
|
|
26
|
+
export interface InferenceContext {
|
|
27
|
+
/** The file's schema entry: enables per-definition root-scope declarations
|
|
28
|
+
* (event `scope = X`, custom loc `type = X`, scripted_gui `scope = X`),
|
|
29
|
+
* structure keys with a documented block scope, and on_action roots. */
|
|
30
|
+
entry?: SchemaEntry | null;
|
|
31
|
+
/** on_action name → expected root scope (parsed from the user's on_actions.log). */
|
|
32
|
+
onActionScopes?: ReadonlyMap<string, string>;
|
|
33
|
+
/** Variable value types: "var:x" / "local_var:x" / "global_var:x" → scope set. */
|
|
34
|
+
varTypes?: ReadonlyMap<string, Set<Scope> | null>;
|
|
35
|
+
/** Variable-list ITEM types (same keying): resolves `every_in_list = { variable = x }`. */
|
|
36
|
+
varListItemTypes?: ReadonlyMap<string, Set<Scope> | null>;
|
|
37
|
+
/** Ad-hoc list ITEM types keyed by plain list name, resolved from the
|
|
38
|
+
* add_to_list/add_to_temporary_list set-sites indexed mod-wide:
|
|
39
|
+
* `every_in_list = { list = x }` resolves through it. */
|
|
40
|
+
adhocListItemTypes?: ReadonlyMap<string, Set<Scope> | null>;
|
|
41
|
+
/** Saved-scope types merged across all indexed save sites of the workspace
|
|
42
|
+
* mods: the fallback for scope: names not saved in the current file. */
|
|
43
|
+
savedScopeTypes?: ReadonlyMap<string, Set<Scope> | null>;
|
|
44
|
+
/** Calling scopes of scripted effects/triggers/modifiers aggregated from
|
|
45
|
+
* their call sites: the root fallback when no `@scope` tag declares one. */
|
|
46
|
+
callSiteScopes?: ReadonlyMap<string, Set<Scope>>;
|
|
47
|
+
/** PdxDoc `@scope` tag of a definition (scripted effects/triggers/values):
|
|
48
|
+
* lets modders declare the calling scope of reusable script. */
|
|
49
|
+
defScopeTag?: (name: string) => Set<Scope> | null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Kinds whose root scope is the CALL SITE's, a PdxDoc `@scope` tag declares it. */
|
|
53
|
+
const SCOPE_TAG_KINDS = new Set(["scripted_effect", "scripted_trigger", "script_value", "scripted_modifier"]);
|
|
54
|
+
|
|
55
|
+
/** Keys that never change scope (grammar keywords, control flow, weights). */
|
|
56
|
+
function isScopeTransparent(key: string): boolean {
|
|
57
|
+
const cls = classifyKeyword(key);
|
|
58
|
+
if (cls === "trigger" || cls === "effect" || cls === "value" || cls === "transparent") {
|
|
59
|
+
// Iterators are classified trigger/effect by classifyKeyword but DO change
|
|
60
|
+
// scope; they are handled before this check.
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const ITERATOR = /^(?:every|any|random|ordered)_/;
|
|
67
|
+
const IN_LIST_ITERATOR = /^(?:every|any|random|ordered)_in_(?:global_|local_)?list$/;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Script-value math keys: as BLOCK keys they open math/desc blocks whose
|
|
71
|
+
* triggers evaluate in the CALLING scope, they must never be resolved as the
|
|
72
|
+
* same-named event-target links (`value` is a wildcard number link, `max` etc.).
|
|
73
|
+
*/
|
|
74
|
+
const MATH_KEYS = new Set([
|
|
75
|
+
"value",
|
|
76
|
+
"add",
|
|
77
|
+
"factor",
|
|
78
|
+
"base",
|
|
79
|
+
"min",
|
|
80
|
+
"max",
|
|
81
|
+
"multiply",
|
|
82
|
+
"divide",
|
|
83
|
+
"subtract",
|
|
84
|
+
"modulo",
|
|
85
|
+
"round",
|
|
86
|
+
"floor",
|
|
87
|
+
"ceiling",
|
|
88
|
+
"desc",
|
|
89
|
+
"format",
|
|
90
|
+
]);
|
|
91
|
+
|
|
92
|
+
const VAR_PREFIX = /^(var|local_var|global_var):(.+)$/;
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Per-definition root-scope declarations, per game (profile.defRootKeys): a key
|
|
96
|
+
* inside the top-level definition body names the root scope of that definition's
|
|
97
|
+
* script blocks. A declared value of `none`/`all` means "any scope" → null.
|
|
98
|
+
*/
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Effective root scopes for the definition enclosing `offset`: per-definition
|
|
102
|
+
* declarations and on_action names override the schema's per-kind rootScopes.
|
|
103
|
+
*/
|
|
104
|
+
export function rootScopesAt(
|
|
105
|
+
parse: ParseResult,
|
|
106
|
+
offset: number,
|
|
107
|
+
fallback: Set<Scope> | null,
|
|
108
|
+
ctx?: InferenceContext
|
|
109
|
+
): Set<Scope> | null {
|
|
110
|
+
const kind = ctx?.entry?.kind;
|
|
111
|
+
if (!kind) return fallback;
|
|
112
|
+
const spec = activeProfile().defRootKeys?.[kind];
|
|
113
|
+
if (!spec && kind !== "on_action" && !SCOPE_TAG_KINDS.has(kind)) return fallback;
|
|
114
|
+
|
|
115
|
+
// Find the enclosing top-level statement.
|
|
116
|
+
let top: Statement | null = null;
|
|
117
|
+
for (const stmt of parse.root.statements) {
|
|
118
|
+
if (offset >= stmt.range.start && offset <= stmt.range.end) {
|
|
119
|
+
top = stmt;
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (!top || top.kind !== "assignment") return fallback;
|
|
124
|
+
|
|
125
|
+
if (kind === "on_action") {
|
|
126
|
+
const scope = ctx?.onActionScopes?.get(top.key.text);
|
|
127
|
+
if (scope && scope !== "none") return new Set([scope.toLowerCase()]);
|
|
128
|
+
return fallback;
|
|
129
|
+
}
|
|
130
|
+
if (SCOPE_TAG_KINDS.has(kind)) {
|
|
131
|
+
// The author's @scope tag wins; otherwise the calling scope aggregated
|
|
132
|
+
// from the mod's call sites of this definition (varTypes.ts).
|
|
133
|
+
return ctx?.defScopeTag?.(top.key.text) ?? ctx?.callSiteScopes?.get(top.key.text) ?? fallback;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (top.value?.kind === "block") {
|
|
137
|
+
for (const s of top.value.statements) {
|
|
138
|
+
if (s.kind !== "assignment" || s.key.quoted || s.key.text !== spec!.key) continue;
|
|
139
|
+
if (s.value?.kind !== "scalar" || s.value.quoted) continue;
|
|
140
|
+
const declared = s.value.text.toLowerCase();
|
|
141
|
+
if (declared === "none" || declared === "all") return null;
|
|
142
|
+
const scope = spec!.values ? spec!.values[declared] : declared;
|
|
143
|
+
if (scope) return new Set([scope]);
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return spec!.default ? new Set([spec!.default]) : fallback;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Structure key with a documented block scope for the entry's kind, if any. */
|
|
151
|
+
function structureKeyScope(entry: SchemaEntry | null | undefined, key: string): string | undefined {
|
|
152
|
+
const structure = entry?.structure;
|
|
153
|
+
if (!structure) return undefined;
|
|
154
|
+
const find = (specs: KeySpec[] | undefined): string | undefined => {
|
|
155
|
+
if (!specs) return undefined;
|
|
156
|
+
for (const s of specs) if (s.key === key && s.scope) return s.scope;
|
|
157
|
+
return undefined;
|
|
158
|
+
};
|
|
159
|
+
const top = find(structure.topLevel);
|
|
160
|
+
if (top) return top;
|
|
161
|
+
for (const specs of Object.values(structure.blocks ?? {})) {
|
|
162
|
+
const hit = find(specs);
|
|
163
|
+
if (hit) return hit;
|
|
164
|
+
}
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Saved-scope types for a file: run the stack walk at each save_scope_as site.
|
|
170
|
+
* One level only (no recursive scope: resolution) to stay cheap and total.
|
|
171
|
+
* `save_scope_value_as` sites save value/boolean/flag scopes;
|
|
172
|
+
* `save_temporary_value_as` (script-value math) always saves a value scope.
|
|
173
|
+
*/
|
|
174
|
+
export function collectSavedScopeTypes(
|
|
175
|
+
parse: ParseResult,
|
|
176
|
+
model: ScopeModel,
|
|
177
|
+
rootScopes: Set<Scope> | null,
|
|
178
|
+
ambient?: ReadonlyArray<{ name: string; type: string }>,
|
|
179
|
+
ctx?: InferenceContext
|
|
180
|
+
): Map<string, Set<Scope> | null> {
|
|
181
|
+
const result = new Map<string, Set<Scope> | null>();
|
|
182
|
+
// Ambient (engine-provided) scopes seed the map so `scope:actor = { … }`
|
|
183
|
+
// infers character even though nothing save_scope_as's it (§B3). Real saves
|
|
184
|
+
// below merge on top.
|
|
185
|
+
for (const a of ambient ?? []) result.set(a.name, new Set([a.type]));
|
|
186
|
+
const merge = (name: string, scopes: Set<Scope> | null) => {
|
|
187
|
+
const prev = result.get(name);
|
|
188
|
+
if (prev !== undefined) {
|
|
189
|
+
if (prev === null || scopes === null) result.set(name, null);
|
|
190
|
+
else for (const s of scopes) prev.add(s);
|
|
191
|
+
} else {
|
|
192
|
+
result.set(name, scopes ? new Set(scopes) : null);
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
walkStatements(parse.root, (stmt) => {
|
|
196
|
+
if (stmt.kind !== "assignment" || stmt.key.quoted) return;
|
|
197
|
+
const key = stmt.key.text;
|
|
198
|
+
if (key === "save_scope_as" || key === "save_temporary_scope_as") {
|
|
199
|
+
if (stmt.value?.kind !== "scalar" || stmt.value.quoted) return;
|
|
200
|
+
// Seed the walk with what is known so far, so a save inside
|
|
201
|
+
// `scope:earlier_save = { … }` resolves (ambient names included).
|
|
202
|
+
const inferred = inferScopeAt(parse, stmt.key.range.start, model, rootScopes, result, ctx);
|
|
203
|
+
merge(stmt.value.text, inferred.scopes ? new Set(inferred.scopes) : null);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (key === "save_temporary_value_as") {
|
|
207
|
+
// Script-value math: saves the current numeric value, read back as scope:x.
|
|
208
|
+
if (stmt.value?.kind !== "scalar" || stmt.value.quoted) return;
|
|
209
|
+
merge(stmt.value.text, new Set(["value"]));
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
if (key === "save_scope_value_as" || key === "save_temporary_scope_value_as") {
|
|
213
|
+
if (stmt.value?.kind !== "block") return;
|
|
214
|
+
let name: string | null = null;
|
|
215
|
+
let valueText: string | null = null;
|
|
216
|
+
for (const s of stmt.value.statements) {
|
|
217
|
+
if (s.kind !== "assignment" || s.key.quoted || s.value?.kind !== "scalar") continue;
|
|
218
|
+
if (s.key.text === "name") name = s.value.text;
|
|
219
|
+
else if (s.key.text === "value") valueText = s.value.text;
|
|
220
|
+
}
|
|
221
|
+
if (!name) return;
|
|
222
|
+
merge(name, new Set([valueScopeType(valueText)]));
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** The scope type a save_scope_value_as value produces (value/boolean/flag). */
|
|
229
|
+
function valueScopeType(valueText: string | null): Scope {
|
|
230
|
+
if (valueText === "yes" || valueText === "no") return "boolean";
|
|
231
|
+
if (valueText?.startsWith("flag:")) return "flag";
|
|
232
|
+
return "value";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export function inferScopeAt(
|
|
236
|
+
parse: ParseResult,
|
|
237
|
+
offset: number,
|
|
238
|
+
model: ScopeModel,
|
|
239
|
+
rootScopes: Set<Scope> | null,
|
|
240
|
+
savedScopes: Map<string, Set<Scope> | null>,
|
|
241
|
+
ctx?: InferenceContext
|
|
242
|
+
): ScopeInference {
|
|
243
|
+
const path = blockPathFromParse(parse, offset);
|
|
244
|
+
const chain: string[] = [];
|
|
245
|
+
const effectiveRoot = rootScopesAt(parse, offset, rootScopes, ctx);
|
|
246
|
+
const rootSet = effectiveRoot ? new Set(effectiveRoot) : null;
|
|
247
|
+
let current: Set<Scope> | null = rootSet ? new Set(rootSet) : null;
|
|
248
|
+
const prevStack: Array<Set<Scope> | null> = [];
|
|
249
|
+
if (rootSet) chain.push([...rootSet].join("|"));
|
|
250
|
+
else chain.push("unknown");
|
|
251
|
+
|
|
252
|
+
const apply = (segment: string, node?: Statement | null): void => {
|
|
253
|
+
if (segment === "<anon>") return;
|
|
254
|
+
// Dot-chains fold FIRST, so `scope:host.culture` resolves scope:host then
|
|
255
|
+
// the culture link (the prefix checks below must see single segments).
|
|
256
|
+
if (segment.includes(".")) {
|
|
257
|
+
for (const part of segment.split(".")) apply(part);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const seg = segment; // keep original case for scope:x names
|
|
261
|
+
const lower = seg.toLowerCase();
|
|
262
|
+
|
|
263
|
+
if (lower === "root") {
|
|
264
|
+
prevStack.push(current);
|
|
265
|
+
current = rootSet ? new Set(rootSet) : null;
|
|
266
|
+
chain.push(`root → ${fmt(current)}`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
if (lower === "this") return;
|
|
270
|
+
if (lower === "prev") {
|
|
271
|
+
// Scope-changing segments push the OLD scope; prev pops back to it, so
|
|
272
|
+
// `prev.prev` walks two levels (each prev consumes one stack entry).
|
|
273
|
+
current = prevStack.length > 0 ? (prevStack.pop() as Set<Scope> | null) : null;
|
|
274
|
+
current = current ? new Set(current) : null;
|
|
275
|
+
chain.push(`prev → ${fmt(current)}`);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
if (seg.startsWith("scope:")) {
|
|
279
|
+
const name = seg.slice(6);
|
|
280
|
+
prevStack.push(current);
|
|
281
|
+
// This file's save sites first; scopes saved elsewhere in the mod fall
|
|
282
|
+
// back to the mod-wide static analysis (`??` also covers a local save
|
|
283
|
+
// the walk could not type, the global merge includes that site too).
|
|
284
|
+
const saved = savedScopes.get(name) ?? ctx?.savedScopeTypes?.get(name);
|
|
285
|
+
current = saved ? new Set(saved) : null;
|
|
286
|
+
chain.push(`scope:${name} → ${fmt(current)}`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const varMatch = VAR_PREFIX.exec(seg);
|
|
290
|
+
if (varMatch) {
|
|
291
|
+
prevStack.push(current);
|
|
292
|
+
const typed = ctx?.varTypes?.get(`${varMatch[1]}:${varMatch[2]}`);
|
|
293
|
+
current = typed ? new Set(typed) : null;
|
|
294
|
+
chain.push(`${seg} → ${fmt(current)}`);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// Script-value math keys open math blocks in the calling scope; never treat
|
|
298
|
+
// them as the same-named links (`value` is a wildcard number link).
|
|
299
|
+
if (MATH_KEYS.has(lower)) return;
|
|
300
|
+
|
|
301
|
+
// Structure keys with a documented block scope (activity `is_valid` runs in
|
|
302
|
+
// activity scope, building blocks in county scope, …) win over links.
|
|
303
|
+
const structScope = structureKeyScope(ctx?.entry, lower);
|
|
304
|
+
if (structScope) {
|
|
305
|
+
prevStack.push(current);
|
|
306
|
+
current = structScope === "none" || structScope === "all" ? null : new Set([structScope]);
|
|
307
|
+
chain.push(`${lower} → ${fmt(current)}`);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (ITERATOR.test(lower)) {
|
|
312
|
+
// Explicitly-listed keywords that only LOOK like iterators keep the
|
|
313
|
+
// scope: random_list / random_valid are weighted wrappers, not lists.
|
|
314
|
+
if (isExplicitKeyword(lower)) return;
|
|
315
|
+
const out = model.outputOf(lower);
|
|
316
|
+
if (out) {
|
|
317
|
+
prevStack.push(current);
|
|
318
|
+
current = new Set(out);
|
|
319
|
+
chain.push(`${lower} → ${fmt(current)}`);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (IN_LIST_ITERATOR.test(lower)) {
|
|
323
|
+
// Variable lists carry an item type from their add_to_*_variable_list
|
|
324
|
+
// set-sites: `every_in_list = { variable = x }` resolves through it.
|
|
325
|
+
// Ad-hoc `list = x` lists resolve through the mod-wide add_to_list
|
|
326
|
+
// site analysis (ctx.adhocListItemTypes).
|
|
327
|
+
let item: Set<Scope> | null = null;
|
|
328
|
+
const varName = node ? childScalar(node, "variable") : null;
|
|
329
|
+
if (varName && ctx?.varListItemTypes) {
|
|
330
|
+
const ns = lower.includes("_in_global_")
|
|
331
|
+
? "global_var"
|
|
332
|
+
: lower.includes("_in_local_")
|
|
333
|
+
? "local_var"
|
|
334
|
+
: "var";
|
|
335
|
+
item = ctx.varListItemTypes.get(`${ns}:${varName}`) ?? null;
|
|
336
|
+
}
|
|
337
|
+
if (!item) {
|
|
338
|
+
const listName = node ? childScalar(node, "list") : null;
|
|
339
|
+
if (listName && ctx?.adhocListItemTypes) item = ctx.adhocListItemTypes.get(listName) ?? null;
|
|
340
|
+
}
|
|
341
|
+
prevStack.push(current);
|
|
342
|
+
current = item ? new Set(item) : null;
|
|
343
|
+
chain.push(`${lower} → ${fmt(current)}`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
// Unknown every_/any_/random_/ordered_ name: most are structure keys or
|
|
347
|
+
// scripted lists we could not resolve, keep the scope (never fatal).
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const link = model.links.get(lower);
|
|
351
|
+
if (link) {
|
|
352
|
+
prevStack.push(current);
|
|
353
|
+
current = link.outputs ? new Set(link.outputs) : null;
|
|
354
|
+
chain.push(`${lower} → ${fmt(current)}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
// Data links used with an argument: `culture:czech`, `special_guest:host`,
|
|
358
|
+
// `title:k_france` …, the link table is keyed by the bare prefix.
|
|
359
|
+
const colon = seg.indexOf(":");
|
|
360
|
+
if (colon > 0) {
|
|
361
|
+
const dataLink = model.links.get(lower.slice(0, colon));
|
|
362
|
+
if (dataLink) {
|
|
363
|
+
prevStack.push(current);
|
|
364
|
+
current = dataLink.outputs ? new Set(dataLink.outputs) : null;
|
|
365
|
+
chain.push(`${lower.slice(0, colon)}:… → ${fmt(current)}`);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (isScopeTransparent(lower)) return;
|
|
370
|
+
// Unknown block key (scripted effect call, block-argument effect like
|
|
371
|
+
// add_opinion): assume it keeps the scope, best effort, never fatal.
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
for (const node of path) {
|
|
375
|
+
apply(node && node.kind === "assignment" ? node.key.text : "<anon>", node);
|
|
376
|
+
}
|
|
377
|
+
return { scopes: current, chain };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Statically resolve a chain of enclosing block keys (outermost first, dotted
|
|
382
|
+
* segments allowed) from a root-scope seed: the resolver behind ad-hoc list
|
|
383
|
+
* item typing (`add_to_list` sites, varTypes.ts). Mirrors inferScopeAt's key
|
|
384
|
+
* handling minus the CST/saved-scope/structure context, scope:/var: anchors
|
|
385
|
+
* resolve to unknown, unknown keys keep the scope (best effort, never fatal).
|
|
386
|
+
*/
|
|
387
|
+
export function resolveKeyChainScopes(
|
|
388
|
+
segments: readonly string[],
|
|
389
|
+
model: ScopeModel,
|
|
390
|
+
rootScopes: Set<Scope> | null
|
|
391
|
+
): Set<Scope> | null {
|
|
392
|
+
const rootSet = rootScopes ? new Set(rootScopes) : null;
|
|
393
|
+
let current: Set<Scope> | null = rootSet ? new Set(rootSet) : null;
|
|
394
|
+
const prevStack: Array<Set<Scope> | null> = [];
|
|
395
|
+
|
|
396
|
+
const apply = (segment: string): void => {
|
|
397
|
+
if (segment.includes(".")) {
|
|
398
|
+
for (const part of segment.split(".")) apply(part);
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
const seg = segment;
|
|
402
|
+
const lower = seg.toLowerCase();
|
|
403
|
+
if (lower === "" || lower === "this") return;
|
|
404
|
+
if (lower === "root") {
|
|
405
|
+
prevStack.push(current);
|
|
406
|
+
current = rootSet ? new Set(rootSet) : null;
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (lower === "prev") {
|
|
410
|
+
current = prevStack.length > 0 ? (prevStack.pop() as Set<Scope> | null) : null;
|
|
411
|
+
current = current ? new Set(current) : null;
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (seg.startsWith("scope:") || VAR_PREFIX.test(seg)) {
|
|
415
|
+
// Saved scopes / variables are per-file runtime context, unknowable here.
|
|
416
|
+
prevStack.push(current);
|
|
417
|
+
current = null;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (MATH_KEYS.has(lower)) return;
|
|
421
|
+
if (ITERATOR.test(lower)) {
|
|
422
|
+
if (isExplicitKeyword(lower)) return;
|
|
423
|
+
const out = model.outputOf(lower);
|
|
424
|
+
if (out) {
|
|
425
|
+
prevStack.push(current);
|
|
426
|
+
current = new Set(out);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (IN_LIST_ITERATOR.test(lower)) {
|
|
430
|
+
prevStack.push(current);
|
|
431
|
+
current = null;
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const link = model.links.get(lower);
|
|
437
|
+
if (link) {
|
|
438
|
+
prevStack.push(current);
|
|
439
|
+
current = link.outputs ? new Set(link.outputs) : null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const colon = seg.indexOf(":");
|
|
443
|
+
if (colon > 0) {
|
|
444
|
+
const dataLink = model.links.get(lower.slice(0, colon));
|
|
445
|
+
if (dataLink) {
|
|
446
|
+
prevStack.push(current);
|
|
447
|
+
current = dataLink.outputs ? new Set(dataLink.outputs) : null;
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
// Grammar keywords and unknown effect/trigger keys keep the scope.
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
for (const seg of segments) apply(seg);
|
|
455
|
+
return current;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** Direct child `key = <scalar>` of a statement's block value, if any. */
|
|
459
|
+
function childScalar(stmt: Statement, key: string): string | null {
|
|
460
|
+
if (stmt.kind !== "assignment" || stmt.value?.kind !== "block") return null;
|
|
461
|
+
for (const s of stmt.value.statements) {
|
|
462
|
+
if (
|
|
463
|
+
s.kind === "assignment" &&
|
|
464
|
+
!s.key.quoted &&
|
|
465
|
+
s.key.text === key &&
|
|
466
|
+
s.value?.kind === "scalar" &&
|
|
467
|
+
!s.value.quoted
|
|
468
|
+
) {
|
|
469
|
+
return s.value.text;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function fmt(scopes: Set<Scope> | null): string {
|
|
476
|
+
if (!scopes || scopes.size === 0) return "unknown";
|
|
477
|
+
return [...scopes].join("|");
|
|
478
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope model (rework plan AD-5): the link table derived from script_docs /
|
|
3
|
+
* wiki token data — event targets with input/output scopes, iterator target
|
|
4
|
+
* scopes, and per-token supported input scopes.
|
|
5
|
+
*
|
|
6
|
+
* Everything here is *derived per patch* from the user's own game logs; there
|
|
7
|
+
* is no hand-maintained scope table to rot.
|
|
8
|
+
*
|
|
9
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
10
|
+
*/
|
|
11
|
+
import type { TokenData } from "@px-lsp/protocol/types";
|
|
12
|
+
|
|
13
|
+
/** Canonical scope names are plain lowercase strings ("character", "landed_title"...). */
|
|
14
|
+
export type Scope = string;
|
|
15
|
+
|
|
16
|
+
export interface LinkInfo {
|
|
17
|
+
/** Scopes the link can be used from; null = unknown/any. */
|
|
18
|
+
inputs: Set<Scope> | null;
|
|
19
|
+
/** Scopes the link produces; null = unknown. */
|
|
20
|
+
outputs: Set<Scope> | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const TARGETS_META = /Supported Targets:\s*(.+)/;
|
|
24
|
+
|
|
25
|
+
function splitScopes(raw: string): Set<Scope> {
|
|
26
|
+
return new Set(
|
|
27
|
+
raw
|
|
28
|
+
.split(/[,\s]+/)
|
|
29
|
+
.map((s) => s.trim().toLowerCase())
|
|
30
|
+
.filter((s) => s !== "" && s !== "none")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Engine-doc gaps: script_docs entries that omit "Output Scopes" for links
|
|
36
|
+
* whose output IS fixed (verified against the wiki / vanilla usage). Applied
|
|
37
|
+
* after parsing so the user's own logs stay authoritative for everything else.
|
|
38
|
+
*/
|
|
39
|
+
const LINK_OUTPUT_PATCHES: Record<string, string[]> = {
|
|
40
|
+
special_guest: ["character"],
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export class ScopeModel {
|
|
44
|
+
/** Event-target links: liege, primary_title, faith, ... */
|
|
45
|
+
readonly links = new Map<string, LinkInfo>();
|
|
46
|
+
/** Iterator base name → scope of the iterated items (every_vassal → character). */
|
|
47
|
+
readonly iteratorTargets = new Map<string, Set<Scope>>();
|
|
48
|
+
/** token "kind:name" → supported input scopes (null = unknown/any). */
|
|
49
|
+
private readonly tokenScopes = new Map<string, Set<Scope> | null>();
|
|
50
|
+
|
|
51
|
+
constructor(tokens: TokenData[]) {
|
|
52
|
+
for (const t of tokens) {
|
|
53
|
+
if (t.kind === "event_target") {
|
|
54
|
+
const inputs = new Set<Scope>();
|
|
55
|
+
const outputs = new Set<Scope>();
|
|
56
|
+
for (const s of t.scopes) {
|
|
57
|
+
const lower = s.toLowerCase();
|
|
58
|
+
if (lower.startsWith("input:")) for (const x of splitScopes(lower.slice(6))) inputs.add(x);
|
|
59
|
+
else if (lower.startsWith("output:")) for (const x of splitScopes(lower.slice(7))) outputs.add(x);
|
|
60
|
+
}
|
|
61
|
+
const existing = this.links.get(t.name);
|
|
62
|
+
if (existing) {
|
|
63
|
+
// Merge duplicate rows (wiki sections list one link per input scope).
|
|
64
|
+
if (existing.inputs && inputs.size > 0) for (const x of inputs) existing.inputs.add(x);
|
|
65
|
+
if (existing.outputs && outputs.size > 0) for (const x of outputs) existing.outputs.add(x);
|
|
66
|
+
} else {
|
|
67
|
+
this.links.set(t.name, {
|
|
68
|
+
inputs: inputs.size > 0 ? inputs : null,
|
|
69
|
+
outputs: outputs.size > 0 ? outputs : null,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (t.kind === "trigger" || t.kind === "effect") {
|
|
76
|
+
// Supported input scopes for validity ranking.
|
|
77
|
+
const inputs = splitScopes(t.scopes.filter((s) => !s.includes(":")).join(" "));
|
|
78
|
+
this.tokenScopes.set(`${t.kind}:${t.name}`, inputs.size > 0 ? inputs : null);
|
|
79
|
+
|
|
80
|
+
// Iterators: every_/any_/random_/ordered_<base>; the iterated scope is
|
|
81
|
+
// the token's "Supported Targets" metadata line.
|
|
82
|
+
const m = /^(every|any|random|ordered)_(.+)$/.exec(t.name);
|
|
83
|
+
if (m && t.traits) {
|
|
84
|
+
const targets = TARGETS_META.exec(t.traits);
|
|
85
|
+
if (targets) {
|
|
86
|
+
const scopes = splitScopes(targets[1]);
|
|
87
|
+
if (scopes.size > 0 && !this.iteratorTargets.has(m[2])) {
|
|
88
|
+
this.iteratorTargets.set(m[2], scopes);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
for (const [name, outputs] of Object.entries(LINK_OUTPUT_PATCHES)) {
|
|
95
|
+
const link = this.links.get(name);
|
|
96
|
+
if (link && !link.outputs) link.outputs = new Set(outputs);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Scripted lists (common/scripted_lists) generate every_/any_/random_/
|
|
102
|
+
* ordered_<name> iterators that script_docs does NOT dump. Each list narrows
|
|
103
|
+
* a `base` iterator/link, so the iterated scope is the base's target scope.
|
|
104
|
+
* Called whenever the definition index changes; replaces the previous set.
|
|
105
|
+
*/
|
|
106
|
+
setScriptedLists(lists: ReadonlyArray<{ name: string; base?: string }>): void {
|
|
107
|
+
for (const name of this.scriptedListNames) this.iteratorTargets.delete(name);
|
|
108
|
+
this.scriptedListNames.clear();
|
|
109
|
+
const byName = new Map<string, string | undefined>();
|
|
110
|
+
for (const l of lists) byName.set(l.name, l.base);
|
|
111
|
+
const resolve = (name: string, seen: Set<string>): Set<Scope> | null => {
|
|
112
|
+
if (this.iteratorTargets.has(name)) return this.iteratorTargets.get(name) ?? null;
|
|
113
|
+
const link = this.links.get(name);
|
|
114
|
+
if (link?.outputs) return link.outputs;
|
|
115
|
+
if (seen.has(name) || !byName.has(name)) return null;
|
|
116
|
+
seen.add(name);
|
|
117
|
+
const base = byName.get(name);
|
|
118
|
+
return base ? resolve(base, seen) : null;
|
|
119
|
+
};
|
|
120
|
+
for (const [name, base] of byName) {
|
|
121
|
+
if (this.iteratorTargets.has(name)) continue; // engine iterator wins
|
|
122
|
+
const targets = base ? resolve(base, new Set([name])) : null;
|
|
123
|
+
if (targets && targets.size > 0) {
|
|
124
|
+
this.iteratorTargets.set(name, new Set(targets));
|
|
125
|
+
this.scriptedListNames.add(name);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
private readonly scriptedListNames = new Set<string>();
|
|
130
|
+
|
|
131
|
+
/** Supported input scopes of an engine trigger/effect; null = unknown/any. */
|
|
132
|
+
inputScopesOf(kind: "trigger" | "effect", name: string): Set<Scope> | null {
|
|
133
|
+
return this.tokenScopes.get(`${kind}:${name}`) ?? null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Output scope of following a link/iterator base from `name`, or null. */
|
|
137
|
+
outputOf(name: string): Set<Scope> | null {
|
|
138
|
+
const iter = /^(?:every|any|random|ordered)_(.+)$/.exec(name);
|
|
139
|
+
if (iter) {
|
|
140
|
+
const targets = this.iteratorTargets.get(iter[1]);
|
|
141
|
+
if (targets) return targets;
|
|
142
|
+
// `every_in_list`-style iterators have no fixed target scope.
|
|
143
|
+
const linked = this.links.get(iter[1]);
|
|
144
|
+
return linked?.outputs ?? null;
|
|
145
|
+
}
|
|
146
|
+
return this.links.get(name)?.outputs ?? null;
|
|
147
|
+
}
|
|
148
|
+
}
|