@px-lsp/server 0.3.0 → 0.3.1
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/README.md +1 -1
- package/data/ck3/skeletons.json +1 -0
- package/data/eu5/skeletons.json +1 -0
- package/data/vic3/skeletons.json +1 -0
- package/dist/browser-data/ck3/docs.json +1 -1
- package/dist/browser-data/ck3/tokens.json +1 -1
- package/dist/browser-data/vic3/tokens.json +1 -1
- package/dist/browser.js +50 -37
- package/dist/server.js +2000 -352
- package/dist/types/packages/server/src/features/blockSnippets.d.ts +20 -4
- package/dist/types/packages/server/src/features/completion.d.ts +11 -0
- package/dist/types/packages/server/src/features/definitionSkeletons.d.ts +24 -0
- package/dist/types/packages/server/src/games/ck3/schema.d.ts +1 -1
- package/dist/types/packages/server/src/games/eu5/index.d.ts +1 -1
- package/dist/types/packages/server/src/games/profile.d.ts +85 -1
- package/dist/types/packages/server/src/schema/skeletons.d.ts +102 -0
- package/dist/types/packages/server/src/schema/types.d.ts +30 -0
- package/package.json +3 -3
- package/src/coa/coa.ts +31 -0
- package/src/coa/coaDesigner.ts +131 -0
- package/src/coa/coaParse.ts +3 -0
- package/src/creators/definitionEdit.ts +198 -0
- package/src/creators/definitionForm.ts +479 -0
- package/src/creators/modifierFormats.ts +0 -0
- package/src/data/docsParser.ts +21 -4
- package/src/features/blockSnippets.ts +151 -20
- package/src/features/calendarDates.ts +8 -5
- package/src/features/completion.ts +38 -4
- package/src/features/definitionSkeletons.ts +109 -0
- package/src/features/inlayHints.ts +5 -2
- package/src/features/locText.ts +223 -0
- package/src/features/snippetList.ts +83 -0
- package/src/games/ck3/index.ts +16 -0
- package/src/games/ck3/meta.ts +81 -1
- package/src/games/ck3/schema.ts +83 -7
- package/src/games/ck3/structures.ts +37 -0
- package/src/games/eu5/index.ts +7 -1
- package/src/games/eu5/meta.ts +5 -1
- package/src/games/profile.ts +77 -1
- package/src/games/vic3/index.ts +4 -0
- package/src/games/vic3/meta.ts +5 -1
- package/src/gui/sourceModel.ts +51 -8
- package/src/gui/textResolve.ts +3 -3
- package/src/overview/dynastyTree.ts +493 -0
- package/src/overview/eventGraph.ts +21 -1
- package/src/overview/eventVocabulary.ts +33 -33
- package/src/overview/exampleWiki.ts +4 -1
- package/src/schema/loader.ts +2 -1
- package/src/schema/skeletons.ts +182 -0
- package/src/schema/types.ts +30 -0
- package/src/server.ts +147 -6
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Definition skeletons: the canonical shape of one definition of a kind,
|
|
3
|
+
* derived from measured vanilla usage by scripts/build-skeletons.ts and shipped
|
|
4
|
+
* as packages/server/data/<game>/skeletons.json.
|
|
5
|
+
*
|
|
6
|
+
* Nothing here is written by hand. A key is in a skeleton because at least
|
|
7
|
+
* SKELETON_MAJORITY of the game's own definitions of that kind carry it; its
|
|
8
|
+
* order is the median position it holds in them; its pre-filled value is the
|
|
9
|
+
* value the corpus uses most, and only where the key's whole measured
|
|
10
|
+
* vocabulary is small enough to be a real choice. A key whose vocabulary is
|
|
11
|
+
* wider but entirely NUMERIC still gets the number the corpus writes most,
|
|
12
|
+
* since a key name is valid script nowhere in a numeric slot. Anything wider
|
|
13
|
+
* than that (loc keys, names, paths) gets its own name as placeholder text,
|
|
14
|
+
* which says "fill this in" without asserting a value.
|
|
15
|
+
*
|
|
16
|
+
* Both insert forms are rendered at once, exactly like features/blockSnippets.ts:
|
|
17
|
+
* `snippet` for clients that declared snippetSupport, `plain` (free of `${`) for
|
|
18
|
+
* the rest.
|
|
19
|
+
*
|
|
20
|
+
* No `vscode` imports: plain data plus pure renderers.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Share of a kind's definitions a key must appear in to enter the skeleton.
|
|
25
|
+
* Half: a skeleton is what a definition of this kind USUALLY looks like, not
|
|
26
|
+
* the union of everything the folder has ever used. Applied identically to the
|
|
27
|
+
* keys of a nested block (share of that block's own occurrences).
|
|
28
|
+
*/
|
|
29
|
+
export const SKELETON_MAJORITY = 0.5;
|
|
30
|
+
|
|
31
|
+
/** One key of a skeleton body. */
|
|
32
|
+
export interface SkeletonKey {
|
|
33
|
+
key: string;
|
|
34
|
+
/**
|
|
35
|
+
* The key's whole measured value vocabulary, most-used first. Set only where
|
|
36
|
+
* the corpus uses few enough distinct values for the list to be a choice
|
|
37
|
+
* rather than a guess; absent = the value is a placeholder.
|
|
38
|
+
*/
|
|
39
|
+
choices?: string[];
|
|
40
|
+
/**
|
|
41
|
+
* The single most-used measured value, set only where the vocabulary is too
|
|
42
|
+
* wide to offer as a choice AND every value the corpus writes for the key is
|
|
43
|
+
* a number. A key name is valid script nowhere in a numeric slot, so a
|
|
44
|
+
* measured number is the honest placeholder; it stays a tabstop, so it still
|
|
45
|
+
* reads as "fill this in".
|
|
46
|
+
*/
|
|
47
|
+
placeholder?: string;
|
|
48
|
+
/**
|
|
49
|
+
* Nested one level: the block this key opens, carrying the keys measured in
|
|
50
|
+
* at least SKELETON_MAJORITY of its occurrences. An empty array = the block
|
|
51
|
+
* has no stable shape, so it is inserted empty.
|
|
52
|
+
*/
|
|
53
|
+
block?: SkeletonKey[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A named child block of a definition, offered on its own. */
|
|
57
|
+
export interface SkeletonBlock {
|
|
58
|
+
/** Occurrences of the block measured across the kind's definitions. */
|
|
59
|
+
sampled: number;
|
|
60
|
+
keys: SkeletonKey[];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface KindSkeleton {
|
|
64
|
+
/** Vanilla definitions of this kind the skeleton was measured over. */
|
|
65
|
+
sampled: number;
|
|
66
|
+
keys: SkeletonKey[];
|
|
67
|
+
/**
|
|
68
|
+
* The file-level key the kind's definition names were measured to derive
|
|
69
|
+
* from: at least SKELETON_MAJORITY of the folder's files declare it as a
|
|
70
|
+
* top-level scalar, and at least that share of the kind's names are
|
|
71
|
+
* `<its value>.<number>` (`namespace = intrigue` → `intrigue.0001`). Absent =
|
|
72
|
+
* names are plain identifiers and files need no header line.
|
|
73
|
+
*/
|
|
74
|
+
nameFromHeader?: string;
|
|
75
|
+
/** Child-block skeletons, keyed by the block's key. */
|
|
76
|
+
blocks?: Record<string, SkeletonBlock>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SkeletonData {
|
|
80
|
+
meta: { generated: string; sources: string[]; majority: number };
|
|
81
|
+
/** Definition kind, as the schema table spells it. */
|
|
82
|
+
kinds: Record<string, KindSkeleton>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Both insert forms of one skeleton. */
|
|
86
|
+
export interface RenderedSkeleton {
|
|
87
|
+
/** `${n:…}` tabstop form; only for clients declaring snippetSupport. */
|
|
88
|
+
snippet: string;
|
|
89
|
+
/** Plain-text skeleton, free of `${`. */
|
|
90
|
+
plain: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
interface Counter {
|
|
94
|
+
n: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function escapeSnippet(text: string): string {
|
|
98
|
+
return text.replace(/[\\$}]/g, "\\$&");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The right-hand side of a leaf key, in one of the two forms. */
|
|
102
|
+
function leafValue(spec: SkeletonKey, snippet: boolean, c: Counter): string {
|
|
103
|
+
const filled = spec.choices?.[0] ?? spec.placeholder ?? spec.key;
|
|
104
|
+
if (!snippet) return filled;
|
|
105
|
+
if (spec.choices && spec.choices.length > 1) return `\${${++c.n}|${spec.choices.join(",")}|}`;
|
|
106
|
+
return `\${${++c.n}:${escapeSnippet(filled)}}`;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function renderKeys(keys: SkeletonKey[], indent: string, snippet: boolean, c: Counter): string {
|
|
110
|
+
let out = "";
|
|
111
|
+
for (const spec of keys) {
|
|
112
|
+
if (spec.block) {
|
|
113
|
+
out += `${indent}${spec.key} = {\n`;
|
|
114
|
+
out +=
|
|
115
|
+
spec.block.length > 0
|
|
116
|
+
? renderKeys(spec.block, indent + "\t", snippet, c)
|
|
117
|
+
: `${indent}\t${snippet ? `$${++c.n}` : ""}\n`;
|
|
118
|
+
out += `${indent}}\n`;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
out += `${indent}${spec.key} = ${leafValue(spec, snippet, c)}\n`;
|
|
122
|
+
}
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SkeletonRenderOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Value of the header key the target document already declares
|
|
129
|
+
* (`namespace = my_events`), so a generated name matches its own file.
|
|
130
|
+
*/
|
|
131
|
+
headerValue?: string;
|
|
132
|
+
/** Write the header line above the definition (the document declares none). */
|
|
133
|
+
withHeader?: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The whole definition, header line included when the document lacks it. */
|
|
137
|
+
export function renderDefinitionSkeleton(
|
|
138
|
+
kind: string,
|
|
139
|
+
skel: KindSkeleton,
|
|
140
|
+
opts: SkeletonRenderOptions = {}
|
|
141
|
+
): RenderedSkeleton {
|
|
142
|
+
const one = (snippet: boolean): string => {
|
|
143
|
+
const c: Counter = { n: 0 };
|
|
144
|
+
// A name built from a header key the insert writes itself mirrors tabstop 1
|
|
145
|
+
// into both places, so editing the namespace renames the definition too.
|
|
146
|
+
const mirrored = skel.nameFromHeader !== undefined && opts.headerValue === undefined;
|
|
147
|
+
if (mirrored) c.n = 1;
|
|
148
|
+
const prefix = mirrored
|
|
149
|
+
? snippet
|
|
150
|
+
? `\${1:my_${escapeSnippet(skel.nameFromHeader!)}}`
|
|
151
|
+
: `my_${skel.nameFromHeader}`
|
|
152
|
+
: opts.headerValue;
|
|
153
|
+
|
|
154
|
+
// The header line is written only when the document has none: a second
|
|
155
|
+
// `namespace =` in one file is exactly the silent failure to avoid.
|
|
156
|
+
const out = opts.withHeader && skel.nameFromHeader ? `${skel.nameFromHeader} = ${prefix}\n\n` : "";
|
|
157
|
+
|
|
158
|
+
const name = skel.nameFromHeader
|
|
159
|
+
? snippet
|
|
160
|
+
? `${prefix}.\${${++c.n}:1}`
|
|
161
|
+
: `${prefix}.1`
|
|
162
|
+
: snippet
|
|
163
|
+
? `\${${++c.n}:my_${escapeSnippet(kind)}}`
|
|
164
|
+
: `my_${kind}`;
|
|
165
|
+
// A kind with no measured body is still worth inserting: the wrapper, the
|
|
166
|
+
// name shape and the header are the parts the game fails silently on.
|
|
167
|
+
const body =
|
|
168
|
+
skel.keys.length > 0 ? renderKeys(skel.keys, "\t", snippet, c) : `\t${snippet ? `$${++c.n}` : ""}\n`;
|
|
169
|
+
return `${out}${name} = {\n${body}}`;
|
|
170
|
+
};
|
|
171
|
+
return { snippet: one(true), plain: one(false) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** One named child block (`option = { name = … }`), inserted on its own. */
|
|
175
|
+
export function renderBlockSkeleton(name: string, block: SkeletonBlock): RenderedSkeleton {
|
|
176
|
+
const one = (snippet: boolean): string => {
|
|
177
|
+
const body =
|
|
178
|
+
block.keys.length > 0 ? renderKeys(block.keys, "\t", snippet, { n: 0 }) : `\t${snippet ? "$1" : ""}\n`;
|
|
179
|
+
return `${name} = {\n${body}}`;
|
|
180
|
+
};
|
|
181
|
+
return { snippet: one(true), plain: one(false) };
|
|
182
|
+
}
|
package/src/schema/types.ts
CHANGED
|
@@ -48,6 +48,13 @@ export interface KeySpec {
|
|
|
48
48
|
* vocabulary, the completion regression rank-eval caught in 2026-07.
|
|
49
49
|
*/
|
|
50
50
|
curated?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* Definition kinds this key's value names, where a global `RefField` would
|
|
53
|
+
* be wrong because the key means different things in different folders
|
|
54
|
+
* (`opposites` is a trait list here and a scripted_relation list there).
|
|
55
|
+
* Per-kind and therefore unambiguous; read by the creators' form request.
|
|
56
|
+
*/
|
|
57
|
+
refKinds?: string[];
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
/** The document shape of a definition kind: top-level keys plus named sub-blocks. */
|
|
@@ -102,6 +109,29 @@ export interface SchemaEntry {
|
|
|
102
109
|
* of this kind actually define.
|
|
103
110
|
*/
|
|
104
111
|
requiredLoc?: string[];
|
|
112
|
+
/**
|
|
113
|
+
* Every loc key the game generates for a definition of this kind, `$` = the
|
|
114
|
+
* name, as the folder's `_*.info` doc states them. A SUPERSET of
|
|
115
|
+
* `requiredLoc`, which may only carry the patterns ≥95% of vanilla actually
|
|
116
|
+
* defines because a diagnostic hangs off it; a creator writes the whole set
|
|
117
|
+
* and asks the modder for each. Absent = `requiredLoc` is the whole set.
|
|
118
|
+
*/
|
|
119
|
+
locPatterns?: string[];
|
|
120
|
+
/**
|
|
121
|
+
* Where the game looks for a definition's default icon, relative to the
|
|
122
|
+
* game/mod root, forward slashes, no trailing slash. Only set where the
|
|
123
|
+
* folder's `_*.info` doc states the path; absent = this kind has no
|
|
124
|
+
* name-derived icon and a creator offers no icon picker for it.
|
|
125
|
+
*/
|
|
126
|
+
iconFolder?: string;
|
|
127
|
+
/**
|
|
128
|
+
* Key inside a definition's body whose value sorts this kind's definitions
|
|
129
|
+
* into families, when one folder holds several (`type = ethos` in
|
|
130
|
+
* common/culture/pillars). A creator that draws one picker per family reads
|
|
131
|
+
* it through paradox/definitionForm; nothing else does. Absent = the kind is
|
|
132
|
+
* one flat list.
|
|
133
|
+
*/
|
|
134
|
+
groupKey?: string;
|
|
105
135
|
/** Scope(s) a definition of this type pushes as root in its script blocks (Phase 3). */
|
|
106
136
|
rootScopes?: string[];
|
|
107
137
|
/** Include definitions of this kind in completion lists (default true).
|
package/src/server.ts
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
configChangedNotification,
|
|
33
33
|
indexChangedNotification,
|
|
34
34
|
indexStatsRequest,
|
|
35
|
+
locTextRequest,
|
|
35
36
|
lookupLocRequest,
|
|
36
37
|
modFileChangedNotification,
|
|
37
38
|
progressNotification,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
type ParadoxSettings,
|
|
42
43
|
type StatusPayload,
|
|
43
44
|
type LocEntryInfo,
|
|
45
|
+
type LocTextParams,
|
|
44
46
|
type LookupLocParams,
|
|
45
47
|
type ModFileChangeParams,
|
|
46
48
|
type ModScopedParams,
|
|
@@ -53,13 +55,21 @@ import {
|
|
|
53
55
|
exampleWikiVariableKinds,
|
|
54
56
|
type ExampleWikiEntryParams,
|
|
55
57
|
type ExampleWikiKind,
|
|
58
|
+
dynastyTreeRequest,
|
|
56
59
|
eventGraphRequest,
|
|
57
60
|
eventValueOptionsRequest,
|
|
58
61
|
eventVocabularyRequest,
|
|
62
|
+
definitionFormRequest,
|
|
63
|
+
definitionEditRequest,
|
|
64
|
+
modifierFormatsRequest,
|
|
65
|
+
type ModifierFormatsParams,
|
|
66
|
+
type DefinitionFormParams,
|
|
67
|
+
type DefinitionEditParams,
|
|
59
68
|
guiTreeRequest,
|
|
60
69
|
locCoverageRequest,
|
|
61
70
|
modOverviewRequest,
|
|
62
71
|
overridesRequest,
|
|
72
|
+
type DynastyTreeParams,
|
|
63
73
|
type EventBannerParams,
|
|
64
74
|
type EventDetailParams,
|
|
65
75
|
type EventGraphParams,
|
|
@@ -89,6 +99,9 @@ import {
|
|
|
89
99
|
type DependenciesParams,
|
|
90
100
|
scopeAtRequest,
|
|
91
101
|
type ScopeAtParams,
|
|
102
|
+
snippetsRequest,
|
|
103
|
+
type SnippetsParams,
|
|
104
|
+
type SnippetsResult,
|
|
92
105
|
type ScopeAtResult,
|
|
93
106
|
} from "@px-lsp/protocol/protocol";
|
|
94
107
|
import { buildGuiTree } from "./features/guiTree";
|
|
@@ -149,6 +162,7 @@ import { ModOriginResolver } from "./index/modOrigin";
|
|
|
149
162
|
import { loadSchema, type SchemaData } from "./schema/loader";
|
|
150
163
|
import { VARIABLE_KINDS } from "./games/jomini/variables";
|
|
151
164
|
import { activeProfile, setActiveProfile } from "./games/active";
|
|
165
|
+
import { resolveConfigDir } from "@px-lsp/protocol/configDir";
|
|
152
166
|
import { resolveProfile } from "./games/registry";
|
|
153
167
|
import type { SchemaEntry } from "./schema/types";
|
|
154
168
|
import { URI } from "vscode-uri";
|
|
@@ -157,7 +171,8 @@ import { CompletionFeature } from "./features/completion";
|
|
|
157
171
|
import { provideHover } from "./features/hover";
|
|
158
172
|
import { setHoverDetail } from "./features/hoverRender";
|
|
159
173
|
import { provideDateHover } from "./features/calendarDates";
|
|
160
|
-
import { sanitizeCalendar } from "@px-lsp/protocol/calendar";
|
|
174
|
+
import { sanitizeCalendar, type CalendarSetting } from "@px-lsp/protocol/calendar";
|
|
175
|
+
import { isCalendarFile, readCalendarFile } from "@px-lsp/protocol/calendarFile";
|
|
161
176
|
import { provideTextureHover } from "./features/textureHover";
|
|
162
177
|
import { provideDefinition, provideLocDefinition } from "./features/definition";
|
|
163
178
|
import { SEMANTIC_LEGEND, provideSemanticTokens } from "./features/semanticTokens";
|
|
@@ -180,14 +195,20 @@ import { provideReferences } from "./features/references";
|
|
|
180
195
|
import { prepareRename, provideRename } from "./features/rename";
|
|
181
196
|
import { provideWorkspaceSymbols } from "./features/workspaceSymbols";
|
|
182
197
|
import { evictParse, getLocParse, getParse } from "./parseCache";
|
|
198
|
+
import { buildSnippetList } from "./features/snippetList";
|
|
183
199
|
import { resolveClientCapabilities, setClientCapabilities } from "./clientMode";
|
|
184
200
|
import { isIgnoredByConfig, isSuppressedInline, scanInlineSuppressions } from "@px-lsp/protocol/suppression";
|
|
185
201
|
import { computeModOverview } from "./overview/modOverview";
|
|
186
202
|
import { computeLocCoverage } from "./overview/locCoverage";
|
|
187
203
|
import { computeOverrides } from "./overview/overrides";
|
|
188
204
|
import { computeEventGraph } from "./overview/eventGraph";
|
|
205
|
+
import { computeDynastyTree } from "./overview/dynastyTree";
|
|
189
206
|
import { computeEventVocabulary, computeValueOptions } from "./overview/eventVocabulary";
|
|
190
207
|
import { computeEventBanner } from "./overview/eventBanner";
|
|
208
|
+
import { computeDefinitionForm } from "./creators/definitionForm";
|
|
209
|
+
import { computeDefinitionEdits } from "./creators/definitionEdit";
|
|
210
|
+
import { computeModifierFormats } from "./creators/modifierFormats";
|
|
211
|
+
import { computeLocText, type LocTextDeps } from "./features/locText";
|
|
191
212
|
import { computeDependencies } from "./overview/dependencies";
|
|
192
213
|
import { wordRangeAt } from "./wordAt";
|
|
193
214
|
|
|
@@ -579,6 +600,30 @@ function workspaceRootOf(fsPath: string): string | null {
|
|
|
579
600
|
return workspaceModRoots().find((r) => lower.startsWith(r.toLowerCase())) ?? null;
|
|
580
601
|
}
|
|
581
602
|
|
|
603
|
+
/**
|
|
604
|
+
* The display calendar a file's dates follow: the owning mod's
|
|
605
|
+
* `<mod>/.px-toolkit/calendar.json` (read once per root, dropped when the file
|
|
606
|
+
* changes), else the window-scoped `calendar` setting. `source` names which one
|
|
607
|
+
* for the hover, so a wrong date can be traced to its declaration.
|
|
608
|
+
*/
|
|
609
|
+
const calendarByRoot = new Map<string, { calendar: CalendarSetting; source: string } | null>();
|
|
610
|
+
function calendarFor(fsPath: string): { calendar: CalendarSetting | undefined; source: string } {
|
|
611
|
+
const root = workspaceRootOf(fsPath);
|
|
612
|
+
if (root) {
|
|
613
|
+
const key = root.toLowerCase();
|
|
614
|
+
let cached = calendarByRoot.get(key);
|
|
615
|
+
if (cached === undefined) {
|
|
616
|
+
const read = readCalendarFile(root, activeProfile());
|
|
617
|
+
cached = read?.calendar
|
|
618
|
+
? { calendar: read.calendar, source: path.relative(root, read.file).replace(/\\/g, "/") }
|
|
619
|
+
: null;
|
|
620
|
+
calendarByRoot.set(key, cached);
|
|
621
|
+
}
|
|
622
|
+
if (cached) return cached;
|
|
623
|
+
}
|
|
624
|
+
return { calendar: settings.calendar, source: "px.calendar" };
|
|
625
|
+
}
|
|
626
|
+
|
|
582
627
|
/** Schema entry for the folder a file lives in (structure/ambient/root-scope seed). */
|
|
583
628
|
function schemaEntryForFile(fsPath: string): SchemaEntry | null {
|
|
584
629
|
const lower = fsPath.toLowerCase();
|
|
@@ -1077,7 +1122,7 @@ function rebuildModNamespaces(): void {
|
|
|
1077
1122
|
|
|
1078
1123
|
/** Ordered parent-mod roots from <mod>/<configDir>/playset.json, if present. */
|
|
1079
1124
|
function readPlayset(modPath: string): string[] {
|
|
1080
|
-
const file = path.join(modPath, activeProfile()
|
|
1125
|
+
const file = path.join(resolveConfigDir(modPath, activeProfile()), "playset.json");
|
|
1081
1126
|
try {
|
|
1082
1127
|
if (!fs.existsSync(file)) return [];
|
|
1083
1128
|
const parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
@@ -1104,6 +1149,7 @@ async function buildIndex(): Promise<void> {
|
|
|
1104
1149
|
const tBuild = Date.now();
|
|
1105
1150
|
const generation = ++scanGeneration;
|
|
1106
1151
|
clearPathCaches();
|
|
1152
|
+
calendarByRoot.clear();
|
|
1107
1153
|
schema = loadSchema([...(settings.modPath ? [settings.modPath] : []), ...workspaceModRoots()], log);
|
|
1108
1154
|
data.completableKinds = new Set([
|
|
1109
1155
|
...schema.entries.filter((e) => e.completable !== false).map((e) => e.kind),
|
|
@@ -1431,7 +1477,11 @@ connection.onInitialized(() => {
|
|
|
1431
1477
|
// ourselves when the client supports dynamic registration.
|
|
1432
1478
|
if (!clientOwnFileWatcher && clientWatchedFilesDynamic) {
|
|
1433
1479
|
void connection.client.register(DidChangeWatchedFilesNotification.type, {
|
|
1434
|
-
watchers: [
|
|
1480
|
+
watchers: [
|
|
1481
|
+
{ globPattern: "**/*.{txt,yml,gui,mod}" },
|
|
1482
|
+
{ globPattern: "**/metadata.json" },
|
|
1483
|
+
{ globPattern: "**/calendar.json" },
|
|
1484
|
+
],
|
|
1435
1485
|
});
|
|
1436
1486
|
}
|
|
1437
1487
|
// Bundled frequency tables for completion ranking (§C3); fail-soft to empty.
|
|
@@ -1546,6 +1596,7 @@ function applyModFileChange(fsPath: string): void {
|
|
|
1546
1596
|
const lower = fsPath.toLowerCase();
|
|
1547
1597
|
if (lower.endsWith(".mod") || lower.endsWith("metadata.json")) refreshModOrigin();
|
|
1548
1598
|
if (lower.endsWith(".gui")) invalidateGuiDefsCache();
|
|
1599
|
+
if (isCalendarFile(fsPath, activeProfile())) calendarByRoot.clear();
|
|
1549
1600
|
// Full re-harvest when a mod defines file or a gui file WITH textformatting
|
|
1550
1601
|
// changed. Not on every .gui: the harvest reads only those out of gui/, and
|
|
1551
1602
|
// autosave fires this after every GUI editor gesture.
|
|
@@ -1586,6 +1637,13 @@ connection.onRequest(eventGraphRequest, (params: EventGraphParams) =>
|
|
|
1586
1637
|
computeEventGraph(data, params ?? {}, focusFilter(params?.modRoot))
|
|
1587
1638
|
);
|
|
1588
1639
|
|
|
1640
|
+
// The dynasty picker list, or one dynasty's houses and members. The whole
|
|
1641
|
+
// character corpus is read once per index revision (see overview/dynastyTree.ts);
|
|
1642
|
+
// a profile without a `dynasty` schema kind answers supported: false.
|
|
1643
|
+
connection.onRequest(dynastyTreeRequest, (params: DynastyTreeParams | null) =>
|
|
1644
|
+
computeDynastyTree(data, params ?? {}, focusFilter(params?.modRoot))
|
|
1645
|
+
);
|
|
1646
|
+
|
|
1589
1647
|
// What an event editor may offer: the profile's structure table, the schema's
|
|
1590
1648
|
// reference fields resolved through the index, and the script_docs tokens.
|
|
1591
1649
|
// Never a hand-written name list.
|
|
@@ -1605,6 +1663,26 @@ connection.onRequest(eventBannerRequest, (params: EventBannerParams) =>
|
|
|
1605
1663
|
computeEventBanner(data, params?.theme ?? "")
|
|
1606
1664
|
);
|
|
1607
1665
|
|
|
1666
|
+
// What a visual creator may offer for one definition kind: the schema entry,
|
|
1667
|
+
// the harvested structure keys, the index (options and the mod's own
|
|
1668
|
+
// definitions) and the modifier tokens. Never a hand-written field list.
|
|
1669
|
+
connection.onRequest(definitionFormRequest, (params: DefinitionFormParams | null) =>
|
|
1670
|
+
computeDefinitionForm(data, schema, params ?? { kind: "" }, focusFilter(params?.modRoot))
|
|
1671
|
+
);
|
|
1672
|
+
|
|
1673
|
+
// The script sibling of guiSourceEdit: the creators' writes as offsets into the
|
|
1674
|
+
// text the host handed over, applied there as one WorkspaceEdit.
|
|
1675
|
+
connection.onRequest(definitionEditRequest, (params: DefinitionEditParams | null) =>
|
|
1676
|
+
computeDefinitionEdits(params)
|
|
1677
|
+
);
|
|
1678
|
+
|
|
1679
|
+
// How the game itself prints a modifier: the profile's format folder, the loc
|
|
1680
|
+
// index and the game's own texticon blocks. A profile that names no formats
|
|
1681
|
+
// source answers null instead of a made-up label.
|
|
1682
|
+
connection.onRequest(modifierFormatsRequest, (params: ModifierFormatsParams | null) =>
|
|
1683
|
+
computeModifierFormats(data, activeProfile().modifierFormats, settings.gamePath, params?.lines)
|
|
1684
|
+
);
|
|
1685
|
+
|
|
1608
1686
|
connection.onRequest(guiTreeRequest, (params: GuiTreeParams) => buildGuiTree(params.text ?? ""));
|
|
1609
1687
|
|
|
1610
1688
|
/** Loc keys resolve through the index (configured language, english files as fallback). */
|
|
@@ -1768,6 +1846,26 @@ connection.onRequest(scopeAtRequest, (params: ScopeAtParams): ScopeAtResult | nu
|
|
|
1768
1846
|
);
|
|
1769
1847
|
});
|
|
1770
1848
|
|
|
1849
|
+
connection.onRequest(snippetsRequest, (params: SnippetsParams): SnippetsResult => {
|
|
1850
|
+
// Open script documents only; anything else is an empty list, not an error:
|
|
1851
|
+
// a host that offers an "Insert Snippet" command asks from wherever the user
|
|
1852
|
+
// happens to be.
|
|
1853
|
+
const doc = params?.uri ? documents.get(params.uri) : undefined;
|
|
1854
|
+
if (!doc || !isScriptLanguage(doc.languageId) || !params.position) return { snippets: [] };
|
|
1855
|
+
const entry = schemaEntryForFile(URI.parse(doc.uri).fsPath);
|
|
1856
|
+
const { result } = getParse(doc);
|
|
1857
|
+
return {
|
|
1858
|
+
snippets: buildSnippetList(
|
|
1859
|
+
result,
|
|
1860
|
+
doc.offsetAt(params.position),
|
|
1861
|
+
entry?.kind ?? null,
|
|
1862
|
+
activeProfile().skeletons,
|
|
1863
|
+
data.tokens,
|
|
1864
|
+
freqs.tokens
|
|
1865
|
+
),
|
|
1866
|
+
};
|
|
1867
|
+
});
|
|
1868
|
+
|
|
1771
1869
|
connection.onRequest(lookupLocRequest, (params: LookupLocParams): LocEntryInfo[] => {
|
|
1772
1870
|
return data.index
|
|
1773
1871
|
.lookup(params.key)
|
|
@@ -1775,6 +1873,39 @@ connection.onRequest(lookupLocRequest, (params: LookupLocParams): LocEntryInfo[]
|
|
|
1775
1873
|
.map((d) => ({ file: d.file, line: d.line, source: d.source, value: d.value }));
|
|
1776
1874
|
});
|
|
1777
1875
|
|
|
1876
|
+
/**
|
|
1877
|
+
* What paradox/locText resolves a value with: the loc index for the words, the
|
|
1878
|
+
* definition index for the kind a `Get<Something>('name')` chain names, and the
|
|
1879
|
+
* active profile's schema for the loc key that kind's names take. A kind the
|
|
1880
|
+
* schema states nothing for falls back to the bare name inside locText.ts, so
|
|
1881
|
+
* every game answers from its own table with no per-game branch here.
|
|
1882
|
+
*/
|
|
1883
|
+
function locTextDeps(): LocTextDeps {
|
|
1884
|
+
return {
|
|
1885
|
+
loc: locValue,
|
|
1886
|
+
kindsOf: (name) => [
|
|
1887
|
+
...new Set(
|
|
1888
|
+
data.index
|
|
1889
|
+
.lookup(name)
|
|
1890
|
+
.filter((d) => d.kind !== "loc_key")
|
|
1891
|
+
.map((d) => d.kind)
|
|
1892
|
+
),
|
|
1893
|
+
],
|
|
1894
|
+
patternsOf: (kind) => {
|
|
1895
|
+
const entry = schema.entries.find((e) => e.kind === kind);
|
|
1896
|
+
// The conservative `requiredLoc` first (the pattern nearly every
|
|
1897
|
+
// definition of the kind defines), then the full `locPatterns` set.
|
|
1898
|
+
return [entry?.requiredLoc?.[0], entry?.locPatterns?.[0]].filter((p): p is string => !!p);
|
|
1899
|
+
},
|
|
1900
|
+
};
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
// A loc value as the PLAYER reads it: markup stripped and the game's own
|
|
1904
|
+
// datafunctions resolved through the loc, definition and schema tables.
|
|
1905
|
+
connection.onRequest(locTextRequest, (params: LocTextParams | null) =>
|
|
1906
|
+
computeLocText(params?.keys ?? [], locTextDeps())
|
|
1907
|
+
);
|
|
1908
|
+
|
|
1778
1909
|
// ---- language features ----------------------------------------------------------
|
|
1779
1910
|
|
|
1780
1911
|
connection.onCompletion((params) =>
|
|
@@ -1845,7 +1976,8 @@ connection.onHover((params) =>
|
|
|
1845
1976
|
if (!isScriptLanguage(doc.languageId)) return null;
|
|
1846
1977
|
const fsPath = URI.parse(doc.uri).fsPath;
|
|
1847
1978
|
const entry = schemaEntryForFile(fsPath);
|
|
1848
|
-
const
|
|
1979
|
+
const { calendar, source } = calendarFor(fsPath);
|
|
1980
|
+
const dateHover = provideDateHover(calendar, doc, params.position, source);
|
|
1849
1981
|
if (dateHover) return dateHover;
|
|
1850
1982
|
const texture = provideTextureHover(settings, doc, params.position, entry?.kind);
|
|
1851
1983
|
if (texture) return texture;
|
|
@@ -1932,11 +2064,20 @@ connection.languages.inlayHint.on((params) =>
|
|
|
1932
2064
|
indexRead(`inlayHint ${perfName(params.textDocument.uri)}`, () => {
|
|
1933
2065
|
const doc = documents.get(params.textDocument.uri);
|
|
1934
2066
|
if (!doc) return [];
|
|
1935
|
-
const
|
|
2067
|
+
const fsPath = URI.parse(doc.uri).fsPath;
|
|
2068
|
+
const entry = schemaEntryForFile(fsPath);
|
|
1936
2069
|
const rootScopes = entry?.rootScopes?.length
|
|
1937
2070
|
? new Set(entry.rootScopes.map((s) => s.toLowerCase()))
|
|
1938
2071
|
: null;
|
|
1939
|
-
return provideInlayHints(
|
|
2072
|
+
return provideInlayHints(
|
|
2073
|
+
data,
|
|
2074
|
+
settings,
|
|
2075
|
+
doc,
|
|
2076
|
+
params.range,
|
|
2077
|
+
rootScopes,
|
|
2078
|
+
entry,
|
|
2079
|
+
calendarFor(fsPath).calendar
|
|
2080
|
+
);
|
|
1940
2081
|
})
|
|
1941
2082
|
);
|
|
1942
2083
|
|