@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,817 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover: engine-token docs (script_docs/wiki), indexed-definition summaries,
|
|
3
|
+
* block-schema structure keys (§B2) and saved-scope cards for `scope:`/`var:`
|
|
4
|
+
* references (§B3).
|
|
5
|
+
*
|
|
6
|
+
* The data-gathering here builds `CardInput` records; visual assembly (badges,
|
|
7
|
+
* scope pills, fenced examples, the single shared scope footer) lives in
|
|
8
|
+
* `hoverRender.ts` so the D2 layout is unit-tested without LSP types.
|
|
9
|
+
*/
|
|
10
|
+
import { MarkupKind, type Hover, type Position } from "vscode-languageserver/node";
|
|
11
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
12
|
+
import * as path from "path";
|
|
13
|
+
import { URI } from "vscode-uri";
|
|
14
|
+
import type { SchemaEntry } from "../schema/types";
|
|
15
|
+
import type { TokenData } from "@px-lsp/protocol/types";
|
|
16
|
+
import { clientCommands } from "@px-lsp/protocol/protocol";
|
|
17
|
+
import { canRunCommand } from "../clientMode";
|
|
18
|
+
import type { ServerData } from "../serverData";
|
|
19
|
+
import type { SchemaData } from "../schema/loader";
|
|
20
|
+
import { scopePrefixBefore, wordRangeAt } from "../wordAt";
|
|
21
|
+
import { getLineText } from "../documents";
|
|
22
|
+
import { getParse, getSavedScopes } from "../parseCache";
|
|
23
|
+
import { structureContextAt } from "../structure";
|
|
24
|
+
import { inferScopeAt } from "../scopes/inference";
|
|
25
|
+
import { inferenceContextFor, variableTypes } from "../scopes/varTypes";
|
|
26
|
+
import { nodeAtOffset, walkStatements } from "../parser";
|
|
27
|
+
import type { RefField } from "../schema/types";
|
|
28
|
+
import { VAR_PREFIX_KINDS } from "../games/jomini/variables";
|
|
29
|
+
import { activeProfile } from "../games/active";
|
|
30
|
+
import { KEYWORD_DOCS, scopeWordDoc } from "../data/keywordDocs";
|
|
31
|
+
import { matchTemplatedModifier, templatedModifierDoc } from "../data/modifierTemplates";
|
|
32
|
+
import type { Scope } from "../scopes/model";
|
|
33
|
+
import {
|
|
34
|
+
renderCard,
|
|
35
|
+
renderDocBody,
|
|
36
|
+
renderHover,
|
|
37
|
+
scopeHereLine,
|
|
38
|
+
scopePill,
|
|
39
|
+
scopeType,
|
|
40
|
+
type CardInput,
|
|
41
|
+
} from "./hoverRender";
|
|
42
|
+
import type { Definition } from "@px-lsp/protocol/types";
|
|
43
|
+
import type { DefineEntry } from "../data/defines";
|
|
44
|
+
|
|
45
|
+
export function provideHover(
|
|
46
|
+
data: ServerData,
|
|
47
|
+
document: TextDocument,
|
|
48
|
+
position: Position,
|
|
49
|
+
rootScopes: Set<Scope> | null,
|
|
50
|
+
entry: SchemaEntry | null = null,
|
|
51
|
+
getSchema?: () => SchemaData,
|
|
52
|
+
/** Definitions extracted from the OPEN document itself, consulted when the
|
|
53
|
+
* index has nothing for the word (same-file inline declarations, #5). */
|
|
54
|
+
docDefs?: (word: string) => Definition[]
|
|
55
|
+
): Hover | null {
|
|
56
|
+
const lineText = getLineText(document, position.line);
|
|
57
|
+
|
|
58
|
+
// `define:NS|CONST` — reassemble the full token (wordPattern splits on `:`/`|`).
|
|
59
|
+
const defineHit = defineRefAt(lineText, position.character);
|
|
60
|
+
if (defineHit) {
|
|
61
|
+
const card = definesCard(data, defineHit.namespace, defineHit.name);
|
|
62
|
+
if (card) {
|
|
63
|
+
return {
|
|
64
|
+
contents: { kind: MarkupKind.Markdown, value: renderHover([card], null) },
|
|
65
|
+
range: {
|
|
66
|
+
start: { line: position.line, character: defineHit.start },
|
|
67
|
+
end: { line: position.line, character: defineHit.end },
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let range = wordRangeAt(lineText, position.character);
|
|
74
|
+
if (!range) return null;
|
|
75
|
+
// Dot chains (`root.location.county`) resolve per segment under the cursor;
|
|
76
|
+
// event ids (`namespace.5.a`, any all-digit segment) stay whole.
|
|
77
|
+
if (range.word.includes(".") && !range.word.split(".").some((p) => /^\d+$/.test(p))) {
|
|
78
|
+
let start = range.start;
|
|
79
|
+
for (const part of range.word.split(".")) {
|
|
80
|
+
const end = start + part.length;
|
|
81
|
+
if (position.character <= end) {
|
|
82
|
+
range = { word: part, start, end };
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
start = end + 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const word = range.word;
|
|
89
|
+
|
|
90
|
+
const cards: string[] = [];
|
|
91
|
+
|
|
92
|
+
// Current scope at the cursor, computed once and shared by the pills and the
|
|
93
|
+
// single footer line (§D2/§D3). null when we can't infer.
|
|
94
|
+
const current = currentScopes(data, document, position, rootScopes, entry);
|
|
95
|
+
|
|
96
|
+
// `scope:x` / `var:x` reference under the cursor → saved-scope card (§B3).
|
|
97
|
+
const prefix = scopePrefixBefore(lineText, range);
|
|
98
|
+
if (prefix === "scope") {
|
|
99
|
+
const card = savedScopeCard(data, document, word, rootScopes, entry);
|
|
100
|
+
if (card) cards.push(card);
|
|
101
|
+
} else if (prefix === "var" || prefix === "local_var" || prefix === "global_var") {
|
|
102
|
+
// Typed variable card: value type from the mod-wide set-site analysis, plus
|
|
103
|
+
// set-site links (namespace-correct: var/local_var/global_var are distinct).
|
|
104
|
+
const varInfo = variableTypes(data, data.rootScopesForFile);
|
|
105
|
+
const typed = varInfo.types.get(`${prefix}:${word}`);
|
|
106
|
+
const itemTyped = varInfo.listItemTypes.get(`${prefix}:${word}`);
|
|
107
|
+
const headTail = typed
|
|
108
|
+
? `→ ${[...typed].map(scopeType).join(" | ")}`
|
|
109
|
+
: itemTyped
|
|
110
|
+
? `→ list of ${[...itemTyped].map(scopeType).join(" | ")}`
|
|
111
|
+
: itemTyped === null
|
|
112
|
+
? `→ list`
|
|
113
|
+
: `· ${prefix.replace(/_/g, " ")}`;
|
|
114
|
+
const setDefs = data.index
|
|
115
|
+
.lookup(word)
|
|
116
|
+
.filter((d) => VAR_PREFIX_KINDS[prefix].includes(d.kind))
|
|
117
|
+
.slice(0, 3);
|
|
118
|
+
const doc =
|
|
119
|
+
setDefs.length > 0
|
|
120
|
+
? setDefs
|
|
121
|
+
.map((d) => `set in [${path.basename(d.file)}:${d.line + 1}](${URI.file(d.file)}#L${d.line + 1})`)
|
|
122
|
+
.join(" \n")
|
|
123
|
+
: undefined;
|
|
124
|
+
cards.push(renderCard({ kind: "saved_scope", badgeLabel: "variable", name: word, headTail, doc }));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// When the word is the VALUE of a schema ref field (`theme = faith`,
|
|
128
|
+
// `add_trait = brave`), the schema names the kinds it can reference — show
|
|
129
|
+
// only those meanings instead of every same-named symbol (the `faith` event
|
|
130
|
+
// target is noise on `theme = faith`). Falls through when nothing matches.
|
|
131
|
+
const expected = getSchema ? refKindsAt(document, position, getSchema().refFields) : null;
|
|
132
|
+
let defs = data.index.lookup(word);
|
|
133
|
+
if (defs.length === 0 && docDefs) defs = docDefs(word);
|
|
134
|
+
const expectedDefs = expected ? defs.filter((d) => expected.includes(d.kind)) : [];
|
|
135
|
+
// Anchor for the "N references" command link: the hovered site itself, so
|
|
136
|
+
// the client can drive the references view from it.
|
|
137
|
+
const at = { uri: document.uri, line: position.line, character: range.start };
|
|
138
|
+
|
|
139
|
+
// On the KEY of an assignment, the key's own structural meaning outranks
|
|
140
|
+
// same-named value identities: a total conversion that saves a scope named
|
|
141
|
+
// `type` in 33 places must not bury what `type =` means in an event.
|
|
142
|
+
const keyPos = !prefix && atKeyPosition(document, position);
|
|
143
|
+
const structureCard =
|
|
144
|
+
keyPos && entry?.kind && getSchema ? structureKeyCard(document, position, word, entry, getSchema) : null;
|
|
145
|
+
|
|
146
|
+
if (expectedDefs.length > 0) {
|
|
147
|
+
cards.push(...definitionCards(data, expectedDefs, at));
|
|
148
|
+
} else {
|
|
149
|
+
if (structureCard) cards.push(structureCard);
|
|
150
|
+
for (const token of data.tokenMap.get(word) ?? []) {
|
|
151
|
+
cards.push(tokenCard(token, current));
|
|
152
|
+
}
|
|
153
|
+
let shownDefs = defs;
|
|
154
|
+
if (keyPos && (cards.length > 0 || defs.some((d) => !VALUE_IDENTITY_KINDS.has(d.kind)))) {
|
|
155
|
+
shownDefs = defs.filter((d) => !VALUE_IDENTITY_KINDS.has(d.kind));
|
|
156
|
+
}
|
|
157
|
+
cards.push(...definitionCards(data, shownDefs, at));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fallback cards, only when nothing else matched, so a real token/def with
|
|
161
|
+
// the same name keeps precedence. Most-specific first: block structure key,
|
|
162
|
+
// enum value of a structure key, macro parameter of the called scripted
|
|
163
|
+
// effect/trigger, event namespace, scope keyword, grammar keyword.
|
|
164
|
+
if (!prefix && cards.length === 0) {
|
|
165
|
+
const card =
|
|
166
|
+
(entry?.kind && getSchema ? structureKeyCard(document, position, word, entry, getSchema) : null) ??
|
|
167
|
+
(entry?.kind && getSchema ? enumValueCard(document, position, word, entry, getSchema) : null) ??
|
|
168
|
+
macroParamCard(data, document, position, word) ??
|
|
169
|
+
relationTriggerCard(data, word) ??
|
|
170
|
+
templatedModifierCard(data, word) ??
|
|
171
|
+
namespaceCard(data, word) ??
|
|
172
|
+
scopeWordCard(word) ??
|
|
173
|
+
keywordCard(word) ??
|
|
174
|
+
effectArgumentCard(data, document, position, word);
|
|
175
|
+
if (card) cards.push(card);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (cards.length === 0) return null;
|
|
179
|
+
|
|
180
|
+
// Scope context appears once, last (§D2). Suppressed for a `scope:` hover
|
|
181
|
+
// (its own card already carries the scope) to match the prior behavior.
|
|
182
|
+
let footer: string | null = null;
|
|
183
|
+
if (prefix !== "scope") {
|
|
184
|
+
const scopes = current && current.size > 0 ? [...current].join(" | ") : "unknown";
|
|
185
|
+
const inference = scopeInference(data, document, position, rootScopes, entry);
|
|
186
|
+
const chain = inference.chain.length > 1 ? inference.chain.join(" · ") : null;
|
|
187
|
+
footer = scopeHereLine(scopes, chain);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
contents: { kind: MarkupKind.Markdown, value: renderHover(cards, footer) },
|
|
192
|
+
range: {
|
|
193
|
+
start: { line: position.line, character: range.start },
|
|
194
|
+
end: { line: position.line, character: range.end },
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* An engine-token card (§D2 mock 1). Teaches USAGE, not usage counts: what the
|
|
201
|
+
* token does, the datatype its VALUE expects, a syntax example, and the scopes
|
|
202
|
+
* it runs in / the scope it returns. No vanilla-frequency line — engine tokens
|
|
203
|
+
* never carry one and the user does not want it.
|
|
204
|
+
*/
|
|
205
|
+
function tokenCard(token: TokenData, current: ReadonlySet<string> | null): string {
|
|
206
|
+
const card: CardInput = { kind: token.kind, name: token.name };
|
|
207
|
+
const { input, output, plain } = partitionScopes(token.scopes);
|
|
208
|
+
|
|
209
|
+
// Event targets return a scope: surface it as the `→ type` head tail — it is
|
|
210
|
+
// this token's real "datatype".
|
|
211
|
+
if (output.length > 0) card.headTail = `→ ${output.map(scopeType).join(" | ")}`;
|
|
212
|
+
|
|
213
|
+
// Description, then the expected value datatype (the user's core ask).
|
|
214
|
+
const docParts: string[] = [];
|
|
215
|
+
if (token.doc) docParts.push(token.doc);
|
|
216
|
+
const shape = valueShape(token);
|
|
217
|
+
if (shape) docParts.push(`Value: ${shape}`);
|
|
218
|
+
if (docParts.length > 0) card.doc = docParts.join("\n\n");
|
|
219
|
+
|
|
220
|
+
// Syntax example, fenced (from a script_docs `usage:` block or the wiki).
|
|
221
|
+
if (token.usage) card.example = token.usage;
|
|
222
|
+
|
|
223
|
+
// Remaining metadata (targets, categories, requires-data, wiki note); the
|
|
224
|
+
// `Traits:` line is already folded into the value datatype above.
|
|
225
|
+
const meta = otherTraitBits(token.traits);
|
|
226
|
+
if (meta.length > 0) card.traits = meta.join(" · ");
|
|
227
|
+
|
|
228
|
+
// Input scope(s) you call it from — matched against the current cursor scope.
|
|
229
|
+
const scopeVals = plain.length > 0 ? plain : input;
|
|
230
|
+
if (scopeVals.length > 0) {
|
|
231
|
+
card.footer = [`Supported scopes: ${scopeVals.map((s) => scopePill(s, current)).join(" ")}`];
|
|
232
|
+
}
|
|
233
|
+
return renderCard(card);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Split raw scope strings into the input / output / plain buckets. */
|
|
237
|
+
function partitionScopes(scopes: string[]): { input: string[]; output: string[]; plain: string[] } {
|
|
238
|
+
const input: string[] = [];
|
|
239
|
+
const output: string[] = [];
|
|
240
|
+
const plain: string[] = [];
|
|
241
|
+
for (const s of scopes) {
|
|
242
|
+
if (s.startsWith("input: ")) input.push(s.slice("input: ".length));
|
|
243
|
+
else if (s.startsWith("output: ")) output.push(s.slice("output: ".length));
|
|
244
|
+
else plain.push(s);
|
|
245
|
+
}
|
|
246
|
+
return { input, output, plain };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Plain-language datatype for a token's VALUE, deduced from its `Traits:` line
|
|
251
|
+
* and kind: boolean, comparison, scope target, database key, block. null when
|
|
252
|
+
* the docs give no basis to say (e.g. a plain event target — the `→ type` head
|
|
253
|
+
* already carries that).
|
|
254
|
+
*/
|
|
255
|
+
function valueShape(token: TokenData): string | null {
|
|
256
|
+
const tMatch = /Traits:\s*([^\n]*)/i.exec(token.traits ?? "");
|
|
257
|
+
const t = (tMatch ? tMatch[1] : "").trim();
|
|
258
|
+
if (token.kind === "trigger") {
|
|
259
|
+
if (/\byes\/no\b/i.test(t)) return "`yes`/`no` (boolean)";
|
|
260
|
+
if (/valid date/i.test(t)) return "a date, or a comparison operator + date";
|
|
261
|
+
if (/[<>]=?|!=/.test(t)) return "a number or script value (comparison: `<` `<=` `=` `!=` `>` `>=`)";
|
|
262
|
+
const scopeM = /\b([A-Za-z_][A-Za-z0-9_]*)\s+(scope|target)\b/i.exec(t);
|
|
263
|
+
if (scopeM) return `a ${scopeM[1]} ${scopeM[2].toLowerCase()}`;
|
|
264
|
+
if (/\bkey\b/i.test(t)) return "a database key";
|
|
265
|
+
if (t) return `one of: ${t}`;
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
if (token.kind === "effect") {
|
|
269
|
+
if (token.usage && token.usage.includes("{")) return "a block — see the example below";
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (token.kind === "modifier") return "a number (the modifier's magnitude)";
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Metadata trait lines minus the `Traits:`/legacy `Example:` lines, flattened. */
|
|
277
|
+
function otherTraitBits(traits: string | undefined): string[] {
|
|
278
|
+
if (!traits) return [];
|
|
279
|
+
return traits
|
|
280
|
+
.split("\n")
|
|
281
|
+
.map((l) => l.trim())
|
|
282
|
+
.filter((l) => l !== "" && !/^Traits:/i.test(l) && !/^Example:/i.test(l));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Definition kinds that name a VALUE-side identity (something you reference
|
|
286
|
+
* with `scope:`/`var:` or as a list), never what an assignment KEY means. */
|
|
287
|
+
const VALUE_IDENTITY_KINDS = new Set(["saved_scope", "list", ...Object.values(VAR_PREFIX_KINDS).flat()]);
|
|
288
|
+
|
|
289
|
+
/** True when the cursor sits on the KEY of an unquoted assignment. */
|
|
290
|
+
function atKeyPosition(document: TextDocument, position: Position): boolean {
|
|
291
|
+
const { result, lineIndex } = getParse(document);
|
|
292
|
+
const offset = lineIndex.offsetAt(position);
|
|
293
|
+
const hit = nodeAtOffset(result.root, offset);
|
|
294
|
+
const last = hit?.path[hit.path.length - 1];
|
|
295
|
+
return (
|
|
296
|
+
!!last &&
|
|
297
|
+
last.kind === "assignment" &&
|
|
298
|
+
!last.key.quoted &&
|
|
299
|
+
offset >= last.key.range.start &&
|
|
300
|
+
offset <= last.key.range.end
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Cards for a set of same-named definitions: one card per KIND, with N same-kind
|
|
306
|
+
* sites collapsed into a single card ("33 sites") instead of 33 identical cards.
|
|
307
|
+
* The name-wide reference count renders once, on the first card, because it is
|
|
308
|
+
* a property of the name, not of any one definition.
|
|
309
|
+
*/
|
|
310
|
+
function definitionCards(
|
|
311
|
+
data: ServerData,
|
|
312
|
+
defs: Array<ReturnType<ServerData["index"]["lookup"]>[number]>,
|
|
313
|
+
at?: { uri: string; line: number; character: number }
|
|
314
|
+
): string[] {
|
|
315
|
+
const order: string[] = [];
|
|
316
|
+
const byKind = new Map<string, typeof defs>();
|
|
317
|
+
for (const def of defs) {
|
|
318
|
+
let group = byKind.get(def.kind);
|
|
319
|
+
if (!group) {
|
|
320
|
+
byKind.set(def.kind, (group = []));
|
|
321
|
+
order.push(def.kind);
|
|
322
|
+
}
|
|
323
|
+
group.push(def);
|
|
324
|
+
}
|
|
325
|
+
const cards: string[] = [];
|
|
326
|
+
let withRefs = true;
|
|
327
|
+
for (const kind of order) {
|
|
328
|
+
const group = byKind.get(kind)!;
|
|
329
|
+
cards.push(
|
|
330
|
+
group.length === 1
|
|
331
|
+
? definitionCard(data, group[0], at, withRefs)
|
|
332
|
+
: definitionGroupCard(data, group, at, withRefs)
|
|
333
|
+
);
|
|
334
|
+
withRefs = false;
|
|
335
|
+
}
|
|
336
|
+
return cards;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/** The "N references" command link (or plain count) for a name, once per hover. */
|
|
340
|
+
function referencesFooter(
|
|
341
|
+
data: ServerData,
|
|
342
|
+
name: string,
|
|
343
|
+
at?: { uri: string; line: number; character: number }
|
|
344
|
+
): string | null {
|
|
345
|
+
const refs = data.refIndex.lookup(name).length;
|
|
346
|
+
if (refs === 0) return null;
|
|
347
|
+
const label = `${refs.toLocaleString("en-US")} reference${refs === 1 ? "" : "s"}`;
|
|
348
|
+
return at && canRunCommand(clientCommands.showReferences)
|
|
349
|
+
? `[${label}](command:${clientCommands.showReferences}?${encodeURIComponent(
|
|
350
|
+
JSON.stringify([at.uri, at.line, at.character])
|
|
351
|
+
)} "Show all references")`
|
|
352
|
+
: label;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** One card for N same-named, same-kind definitions: origin + site count up
|
|
356
|
+
* front, the first few sites as links, the rest as a count. */
|
|
357
|
+
function definitionGroupCard(
|
|
358
|
+
data: ServerData,
|
|
359
|
+
group: Array<ReturnType<ServerData["index"]["lookup"]>[number]>,
|
|
360
|
+
at?: { uri: string; line: number; character: number },
|
|
361
|
+
withRefs = true
|
|
362
|
+
): string {
|
|
363
|
+
const def = group[0];
|
|
364
|
+
const origins = [...new Set(group.map((d) => data.originLabel(d)))];
|
|
365
|
+
const card: CardInput = {
|
|
366
|
+
kind: def.kind,
|
|
367
|
+
badgeLabel: def.kind.replace(/_/g, " "),
|
|
368
|
+
name: def.name,
|
|
369
|
+
headTail:
|
|
370
|
+
origins.length === 1
|
|
371
|
+
? `· ${origins[0]} (${group.length} sites)`
|
|
372
|
+
: `· ${group.length} sites in ${origins.join(", ")}`,
|
|
373
|
+
};
|
|
374
|
+
const footer = group.slice(0, 3).map(provenance);
|
|
375
|
+
if (group.length > 3) footer.push(`+${group.length - 3} more`);
|
|
376
|
+
if (withRefs) {
|
|
377
|
+
const refs = referencesFooter(data, def.name, at);
|
|
378
|
+
if (refs) footer.push(refs);
|
|
379
|
+
}
|
|
380
|
+
card.footer = footer;
|
|
381
|
+
return renderCard(card);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/** An indexed-definition card: badge, name, `· source`, provenance link (§D2 mock 2). */
|
|
385
|
+
function definitionCard(
|
|
386
|
+
data: ServerData,
|
|
387
|
+
def: ReturnType<ServerData["index"]["lookup"]>[number],
|
|
388
|
+
at?: { uri: string; line: number; character: number },
|
|
389
|
+
withRefs = true
|
|
390
|
+
): string {
|
|
391
|
+
const card: CardInput = { kind: def.kind, badgeLabel: def.kind.replace(/_/g, " "), name: def.name };
|
|
392
|
+
// Origin: the owning mod's descriptor name where known ("· My Mod"), the raw
|
|
393
|
+
// source tag ("· vanilla") otherwise.
|
|
394
|
+
const origin = data.originLabel(def);
|
|
395
|
+
card.headTail = `· ${origin}`;
|
|
396
|
+
if (def.kind === "loc_key" && def.value !== undefined) card.doc = `"${def.value}"`;
|
|
397
|
+
// Ad-hoc lists carry a statically resolved item type (varTypes.ts).
|
|
398
|
+
if (def.kind === "list") {
|
|
399
|
+
const item = variableTypes(data, data.rootScopesForFile).adhocListItemTypes.get(def.name);
|
|
400
|
+
if (item && item.size > 0) {
|
|
401
|
+
card.headTail = `of ${[...item].map(scopeType).join(" | ")} · ${origin}`;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Doc-comment prose + structured tags (§E3). Prose first, then tags; `@example`
|
|
406
|
+
// fills the fenced slot; `@deprecated` strikes the name. Empty when absent.
|
|
407
|
+
const body = extractDoc(def);
|
|
408
|
+
if (body.doc) card.doc = body.doc;
|
|
409
|
+
if (body.example) card.example = body.example;
|
|
410
|
+
if (body.deprecated) card.name = `~~${def.name}~~`;
|
|
411
|
+
|
|
412
|
+
const footer: string[] = [provenance(def)];
|
|
413
|
+
// Full count including key-position call sites (usageCount excludes those).
|
|
414
|
+
// A command link opens the references view; the client's hover middleware
|
|
415
|
+
// trusts exactly this command (extension.ts). Plain text without an anchor.
|
|
416
|
+
// Only the hover's first definition card carries it: the count belongs to
|
|
417
|
+
// the NAME, so repeating it per meaning taught nothing.
|
|
418
|
+
if (withRefs) {
|
|
419
|
+
const refs = referencesFooter(data, def.name, at);
|
|
420
|
+
if (refs) footer.push(refs);
|
|
421
|
+
}
|
|
422
|
+
card.footer = footer;
|
|
423
|
+
return renderCard(card);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** `file.txt:line` provenance, as a markdown link when a file URI is feasible. */
|
|
427
|
+
function provenance(def: { file: string; line: number }): string {
|
|
428
|
+
const label = `${path.basename(def.file)}:${def.line + 1}`;
|
|
429
|
+
// Plain text when no absolute path is available (fail-soft, e.g. synthetic defs).
|
|
430
|
+
if (!def.file || !path.isAbsolute(def.file)) return label;
|
|
431
|
+
const target = URI.file(def.file)
|
|
432
|
+
.with({ fragment: String(def.line + 1) })
|
|
433
|
+
.toString();
|
|
434
|
+
return `[${label}](${target})`;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Doc-comment / example extraction (§D2 mock 2, §E3). Reads the PdxDoc fields
|
|
439
|
+
* captured at index time (`Definition.doc`/`.tags`) and renders prose + tags.
|
|
440
|
+
* Fail-soft: an undocumented definition yields an empty body.
|
|
441
|
+
*/
|
|
442
|
+
function extractDoc(def: Definition): ReturnType<typeof renderDocBody> {
|
|
443
|
+
return renderDocBody(def);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/** A saved-scope card: badge, name, `→ type`, save-site link, ambient doc (§D2 mock 3). */
|
|
447
|
+
function savedScopeCard(
|
|
448
|
+
data: ServerData,
|
|
449
|
+
document: TextDocument,
|
|
450
|
+
name: string,
|
|
451
|
+
rootScopes: Set<Scope> | null,
|
|
452
|
+
entry: SchemaEntry | null
|
|
453
|
+
): string {
|
|
454
|
+
const ambient = entry?.ambientScopes?.find((a) => a.name === name);
|
|
455
|
+
const ictx = inferenceContextFor(data, entry);
|
|
456
|
+
const saved = getSavedScopes(document, data.scopeModel, rootScopes, entry?.ambientScopes, ictx);
|
|
457
|
+
const inferred = saved.get(name);
|
|
458
|
+
// Cross-file fallback: types merged over every indexed save site of the mod.
|
|
459
|
+
const global = inferred === undefined || inferred === null ? ictx.savedScopeTypes?.get(name) : undefined;
|
|
460
|
+
const typed = inferred && inferred.size > 0 ? inferred : global && global.size > 0 ? global : null;
|
|
461
|
+
const type = ambient?.type ?? (typed ? [...typed].join(" | ") : "unknown");
|
|
462
|
+
|
|
463
|
+
const card: CardInput = {
|
|
464
|
+
kind: "saved_scope",
|
|
465
|
+
name: `scope:${name}`,
|
|
466
|
+
headTail: `→ ${scopeType(type)}`,
|
|
467
|
+
};
|
|
468
|
+
|
|
469
|
+
const doc: string[] = [];
|
|
470
|
+
if (ambient) doc.push(`${ambient.doc}${entry ? ` *(${entry.kind.replace(/_/g, " ")})*` : ""}`);
|
|
471
|
+
|
|
472
|
+
const site = firstSaveSite(document, name);
|
|
473
|
+
if (site !== null) doc.push(`Saved in this file: ${path.basename(URIToPath(document.uri))}:${site + 1}`);
|
|
474
|
+
else if (ambient) doc.push(`Engine-provided (not saved in this file).`);
|
|
475
|
+
else if (!inferred) {
|
|
476
|
+
// Not saved here: link the mod's save sites (like the variable card does).
|
|
477
|
+
const sites = data.index
|
|
478
|
+
.lookup(name)
|
|
479
|
+
.filter((d) => d.kind === "saved_scope")
|
|
480
|
+
.slice(0, 3);
|
|
481
|
+
if (sites.length > 0) {
|
|
482
|
+
doc.push(
|
|
483
|
+
sites
|
|
484
|
+
.map((d) => `saved in [${path.basename(d.file)}:${d.line + 1}](${URI.file(d.file)}#L${d.line + 1})`)
|
|
485
|
+
.join(" \n")
|
|
486
|
+
);
|
|
487
|
+
} else {
|
|
488
|
+
doc.push(`Saved elsewhere in the mod.`);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
if (doc.length > 0) card.doc = doc.join(" \n");
|
|
493
|
+
return renderCard(card);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/** Line (0-based) of the first `save_scope_as`/`save_temporary_scope_as`/
|
|
497
|
+
* `save_temporary_value_as = name` in the file. */
|
|
498
|
+
const SAVE_SITE_KEYS = new Set(["save_scope_as", "save_temporary_scope_as", "save_temporary_value_as"]);
|
|
499
|
+
function firstSaveSite(document: TextDocument, name: string): number | null {
|
|
500
|
+
const { result, lineIndex } = getParse(document);
|
|
501
|
+
let line: number | null = null;
|
|
502
|
+
walkStatements(result.root, (stmt) => {
|
|
503
|
+
if (line !== null) return;
|
|
504
|
+
if (stmt.kind !== "assignment" || stmt.key.quoted) return;
|
|
505
|
+
if (!SAVE_SITE_KEYS.has(stmt.key.text)) return;
|
|
506
|
+
if (stmt.value?.kind === "scalar" && !stmt.value.quoted && stmt.value.text === name) {
|
|
507
|
+
line = lineIndex.positionAt(stmt.value.range.start).line;
|
|
508
|
+
}
|
|
509
|
+
});
|
|
510
|
+
return line;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/** A structure-key card: KeySpec doc plus provenance (§B2). */
|
|
514
|
+
function structureKeyCard(
|
|
515
|
+
document: TextDocument,
|
|
516
|
+
position: Position,
|
|
517
|
+
word: string,
|
|
518
|
+
entry: SchemaEntry,
|
|
519
|
+
getSchema: () => SchemaData
|
|
520
|
+
): string | null {
|
|
521
|
+
const { result, lineIndex } = getParse(document);
|
|
522
|
+
const offset = lineIndex.offsetAt(position);
|
|
523
|
+
const ctx = structureContextAt(result, offset, entry.kind, getSchema().structures);
|
|
524
|
+
if (!ctx) return null;
|
|
525
|
+
const spec = ctx.keys.get(word);
|
|
526
|
+
if (!spec) return null;
|
|
527
|
+
const source = getSchema().structures.source(entry.kind) ?? entry.kind;
|
|
528
|
+
const where = ctx.block ? `in \`${ctx.block}\`` : "";
|
|
529
|
+
const card: CardInput = {
|
|
530
|
+
kind: "structure_key",
|
|
531
|
+
badgeLabel: `${entry.kind.replace(/_/g, " ")} key`,
|
|
532
|
+
name: word,
|
|
533
|
+
};
|
|
534
|
+
if (where) card.headTail = where;
|
|
535
|
+
card.doc = spec.doc ? `${spec.doc} *(${source})*` : `*(${source})*`;
|
|
536
|
+
return renderCard(card);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/** `type = character_event` — the value is a member of the key's structure enum. */
|
|
540
|
+
function enumValueCard(
|
|
541
|
+
document: TextDocument,
|
|
542
|
+
position: Position,
|
|
543
|
+
word: string,
|
|
544
|
+
entry: SchemaEntry,
|
|
545
|
+
getSchema: () => SchemaData
|
|
546
|
+
): string | null {
|
|
547
|
+
const { result, lineIndex } = getParse(document);
|
|
548
|
+
const offset = lineIndex.offsetAt(position);
|
|
549
|
+
const hit = nodeAtOffset(result.root, offset);
|
|
550
|
+
const last = hit?.path[hit.path.length - 1];
|
|
551
|
+
if (!last || last.kind !== "assignment" || last.key.quoted) return null;
|
|
552
|
+
if (last.value?.kind !== "scalar" || last.value.quoted) return null;
|
|
553
|
+
if (offset < last.value.range.start || offset > last.value.range.end) return null;
|
|
554
|
+
const ctx = structureContextAt(result, offset, entry.kind, getSchema().structures);
|
|
555
|
+
const spec = ctx?.keys.get(last.key.text);
|
|
556
|
+
if (!spec?.values?.startsWith("enum:")) return null;
|
|
557
|
+
const members = spec.values.slice(5).split("|");
|
|
558
|
+
if (!members.includes(word)) return null;
|
|
559
|
+
return renderCard({
|
|
560
|
+
kind: "structure_key",
|
|
561
|
+
badgeLabel: `${last.key.text} value`,
|
|
562
|
+
name: word,
|
|
563
|
+
doc: `One of: ${members.map((m) => (m === word ? `**${m}**` : m)).join(" · ")}`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/** `my_effect = { AMOUNT = 3 }` — the key is a $PARAM$ of the called scripted effect/trigger. */
|
|
568
|
+
function macroParamCard(
|
|
569
|
+
data: ServerData,
|
|
570
|
+
document: TextDocument,
|
|
571
|
+
position: Position,
|
|
572
|
+
word: string
|
|
573
|
+
): string | null {
|
|
574
|
+
const { result, lineIndex } = getParse(document);
|
|
575
|
+
const offset = lineIndex.offsetAt(position);
|
|
576
|
+
const hit = nodeAtOffset(result.root, offset);
|
|
577
|
+
const last = hit?.path[hit.path.length - 1];
|
|
578
|
+
if (!last || last.kind !== "assignment" || last.key.quoted) return null;
|
|
579
|
+
if (offset < last.key.range.start || offset > last.key.range.end) return null;
|
|
580
|
+
const enclosing = hit!.path[hit!.path.length - 2];
|
|
581
|
+
if (!enclosing || enclosing.kind !== "assignment" || enclosing.key.quoted) return null;
|
|
582
|
+
const callee = enclosing.key.text;
|
|
583
|
+
for (const def of data.index.lookup(callee)) {
|
|
584
|
+
if (def.kind !== "scripted_effect" && def.kind !== "scripted_trigger") continue;
|
|
585
|
+
if (!def.params?.includes(word)) continue;
|
|
586
|
+
return renderCard({
|
|
587
|
+
kind: "macro_param",
|
|
588
|
+
badgeLabel: "parameter",
|
|
589
|
+
name: word,
|
|
590
|
+
headTail: `of ${def.kind.replace(/_/g, " ")} \`${callee}\``,
|
|
591
|
+
doc: `Replaces \`$${word}$\` in the ${def.kind.replace(/_/g, " ")}'s body.`,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* `has_relation_dao_guide` — triggers/effects the engine generates per scripted
|
|
599
|
+
* relation (`has_relation_X`, `set_relation_X`, `remove_relation_X`).
|
|
600
|
+
*/
|
|
601
|
+
function relationTriggerCard(data: ServerData, word: string): string | null {
|
|
602
|
+
const m = /^(has|set|remove)_relation_([A-Za-z0-9_]+)$/.exec(word);
|
|
603
|
+
if (!m) return null;
|
|
604
|
+
const def = data.index.lookup(m[2]).find((d) => d.kind === "scripted_relation");
|
|
605
|
+
if (!def) return null;
|
|
606
|
+
const verb =
|
|
607
|
+
m[1] === "has"
|
|
608
|
+
? "Trigger: the scoped character has"
|
|
609
|
+
: m[1] === "set"
|
|
610
|
+
? "Effect: gives the scoped character"
|
|
611
|
+
: "Effect: removes the scoped character's";
|
|
612
|
+
return renderCard({
|
|
613
|
+
kind: m[1] === "has" ? "trigger" : "effect",
|
|
614
|
+
name: word,
|
|
615
|
+
headTail: `· generated from \`${m[2]}\``,
|
|
616
|
+
doc: `${verb} the scripted relation \`${m[2]}\` (${path.basename(def.file)}:${def.line + 1}) with the target character.`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* `french_opinion` — modifiers the engine generates per definition, matched
|
|
622
|
+
* lazily against the templated modifiers.log tags ($CULTURE$_opinion) and the
|
|
623
|
+
* definition index (never materialized: AGOT-scale mods define thousands of
|
|
624
|
+
* cultures).
|
|
625
|
+
*/
|
|
626
|
+
function templatedModifierCard(data: ServerData, word: string): string | null {
|
|
627
|
+
const m = matchTemplatedModifier(word, data.modifierTemplates, (n) => data.index.lookup(n));
|
|
628
|
+
if (!m) return null;
|
|
629
|
+
const card: CardInput = {
|
|
630
|
+
kind: "modifier",
|
|
631
|
+
name: word,
|
|
632
|
+
headTail: `· generated from \`${m.template.name}\``,
|
|
633
|
+
doc: templatedModifierDoc(m),
|
|
634
|
+
};
|
|
635
|
+
if (m.template.traits) card.traits = m.template.traits.split("\n").join(" · ");
|
|
636
|
+
return renderCard(card);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* `start_scheme = { target_character = … }` — the key is a block argument of an
|
|
641
|
+
* engine effect/trigger call; surface the call's own doc, which describes its
|
|
642
|
+
* arguments. Last-resort fallback: anything more specific wins.
|
|
643
|
+
*/
|
|
644
|
+
function effectArgumentCard(
|
|
645
|
+
data: ServerData,
|
|
646
|
+
document: TextDocument,
|
|
647
|
+
position: Position,
|
|
648
|
+
word: string
|
|
649
|
+
): string | null {
|
|
650
|
+
const { result, lineIndex } = getParse(document);
|
|
651
|
+
const offset = lineIndex.offsetAt(position);
|
|
652
|
+
const hit = nodeAtOffset(result.root, offset);
|
|
653
|
+
const last = hit?.path[hit.path.length - 1];
|
|
654
|
+
if (!last || last.kind !== "assignment" || last.key.quoted) return null;
|
|
655
|
+
if (offset < last.key.range.start || offset > last.key.range.end) return null;
|
|
656
|
+
const enclosing = hit!.path[hit!.path.length - 2];
|
|
657
|
+
if (!enclosing || enclosing.kind !== "assignment" || enclosing.key.quoted) return null;
|
|
658
|
+
const token = data.tokenMap.get(enclosing.key.text)?.[0];
|
|
659
|
+
if (!token) return null;
|
|
660
|
+
const card: CardInput = {
|
|
661
|
+
kind: "structure_key",
|
|
662
|
+
badgeLabel: "argument",
|
|
663
|
+
name: word,
|
|
664
|
+
headTail: `of ${token.kind.replace(/_/g, " ")} \`${token.name}\``,
|
|
665
|
+
};
|
|
666
|
+
if (token.doc) card.doc = token.doc;
|
|
667
|
+
return renderCard(card);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/** `cultivation_ruin` in `trigger_event = cultivation_ruin.5` etc. — a declared event namespace. */
|
|
671
|
+
function namespaceCard(data: ServerData, word: string): string | null {
|
|
672
|
+
if (!data.modNamespaces.has(word)) return null;
|
|
673
|
+
return renderCard({
|
|
674
|
+
kind: "namespace",
|
|
675
|
+
badgeLabel: "event namespace",
|
|
676
|
+
name: word,
|
|
677
|
+
doc: `Events in this namespace are named \`${word}.<n>\`.`,
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/** The `define:NS|CONST` reference spanning the cursor, or null. */
|
|
682
|
+
function defineRefAt(
|
|
683
|
+
lineText: string,
|
|
684
|
+
character: number
|
|
685
|
+
): { namespace: string; name: string; start: number; end: number } | null {
|
|
686
|
+
const re = /define:([A-Za-z0-9_]+)\|([A-Za-z0-9_]+)/g;
|
|
687
|
+
let m: RegExpExecArray | null;
|
|
688
|
+
while ((m = re.exec(lineText)) !== null) {
|
|
689
|
+
const start = m.index;
|
|
690
|
+
const end = m.index + m[0].length;
|
|
691
|
+
if (character >= start && character <= end) return { namespace: m[1], name: m[2], start, end };
|
|
692
|
+
}
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** A define card: name, resolved value, source file + layer, "overrides <layer>". */
|
|
697
|
+
function definesCard(data: ServerData, namespace: string, name: string): string | null {
|
|
698
|
+
const res = data.defines.resolve(namespace, name);
|
|
699
|
+
if (!res) return null;
|
|
700
|
+
const footer = [defineSourceLink(res.winner)];
|
|
701
|
+
if (res.shadowed.length > 0) footer.push(`overrides ${res.shadowed.map((s) => s.layer).join(", ")}`);
|
|
702
|
+
return renderCard({
|
|
703
|
+
kind: "define",
|
|
704
|
+
badgeLabel: "define",
|
|
705
|
+
name: `${namespace}|${name}`,
|
|
706
|
+
headTail: `= ${res.winner.value}`,
|
|
707
|
+
footer,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/** `<layer> · [common/defines/…:line](uri)` provenance for a define entry. */
|
|
712
|
+
function defineSourceLink(e: DefineEntry): string {
|
|
713
|
+
const norm = e.file.replace(/\\/g, "/");
|
|
714
|
+
const i = norm.toLowerCase().lastIndexOf("/common/defines/");
|
|
715
|
+
const rel = i >= 0 ? `${norm.slice(i + 1)}:${e.line + 1}` : `${path.basename(e.file)}:${e.line + 1}`;
|
|
716
|
+
const target = URI.file(e.file)
|
|
717
|
+
.with({ fragment: String(e.line + 1) })
|
|
718
|
+
.toString();
|
|
719
|
+
return `${e.layer} · [${rel}](${target})`;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/** root/ROOT/this/prev(prev…)/from(from…) — scope navigation keywords. */
|
|
723
|
+
function scopeWordCard(word: string): string | null {
|
|
724
|
+
const hit = scopeWordDoc(word);
|
|
725
|
+
if (!hit) return null;
|
|
726
|
+
return renderCard({ kind: "scope_word", badgeLabel: "scope", name: hit.name, doc: hit.doc });
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/** Grammar/math glue vocabulary (limit, NOT, base, days…): curated docs. */
|
|
730
|
+
function keywordCard(word: string): string | null {
|
|
731
|
+
const doc = KEYWORD_DOCS[word];
|
|
732
|
+
if (!doc) return null;
|
|
733
|
+
return renderCard({ kind: "keyword", name: word, doc });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* The definition kinds a ref field expects at this position, when the cursor
|
|
738
|
+
* sits on the VALUE of such a field (`theme = X` scalar form, `events = { X }`
|
|
739
|
+
* list form). null anywhere else — key position, non-ref keys, quoted values.
|
|
740
|
+
*/
|
|
741
|
+
function refKindsAt(
|
|
742
|
+
document: TextDocument,
|
|
743
|
+
position: Position,
|
|
744
|
+
refFields: Map<string, RefField>
|
|
745
|
+
): string[] | null {
|
|
746
|
+
const { result, lineIndex } = getParse(document);
|
|
747
|
+
const offset = lineIndex.offsetAt(position);
|
|
748
|
+
const hit = nodeAtOffset(result.root, offset);
|
|
749
|
+
if (!hit) return null;
|
|
750
|
+
const last = hit.path[hit.path.length - 1];
|
|
751
|
+
|
|
752
|
+
// `key = word` — scalar value of an assignment.
|
|
753
|
+
if (
|
|
754
|
+
last.kind === "assignment" &&
|
|
755
|
+
!last.key.quoted &&
|
|
756
|
+
last.value?.kind === "scalar" &&
|
|
757
|
+
!last.value.quoted &&
|
|
758
|
+
offset >= last.value.range.start &&
|
|
759
|
+
offset <= last.value.range.end
|
|
760
|
+
) {
|
|
761
|
+
const field = refFields.get(last.key.text);
|
|
762
|
+
if (field) return field.form !== "list" ? field.kinds : null;
|
|
763
|
+
// Block-scoped ref keys (`trigger_event = { id = X }`,
|
|
764
|
+
// `override_background = { reference = X }`).
|
|
765
|
+
const enclosing = hit.path[hit.path.length - 2];
|
|
766
|
+
if (enclosing?.kind === "assignment" && !enclosing.key.quoted) {
|
|
767
|
+
return activeProfile().blockRefFields[enclosing.key.text.toLowerCase()]?.[last.key.text] ?? null;
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// `key = { word ... }` — bare list element; the owning assignment is one up.
|
|
773
|
+
if (last.kind === "value" && last.value.kind === "scalar" && !last.value.quoted) {
|
|
774
|
+
const parent = hit.path[hit.path.length - 2];
|
|
775
|
+
if (parent?.kind === "assignment" && !parent.key.quoted) {
|
|
776
|
+
const field = refFields.get(parent.key.text);
|
|
777
|
+
return field && field.form !== "scalar" ? field.kinds : null;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return null;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
/** The inferred scope set at the cursor, or null when inference is unavailable. */
|
|
784
|
+
function currentScopes(
|
|
785
|
+
data: ServerData,
|
|
786
|
+
document: TextDocument,
|
|
787
|
+
position: Position,
|
|
788
|
+
rootScopes: Set<Scope> | null,
|
|
789
|
+
entry: SchemaEntry | null
|
|
790
|
+
): Set<string> | null {
|
|
791
|
+
const inference = scopeInference(data, document, position, rootScopes, entry);
|
|
792
|
+
return inference.scopes && inference.scopes.size > 0 ? inference.scopes : null;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function scopeInference(
|
|
796
|
+
data: ServerData,
|
|
797
|
+
document: TextDocument,
|
|
798
|
+
position: Position,
|
|
799
|
+
rootScopes: Set<Scope> | null,
|
|
800
|
+
entry: SchemaEntry | null
|
|
801
|
+
): ReturnType<typeof inferScopeAt> {
|
|
802
|
+
const { result, lineIndex } = getParse(document);
|
|
803
|
+
const offset = lineIndex.offsetAt(position);
|
|
804
|
+
const ictx = inferenceContextFor(data, entry);
|
|
805
|
+
return inferScopeAt(
|
|
806
|
+
result,
|
|
807
|
+
offset,
|
|
808
|
+
data.scopeModel,
|
|
809
|
+
rootScopes,
|
|
810
|
+
getSavedScopes(document, data.scopeModel, rootScopes, entry?.ambientScopes, ictx),
|
|
811
|
+
ictx
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function URIToPath(uri: string): string {
|
|
816
|
+
return uri.replace(/^file:\/\/\/?/, "").replace(/\//g, path.sep);
|
|
817
|
+
}
|