@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hover markdown builders (§D). Pure string assembly, no LSP or `vscode` types,
|
|
3
|
+
* so the card layout is unit-testable in plain Node against the D2 mocks.
|
|
4
|
+
*
|
|
5
|
+
* Rendering contract (§D1): the only HTML emitted is sanitized
|
|
6
|
+
* `<span style="color:var(--vscode-*)">…</span>` — VS Code renders the color
|
|
7
|
+
* when `supportHtml` is on and strips the tag (keeping the plain text) when it
|
|
8
|
+
* is not. Every span's *content* is therefore self-sufficient plain text (a
|
|
9
|
+
* "■ trigger" badge, a scope name), so older clients degrade to markdown with
|
|
10
|
+
* no loss of meaning. Do not introduce any other HTML here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** `--vscode-charts-*` / semantic color slot per kind family (§D2). */
|
|
14
|
+
type ChartColor = "purple" | "red" | "yellow" | "green" | "orange" | "blue" | "foreground";
|
|
15
|
+
|
|
16
|
+
import { hoverHtml } from "../clientMode";
|
|
17
|
+
|
|
18
|
+
function colorVar(c: ChartColor): string {
|
|
19
|
+
return c === "foreground" ? "var(--vscode-charts-foreground)" : `var(--vscode-charts-${c})`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A sanitized colored span. Content is plain text so it survives HTML stripping.
|
|
23
|
+
* Plain-markdown clients (no `client.hoverHtml`) get the bare text. */
|
|
24
|
+
function span(color: ChartColor, text: string): string {
|
|
25
|
+
if (!hoverHtml()) return text;
|
|
26
|
+
return `<span style="color:${colorVar(color)};">${text}</span>`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Kind → badge color (fixed per kind family, §D2). Definition kinds default green. */
|
|
30
|
+
function badgeColor(kind: string): ChartColor {
|
|
31
|
+
switch (kind) {
|
|
32
|
+
case "trigger":
|
|
33
|
+
return "purple";
|
|
34
|
+
case "effect":
|
|
35
|
+
return "red";
|
|
36
|
+
case "structure_key":
|
|
37
|
+
case "keyword":
|
|
38
|
+
case "text_format":
|
|
39
|
+
return "yellow";
|
|
40
|
+
case "define":
|
|
41
|
+
return "blue";
|
|
42
|
+
case "saved_scope":
|
|
43
|
+
case "scope_word":
|
|
44
|
+
case "macro_param":
|
|
45
|
+
return "orange";
|
|
46
|
+
case "loc_key":
|
|
47
|
+
return "foreground";
|
|
48
|
+
default:
|
|
49
|
+
// Definition kinds (scripted_effect/trigger/modifier, script_value, event,
|
|
50
|
+
// decision, …) all read green.
|
|
51
|
+
return "green";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Human label for a kind badge ("scripted trigger", "trigger", "saved scope"). */
|
|
56
|
+
function kindLabel(kind: string): string {
|
|
57
|
+
if (kind === "structure_key") return "key";
|
|
58
|
+
return kind.replace(/_/g, " ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** `■ kind` badge, colored per family. */
|
|
62
|
+
export function kindBadge(kind: string, label = kindLabel(kind)): string {
|
|
63
|
+
return span(badgeColor(kind), `■ ${label}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A scope pill: blue when it matches the current cursor scope, muted otherwise
|
|
68
|
+
* (§D3). Content is the plain scope name so stripping the span leaves it legible.
|
|
69
|
+
*/
|
|
70
|
+
export function scopePill(scope: string, current: ReadonlySet<string> | null): string {
|
|
71
|
+
const matches = current !== null && current.has(scope.toLowerCase());
|
|
72
|
+
if (matches) return span("blue", scope);
|
|
73
|
+
if (!hoverHtml()) return scope;
|
|
74
|
+
return `<span style="color:var(--vscode-descriptionForeground);">${scope}</span>`;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Blue scope-type span for the "→ character" tail on badges/pills. */
|
|
78
|
+
export function scopeType(type: string): string {
|
|
79
|
+
return span("blue", type);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* A ■ swatch in an actual game color (not a theme var): the only place an
|
|
84
|
+
* inline `color:rgb(...)` is emitted, since these are literal loc colors the
|
|
85
|
+
* theme must not recolor. Content is a ■ so stripping the span leaves a marker.
|
|
86
|
+
*/
|
|
87
|
+
export function colorSwatch(rgb: [number, number, number]): string {
|
|
88
|
+
if (!hoverHtml()) return "■";
|
|
89
|
+
const to255 = (v: number) => Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
90
|
+
return `<span style="color:rgb(${to255(rgb[0])}, ${to255(rgb[1])}, ${to255(rgb[2])});">■</span>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Card model
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
export interface CardInput {
|
|
98
|
+
kind: string;
|
|
99
|
+
/** Badge label override (e.g. "scripted trigger" for a mod def). */
|
|
100
|
+
badgeLabel?: string;
|
|
101
|
+
name: string;
|
|
102
|
+
/** Rendered inline after the name on line 1: pills, `· mod`, `→ character`. */
|
|
103
|
+
headTail?: string;
|
|
104
|
+
/** Doc prose (blank slot workstream E extends). */
|
|
105
|
+
doc?: string;
|
|
106
|
+
/** Italic traits line ("*Traits: yes/no · comparison ok*"). */
|
|
107
|
+
traits?: string;
|
|
108
|
+
/** A fenced example body (rendered ```paradox); short bodies only (§D2 mock 2). */
|
|
109
|
+
example?: string;
|
|
110
|
+
/** Footer fragments merged into one compact line (provenance, refs). */
|
|
111
|
+
footer?: string[];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The language id our TextMate grammar registers (package.json `languages`). */
|
|
115
|
+
const FENCE_LANG = "paradox";
|
|
116
|
+
|
|
117
|
+
/** Build one card's markdown per the D2 layout. */
|
|
118
|
+
export function renderCard(card: CardInput): string {
|
|
119
|
+
const badge = kindBadge(card.kind, card.badgeLabel);
|
|
120
|
+
const head = card.headTail ? `${badge} **${card.name}** ${card.headTail}` : `${badge} **${card.name}**`;
|
|
121
|
+
const lines: string[] = [head];
|
|
122
|
+
|
|
123
|
+
if (card.doc) lines.push(card.doc);
|
|
124
|
+
if (card.example) lines.push(`\`\`\`${FENCE_LANG}\n${card.example}\n\`\`\``);
|
|
125
|
+
if (card.traits) lines.push(`*${card.traits}*`);
|
|
126
|
+
|
|
127
|
+
let md = lines.join("\n\n");
|
|
128
|
+
if (card.footer && card.footer.length > 0) {
|
|
129
|
+
md += `\n\n---\n${card.footer.join(" · ")}`;
|
|
130
|
+
}
|
|
131
|
+
return md;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Join up to `MAX_CARDS` cards, appending "*n more meanings*" when capped, then
|
|
136
|
+
* the single shared scope-context footer line (§D2: scope context always last,
|
|
137
|
+
* once per hover).
|
|
138
|
+
*/
|
|
139
|
+
export const MAX_CARDS = 3;
|
|
140
|
+
|
|
141
|
+
export function renderHover(cards: string[], scopeFooter: string | null): string {
|
|
142
|
+
const shown = cards.slice(0, MAX_CARDS);
|
|
143
|
+
const parts = [...shown];
|
|
144
|
+
const extra = cards.length - shown.length;
|
|
145
|
+
if (extra > 0) parts.push(`*${extra} more meaning${extra === 1 ? "" : "s"}*`);
|
|
146
|
+
let md = parts.join("\n\n---\n\n");
|
|
147
|
+
if (scopeFooter) md += `\n\n${scopeFooter}`;
|
|
148
|
+
return md;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** "Scope here: **X** (chain)" — the shared footer line appended once (§D2). */
|
|
152
|
+
export function scopeHereLine(scopes: string, chain: string | null): string {
|
|
153
|
+
return chain ? `Scope here: **${scopes}** (${chain})` : `Scope here: **${scopes}**`;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** True when a definition body is short enough to inline as an example (§D2). */
|
|
157
|
+
export function isShortExample(body: string): boolean {
|
|
158
|
+
const lines = body.split("\n").filter((l) => l.trim() !== "");
|
|
159
|
+
return lines.length > 0 && lines.length < 4;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// PdxDoc rendering (§E3)
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
interface DocTagLike {
|
|
167
|
+
tag: string;
|
|
168
|
+
text: string;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface DocBody {
|
|
172
|
+
/** Prose + structured-tag markdown for the card's `doc` slot. Empty → undefined. */
|
|
173
|
+
doc?: string;
|
|
174
|
+
/** `@example` body for the card's fenced `example` slot. */
|
|
175
|
+
example?: string;
|
|
176
|
+
/** True when `@deprecated` is present — the card renders the name prominently. */
|
|
177
|
+
deprecated?: boolean;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Turn PdxDoc prose + tags (§E) into the card's `doc`/`example` slots.
|
|
182
|
+
* Prose renders first, then structured tags compactly (§E3): `@param` as
|
|
183
|
+
* `*@param NAME — desc*` lines, `@deprecated` as a prominent ⚠ line, other
|
|
184
|
+
* recognized/unknown tags as compact italic lines. `@example` fills the fenced
|
|
185
|
+
* slot. Fail-soft: absent fields yield an empty body.
|
|
186
|
+
*/
|
|
187
|
+
export function renderDocBody(def: { doc?: string; tags?: DocTagLike[] }): DocBody {
|
|
188
|
+
const out: DocBody = {};
|
|
189
|
+
const lines: string[] = [];
|
|
190
|
+
if (def.doc) lines.push(def.doc);
|
|
191
|
+
|
|
192
|
+
const tagLines: string[] = [];
|
|
193
|
+
for (const t of def.tags ?? []) {
|
|
194
|
+
switch (t.tag) {
|
|
195
|
+
case "example":
|
|
196
|
+
if (t.text && !out.example) out.example = t.text;
|
|
197
|
+
break;
|
|
198
|
+
case "deprecated":
|
|
199
|
+
out.deprecated = true;
|
|
200
|
+
tagLines.push(t.text ? `⚠ **Deprecated** — ${t.text}` : `⚠ **Deprecated**`);
|
|
201
|
+
break;
|
|
202
|
+
case "param": {
|
|
203
|
+
// `@param NAME desc` → `*@param NAME — desc*`.
|
|
204
|
+
const m = /^(\S+)\s*(.*)$/.exec(t.text);
|
|
205
|
+
if (m) {
|
|
206
|
+
const desc = m[2].trim();
|
|
207
|
+
tagLines.push(desc ? `*@param ${m[1]} — ${desc}*` : `*@param ${m[1]}*`);
|
|
208
|
+
} else {
|
|
209
|
+
tagLines.push(`*@param*`);
|
|
210
|
+
}
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
default:
|
|
214
|
+
// @scope, @saves, @returns, and unknown tags: compact italic line.
|
|
215
|
+
tagLines.push(t.text ? `*@${t.tag} ${t.text}*` : `*@${t.tag}*`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (tagLines.length > 0) lines.push(tagLines.join(" \n"));
|
|
219
|
+
|
|
220
|
+
if (lines.length > 0) out.doc = lines.join("\n\n");
|
|
221
|
+
return out;
|
|
222
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inlay hints, both directions of the localization workflow:
|
|
3
|
+
* - in script files: the resolved loc text next to loc-key references
|
|
4
|
+
* ("missing loc" for strict properties with no entry);
|
|
5
|
+
* - in translated loc files: the reference-language text next to each entry,
|
|
6
|
+
* so a translator never has to switch back to the source file.
|
|
7
|
+
*/
|
|
8
|
+
import { MarkupKind, type InlayHint, type Range } from "vscode-languageserver/node";
|
|
9
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
10
|
+
import { URI } from "vscode-uri";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import type { ServerData } from "../serverData";
|
|
13
|
+
import type { ParadoxSettings } from "@px-lsp/protocol/protocol";
|
|
14
|
+
import { findLocKeyRefs, locKeyOnLine } from "@px-lsp/protocol/locRefs";
|
|
15
|
+
import { detectLocFileLanguage } from "@px-lsp/protocol/translationCore";
|
|
16
|
+
import { getLineText } from "../documents";
|
|
17
|
+
import { getParse, getSavedScopes } from "../parseCache";
|
|
18
|
+
import { inferScopeAt } from "../scopes/inference";
|
|
19
|
+
import { inferenceContextFor } from "../scopes/varTypes";
|
|
20
|
+
import type { SchemaEntry } from "../schema/types";
|
|
21
|
+
import type { Scope } from "../scopes/model";
|
|
22
|
+
import { walkStatements } from "../parser";
|
|
23
|
+
|
|
24
|
+
const HINT_MAX_LEN = 60;
|
|
25
|
+
|
|
26
|
+
function truncate(text: string, max: number): string {
|
|
27
|
+
return text.length > max ? text.slice(0, max - 1) + "…" : text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function provideInlayHints(
|
|
31
|
+
data: ServerData,
|
|
32
|
+
settings: ParadoxSettings,
|
|
33
|
+
document: TextDocument,
|
|
34
|
+
range: Range,
|
|
35
|
+
rootScopes: Set<Scope> | null,
|
|
36
|
+
entry: SchemaEntry | null
|
|
37
|
+
): InlayHint[] {
|
|
38
|
+
if (document.languageId === "paradox-loc") return translationOverlayHints(data, settings, document, range);
|
|
39
|
+
const hints = locPreviewHints(data, document, range);
|
|
40
|
+
if (settings.scopeInlayHints) hints.push(...scopeHints(data, document, range, rootScopes, entry));
|
|
41
|
+
return hints;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Optional (off by default): the inferred scope after each scope-changing block opener. */
|
|
45
|
+
function scopeHints(
|
|
46
|
+
data: ServerData,
|
|
47
|
+
document: TextDocument,
|
|
48
|
+
range: Range,
|
|
49
|
+
rootScopes: Set<Scope> | null,
|
|
50
|
+
entry: SchemaEntry | null
|
|
51
|
+
): InlayHint[] {
|
|
52
|
+
const { result, lineIndex } = getParse(document);
|
|
53
|
+
const ictx = inferenceContextFor(data, entry);
|
|
54
|
+
const savedScopes = getSavedScopes(document, data.scopeModel, rootScopes, entry?.ambientScopes, ictx);
|
|
55
|
+
const hints: InlayHint[] = [];
|
|
56
|
+
walkStatements(result.root, (stmt) => {
|
|
57
|
+
if (stmt.kind !== "assignment" || stmt.key.quoted || stmt.value?.kind !== "block") return;
|
|
58
|
+
const key = stmt.key.text.toLowerCase();
|
|
59
|
+
const changes =
|
|
60
|
+
/^(?:every|any|random|ordered)_/.test(key) ||
|
|
61
|
+
key.startsWith("scope:") ||
|
|
62
|
+
data.scopeModel.links.has(key);
|
|
63
|
+
if (!changes) return;
|
|
64
|
+
const pos = lineIndex.positionAt(stmt.value.openBrace + 1);
|
|
65
|
+
if (pos.line < range.start.line || pos.line > range.end.line) return;
|
|
66
|
+
const inference = inferScopeAt(
|
|
67
|
+
result,
|
|
68
|
+
stmt.value.openBrace + 1,
|
|
69
|
+
data.scopeModel,
|
|
70
|
+
rootScopes,
|
|
71
|
+
savedScopes,
|
|
72
|
+
ictx
|
|
73
|
+
);
|
|
74
|
+
if (!inference.scopes || inference.scopes.size === 0) return;
|
|
75
|
+
hints.push({
|
|
76
|
+
position: pos,
|
|
77
|
+
label: ` ${[...inference.scopes].join("|")}`,
|
|
78
|
+
paddingLeft: true,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
return hints;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Script files: resolved loc text (or "missing loc") after each loc-key reference. */
|
|
85
|
+
function locPreviewHints(data: ServerData, document: TextDocument, range: Range): InlayHint[] {
|
|
86
|
+
const hints: InlayHint[] = [];
|
|
87
|
+
for (let lineNo = range.start.line; lineNo <= range.end.line && lineNo < document.lineCount; lineNo++) {
|
|
88
|
+
const lineText = getLineText(document, lineNo);
|
|
89
|
+
for (const ref of findLocKeyRefs(lineText)) {
|
|
90
|
+
const defs = data.index.lookup(ref.key).filter((d) => d.kind === "loc_key");
|
|
91
|
+
const position = { line: lineNo, character: ref.end };
|
|
92
|
+
if (defs.length > 0) {
|
|
93
|
+
const def = defs[0];
|
|
94
|
+
hints.push({
|
|
95
|
+
position,
|
|
96
|
+
label: truncate(def.value ?? "", HINT_MAX_LEN),
|
|
97
|
+
paddingLeft: true,
|
|
98
|
+
tooltip: {
|
|
99
|
+
kind: MarkupKind.Markdown,
|
|
100
|
+
value: `${def.value ?? ""}\n\n*${path.basename(def.file)}:${def.line + 1} (${def.source})*`,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} else if (ref.strictness === "strict") {
|
|
104
|
+
hints.push({
|
|
105
|
+
position,
|
|
106
|
+
label: "missing loc",
|
|
107
|
+
paddingLeft: true,
|
|
108
|
+
tooltip: `No "${ref.key}" entry in the indexed localization files.`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return hints;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Translated loc files: overlay the reference-language text per entry. */
|
|
117
|
+
function translationOverlayHints(
|
|
118
|
+
data: ServerData,
|
|
119
|
+
settings: ParadoxSettings,
|
|
120
|
+
document: TextDocument,
|
|
121
|
+
range: Range
|
|
122
|
+
): InlayHint[] {
|
|
123
|
+
const fsPath = URI.parse(document.uri).fsPath;
|
|
124
|
+
const fileLang = detectLocFileLanguage(fsPath);
|
|
125
|
+
// Only overlay in files of another language than the reference itself.
|
|
126
|
+
if (!fileLang || fileLang === settings.locLanguage) return [];
|
|
127
|
+
|
|
128
|
+
const hints: InlayHint[] = [];
|
|
129
|
+
for (let lineNo = range.start.line; lineNo <= range.end.line && lineNo < document.lineCount; lineNo++) {
|
|
130
|
+
const lineText = getLineText(document, lineNo);
|
|
131
|
+
const key = locKeyOnLine(lineText);
|
|
132
|
+
if (!key) continue;
|
|
133
|
+
const def = data.index.lookup(key).find((d) => d.kind === "loc_key");
|
|
134
|
+
if (!def || def.value === undefined) continue;
|
|
135
|
+
const label = def.value.length > 80 ? def.value.slice(0, 79) + "…" : def.value;
|
|
136
|
+
hints.push({
|
|
137
|
+
position: { line: lineNo, character: lineText.length },
|
|
138
|
+
label: `⟵ ${settings.locLanguage}: ${label}`,
|
|
139
|
+
paddingLeft: true,
|
|
140
|
+
tooltip: {
|
|
141
|
+
kind: MarkupKind.Markdown,
|
|
142
|
+
value: `**${settings.locLanguage}**: ${def.value}\n\n*${path.basename(def.file)}:${def.line + 1} (${def.source})*`,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return hints;
|
|
147
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Completion and hover for `#G`, `#bold`, … text-formatting tags inside a
|
|
3
|
+
* localization VALUE string (paradox-loc). Tag names come from the harvested
|
|
4
|
+
* textformatting index (data/textFormatting.ts). The tag context ends at `#!`.
|
|
5
|
+
*
|
|
6
|
+
* Completion docs and the hover card show the format chain plus, when known,
|
|
7
|
+
* the resolved color as `rgb(r, g, b)` text (completion docs cannot color a
|
|
8
|
+
* swatch) or an inline colored ■ span (hover, which supports HTML).
|
|
9
|
+
*/
|
|
10
|
+
import { CompletionItemKind, MarkupKind, type CompletionItem } from "vscode-languageserver/node";
|
|
11
|
+
import * as path from "path";
|
|
12
|
+
import { URI } from "vscode-uri";
|
|
13
|
+
import type { TextFormattingIndex, FormatEntry } from "../data/textFormatting";
|
|
14
|
+
import { rgbCss } from "../data/textFormatting";
|
|
15
|
+
import { finalize, MAX_ITEMS, type CompletionResult } from "./completion";
|
|
16
|
+
import { colorSwatch, renderCard, type CardInput } from "./hoverRender";
|
|
17
|
+
|
|
18
|
+
/** True when `prefix` ends inside an open loc value (odd count of unescaped `"`). */
|
|
19
|
+
function insideLocValue(prefix: string): boolean {
|
|
20
|
+
let quotes = 0;
|
|
21
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
22
|
+
if (prefix[i] === "\\") {
|
|
23
|
+
i++;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (prefix[i] === '"') quotes++;
|
|
27
|
+
}
|
|
28
|
+
return quotes % 2 === 1;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A one-line summary of a resolved tag: `→ instruction → G` · rgb(0, 255, 0). */
|
|
32
|
+
function summarize(index: TextFormattingIndex, name: string): { chain: string; color: string | null } {
|
|
33
|
+
const { chain, rgb } = index.resolve(name);
|
|
34
|
+
const rest = chain.slice(1);
|
|
35
|
+
return {
|
|
36
|
+
chain: rest.length > 0 ? `→ ${rest.join(" → ")}` : "(direct)",
|
|
37
|
+
color: rgb ? rgbCss(rgb) : null,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `common/…`-style tail of a source path, from the `gui/` segment onward. */
|
|
42
|
+
function guiRelTail(file: string): string {
|
|
43
|
+
const norm = file.replace(/\\/g, "/");
|
|
44
|
+
const i = norm.toLowerCase().lastIndexOf("/gui/");
|
|
45
|
+
return i >= 0 ? norm.slice(i + 1) : path.basename(file);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* `#` tag-name completion inside a loc value, or null when the cursor is not in
|
|
50
|
+
* that context (caller falls through).
|
|
51
|
+
*/
|
|
52
|
+
export function provideFormatTagCompletion(
|
|
53
|
+
index: TextFormattingIndex,
|
|
54
|
+
linePrefix: string
|
|
55
|
+
): CompletionResult | null {
|
|
56
|
+
const m = /#([A-Za-z0-9_]*)$/.exec(linePrefix);
|
|
57
|
+
if (!m) return null;
|
|
58
|
+
if (!insideLocValue(linePrefix.slice(0, m.index))) return null;
|
|
59
|
+
const items: CompletionItem[] = index.names().map((name) => {
|
|
60
|
+
const { chain, color } = summarize(index, name);
|
|
61
|
+
const item: CompletionItem = {
|
|
62
|
+
label: name,
|
|
63
|
+
kind: CompletionItemKind.EnumMember,
|
|
64
|
+
detail: `text format ${chain}${color ? ` · ${color}` : ""}`,
|
|
65
|
+
sortText: name.toLowerCase(),
|
|
66
|
+
};
|
|
67
|
+
const entry = index.winner(name)!;
|
|
68
|
+
const docParts = [
|
|
69
|
+
entry.layer === "builtin" ? "Engine built-in format." : `\`${entry.format || "(style only)"}\``,
|
|
70
|
+
];
|
|
71
|
+
if (color) docParts.push(`Color: ${color}`);
|
|
72
|
+
item.documentation = { kind: MarkupKind.Markdown, value: docParts.join("\n\n") };
|
|
73
|
+
return item;
|
|
74
|
+
});
|
|
75
|
+
return finalize(items, m[1], MAX_ITEMS);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The `#tag` under the cursor inside a loc value, or null. */
|
|
79
|
+
function tagAt(lineText: string, character: number): { name: string; start: number; end: number } | null {
|
|
80
|
+
const re = /#([A-Za-z0-9_]+)/g;
|
|
81
|
+
let m: RegExpExecArray | null;
|
|
82
|
+
while ((m = re.exec(lineText)) !== null) {
|
|
83
|
+
const start = m.index;
|
|
84
|
+
const end = m.index + m[0].length;
|
|
85
|
+
if (character >= start && character <= end && insideLocValue(lineText.slice(0, start))) {
|
|
86
|
+
return { name: m[1], start, end };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FormatTagHover {
|
|
93
|
+
markdown: string;
|
|
94
|
+
start: number;
|
|
95
|
+
end: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Hover card for a `#tag` in a loc value, or null when there is none / unknown. */
|
|
99
|
+
export function provideFormatTagHover(
|
|
100
|
+
index: TextFormattingIndex,
|
|
101
|
+
lineText: string,
|
|
102
|
+
character: number
|
|
103
|
+
): FormatTagHover | null {
|
|
104
|
+
const hit = tagAt(lineText, character);
|
|
105
|
+
if (!hit) return null;
|
|
106
|
+
const entry = index.winner(hit.name);
|
|
107
|
+
if (!entry) return null;
|
|
108
|
+
const { chain, rgb } = index.resolve(hit.name);
|
|
109
|
+
const card: CardInput = { kind: "text_format", badgeLabel: "text format", name: `#${hit.name}` };
|
|
110
|
+
const rest = chain.slice(1);
|
|
111
|
+
const doc: string[] = [];
|
|
112
|
+
if (rest.length > 0) doc.push(`Format chain: ${chain.map((c) => `\`${c}\``).join(" → ")}`);
|
|
113
|
+
else if (entry.format) doc.push(`\`${entry.format}\``);
|
|
114
|
+
if (rgb) doc.push(`Color: ${colorSwatch(rgb)} ${rgbCss(rgb)}`);
|
|
115
|
+
if (entry.layer === "builtin") doc.push("Engine built-in format.");
|
|
116
|
+
if (doc.length > 0) card.doc = doc.join(" \n");
|
|
117
|
+
if (entry.file) card.footer = [sourceLink(entry)];
|
|
118
|
+
return { markdown: renderCard(card), start: hit.start, end: hit.end };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sourceLink(entry: FormatEntry): string {
|
|
122
|
+
const rel = guiRelTail(entry.file);
|
|
123
|
+
const target = URI.file(entry.file)
|
|
124
|
+
.with({ fragment: String(entry.line + 1) })
|
|
125
|
+
.toString();
|
|
126
|
+
return `${entry.layer} · [${rel}](${target})`;
|
|
127
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Find-all-references over the reference index (workspace-mod usage sites)
|
|
3
|
+
* merged with the on-demand vanilla/parent scan (#3 — without it, a name used
|
|
4
|
+
* only by vanilla files listed nothing but its definitions), optionally
|
|
5
|
+
* including the definition sites.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import type { Location, Position } from "vscode-languageserver/node";
|
|
9
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
10
|
+
import { URI } from "vscode-uri";
|
|
11
|
+
import type { Reference } from "@px-lsp/protocol/types";
|
|
12
|
+
import { pushAll } from "@px-lsp/protocol/arrays";
|
|
13
|
+
import type { ServerData } from "../serverData";
|
|
14
|
+
import { wordRangeAt } from "../wordAt";
|
|
15
|
+
import { getLineText } from "../documents";
|
|
16
|
+
|
|
17
|
+
/** file+line key with the DefinitionIndex's path normalization (win32 is
|
|
18
|
+
* case-insensitive), so lazy-scan paths compare equal to indexed ones. */
|
|
19
|
+
function siteKey(file: string, line: number): string {
|
|
20
|
+
const n = path.normalize(file);
|
|
21
|
+
return `${process.platform === "win32" ? n.toLowerCase() : n}|${line}`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function provideReferences(
|
|
25
|
+
data: ServerData,
|
|
26
|
+
document: TextDocument,
|
|
27
|
+
position: Position,
|
|
28
|
+
includeDeclaration: boolean,
|
|
29
|
+
lazyRefs?: (name: string) => Promise<Reference[]>
|
|
30
|
+
): Promise<Location[]> {
|
|
31
|
+
const range = wordRangeAt(getLineText(document, position.line), position.character);
|
|
32
|
+
if (!range) return [];
|
|
33
|
+
const name = stripPrefix(range.word);
|
|
34
|
+
|
|
35
|
+
const refs: Reference[] = data.refIndex.lookup(name).slice();
|
|
36
|
+
if (lazyRefs) {
|
|
37
|
+
// The textual scan cannot tell a non-top-level definition site (inline
|
|
38
|
+
// scripted_trigger, nested title) from a use: drop hits the definition
|
|
39
|
+
// index already knows, they are appended under includeDeclaration below.
|
|
40
|
+
const defSites = new Set(data.index.lookupAll(name).map((d) => siteKey(d.file, d.line)));
|
|
41
|
+
pushAll(
|
|
42
|
+
refs,
|
|
43
|
+
(await lazyRefs(name)).filter((r) => !defSites.has(siteKey(r.file, r.line)))
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const locations: Location[] = refs.map((r) => ({
|
|
48
|
+
uri: URI.file(r.file).toString(),
|
|
49
|
+
range: {
|
|
50
|
+
start: { line: r.line, character: r.startChar },
|
|
51
|
+
end: { line: r.line, character: r.endChar },
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
if (includeDeclaration) {
|
|
56
|
+
for (const d of data.index.lookupAll(name)) {
|
|
57
|
+
locations.push({
|
|
58
|
+
uri: URI.file(d.file).toString(),
|
|
59
|
+
range: { start: { line: d.line, character: 0 }, end: { line: d.line, character: 0 } },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return locations;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** `scope:x` / `culture:x` under the cursor → the name part. */
|
|
67
|
+
export function stripPrefix(word: string): string {
|
|
68
|
+
const colon = word.lastIndexOf(":");
|
|
69
|
+
return colon >= 0 ? word.slice(colon + 1) : word;
|
|
70
|
+
}
|