@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.
Files changed (163) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +334 -0
  3. package/THIRD-PARTY-NOTICES.md +83 -0
  4. package/data/ck3/dataTypes.json +2195 -0
  5. package/data/ck3/data_types/data_types_common.txt +2040 -0
  6. package/data/ck3/data_types/data_types_gui.txt +5264 -0
  7. package/data/ck3/data_types/data_types_internalclausewitzgui.txt +14843 -0
  8. package/data/ck3/data_types/data_types_script.txt +4251 -0
  9. package/data/ck3/data_types/data_types_uncategorized.txt +109984 -0
  10. package/data/ck3/freqs.json +1 -0
  11. package/data/ck3/guiSchema.json +6344 -0
  12. package/data/ck3/script_docs/effects.log +16059 -0
  13. package/data/ck3/script_docs/event_targets.log +2098 -0
  14. package/data/ck3/script_docs/modifiers.log +2228 -0
  15. package/data/ck3/script_docs/on_actions.log +5275 -0
  16. package/data/ck3/script_docs/triggers.log +11991 -0
  17. package/data/ck3/structures.json +9743 -0
  18. package/data/ck3/wikidocs/ATTRIBUTION.md +18 -0
  19. package/data/ck3/wikidocs/Data_types.md +2568 -0
  20. package/data/ck3/wikidocs/Effects_list.md +1176 -0
  21. package/data/ck3/wikidocs/Scopes_list.md +341 -0
  22. package/data/ck3/wikidocs/Triggers_list.md +1097 -0
  23. package/data/eu5/data_types/data_types_common.txt +2087 -0
  24. package/data/eu5/data_types/data_types_gui.txt +6732 -0
  25. package/data/eu5/data_types/data_types_internalclausewitzgui.txt +19276 -0
  26. package/data/eu5/data_types/data_types_script.txt +5688 -0
  27. package/data/eu5/data_types/data_types_uncategorized.txt +135569 -0
  28. package/data/vic3/data_types/data_types_common.txt +2021 -0
  29. package/data/vic3/data_types/data_types_gui.txt +5592 -0
  30. package/data/vic3/data_types/data_types_internalclausewitzgui.txt +17304 -0
  31. package/data/vic3/data_types/data_types_script.txt +2817 -0
  32. package/data/vic3/data_types/data_types_uncategorized.txt +84354 -0
  33. package/data/vic3/freqs.json +1 -0
  34. package/data/vic3/guiSchema.json +5578 -0
  35. package/data/vic3/script_docs/effects.log +38135 -0
  36. package/data/vic3/script_docs/event_targets.log +2028 -0
  37. package/data/vic3/script_docs/modifiers.log +18954 -0
  38. package/data/vic3/script_docs/on_actions.log +1561 -0
  39. package/data/vic3/script_docs/triggers.log +15738 -0
  40. package/data/vic3/structures.json +10189 -0
  41. package/dist/server.js +63668 -0
  42. package/media/px-lsp.svg +12 -0
  43. package/package.json +50 -0
  44. package/src/clientMode.ts +60 -0
  45. package/src/coa/coa.ts +184 -0
  46. package/src/coa/coaParse.ts +267 -0
  47. package/src/context.ts +78 -0
  48. package/src/contextKeywords.ts +224 -0
  49. package/src/data/dataBindingMacros.ts +82 -0
  50. package/src/data/dataFnDocs.ts +152 -0
  51. package/src/data/dataFnUsage.ts +431 -0
  52. package/src/data/dataTypes.ts +279 -0
  53. package/src/data/defines.ts +123 -0
  54. package/src/data/docsParser.ts +453 -0
  55. package/src/data/keywordDocs.ts +98 -0
  56. package/src/data/modifierTemplates.ts +143 -0
  57. package/src/data/textFormatting.ts +165 -0
  58. package/src/data/wikiDocs.ts +187 -0
  59. package/src/dds/decoder.ts +1007 -0
  60. package/src/dds/encode.ts +235 -0
  61. package/src/dds/index.ts +58 -0
  62. package/src/dds/png.ts +96 -0
  63. package/src/dds/tga.ts +62 -0
  64. package/src/documents.ts +35 -0
  65. package/src/features/assetPaths.ts +169 -0
  66. package/src/features/codeActions.ts +148 -0
  67. package/src/features/colors.ts +244 -0
  68. package/src/features/completion.ts +961 -0
  69. package/src/features/datafunction.ts +729 -0
  70. package/src/features/definition.ts +84 -0
  71. package/src/features/diagnostics.ts +244 -0
  72. package/src/features/folding.ts +106 -0
  73. package/src/features/formatting.ts +60 -0
  74. package/src/features/guiLanguage.ts +366 -0
  75. package/src/features/guiNavigation.ts +140 -0
  76. package/src/features/guiTree.ts +97 -0
  77. package/src/features/hover.ts +817 -0
  78. package/src/features/hoverRender.ts +222 -0
  79. package/src/features/inlayHints.ts +147 -0
  80. package/src/features/locFormatting.ts +127 -0
  81. package/src/features/references.ts +70 -0
  82. package/src/features/rename.ts +135 -0
  83. package/src/features/scopeAt.ts +65 -0
  84. package/src/features/semanticTokens.ts +188 -0
  85. package/src/features/signatureHelp.ts +72 -0
  86. package/src/features/symbols.ts +241 -0
  87. package/src/features/textureHover.ts +143 -0
  88. package/src/features/workspaceSymbols.ts +69 -0
  89. package/src/games/active.ts +19 -0
  90. package/src/games/ck3/ambientScopes.ts +273 -0
  91. package/src/games/ck3/index.ts +38 -0
  92. package/src/games/ck3/meta.ts +28 -0
  93. package/src/games/ck3/modifierPlaceholders.ts +61 -0
  94. package/src/games/ck3/saveSchema.ts +134 -0
  95. package/src/games/ck3/scaffolds.ts +197 -0
  96. package/src/games/ck3/schema.ts +422 -0
  97. package/src/games/ck3/structures.ts +887 -0
  98. package/src/games/eu5/index.ts +75 -0
  99. package/src/games/eu5/meta.ts +44 -0
  100. package/src/games/eu5/scaffolds.ts +49 -0
  101. package/src/games/eu5/schema.generated.ts +1043 -0
  102. package/src/games/jomini/variables.ts +134 -0
  103. package/src/games/profile.ts +205 -0
  104. package/src/games/registry.ts +27 -0
  105. package/src/games/vic3/index.ts +52 -0
  106. package/src/games/vic3/meta.ts +55 -0
  107. package/src/games/vic3/saveSchema.ts +77 -0
  108. package/src/games/vic3/scaffolds.ts +135 -0
  109. package/src/games/vic3/schema.ts +650 -0
  110. package/src/games/vic3/structures.ts +33 -0
  111. package/src/gui/anchorSpec.ts +66 -0
  112. package/src/gui/declMarkers.ts +30 -0
  113. package/src/gui/fillGeometry.ts +101 -0
  114. package/src/gui/guiDefs.ts +386 -0
  115. package/src/gui/guiDependencies.ts +352 -0
  116. package/src/gui/guiLinks.ts +64 -0
  117. package/src/gui/layoutEngine.ts +1998 -0
  118. package/src/gui/layoutService.ts +221 -0
  119. package/src/gui/measuredMetrics.ts +21 -0
  120. package/src/gui/previewService.ts +89 -0
  121. package/src/gui/saveSchema.ts +220 -0
  122. package/src/gui/saveValues.ts +399 -0
  123. package/src/gui/saveZip.ts +60 -0
  124. package/src/gui/sourceEdit.ts +535 -0
  125. package/src/gui/sourceEditService.ts +439 -0
  126. package/src/gui/sourceModel.ts +603 -0
  127. package/src/gui/textResolve.ts +145 -0
  128. package/src/gui/textureInfo.ts +106 -0
  129. package/src/gui/vocabulary.ts +149 -0
  130. package/src/gui/widgetEdit.ts +52 -0
  131. package/src/gui/widgetInfo.ts +245 -0
  132. package/src/index/docComments.ts +103 -0
  133. package/src/index/extract.ts +252 -0
  134. package/src/index/indexer.ts +369 -0
  135. package/src/index/intern.ts +101 -0
  136. package/src/index/lazyRefs.ts +145 -0
  137. package/src/index/modOrigin.ts +69 -0
  138. package/src/index/references.ts +534 -0
  139. package/src/overview/dependencies.ts +240 -0
  140. package/src/overview/eventBanner.ts +95 -0
  141. package/src/overview/eventDetail.ts +482 -0
  142. package/src/overview/eventGraph.ts +617 -0
  143. package/src/overview/eventVocabulary.ts +214 -0
  144. package/src/overview/locCoverage.ts +138 -0
  145. package/src/overview/modOverview.ts +29 -0
  146. package/src/overview/overrides.ts +89 -0
  147. package/src/parseCache.ts +81 -0
  148. package/src/parser/cst.ts +257 -0
  149. package/src/parser/encoding.ts +106 -0
  150. package/src/parser/index.ts +7 -0
  151. package/src/parser/lexer.ts +245 -0
  152. package/src/parser/locParser.ts +276 -0
  153. package/src/parser/parser.ts +360 -0
  154. package/src/schema/freqs.ts +70 -0
  155. package/src/schema/loader.ts +113 -0
  156. package/src/schema/types.ts +142 -0
  157. package/src/scopes/inference.ts +478 -0
  158. package/src/scopes/model.ts +148 -0
  159. package/src/scopes/varTypes.ts +290 -0
  160. package/src/server.ts +1894 -0
  161. package/src/serverData.ts +98 -0
  162. package/src/structure.ts +56 -0
  163. package/src/wordAt.ts +49 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Safe rename across script and localization: definition sites + every indexed
