@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,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Templated modifier tags from modifiers.log (`$CULTURE$_opinion`,
|
|
3
|
+
* `stationed_$MEN_AT_ARMS_TYPE$_damage_add`, …): the engine generates one
|
|
4
|
+
* concrete modifier per definition (e.g. `french_opinion` for the culture
|
|
5
|
+
* `french`). Like the generated `has_relation_X` cards in hover.ts, expansion
|
|
6
|
+
* is LAZY — matched against the definition index on hover / expanded once per
|
|
7
|
+
* completion cache build — never materialized into tokenMap (huge mods define
|
|
8
|
+
* thousands of cultures).
|
|
9
|
+
*
|
|
10
|
+
* The placeholder table itself is game knowledge and lives in the game
|
|
11
|
+
* profile (games/<id>/modifierPlaceholders.ts).
|
|
12
|
+
*
|
|
13
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
14
|
+
*/
|
|
15
|
+
import * as path from "path";
|
|
16
|
+
import type { Definition, TokenData } from "@px-lsp/protocol/types";
|
|
17
|
+
import { activeProfile } from "../games/active";
|
|
18
|
+
|
|
19
|
+
export interface PlaceholderSpec {
|
|
20
|
+
/** Definition-index kind the placeholder expands over. */
|
|
21
|
+
kind?: string;
|
|
22
|
+
/** Fixed engine value set, for placeholders not backed by the index. */
|
|
23
|
+
values?: readonly string[];
|
|
24
|
+
/** Human label for hover/completion docs ("culture", "men-at-arms base type"). */
|
|
25
|
+
label: string;
|
|
26
|
+
/** Values that do NOT exist for templates with the given prefix
|
|
27
|
+
* (verified per-game exceptions to `values`). */
|
|
28
|
+
excludeValues?: Record<string, readonly string[]>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ModifierTemplate {
|
|
32
|
+
/** The raw templated tag, e.g. `stationed_$MEN_AT_ARMS_TYPE$_damage_add`. */
|
|
33
|
+
name: string;
|
|
34
|
+
prefix: string;
|
|
35
|
+
suffix: string;
|
|
36
|
+
placeholder: string;
|
|
37
|
+
spec: PlaceholderSpec;
|
|
38
|
+
/** Metadata lines from the log ("Use areas: character"). */
|
|
39
|
+
traits?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const TEMPLATE_NAME = /^([a-z0-9_]*)\$([A-Z_]+)\$([a-z0-9_]*)$/;
|
|
43
|
+
|
|
44
|
+
/** Compile raw templated tokens into matchable templates; unknown placeholders are dropped. */
|
|
45
|
+
export function compileModifierTemplates(raw: TokenData[]): ModifierTemplate[] {
|
|
46
|
+
const placeholders = activeProfile().modifierPlaceholders;
|
|
47
|
+
const templates: ModifierTemplate[] = [];
|
|
48
|
+
for (const t of raw) {
|
|
49
|
+
const m = TEMPLATE_NAME.exec(t.name);
|
|
50
|
+
if (!m) continue;
|
|
51
|
+
let spec = placeholders[m[2]];
|
|
52
|
+
if (!spec) continue;
|
|
53
|
+
const excluded = spec.values && spec.excludeValues?.[m[1]];
|
|
54
|
+
if (excluded) {
|
|
55
|
+
spec = { ...spec, values: spec.values!.filter((v) => !excluded.includes(v)) };
|
|
56
|
+
}
|
|
57
|
+
templates.push({ name: t.name, prefix: m[1], suffix: m[3], placeholder: m[2], spec, traits: t.traits });
|
|
58
|
+
}
|
|
59
|
+
return templates;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TemplateMatch {
|
|
63
|
+
template: ModifierTemplate;
|
|
64
|
+
/** The definition/base-type name the placeholder matched ("french"). */
|
|
65
|
+
base: string;
|
|
66
|
+
/** The matched definition; undefined for fixed-value placeholders. */
|
|
67
|
+
def?: Definition;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Match a concrete name (`french_opinion`) against the templates: the first
|
|
72
|
+
* template whose placeholder slice resolves — to a fixed engine value or to an
|
|
73
|
+
* indexed definition of the mapped kind — wins. Shared-suffix ambiguity
|
|
74
|
+
* (`_opinion` appears in six templates) is resolved by that lookup.
|
|
75
|
+
*/
|
|
76
|
+
export function matchTemplatedModifier(
|
|
77
|
+
word: string,
|
|
78
|
+
templates: ModifierTemplate[],
|
|
79
|
+
lookup: (name: string) => Definition[]
|
|
80
|
+
): TemplateMatch | null {
|
|
81
|
+
for (const template of templates) {
|
|
82
|
+
const minLen = template.prefix.length + template.suffix.length + 1;
|
|
83
|
+
if (word.length < minLen) continue;
|
|
84
|
+
if (!word.startsWith(template.prefix) || !word.endsWith(template.suffix)) continue;
|
|
85
|
+
const base = word.slice(template.prefix.length, word.length - template.suffix.length);
|
|
86
|
+
if (template.spec.values) {
|
|
87
|
+
if (template.spec.values.includes(base)) return { template, base };
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const def = lookup(base).find((d) => d.kind === template.spec.kind);
|
|
91
|
+
if (def) return { template, base, def };
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** One-line doc for a matched expansion, shared by hover and completion resolve. */
|
|
97
|
+
export function templatedModifierDoc(m: TemplateMatch): string {
|
|
98
|
+
if (m.def) {
|
|
99
|
+
return `Modifier the engine generates for the ${m.template.spec.label} \`${m.base}\` (${path.basename(m.def.file)}:${m.def.line + 1}).`;
|
|
100
|
+
}
|
|
101
|
+
return `Modifier the engine generates for the built-in ${m.template.spec.label} \`${m.base}\`.`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface TemplateExpansion {
|
|
105
|
+
name: string;
|
|
106
|
+
template: ModifierTemplate;
|
|
107
|
+
/** The definition the expansion came from; undefined for fixed value sets. */
|
|
108
|
+
def?: Definition;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Expand every template against the definition index (one pass) plus the fixed
|
|
113
|
+
* value sets. Used to build the completion list for modifier contexts; hover
|
|
114
|
+
* uses matchTemplatedModifier instead and never needs the full expansion.
|
|
115
|
+
*/
|
|
116
|
+
export function expandModifierTemplates(
|
|
117
|
+
templates: ModifierTemplate[],
|
|
118
|
+
index: { entries(filter?: (def: Definition) => boolean): IterableIterator<Definition> },
|
|
119
|
+
completableKinds: ReadonlySet<string>
|
|
120
|
+
): TemplateExpansion[] {
|
|
121
|
+
const byKind = new Map<string, ModifierTemplate[]>();
|
|
122
|
+
const out: TemplateExpansion[] = [];
|
|
123
|
+
for (const template of templates) {
|
|
124
|
+
if (template.spec.values) {
|
|
125
|
+
for (const v of template.spec.values)
|
|
126
|
+
out.push({ name: template.prefix + v + template.suffix, template });
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
const kind = template.spec.kind!;
|
|
130
|
+
if (completableKinds.size > 0 && !completableKinds.has(kind)) continue;
|
|
131
|
+
const list = byKind.get(kind);
|
|
132
|
+
if (list) list.push(template);
|
|
133
|
+
else byKind.set(kind, [template]);
|
|
134
|
+
}
|
|
135
|
+
if (byKind.size > 0) {
|
|
136
|
+
for (const def of index.entries((d) => byKind.has(d.kind))) {
|
|
137
|
+
for (const template of byKind.get(def.kind) ?? []) {
|
|
138
|
+
out.push({ name: template.prefix + def.name + template.suffix, template, def });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loc text-formatting tag harvest: `#G`, `#bold`, … tags come from
|
|
3
|
+
* `textformatting = { format = { name = G format = "color:{0,1,0}" } … }`
|
|
4
|
+
* blocks in .gui files. Layers in load order: jomini
|
|
5
|
+
* (gui/jomini/basetextformatting.gui base set), game (gui/**), read-only
|
|
6
|
+
* dependency parents, then the mod.
|
|
7
|
+
*
|
|
8
|
+
* Resolution is gui FIOS (AGENTS.md): the FIRST-loaded definition of a name
|
|
9
|
+
* wins, and `override = no` (the base set's default) confirms that; a later
|
|
10
|
+
* definition only replaces it with `override = yes`. So the jomini base `G`
|
|
11
|
+
* (green, override=no) is NOT clobbered by game's `G` (which sets no override) —
|
|
12
|
+
* matching the engine and the Paradox modding convention (a mod overrides a base
|
|
13
|
+
* format with `override = yes`). Callers must addLayer in load order.
|
|
14
|
+
*
|
|
15
|
+
* Aliases chain (`I` → `instruction` → `G;italic` → G's color); the resolved
|
|
16
|
+
* [r,g,b] is the first `color:{r,g,b}` reached along the chain (0..1 floats).
|
|
17
|
+
*
|
|
18
|
+
* No `vscode` imports: unit-tested in plain Node.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from "fs";
|
|
21
|
+
import { LineIndex, parseScript, type BlockNode } from "../parser";
|
|
22
|
+
import { listFiles } from "@px-lsp/protocol/fsWalk";
|
|
23
|
+
import * as path from "path";
|
|
24
|
+
|
|
25
|
+
export type FormatLayer = "jomini" | "game" | "parent" | "mod" | "builtin";
|
|
26
|
+
|
|
27
|
+
export interface FormatEntry {
|
|
28
|
+
name: string;
|
|
29
|
+
/** The raw `format = "…"` string (empty for style-only tags like editor_formatting). */
|
|
30
|
+
format: string;
|
|
31
|
+
file: string;
|
|
32
|
+
line: number; // 0-based
|
|
33
|
+
layer: FormatLayer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type Rgb = [number, number, number];
|
|
37
|
+
|
|
38
|
+
export interface FormatResolution {
|
|
39
|
+
/** Names visited resolving the tag, e.g. ["I","instruction","G","italic"]. */
|
|
40
|
+
chain: string[];
|
|
41
|
+
/** First color reached (0..1 floats), or null for style-only / unknown tags. */
|
|
42
|
+
rgb: Rgb | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Engine-native formats referenced by the harvested format strings but never
|
|
47
|
+
* declared as `format` blocks: the style primitives the format-string grammar
|
|
48
|
+
* uses. Included so completion/hover recognize `#bold`, `#italic`.
|
|
49
|
+
*/
|
|
50
|
+
const ENGINE_BUILTINS = ["bold", "italic"];
|
|
51
|
+
|
|
52
|
+
/** Parse `color:{r,g,b}` (0..1 floats, space/comma separated) → [r,g,b]. */
|
|
53
|
+
function parseColor(segment: string): Rgb | null {
|
|
54
|
+
const m = /^color\s*:\s*\{\s*([\d.]+)[\s,]+([\d.]+)[\s,]+([\d.]+)\s*\}$/.exec(segment.trim());
|
|
55
|
+
if (!m) return null;
|
|
56
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class TextFormattingIndex {
|
|
60
|
+
/** name -> the FIOS winner (first loaded, unless a later `override = yes`). */
|
|
61
|
+
private byName = new Map<string, FormatEntry>();
|
|
62
|
+
|
|
63
|
+
constructor() {
|
|
64
|
+
for (const name of ENGINE_BUILTINS) {
|
|
65
|
+
this.byName.set(name, { name, format: "", file: "", line: 0, layer: "builtin" });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Harvest every .gui file under `root/gui` whose text mentions `textformatting`. */
|
|
70
|
+
addLayer(root: string, layer: FormatLayer): void {
|
|
71
|
+
for (const file of listFiles(path.join(root, "gui"), ".gui").sort()) {
|
|
72
|
+
let text: string;
|
|
73
|
+
try {
|
|
74
|
+
text = fs.readFileSync(file, "utf8").replace(/^/, "");
|
|
75
|
+
} catch {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (!text.includes("textformatting")) continue; // cheap prefilter
|
|
79
|
+
this.harvestText(text, file, layer);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
harvestText(text: string, file: string, layer: FormatLayer): void {
|
|
84
|
+
const { root } = parseScript(text);
|
|
85
|
+
const lines = new LineIndex(text);
|
|
86
|
+
for (const stmt of root.statements) {
|
|
87
|
+
if (stmt.kind !== "assignment" || stmt.key.text.toLowerCase() !== "textformatting") continue;
|
|
88
|
+
if (stmt.value?.kind !== "block") continue;
|
|
89
|
+
this.harvestBlock(stmt.value, file, layer, lines);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private harvestBlock(block: BlockNode, file: string, layer: FormatLayer, lines: LineIndex): void {
|
|
94
|
+
for (const stmt of block.statements) {
|
|
95
|
+
if (stmt.kind !== "assignment" || stmt.key.text.toLowerCase() !== "format") continue;
|
|
96
|
+
if (stmt.value?.kind !== "block") continue;
|
|
97
|
+
let name: string | null = null;
|
|
98
|
+
let format = "";
|
|
99
|
+
let nameLine = 0;
|
|
100
|
+
let override = false; // FIOS default (absent / `override = no`)
|
|
101
|
+
for (const inner of stmt.value.statements) {
|
|
102
|
+
if (inner.kind !== "assignment" || inner.value?.kind !== "scalar") continue;
|
|
103
|
+
const key = inner.key.text.toLowerCase();
|
|
104
|
+
if (key === "name") {
|
|
105
|
+
name = inner.value.text;
|
|
106
|
+
nameLine = lines.positionAt(inner.key.range.start).line;
|
|
107
|
+
} else if (key === "format") {
|
|
108
|
+
format = inner.value.text;
|
|
109
|
+
} else if (key === "override") {
|
|
110
|
+
override = inner.value.text.toLowerCase() === "yes";
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (name === null) continue;
|
|
114
|
+
// FIOS: keep the first definition unless this one explicitly overrides.
|
|
115
|
+
if (this.byName.has(name) && !override) continue;
|
|
116
|
+
this.byName.set(name, { name, format, file, line: nameLine, layer });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Winning entry for a tag name, or null when unknown. */
|
|
121
|
+
winner(name: string): FormatEntry | null {
|
|
122
|
+
return this.byName.get(name) ?? null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
names(): string[] {
|
|
126
|
+
return [...this.byName.keys()].sort();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Follow the alias chain, collecting the visited names and the first color. */
|
|
130
|
+
resolve(name: string): FormatResolution {
|
|
131
|
+
const chain: string[] = [];
|
|
132
|
+
const seen = new Set<string>();
|
|
133
|
+
let rgb: Rgb | null = null;
|
|
134
|
+
const visit = (n: string): void => {
|
|
135
|
+
if (seen.has(n)) return;
|
|
136
|
+
seen.add(n);
|
|
137
|
+
chain.push(n);
|
|
138
|
+
const entry = this.winner(n);
|
|
139
|
+
if (!entry) return; // builtin / unknown leaf
|
|
140
|
+
for (const seg of entry.format
|
|
141
|
+
.split(";")
|
|
142
|
+
.map((s) => s.trim())
|
|
143
|
+
.filter(Boolean)) {
|
|
144
|
+
const color = parseColor(seg);
|
|
145
|
+
if (color) {
|
|
146
|
+
if (!rgb) rgb = color;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
visit(seg);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
visit(name);
|
|
153
|
+
return { chain, rgb };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
get count(): number {
|
|
157
|
+
return this.byName.size;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Format a 0..1 rgb triple as `rgb(0, 255, 0)` (0..255 ints) for docs. */
|
|
162
|
+
export function rgbCss(rgb: Rgb): string {
|
|
163
|
+
const to255 = (v: number) => Math.round(Math.max(0, Math.min(1, v)) * 255);
|
|
164
|
+
return `rgb(${to255(rgb[0])}, ${to255(rgb[1])}, ${to255(rgb[2])})`;
|
|
165
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for the wiki reference lists bundled in wikidocs/ (Markdown mirrors
|
|
3
|
+
* of the Paradox wiki; see wikidocs/ATTRIBUTION.md for provenance).
|
|
4
|
+
*
|
|
5
|
+
* These are a fallback and enrichment source for engine tokens: script_docs
|
|
6
|
+
* output is authoritative for the user's exact game version, but the wiki lists
|
|
7
|
+
* work without ever launching the game in debug mode and carry usage examples
|
|
8
|
+
* that script_docs lacks.
|
|
9
|
+
*
|
|
10
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
11
|
+
*/
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import type { TokenData, TokenKind } from "@px-lsp/protocol/types";
|
|
15
|
+
import { activeProfile } from "../games/active";
|
|
16
|
+
|
|
17
|
+
const NAME_RE = /^[A-Za-z0-9_.:]+$/;
|
|
18
|
+
|
|
19
|
+
/** Strip markdown/HTML noise from a table cell, keeping <br> as line breaks. */
|
|
20
|
+
export function cleanCell(cell: string): string {
|
|
21
|
+
return cell
|
|
22
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
23
|
+
.replace(/<\/?code[^>]*>/gi, "")
|
|
24
|
+
.replace(/!\[[^\]]*\]\([^)]*\)/g, "") // images
|
|
25
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // links -> text
|
|
26
|
+
.replace(/\*\*([^*]*)\*\*/g, "$1")
|
|
27
|
+
.replace(/`/g, "")
|
|
28
|
+
.replace(/</g, "<")
|
|
29
|
+
.replace(/>/g, ">")
|
|
30
|
+
.replace(/"/g, '"')
|
|
31
|
+
.replace(/�?39;/g, "'")
|
|
32
|
+
.replace(/|/g, "|")
|
|
33
|
+
.replace(/&/g, "&")
|
|
34
|
+
.split("\n")
|
|
35
|
+
.map((l) => l.trim())
|
|
36
|
+
.join("\n")
|
|
37
|
+
.trim();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Split a markdown table row into cleaned cells; returns null for non-row lines. */
|
|
41
|
+
function splitRow(line: string): string[] | null {
|
|
42
|
+
const trimmed = line.trim();
|
|
43
|
+
if (!trimmed.startsWith("|")) return null;
|
|
44
|
+
const inner = trimmed.replace(/^\|/, "").replace(/\|\s*$/, "");
|
|
45
|
+
return inner.split("|").map(cleanCell);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isHeaderOrSeparator(cells: string[]): boolean {
|
|
49
|
+
return cells.every((c) => c === "" || /^-{3,}$/.test(c)) || cells.some((c) => /^Name$|^Scope$/i.test(c));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function makeToken(
|
|
53
|
+
kind: TokenKind,
|
|
54
|
+
name: string,
|
|
55
|
+
doc: string,
|
|
56
|
+
scopes: string[],
|
|
57
|
+
traitLines: string[],
|
|
58
|
+
usage?: string
|
|
59
|
+
): TokenData {
|
|
60
|
+
const token: TokenData = { name, kind, doc, scopes: scopes.filter((s) => s !== "") };
|
|
61
|
+
// Sections separated by blank lines so consumers can pick out e.g. the Example block.
|
|
62
|
+
const traits = traitLines.filter((t) => t !== "").join("\n\n");
|
|
63
|
+
if (traits) token.traits = traits;
|
|
64
|
+
if (usage) token.usage = usage;
|
|
65
|
+
return token;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function splitScopes(cell: string): string[] {
|
|
69
|
+
return cell
|
|
70
|
+
.split(/[\n,]/)
|
|
71
|
+
.map((s) => s.trim())
|
|
72
|
+
.filter((s) => s !== "");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Effects_list.md: | Name | Desc | Example | Scopes | Target | */
|
|
76
|
+
export function parseWikiEffects(md: string): TokenData[] {
|
|
77
|
+
const tokens: TokenData[] = [];
|
|
78
|
+
for (const line of md.split(/\r?\n/)) {
|
|
79
|
+
const cells = splitRow(line);
|
|
80
|
+
if (!cells || cells.length < 4 || isHeaderOrSeparator(cells)) continue;
|
|
81
|
+
const [name, desc, example, scopes, target] = cells;
|
|
82
|
+
if (!NAME_RE.test(name)) continue;
|
|
83
|
+
tokens.push(
|
|
84
|
+
makeToken(
|
|
85
|
+
"effect",
|
|
86
|
+
name,
|
|
87
|
+
desc,
|
|
88
|
+
splitScopes(scopes ?? ""),
|
|
89
|
+
[target ? `Target: ${target}` : ""],
|
|
90
|
+
example || undefined
|
|
91
|
+
)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return tokens;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Triggers_list.md: | Name | Description | Usage | Traits | Supported Scopes | Supported Targets | */
|
|
98
|
+
export function parseWikiTriggers(md: string): TokenData[] {
|
|
99
|
+
const tokens: TokenData[] = [];
|
|
100
|
+
for (const line of md.split(/\r?\n/)) {
|
|
101
|
+
const cells = splitRow(line);
|
|
102
|
+
if (!cells || cells.length < 5 || isHeaderOrSeparator(cells)) continue;
|
|
103
|
+
const [name, desc, usage, traits, scopes, targets] = cells;
|
|
104
|
+
if (!NAME_RE.test(name)) continue;
|
|
105
|
+
tokens.push(
|
|
106
|
+
makeToken(
|
|
107
|
+
"trigger",
|
|
108
|
+
name,
|
|
109
|
+
desc,
|
|
110
|
+
splitScopes(scopes ?? ""),
|
|
111
|
+
[traits ? `Traits: ${traits}` : "", targets ? `Supported Targets: ${targets}` : ""],
|
|
112
|
+
usage || undefined
|
|
113
|
+
)
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
return tokens;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Scopes_list.md: sections (## Character, ## Culture, ...) each with
|
|
121
|
+
* | Scope | Description | To scope | Version added | tables. The section is the
|
|
122
|
+
* input scope, "To scope" the output scope.
|
|
123
|
+
*/
|
|
124
|
+
export function parseWikiEventTargets(md: string): TokenData[] {
|
|
125
|
+
const tokens: TokenData[] = [];
|
|
126
|
+
let section = "";
|
|
127
|
+
for (const line of md.split(/\r?\n/)) {
|
|
128
|
+
const heading = /^##\s+(.+)$/.exec(line.trim());
|
|
129
|
+
if (heading) {
|
|
130
|
+
section = heading[1].trim().toLowerCase();
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
const cells = splitRow(line);
|
|
134
|
+
if (!cells || cells.length < 3 || isHeaderOrSeparator(cells)) continue;
|
|
135
|
+
const [name, desc, toScope, version] = cells;
|
|
136
|
+
if (!NAME_RE.test(name)) continue;
|
|
137
|
+
const scopes: string[] = [];
|
|
138
|
+
if (section && section !== "primitive scopes") scopes.push(`input: ${section}`);
|
|
139
|
+
if (toScope) scopes.push(`output: ${toScope}`);
|
|
140
|
+
tokens.push(makeToken("event_target", name, desc, scopes, [version ? `Version added: ${version}` : ""]));
|
|
141
|
+
}
|
|
142
|
+
return tokens;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export const WIKI_FILES: Array<{ file: string; parse: (md: string) => TokenData[] }> = [
|
|
146
|
+
{ file: "Effects_list.md", parse: parseWikiEffects },
|
|
147
|
+
{ file: "Triggers_list.md", parse: parseWikiTriggers },
|
|
148
|
+
{ file: "Scopes_list.md", parse: parseWikiEventTargets },
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
/** Parse all bundled wiki list files found in `dir`. Missing files are skipped. */
|
|
152
|
+
export function loadWikiTokens(dir: string): TokenData[] {
|
|
153
|
+
const tokens: TokenData[] = [];
|
|
154
|
+
for (const { file, parse } of WIKI_FILES) {
|
|
155
|
+
try {
|
|
156
|
+
tokens.push(...parse(fs.readFileSync(path.join(dir, file), "utf8")));
|
|
157
|
+
} catch {
|
|
158
|
+
// Bundled file missing/unreadable: skip, the extension works without it.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return tokens;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Merge script_docs tokens (authoritative) with wiki tokens (fallback/enrichment):
|
|
166
|
+
* wiki-only tokens are added with a provenance note; tokens present in both keep
|
|
167
|
+
* the script_docs doc and scopes but gain the wiki's usage example.
|
|
168
|
+
*/
|
|
169
|
+
export function mergeWikiTokens(scriptDocs: TokenData[], wiki: TokenData[]): TokenData[] {
|
|
170
|
+
const byKey = new Map<string, TokenData>();
|
|
171
|
+
for (const t of scriptDocs) byKey.set(`${t.kind}:${t.name}`, t);
|
|
172
|
+
|
|
173
|
+
const merged = [...scriptDocs];
|
|
174
|
+
const wikiNote = activeProfile().wikiNote;
|
|
175
|
+
for (const w of wiki) {
|
|
176
|
+
const existing = byKey.get(`${w.kind}:${w.name}`);
|
|
177
|
+
if (!existing) {
|
|
178
|
+
const note = w.traits ? `${w.traits}\n${wikiNote}` : wikiNote;
|
|
179
|
+
merged.push({ ...w, traits: note });
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (existing.doc === "" && w.doc !== "") existing.doc = w.doc;
|
|
183
|
+
// script_docs is authoritative; the wiki fills a missing syntax example.
|
|
184
|
+
if (!existing.usage && w.usage) existing.usage = w.usage;
|
|
185
|
+
}
|
|
186
|
+
return merged;
|
|
187
|
+
}
|