@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,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/eventVocabulary: what an event editor is allowed to offer.
|
|
3
|
+
*
|
|
4
|
+
* The graph inspector guides a new modder with dropdowns, and a dropdown is
|
|
5
|
+
* only as honest as its source. So every list here is derived from data the
|
|
6
|
+
* server already holds:
|
|
7
|
+
*
|
|
8
|
+
* keys the active profile's structure table (harvested `_*.info`),
|
|
9
|
+
* ordered by the usage counts it carries
|
|
10
|
+
* value sets a `values: "enum:a|b|c"` spec, or the schema's reference
|
|
11
|
+
* fields resolved through the definition index (`theme` ->
|
|
12
|
+
* every indexed event_theme, mod entries first)
|
|
13
|
+
* effects script_docs tokens (the user's own logs, or the bundled
|
|
14
|
+
* triggers wiki fallback), with the log's own description
|
|
15
|
+
* saved scopes the mod's own `save_scope_as` sites, from the index
|
|
16
|
+
*
|
|
17
|
+
* Nothing is hand-written, so a game patch that adds a theme or renames an
|
|
18
|
+
* effect changes the dropdown without a release. No `vscode` imports.
|
|
19
|
+
*/
|
|
20
|
+
import {
|
|
21
|
+
EVENT_VOCABULARY_MAX_TOKENS,
|
|
22
|
+
EVENT_VOCABULARY_MAX_VALUES,
|
|
23
|
+
type EventValueOptionsResult,
|
|
24
|
+
type EventVocabularyItem,
|
|
25
|
+
type EventVocabularyResult,
|
|
26
|
+
} from "@px-lsp/protocol/protocol";
|
|
27
|
+
import type { ServerData } from "../serverData";
|
|
28
|
+
import type { SchemaData } from "../schema/loader";
|
|
29
|
+
import type { KeySpec } from "../schema/types";
|
|
30
|
+
|
|
31
|
+
/** One line is what a menu row shows; the rest is noise at that size. */
|
|
32
|
+
const MAX_DOC = 220;
|
|
33
|
+
|
|
34
|
+
function short(doc: string | undefined): string | undefined {
|
|
35
|
+
if (!doc) return undefined;
|
|
36
|
+
const oneLine = doc.replace(/\s+/g, " ").trim();
|
|
37
|
+
if (oneLine === "") return undefined;
|
|
38
|
+
return oneLine.length > MAX_DOC ? oneLine.slice(0, MAX_DOC - 1) + "…" : oneLine;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Structure keys of one block, most used first (freq is the harvest's count). */
|
|
42
|
+
function keyItems(specs: Map<string, KeySpec> | undefined): EventVocabularyItem[] {
|
|
43
|
+
if (!specs) return [];
|
|
44
|
+
const out = [...specs.values()].sort((a, b) => (b.freq ?? 0) - (a.freq ?? 0));
|
|
45
|
+
return out.map((spec) => ({
|
|
46
|
+
value: spec.key,
|
|
47
|
+
doc: short(spec.doc),
|
|
48
|
+
hint:
|
|
49
|
+
spec.values === "block"
|
|
50
|
+
? "block"
|
|
51
|
+
: spec.values === "bool"
|
|
52
|
+
? "yes/no"
|
|
53
|
+
: spec.values === "loc"
|
|
54
|
+
? "loc"
|
|
55
|
+
: undefined,
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function computeEventVocabulary(
|
|
60
|
+
data: ServerData,
|
|
61
|
+
schema: SchemaData,
|
|
62
|
+
inFocus: (file: string) => boolean = () => true
|
|
63
|
+
): EventVocabularyResult {
|
|
64
|
+
const blocks = schema.structures.keysByKindBlock.get("event");
|
|
65
|
+
const eventKeys = keyItems(blocks?.get(""));
|
|
66
|
+
const optionKeys = keyItems(blocks?.get("option"));
|
|
67
|
+
|
|
68
|
+
// Value sets, for the keys that have one. Two sources, in this order: a
|
|
69
|
+
// declared enumeration wins (it is the exact vocabulary), otherwise the
|
|
70
|
+
// schema's reference field names the definition kinds the index can list.
|
|
71
|
+
const values: Record<string, EventVocabularyItem[]> = {};
|
|
72
|
+
const byKind = new Map<string, EventVocabularyItem[]>();
|
|
73
|
+
const definitionsOf = (kind: string): EventVocabularyItem[] => {
|
|
74
|
+
let cached = byKind.get(kind);
|
|
75
|
+
if (cached) return cached;
|
|
76
|
+
const seen = new Set<string>();
|
|
77
|
+
const mod: EventVocabularyItem[] = [];
|
|
78
|
+
const rest: EventVocabularyItem[] = [];
|
|
79
|
+
for (const def of data.index.allDefinitions()) {
|
|
80
|
+
if (def.kind !== kind || seen.has(def.name)) continue;
|
|
81
|
+
if (def.source === "mod" && !inFocus(def.file)) continue;
|
|
82
|
+
seen.add(def.name);
|
|
83
|
+
(def.source === "mod" ? mod : rest).push({
|
|
84
|
+
value: def.name,
|
|
85
|
+
doc: short(def.doc),
|
|
86
|
+
hint: def.source === "mod" ? "this mod" : def.source,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
mod.sort((a, b) => a.value.localeCompare(b.value));
|
|
90
|
+
rest.sort((a, b) => a.value.localeCompare(b.value));
|
|
91
|
+
cached = [...mod, ...rest].slice(0, EVENT_VOCABULARY_MAX_VALUES);
|
|
92
|
+
byKind.set(kind, cached);
|
|
93
|
+
return cached;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const specs of [blocks?.get(""), blocks?.get("option")]) {
|
|
97
|
+
for (const spec of specs?.values() ?? []) {
|
|
98
|
+
if (values[spec.key]) continue;
|
|
99
|
+
if (spec.values?.startsWith("enum:")) {
|
|
100
|
+
values[spec.key] = spec.values
|
|
101
|
+
.slice(5)
|
|
102
|
+
.split("|")
|
|
103
|
+
.filter((v) => v !== "")
|
|
104
|
+
.map((v) => ({ value: v }));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (spec.values === "bool") {
|
|
108
|
+
values[spec.key] = [{ value: "yes" }, { value: "no" }];
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const field = schema.refFields.get(spec.key);
|
|
112
|
+
if (!field) continue;
|
|
113
|
+
const items: EventVocabularyItem[] = [];
|
|
114
|
+
for (const kind of field.kinds) items.push(...definitionsOf(kind));
|
|
115
|
+
if (items.length > 0) values[spec.key] = items.slice(0, EVENT_VOCABULARY_MAX_VALUES);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Engine tokens. `data.tokens` is the parsed script_docs (or the bundled wiki
|
|
120
|
+
// baseline); the reference index's non-call usage counts order them, so the
|
|
121
|
+
// effects a real corpus writes come first instead of the alphabet.
|
|
122
|
+
const effects: EventVocabularyItem[] = [];
|
|
123
|
+
const triggers: EventVocabularyItem[] = [];
|
|
124
|
+
const seenToken = new Set<string>();
|
|
125
|
+
const ranked = [...data.tokens].sort(
|
|
126
|
+
(a, b) => data.refIndex.usageCount(b.name) - data.refIndex.usageCount(a.name)
|
|
127
|
+
);
|
|
128
|
+
for (const token of ranked) {
|
|
129
|
+
const bucket = token.kind === "effect" ? effects : token.kind === "trigger" ? triggers : null;
|
|
130
|
+
if (!bucket || bucket.length >= EVENT_VOCABULARY_MAX_TOKENS) continue;
|
|
131
|
+
const key = `${token.kind}:${token.name}`;
|
|
132
|
+
if (seenToken.has(key)) continue;
|
|
133
|
+
seenToken.add(key);
|
|
134
|
+
bucket.push({
|
|
135
|
+
value: token.name,
|
|
136
|
+
doc: short(token.doc),
|
|
137
|
+
hint: token.scopes.length > 0 ? token.scopes.slice(0, 3).join(", ") : undefined,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const savedScopes: EventVocabularyItem[] = [];
|
|
142
|
+
const seenScope = new Set<string>();
|
|
143
|
+
for (const def of data.index.allDefinitions()) {
|
|
144
|
+
if (def.kind !== "saved_scope" || seenScope.has(def.name)) continue;
|
|
145
|
+
if (!inFocus(def.file)) continue;
|
|
146
|
+
seenScope.add(def.name);
|
|
147
|
+
savedScopes.push({ value: def.name, hint: def.container });
|
|
148
|
+
}
|
|
149
|
+
savedScopes.sort((a, b) => a.value.localeCompare(b.value));
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
eventKeys,
|
|
153
|
+
optionKeys,
|
|
154
|
+
values,
|
|
155
|
+
effects,
|
|
156
|
+
triggers,
|
|
157
|
+
savedScopes: savedScopes.slice(0, EVENT_VOCABULARY_MAX_VALUES),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Kinds whose full listing is not a useful dropdown: sites rather than
|
|
163
|
+
* choices (loc keys, saved scopes, variables), bodies rather than values
|
|
164
|
+
* (scripted effects/triggers), and the id spaces so large a capped list
|
|
165
|
+
* would silently lie (events, on_actions, decisions — those get inline
|
|
166
|
+
* completions instead).
|
|
167
|
+
*/
|
|
168
|
+
const VALUE_KIND_SKIP = new Set([
|
|
169
|
+
"event",
|
|
170
|
+
"on_action",
|
|
171
|
+
"decision",
|
|
172
|
+
"loc_key",
|
|
173
|
+
"saved_scope",
|
|
174
|
+
"variable",
|
|
175
|
+
"scripted_effect",
|
|
176
|
+
"scripted_trigger",
|
|
177
|
+
"script_value",
|
|
178
|
+
]);
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The value set `value` belongs to: resolve it through the definition index
|
|
182
|
+
* (`secret_cultivator` is a `secret`), then list every definition of that
|
|
183
|
+
* kind, mod entries first. Null when the value resolves to nothing
|
|
184
|
+
* enumerable — a number, yes/no, a scope chain, an unindexed name.
|
|
185
|
+
*/
|
|
186
|
+
export function computeValueOptions(
|
|
187
|
+
data: ServerData,
|
|
188
|
+
value: string,
|
|
189
|
+
inFocus: (file: string) => boolean = () => true
|
|
190
|
+
): EventValueOptionsResult | null {
|
|
191
|
+
const name = value.trim();
|
|
192
|
+
if (name === "" || name === "yes" || name === "no") return null;
|
|
193
|
+
if (/^-?[\d.]+$/.test(name) || name.includes(":") || name.includes(".")) return null;
|
|
194
|
+
const def = data.index.lookup(name).find((d) => !VALUE_KIND_SKIP.has(d.kind));
|
|
195
|
+
if (!def) return null;
|
|
196
|
+
const seen = new Set<string>();
|
|
197
|
+
const mod: EventVocabularyItem[] = [];
|
|
198
|
+
const rest: EventVocabularyItem[] = [];
|
|
199
|
+
for (const d of data.index.allDefinitions()) {
|
|
200
|
+
if (d.kind !== def.kind || seen.has(d.name)) continue;
|
|
201
|
+
if (d.source === "mod" && !inFocus(d.file)) continue;
|
|
202
|
+
seen.add(d.name);
|
|
203
|
+
(d.source === "mod" ? mod : rest).push({
|
|
204
|
+
value: d.name,
|
|
205
|
+
doc: short(d.doc),
|
|
206
|
+
hint: d.source === "mod" ? "this mod" : d.source,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
mod.sort((a, b) => a.value.localeCompare(b.value));
|
|
210
|
+
rest.sort((a, b) => a.value.localeCompare(b.value));
|
|
211
|
+
const items = [...mod, ...rest].slice(0, EVENT_VOCABULARY_MAX_VALUES);
|
|
212
|
+
// A single entry (the value itself) enumerates nothing worth a menu.
|
|
213
|
+
return items.length > 1 ? { kind: def.kind, items } : null;
|
|
214
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/locCoverage: per-language localization health for the mod —
|
|
3
|
+
* referenced-but-missing keys, defined-but-orphaned keys, and untranslated
|
|
4
|
+
* keys (value identical to the source language). Feeds the coverage tree and
|
|
5
|
+
* the translation workflow.
|
|
6
|
+
*/
|
|
7
|
+
import * as path from "path";
|
|
8
|
+
import * as fs from "fs";
|
|
9
|
+
import type { LocCoverage, LocIssue } from "@px-lsp/protocol/protocol";
|
|
10
|
+
import type { SchemaEntry } from "../schema/types";
|
|
11
|
+
import { listFiles } from "@px-lsp/protocol/fsWalk";
|
|
12
|
+
import { detectLocFileLanguage } from "@px-lsp/protocol/translationCore";
|
|
13
|
+
import { parseLoc } from "../parser";
|
|
14
|
+
import type { ServerData } from "../serverData";
|
|
15
|
+
|
|
16
|
+
const ISSUE_CAP = 500;
|
|
17
|
+
|
|
18
|
+
interface LocEntrySite {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
file: string;
|
|
22
|
+
line: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The mod-relative localization roots, from the profile's loc_key schema entries
|
|
26
|
+
* (some games nest localization under a load-stage folder rather than the top level). */
|
|
27
|
+
function locRoots(schemaEntries: SchemaEntry[]): string[] {
|
|
28
|
+
const roots = schemaEntries.filter((e) => e.kind === "loc_key").map((e) => e.path);
|
|
29
|
+
return roots.length > 0 ? [...new Set(roots)] : ["localization"];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** All loc entries in the mod, grouped per language (read fresh — mods are small). */
|
|
33
|
+
function modLocByLanguage(
|
|
34
|
+
modPath: string,
|
|
35
|
+
schemaEntries: SchemaEntry[]
|
|
36
|
+
): Map<string, Map<string, LocEntrySite>> {
|
|
37
|
+
const byLang = new Map<string, Map<string, LocEntrySite>>();
|
|
38
|
+
for (const root of locRoots(schemaEntries)) {
|
|
39
|
+
const locDir = path.join(modPath, root);
|
|
40
|
+
for (const file of listFiles(locDir, ".yml")) {
|
|
41
|
+
const lang = detectLocFileLanguage(file);
|
|
42
|
+
if (!lang) continue;
|
|
43
|
+
let content: string;
|
|
44
|
+
try {
|
|
45
|
+
content = fs.readFileSync(file, "utf8");
|
|
46
|
+
} catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
let entries = byLang.get(lang);
|
|
50
|
+
if (!entries) byLang.set(lang, (entries = new Map()));
|
|
51
|
+
for (const e of parseLoc(content).entries) {
|
|
52
|
+
entries.set(e.key, { key: e.key, value: e.value, file, line: e.line });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return byLang;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function computeLocCoverage(
|
|
60
|
+
data: ServerData,
|
|
61
|
+
modPath: string | null,
|
|
62
|
+
sourceLanguage: string,
|
|
63
|
+
schemaEntries: SchemaEntry[],
|
|
64
|
+
inFocus: (file: string) => boolean = () => true
|
|
65
|
+
): LocCoverage[] {
|
|
66
|
+
if (!modPath) return [];
|
|
67
|
+
const byLang = modLocByLanguage(modPath, schemaEntries);
|
|
68
|
+
if (byLang.size === 0) return [];
|
|
69
|
+
|
|
70
|
+
// Keys this mod's script uses (recorded loc references), plus schema-required
|
|
71
|
+
// keys — both restricted to the focus mod's own files in multi-mod workspaces.
|
|
72
|
+
const referenced = new Map<string, { file: string; line: number }>();
|
|
73
|
+
for (const ref of data.refIndex.allOfKind("loc_key")) {
|
|
74
|
+
if (!inFocus(ref.file)) continue;
|
|
75
|
+
if (!referenced.has(ref.name)) referenced.set(ref.name, { file: ref.file, line: ref.line });
|
|
76
|
+
}
|
|
77
|
+
const requiredByKind = new Map<string, string[]>();
|
|
78
|
+
for (const e of schemaEntries) {
|
|
79
|
+
if (e.requiredLoc && e.requiredLoc.length > 0) requiredByKind.set(e.kind, e.requiredLoc);
|
|
80
|
+
}
|
|
81
|
+
for (const def of data.index.allDefinitions()) {
|
|
82
|
+
if (def.source !== "mod" || !inFocus(def.file)) continue;
|
|
83
|
+
const patterns = requiredByKind.get(def.kind);
|
|
84
|
+
if (!patterns) continue;
|
|
85
|
+
for (const p of patterns) {
|
|
86
|
+
const key = p.replace(/\$/g, def.name);
|
|
87
|
+
if (!referenced.has(key)) referenced.set(key, { file: def.file, line: def.line });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Key defined outside this mod (vanilla, parents, other workspace mods, in
|
|
92
|
+
* the configured language index)? Then it's an override/covered. */
|
|
93
|
+
const inheritedLoc = (key: string): boolean =>
|
|
94
|
+
data.index.lookupAll(key).some((d) => d.kind === "loc_key" && (d.source !== "mod" || !inFocus(d.file)));
|
|
95
|
+
|
|
96
|
+
const source = byLang.get(sourceLanguage) ?? new Map<string, LocEntrySite>();
|
|
97
|
+
const result: LocCoverage[] = [];
|
|
98
|
+
|
|
99
|
+
for (const [language, entries] of [...byLang.entries()].sort((a, b) => a[0].localeCompare(b[0]))) {
|
|
100
|
+
const missing: LocIssue[] = [];
|
|
101
|
+
for (const [key, site] of referenced) {
|
|
102
|
+
if (entries.has(key)) continue;
|
|
103
|
+
if (inheritedLoc(key)) continue;
|
|
104
|
+
if (missing.length >= ISSUE_CAP) break;
|
|
105
|
+
missing.push({ key, file: site.file, line: site.line });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const orphaned: LocIssue[] = [];
|
|
109
|
+
for (const [key, site] of entries) {
|
|
110
|
+
if (referenced.has(key)) continue;
|
|
111
|
+
if (inheritedLoc(key)) continue; // overriding vanilla text is intentional
|
|
112
|
+
if (data.refIndex.lookup(key).length > 0) continue; // referenced under a non-loc kind
|
|
113
|
+
if (orphaned.length >= ISSUE_CAP) break;
|
|
114
|
+
orphaned.push({ key, file: site.file, line: site.line });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const untranslated: LocIssue[] = [];
|
|
118
|
+
if (language !== sourceLanguage) {
|
|
119
|
+
for (const [key, site] of entries) {
|
|
120
|
+
const src = source.get(key);
|
|
121
|
+
if (!src) continue;
|
|
122
|
+
// Untranslated = still the source text verbatim, OR a blank value
|
|
123
|
+
// (the translation scaffold blanks values so nothing fake ships).
|
|
124
|
+
const isCopy = src.value === site.value && site.value.trim() !== "";
|
|
125
|
+
const isBlank = site.value.trim() === "";
|
|
126
|
+
if (!isCopy && !isBlank) continue;
|
|
127
|
+
if (untranslated.length >= ISSUE_CAP) break;
|
|
128
|
+
untranslated.push({ key, file: site.file, line: site.line, value: src.value });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
missing.sort((a, b) => a.key.localeCompare(b.key));
|
|
133
|
+
orphaned.sort((a, b) => a.key.localeCompare(b.key));
|
|
134
|
+
untranslated.sort((a, b) => a.key.localeCompare(b.key));
|
|
135
|
+
result.push({ language, defined: entries.size, missing, orphaned, untranslated });
|
|
136
|
+
}
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/modOverview: the mod's content inventory by kind, for the Mod Overview
|
|
3
|
+
* tree. Definition lists are capped per kind; counts stay exact.
|
|
4
|
+
*/
|
|
5
|
+
import type { ModOverview, OverviewKind } from "@px-lsp/protocol/protocol";
|
|
6
|
+
import type { ServerData } from "../serverData";
|
|
7
|
+
|
|
8
|
+
const DEFS_CAP = 500;
|
|
9
|
+
|
|
10
|
+
export function computeModOverview(
|
|
11
|
+
data: ServerData,
|
|
12
|
+
inFocus: (file: string) => boolean = () => true
|
|
13
|
+
): ModOverview {
|
|
14
|
+
const byKind = new Map<string, OverviewKind>();
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (const def of data.index.allDefinitions()) {
|
|
17
|
+
if (def.source !== "mod" || !inFocus(def.file)) continue;
|
|
18
|
+
total++;
|
|
19
|
+
let bucket = byKind.get(def.kind);
|
|
20
|
+
if (!bucket) byKind.set(def.kind, (bucket = { kind: def.kind, count: 0, defs: [] }));
|
|
21
|
+
bucket.count++;
|
|
22
|
+
if (bucket.defs.length < DEFS_CAP) {
|
|
23
|
+
bucket.defs.push({ name: def.name, file: def.file, line: def.line });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const kinds = [...byKind.values()].sort((a, b) => b.count - a.count || a.kind.localeCompare(b.kind));
|
|
27
|
+
for (const k of kinds) k.defs.sort((a, b) => a.name.localeCompare(b.name));
|
|
28
|
+
return { kinds, totalDefs: total, totalRefs: data.refIndex.size };
|
|
29
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* paradox/overrides: mod definitions that shadow vanilla or parent-mod content,
|
|
3
|
+
* annotated with the folder's load rule — script folders are last-in-or-same
|
|
4
|
+
* ("LIOS", the mod wins), GUI is first-in-only ("FIOS", vanilla wins unless
|
|
5
|
+
* the mod replaces the whole file at the same relative path). This is
|
|
6
|
+
* database_conflicts.log brought into the editor, before launch.
|
|
7
|
+
*
|
|
8
|
+
* Multi-mod workspaces: definitions of OTHER workspace mods count as shadowed
|
|
9
|
+
* sites too (labeled with their mod's name); between two enabled mods the
|
|
10
|
+
* launcher's load order decides, which we cannot know — the note says so.
|
|
11
|
+
*/
|
|
12
|
+
import type { OverrideInfo } from "@px-lsp/protocol/protocol";
|
|
13
|
+
import type { ServerData } from "../serverData";
|
|
14
|
+
|
|
15
|
+
const CAP = 2000;
|
|
16
|
+
|
|
17
|
+
/** Kinds whose files load first-in-only. */
|
|
18
|
+
const FIOS_KINDS = new Set(["gui_type"]);
|
|
19
|
+
|
|
20
|
+
function relUnder(root: string, file: string): string | null {
|
|
21
|
+
const lower = file.toLowerCase().replace(/\\/g, "/");
|
|
22
|
+
const rootLower = root.toLowerCase().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
23
|
+
return lower.startsWith(rootLower + "/") ? lower.slice(rootLower.length + 1) : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function computeOverrides(
|
|
27
|
+
data: ServerData,
|
|
28
|
+
gamePath: string | null,
|
|
29
|
+
inFocus: (file: string) => boolean = () => true
|
|
30
|
+
): OverrideInfo[] {
|
|
31
|
+
const out: OverrideInfo[] = [];
|
|
32
|
+
const seen = new Set<string>();
|
|
33
|
+
|
|
34
|
+
for (const def of data.index.allDefinitions()) {
|
|
35
|
+
if (def.source !== "mod" || !inFocus(def.file)) continue;
|
|
36
|
+
const ownRoot = data.modRootOf(def.file);
|
|
37
|
+
const dedupe = `${ownRoot ?? ""}:${def.kind}:${def.name}`;
|
|
38
|
+
if (seen.has(dedupe)) continue;
|
|
39
|
+
const shadowed = data.index
|
|
40
|
+
.lookupAll(def.name)
|
|
41
|
+
.filter(
|
|
42
|
+
(d) => d.kind === def.kind && d !== def && (d.source !== "mod" || data.modRootOf(d.file) !== ownRoot)
|
|
43
|
+
);
|
|
44
|
+
if (shadowed.length === 0) continue;
|
|
45
|
+
seen.add(dedupe);
|
|
46
|
+
|
|
47
|
+
const rule = FIOS_KINDS.has(def.kind) ? "FIOS" : "LIOS";
|
|
48
|
+
let winner: "mod" | "other" = rule === "LIOS" ? "mod" : "other";
|
|
49
|
+
let note: string | undefined;
|
|
50
|
+
if (rule === "FIOS") {
|
|
51
|
+
// Whole-file replacement at the same relative path still wins.
|
|
52
|
+
const modRel = ownRoot ? relUnder(ownRoot, def.file) : null;
|
|
53
|
+
const replacesFile =
|
|
54
|
+
modRel !== null && gamePath !== null && shadowed.some((s) => relUnder(gamePath, s.file) === modRel);
|
|
55
|
+
if (replacesFile) {
|
|
56
|
+
winner = "mod";
|
|
57
|
+
note = "whole-file replacement (same relative path)";
|
|
58
|
+
} else {
|
|
59
|
+
note = "GUI loads first-in-only: the vanilla definition wins unless the mod replaces the whole file";
|
|
60
|
+
}
|
|
61
|
+
} else if (shadowed.some((s) => s.source === "mod")) {
|
|
62
|
+
note = "another enabled mod defines this too; the launcher load order decides which wins";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
out.push({
|
|
66
|
+
name: def.name,
|
|
67
|
+
kind: def.kind,
|
|
68
|
+
mod: { source: "mod", label: data.originLabel(def), file: def.file, line: def.line },
|
|
69
|
+
shadowed: shadowed.map((s) => ({
|
|
70
|
+
source:
|
|
71
|
+
s.source === "mod"
|
|
72
|
+
? ("mod" as const)
|
|
73
|
+
: s.source === "parent"
|
|
74
|
+
? ("parent" as const)
|
|
75
|
+
: ("vanilla" as const),
|
|
76
|
+
label: data.originLabel(s),
|
|
77
|
+
file: s.file,
|
|
78
|
+
line: s.line,
|
|
79
|
+
})),
|
|
80
|
+
rule,
|
|
81
|
+
winner,
|
|
82
|
+
note,
|
|
83
|
+
});
|
|
84
|
+
if (out.length >= CAP) break;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
out.sort((a, b) => a.kind.localeCompare(b.kind) || a.name.localeCompare(b.name));
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-document parse cache: one CST (or loc parse) per open document version,
|
|
3
|
+
* so completion, symbols, folding, diagnostics and semantic tokens share a
|
|
4
|
+
* single parse per edit instead of each re-scanning the text.
|
|
5
|
+
*
|
|
6
|
+
* Keyed by document version (equivalent to a content hash for open documents,
|
|
7
|
+
* and cheaper). Entries are evicted when the document closes.
|
|
8
|
+
*/
|
|
9
|
+
import type { TextDocument } from "vscode-languageserver-textdocument";
|
|
10
|
+
import { LineIndex, parseLoc, parseScript, type LocParseResult, type ParseResult } from "./parser";
|
|
11
|
+
import { collectSavedScopeTypes, type InferenceContext } from "./scopes/inference";
|
|
12
|
+
import type { Scope, ScopeModel } from "./scopes/model";
|
|
13
|
+
|
|
14
|
+
export interface CachedParse {
|
|
15
|
+
version: number;
|
|
16
|
+
result: ParseResult;
|
|
17
|
+
lineIndex: LineIndex;
|
|
18
|
+
/** Lazily computed save_scope_as → scope types for this document version. */
|
|
19
|
+
savedScopes?: Map<string, Set<Scope> | null>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CachedLocParse {
|
|
23
|
+
version: number;
|
|
24
|
+
result: LocParseResult;
|
|
25
|
+
lineIndex: LineIndex;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const scriptCache = new Map<string, CachedParse>();
|
|
29
|
+
const locCache = new Map<string, CachedLocParse>();
|
|
30
|
+
|
|
31
|
+
export function getParse(document: TextDocument): CachedParse {
|
|
32
|
+
const cached = scriptCache.get(document.uri);
|
|
33
|
+
if (cached && cached.version === document.version) return cached;
|
|
34
|
+
const text = document.getText();
|
|
35
|
+
const entry: CachedParse = {
|
|
36
|
+
version: document.version,
|
|
37
|
+
result: parseScript(text),
|
|
38
|
+
lineIndex: new LineIndex(text),
|
|
39
|
+
};
|
|
40
|
+
scriptCache.set(document.uri, entry);
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getLocParse(document: TextDocument): CachedLocParse {
|
|
45
|
+
const cached = locCache.get(document.uri);
|
|
46
|
+
if (cached && cached.version === document.version) return cached;
|
|
47
|
+
const text = document.getText();
|
|
48
|
+
const entry: CachedLocParse = {
|
|
49
|
+
version: document.version,
|
|
50
|
+
result: parseLoc(text),
|
|
51
|
+
lineIndex: new LineIndex(text),
|
|
52
|
+
};
|
|
53
|
+
locCache.set(document.uri, entry);
|
|
54
|
+
return entry;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function evictParse(uri: string): void {
|
|
58
|
+
scriptCache.delete(uri);
|
|
59
|
+
locCache.delete(uri);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Saved-scope types for a document. The save-site scan is cached per version;
|
|
63
|
+
* `ambient` (engine-provided scopes from the file's schema entry, §B3) seeds
|
|
64
|
+
* the collection so saves INSIDE `scope:ambient = { … }` blocks type correctly,
|
|
65
|
+
* and `ctx` carries the per-definition root/structure/variable context.
|
|
66
|
+
* The FIRST caller per document version bakes `ambient`/`ctx` into the cache,
|
|
67
|
+
* so every caller must pass the file's `entry?.ambientScopes` and
|
|
68
|
+
* `inferenceContextFor(data, entry)` (scopes/varTypes.ts) — never a subset. */
|
|
69
|
+
export function getSavedScopes(
|
|
70
|
+
document: TextDocument,
|
|
71
|
+
model: ScopeModel,
|
|
72
|
+
rootScopes: Set<Scope> | null,
|
|
73
|
+
ambient?: ReadonlyArray<{ name: string; type: string }>,
|
|
74
|
+
ctx?: InferenceContext
|
|
75
|
+
): Map<string, Set<Scope> | null> {
|
|
76
|
+
const entry = getParse(document);
|
|
77
|
+
if (!entry.savedScopes) {
|
|
78
|
+
entry.savedScopes = collectSavedScopeTypes(entry.result, model, rootScopes, ambient, ctx);
|
|
79
|
+
}
|
|
80
|
+
return entry.savedScopes;
|
|
81
|
+
}
|