3
+ * reference (scope:x, ref fields, loc-key usages). Only names whose every
4
+ * definition lives in the mod are renameable — renaming a vanilla override
5
+ * would silently un-override it.
6
+ */
7
+ import {
8
+ ResponseError,
9
+ type Position,
10
+ type Range as LspRange,
11
+ type TextEdit,
12
+ type WorkspaceEdit,
13
+ } from "vscode-languageserver/node";
14
+ import type { TextDocument } from "vscode-languageserver-textdocument";
15
+ import { URI } from "vscode-uri";
16
+ import * as fs from "fs";
17
+ import type { Definition } from "@px-lsp/protocol/types";
18
+ import { wholeNamePattern } from "@px-lsp/protocol/regex";
19
+ import type { ServerData } from "../serverData";
20
+ import { activeProfile } from "../games/active";
21
+ import { wordRangeAt } from "../wordAt";
22
+ import { getLineText } from "../documents";
23
+ import { stripPrefix } from "./references";
24
+
25
+ const VALID_NAME = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;
26
+
27
+ interface RenameTarget {
28
+ name: string;
29
+ defs: Definition[];
30
+ /** Range of the name under the cursor. */
31
+ range: LspRange;
32
+ }
33
+
34
+ function targetAt(data: ServerData, document: TextDocument, position: Position): RenameTarget {
35
+ const lineText = getLineText(document, position.line);
36
+ const range = wordRangeAt(lineText, position.character);
37
+ if (!range) throw new ResponseError(0, "No renameable name at the cursor.");
38
+ const name = stripPrefix(range.word);
39
+ const prefixLen = range.word.length - name.length;
40
+
41
+ const defs = data.index.lookupAll(name);
42
+ if (defs.length === 0) {
43
+ throw new ResponseError(
44
+ 0,
45
+ `No indexed definition of "${name}" — only indexed names can be renamed safely.`
46
+ );
47
+ }
48
+ const foreign = defs.find((d) => d.source !== "mod");
49
+ if (foreign) {
50
+ throw new ResponseError(
51
+ 0,
52
+ `"${name}" is defined in ${foreign.source} content; only names defined solely by the mod can be renamed.`
53
+ );
54
+ }
55
+ return {
56
+ name,
57
+ defs,
58
+ range: {
59
+ start: { line: position.line, character: range.start + prefixLen },
60
+ end: { line: position.line, character: range.end },
61
+ },
62
+ };
63
+ }
64
+
65
+ export function prepareRename(data: ServerData, document: TextDocument, position: Position): LspRange {
66
+ return targetAt(data, document, position).range;
67
+ }
68
+
69
+ export function provideRename(
70
+ data: ServerData,
71
+ document: TextDocument,
72
+ position: Position,
73
+ newName: string,
74
+ readOpenDocument: (uri: string) => TextDocument | undefined
75
+ ): WorkspaceEdit {
76
+ const target = targetAt(data, document, position);
77
+ if (!VALID_NAME.test(newName)) {
78
+ throw new ResponseError(0, `"${newName}" is not a valid ${activeProfile().shortName} identifier.`);
79
+ }
80
+
81
+ const editsByUri = new Map<string, TextEdit[]>();
82
+ const seen = new Set<string>();
83
+ const addEdit = (file: string, line: number, startChar: number, endChar: number) => {
84
+ const uri = URI.file(file).toString();
85
+ const key = `${uri}:${line}:${startChar}`;
86
+ if (seen.has(key)) return;
87
+ seen.add(key);
88
+ let list = editsByUri.get(uri);
89
+ if (!list) editsByUri.set(uri, (list = []));
90
+ list.push({
91
+ range: { start: { line, character: startChar }, end: { line, character: endChar } },
92
+ newText: newName,
93
+ });
94
+ };
95
+
96
+ // Reference sites carry precise ranges.
97
+ for (const ref of data.refIndex.lookup(target.name)) {
98
+ addEdit(ref.file, ref.line, ref.startChar, ref.endChar);
99
+ }
100
+
101
+ // Definition sites: locate the name on its recorded line.
102
+ for (const def of target.defs) {
103
+ const lineText = readLine(def, readOpenDocument);
104
+ if (lineText === null) continue;
105
+ const col = findNameOnLine(lineText, target.name);
106
+ if (col < 0) continue;
107
+ addEdit(def.file, def.line, col, col + target.name.length);
108
+ }
109
+
110
+ const changes: Record<string, TextEdit[]> = {};
111
+ for (const [uri, edits] of editsByUri) changes[uri] = edits;
112
+ return { changes };
113
+ }
114
+
115
+ function readLine(
116
+ def: Definition,
117
+ readOpenDocument: (uri: string) => TextDocument | undefined
118
+ ): string | null {
119
+ const uri = URI.file(def.file).toString();
120
+ const open = readOpenDocument(uri);
121
+ if (open) return getLineText(open, def.line);
122
+ try {
123
+ const lines = fs.readFileSync(def.file, "utf8").replace(/^/, "").split(/\r?\n/);
124
+ return lines[def.line] ?? null;
125
+ } catch {
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /** First word-boundary occurrence of `name` on the line. */
131
+ function findNameOnLine(lineText: string, name: string): number {
132
+ const re = new RegExp(wholeNamePattern(name));
133
+ const m = re.exec(lineText);
134
+ return m ? m.index : -1;
135
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * paradox/scopeAt: the inferred scope chain at a cursor position, for an
3
+ * embedder's scope status bar ("Scope: character →[every_vassal]→ character").
4
+ *
5
+ * Strictly a read-out of the AD-5 inference that completion, hover and inlay
6
+ * hints already run at a position — same parse cache, same canonical
7
+ * InferenceContext — so what a client displays can never disagree with what
8
+ * ranking used. Nothing here influences inference or ranking.
9
+ */
10
+ import type { Position } from "vscode-languageserver/node";
11
+ import type { TextDocument } from "vscode-languageserver-textdocument";
12
+ import type { ScopeAtResult, ScopeChainStep } from "@px-lsp/protocol/protocol";
13
+ import { getParse, getSavedScopes } from "../parseCache";
14
+ import { inferScopeAt } from "../scopes/inference";
15
+ import { inferenceContextFor } from "../scopes/varTypes";
16
+ import type { Scope } from "../scopes/model";
17
+ import type { SchemaEntry } from "../schema/types";
18
+ import type { ServerData } from "../serverData";
19
+
20
+ export function computeScopeAt(
21
+ data: ServerData,
22
+ document: TextDocument,
23
+ position: Position,
24
+ rootScopes: Set<Scope> | null,
25
+ entry: SchemaEntry | null
26
+ ): ScopeAtResult {
27
+ const { result, lineIndex } = getParse(document);
28
+ const ictx = inferenceContextFor(data, entry);
29
+ const saved = getSavedScopes(document, data.scopeModel, rootScopes, entry?.ambientScopes, ictx);
30
+ const inference = inferScopeAt(
31
+ result,
32
+ lineIndex.offsetAt(position),
33
+ data.scopeModel,
34
+ rootScopes,
35
+ saved,
36
+ ictx
37
+ );
38
+ return {
39
+ scopes: list(inference.scopes),
40
+ chain: inference.chain.map(parseStep),
41
+ savedScopes: [...saved]
42
+ .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
43
+ .map(([name, scopes]) => ({ name, scopes: list(scopes) })),
44
+ };
45
+ }
46
+
47
+ /**
48
+ * inferScopeAt reports its chain as display strings — the seed scope alone
49
+ * first, then `<key> → <scopes>` per step, with "unknown" for an unresolved
50
+ * set and "|" between alternatives. Splitting them back apart is what keeps
51
+ * this request read-only over the inference module.
52
+ */
53
+ function parseStep(text: string): ScopeChainStep {
54
+ const sep = text.indexOf(" → ");
55
+ if (sep < 0) return { scopes: parseScopes(text) };
56
+ return { entryKeyword: text.slice(0, sep), scopes: parseScopes(text.slice(sep + 3)) };
57
+ }
58
+
59
+ function parseScopes(text: string): string[] {
60
+ return text === "unknown" ? [] : text.split("|");
61
+ }
62
+
63
+ function list(scopes: Set<Scope> | null): string[] {
64
+ return scopes ? [...scopes] : [];
65
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Semantic highlighting: colors identifiers by what they actually are, using the
3
+ * script_docs/wiki token data and the definition index. Standard token types
4
+ * only, so every theme colors them without configuration.
5
+ *
6
+ * Based on the CST (comments and quoted strings are simply never visited),
7
+ * replacing the v0.3 per-line mask-and-regex scan.
8
+ */
9
+ import { SemanticTokensBuilder } from "vscode-languageserver/node";
10
+ import type { SemanticTokens, SemanticTokensLegend } from "vscode-languageserver/node";
11
+ import type { TextDocument } from "vscode-languageserver-textdocument";
12
+ import type { TokenKind } from "@px-lsp/protocol/types";
13
+ import type { SchemaEntry, RefField } from "../schema/types";
14
+ import { dynamicRefKinds } from "../games/jomini/variables";
15
+ import type { StructureIndex } from "../schema/loader";
16
+ import type { ServerData } from "../serverData";
17
+ import { walkStatements, type AssignmentNode, type BlockNode, type ScalarNode } from "../parser";
18
+ import { getParse } from "../parseCache";
19
+
20
+ const TOKEN_TYPES = [
21
+ "method", // engine effects
22
+ "function", // engine triggers
23
+ "variable", // event targets
24
+ "property", // modifiers
25
+ "macro", // scripted effects/triggers (reusable script)
26
+ "event", // event IDs and on_actions
27
+ "enumMember", // script values
28
+ "string", // loc keys referenced from script
29
+ ] as const;
30
+
31
+ const TOKEN_MODIFIERS = ["defaultLibrary"] as const; // vanilla/engine vs mod
32
+
33
+ export const SEMANTIC_LEGEND: SemanticTokensLegend = {
34
+ tokenTypes: [...TOKEN_TYPES],
35
+ tokenModifiers: [...TOKEN_MODIFIERS],
36
+ };
37
+
38
+ const TYPE_INDEX: Record<string, number> = Object.fromEntries(TOKEN_TYPES.map((t, i) => [t, i]));
39
+ const VANILLA_BIT = 1 << 0;
40
+
41
+ const ENGINE_TYPE: Record<TokenKind, string> = {
42
+ effect: "method",
43
+ trigger: "function",
44
+ event_target: "variable",
45
+ modifier: "property",
46
+ };
47
+
48
+ // Schema-driven kinds are an open set; unlisted ones read as enumMember.
49
+ const DEF_TYPE: Record<string, string> = {
50
+ scripted_effect: "macro",
51
+ scripted_trigger: "macro",
52
+ event: "event",
53
+ on_action: "event",
54
+ script_value: "enumMember",
55
+ scripted_modifier: "property",
56
+ loc_key: "string",
57
+ saved_scope: "variable",
58
+ variable: "variable",
59
+ local_variable: "variable",
60
+ global_variable: "variable",
61
+ variable_list: "variable",
62
+ local_variable_list: "variable",
63
+ global_variable_list: "variable",
64
+ flag: "variable",
65
+ list: "variable",
66
+ };
67
+ const DEF_TYPE_FALLBACK = "enumMember";
68
+
69
+ const IDENTIFIER_START = /^[A-Za-z_]/;
70
+
71
+ export function provideSemanticTokens(
72
+ data: ServerData,
73
+ document: TextDocument,
74
+ refFields?: Map<string, RefField>,
75
+ entry?: SchemaEntry | null,
76
+ structures?: StructureIndex
77
+ ): SemanticTokens {
78
+ const { result, lineIndex } = getParse(document);
79
+ const builder = new SemanticTokensBuilder();
80
+ const byBlock = entry?.kind && structures ? structures.keysByKindBlock.get(entry.kind) : undefined;
81
+
82
+ const pushScalar = (scalar: ScalarNode, expectedKinds?: string[]) => {
83
+ if (scalar.quoted) return;
84
+ let word = scalar.text;
85
+ if (!IDENTIFIER_START.test(word)) return;
86
+ // For prefixed references (scope:x, culture:czech) classify the prefix only —
87
+ // the grammar colors the whole reference, we refine the head word.
88
+ const colon = word.indexOf(":");
89
+ if (colon >= 0) word = word.slice(0, colon);
90
+ if (word === "") return;
91
+ const classified = classify(data, word, colon >= 0 ? undefined : expectedKinds);
92
+ if (!classified) return;
93
+ const pos = lineIndex.positionAt(scalar.range.start);
94
+ builder.push(pos.line, pos.character, word.length, classified.type, classified.modifiers);
95
+ };
96
+
97
+ /** Kinds a ref field expects for values under `key` (scalar or list form). */
98
+ const fieldKinds = (key: ScalarNode, form: "scalar" | "list"): string[] | undefined => {
99
+ if (key.quoted) return undefined;
100
+ const field = refFields?.get(key.text);
101
+ if (!field) {
102
+ // Pattern families (has_*_flag, is_in_list) — scalar form only.
103
+ return refFields && form === "scalar" ? (dynamicRefKinds(key.text) ?? undefined) : undefined;
104
+ }
105
+ return field.form !== (form === "scalar" ? "list" : "scalar") ? field.kinds : undefined;
106
+ };
107
+
108
+ /** `type = character_event`: the value is a member of the key's structure enum. */
109
+ const isEnumMember = (
110
+ key: ScalarNode,
111
+ value: ScalarNode,
112
+ ancestors: readonly (AssignmentNode | BlockNode)[]
113
+ ): boolean => {
114
+ if (!byBlock || key.quoted || value.quoted) return false;
115
+ const named = ancestors.filter((a): a is AssignmentNode => a.kind === "assignment" && !a.key.quoted);
116
+ if (named.length === 0) return false;
117
+ const keys =
118
+ named.length === 1 ? byBlock.get("") : byBlock.get(named[named.length - 1].key.text.toLowerCase());
119
+ const spec = keys?.get(key.text);
120
+ if (!spec?.values?.startsWith("enum:")) return false;
121
+ return spec.values.slice(5).split("|").includes(value.text);
122
+ };
123
+
124
+ const pushAs = (scalar: ScalarNode, type: string, modifiers: number) => {
125
+ const pos = lineIndex.positionAt(scalar.range.start);
126
+ builder.push(pos.line, pos.character, scalar.text.length, TYPE_INDEX[type], modifiers);
127
+ };
128
+
129
+ walkStatements(result.root, (stmt, ancestors) => {
130
+ if (stmt.kind === "assignment") {
131
+ pushScalar(stmt.key);
132
+ if (stmt.value?.kind === "scalar") {
133
+ const key = stmt.key;
134
+ const value = stmt.value;
135
+ if (!key.quoted && key.text === "namespace" && !value.quoted && IDENTIFIER_START.test(value.text)) {
136
+ // Event namespaces read as the event family.
137
+ pushAs(value, "event", 0);
138
+ } else if (isEnumMember(key, value, ancestors)) {
139
+ pushAs(value, "enumMember", VANILLA_BIT);
140
+ } else {
141
+ pushScalar(value, fieldKinds(key, "scalar"));
142
+ }
143
+ }
144
+ if (stmt.value?.kind === "tagged-block") pushScalar(stmt.value.tag);
145
+ } else if (stmt.value.kind === "scalar") {
146
+ // Bare list element: the owning assignment is two frames up (assignment, block).
147
+ const parent = ancestors.length >= 2 ? ancestors[ancestors.length - 2] : undefined;
148
+ const kinds = parent?.kind === "assignment" ? fieldKinds(parent.key, "list") : undefined;
149
+ pushScalar(stmt.value, kinds);
150
+ } else if (stmt.value.kind === "tagged-block") {
151
+ pushScalar(stmt.value.tag);
152
+ }
153
+ });
154
+
155
+ return builder.build();
156
+ }
157
+
158
+ function classify(
159
+ data: ServerData,
160
+ word: string,
161
+ expectedKinds?: string[]
162
+ ): { type: number; modifiers: number } | null {
163
+ // A ref-field value names a definition of a known kind — that reading wins
164
+ // over a same-named engine token (`theme = faith` is an event theme, not the
165
+ // faith event target).
166
+ if (expectedKinds) {
167
+ const match = data.index.lookup(word).find((d) => expectedKinds.includes(d.kind));
168
+ if (match) {
169
+ return {
170
+ type: TYPE_INDEX[DEF_TYPE[match.kind] ?? DEF_TYPE_FALLBACK],
171
+ modifiers: match.source === "mod" ? 0 : VANILLA_BIT,
172
+ };
173
+ }
174
+ }
175
+ const tokens = data.tokenMap.get(word);
176
+ if (tokens && tokens.length > 0) {
177
+ return { type: TYPE_INDEX[ENGINE_TYPE[tokens[0].kind]], modifiers: VANILLA_BIT };
178
+ }
179
+ const defs = data.index.lookup(word);
180
+ if (defs.length > 0) {
181
+ const def = defs[0];
182
+ return {
183
+ type: TYPE_INDEX[DEF_TYPE[def.kind] ?? DEF_TYPE_FALLBACK],
184
+ modifiers: def.source === "mod" ? 0 : VANILLA_BIT,
185
+ };
186
+ }
187
+ return null;
188
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Signature help for scripted effects/triggers with $PARAM$ parameters,
3
+ * harvested at index time: inside `my_effect = { PARAM = ... }` call blocks
4
+ * the parameter list is shown with the active parameter highlighted.
5
+ */
6
+ import type { Position, SignatureHelp } from "vscode-languageserver/node";
7
+ import type { TextDocument } from "vscode-languageserver-textdocument";
8
+ import type { ServerData } from "../serverData";
9
+ import { getParse } from "../parseCache";
10
+ import { nodeAtOffset, type Statement } from "../parser";
11
+ import { getLineText } from "../documents";
12
+
13
+ export function provideSignatureHelp(
14
+ data: ServerData,
15
+ document: TextDocument,
16
+ position: Position
17
+ ): SignatureHelp | null {
18
+ const { result, lineIndex } = getParse(document);
19
+ const offset = lineIndex.offsetAt(position);
20
+ const hit = nodeAtOffset(result.root, offset);
21
+ if (!hit) return null;
22
+
23
+ // Innermost enclosing assignment whose key is a parameterized definition.
24
+ for (let i = hit.path.length - 1; i >= 0; i--) {
25
+ const stmt: Statement = hit.path[i];
26
+ if (stmt.kind !== "assignment" || stmt.key.quoted) continue;
27
+ if (stmt.value?.kind !== "block") continue;
28
+ if (offset <= stmt.value.openBrace) continue;
29
+ const defs = data.index.lookup(stmt.key.text);
30
+ const def = defs.find((d) => d.params && d.params.length > 0);
31
+ if (!def || !def.params) continue;
32
+
33
+ const params = def.params;
34
+ const label = `${def.name} = { ${params.map((p) => `${p} = …`).join(" ")} }`;
35
+
36
+ // Active parameter: the key being written on the current line, if it matches.
37
+ const lineText = getLineText(document, position.line);
38
+ const keyMatch = /([A-Za-z0-9_]+)\s*=?[^=]*$/.exec(lineText.slice(0, position.character));
39
+ let active = 0;
40
+ if (keyMatch) {
41
+ const idx = params.indexOf(keyMatch[1]);
42
+ if (idx >= 0) active = idx;
43
+ }
44
+
45
+ // Align PdxDoc `@param NAME desc` tags (§E3) to the $PARAM$ names by NAME, so
46
+ // the active parameter shows its description. Fail-soft: no doc → no text.
47
+ const paramDoc = new Map<string, string>();
48
+ for (const t of def.tags ?? []) {
49
+ if (t.tag !== "param") continue;
50
+ const m = /^(\S+)\s*(.*)$/.exec(t.text);
51
+ if (m && m[2].trim()) paramDoc.set(m[1], m[2].trim());
52
+ }
53
+
54
+ const prose = def.doc ? `${def.doc}\n\n` : "";
55
+
56
+ return {
57
+ signatures: [
58
+ {
59
+ label,
60
+ documentation: `${prose}${def.kind.replace(/_/g, " ")} (${def.source}) with ${params.length} parameter(s)`,
61
+ parameters: params.map((p) => {
62
+ const desc = paramDoc.get(p);
63
+ return desc ? { label: `${p} = …`, documentation: desc } : { label: `${p} = …` };
64
+ }),
65
+ },
66
+ ],
67
+ activeSignature: 0,
68
+ activeParameter: active,
69
+ };
70
+ }
71
+ return null;
72
+ }