@px-lsp/protocol 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 +25 -0
- package/dist/arrays.d.ts +13 -0
- package/dist/arrays.js +19 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.js +10 -0
- package/dist/descriptorMetadata.d.ts +51 -0
- package/dist/descriptorMetadata.js +98 -0
- package/dist/descriptorMod.d.ts +66 -0
- package/dist/descriptorMod.js +335 -0
- package/dist/errorLogParser.d.ts +33 -0
- package/dist/errorLogParser.js +125 -0
- package/dist/fsWalk.d.ts +20 -0
- package/dist/fsWalk.js +159 -0
- package/dist/locProperties.d.ts +13 -0
- package/dist/locProperties.js +46 -0
- package/dist/locRefs.d.ts +11 -0
- package/dist/locRefs.js +31 -0
- package/dist/modName.d.ts +6 -0
- package/dist/modName.js +53 -0
- package/dist/protocol.d.ts +1462 -0
- package/dist/protocol.js +201 -0
- package/dist/regex.d.ts +13 -0
- package/dist/regex.js +21 -0
- package/dist/suppression.d.ts +52 -0
- package/dist/suppression.js +173 -0
- package/dist/tigerParser.d.ts +28 -0
- package/dist/tigerParser.js +72 -0
- package/dist/translationCore.d.ts +26 -0
- package/dist/translationCore.js +162 -0
- package/dist/types.d.ts +82 -0
- package/dist/types.js +3 -0
- package/package.json +39 -0
- package/src/arrays.ts +16 -0
- package/src/constants.ts +12 -0
- package/src/descriptorMetadata.ts +101 -0
- package/src/descriptorMod.ts +354 -0
- package/src/errorLogParser.ts +136 -0
- package/src/fsWalk.ts +126 -0
- package/src/locProperties.ts +43 -0
- package/src/locRefs.ts +38 -0
- package/src/modName.ts +18 -0
- package/src/protocol.ts +1459 -0
- package/src/regex.ts +19 -0
- package/src/suppression.ts +178 -0
- package/src/tigerParser.ts +79 -0
- package/src/translationCore.ts +140 -0
- package/src/types.ts +90 -0
package/src/regex.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One correct copy of the regex-escape both sides need. It lived inline at five
|
|
3
|
+
* call sites and one of them had an extra backslash, which silently turned the
|
|
4
|
+
* escape into a no-op (it matched a metacharacter followed by two literal
|
|
5
|
+
* backslashes, so nothing was ever escaped).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** Escape every regex metacharacter in `literal` so it matches itself. */
|
|
9
|
+
export function escapeRegExp(literal: string): string {
|
|
10
|
+
return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A pattern matching `name` only as a whole script identifier: not when it is
|
|
15
|
+
* a substring of a longer name, and not across a dot-chain segment boundary.
|
|
16
|
+
*/
|
|
17
|
+
export function wholeNamePattern(name: string): string {
|
|
18
|
+
return `(?<![A-Za-z0-9_.\\-])${escapeRegExp(name)}(?![A-Za-z0-9_.\\-])`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diagnostic suppression, shared by the server (own structural/loc diagnostics)
|
|
3
|
+
* and the client (tiger-forwarded reports) so one habit works across both tools.
|
|
4
|
+
*
|
|
5
|
+
* No `vscode` imports: plain data in, plain predicates out. Everything here is
|
|
6
|
+
* fail-soft — bad setting values or malformed comments are ignored, never thrown.
|
|
7
|
+
*
|
|
8
|
+
* Two mechanisms:
|
|
9
|
+
* 1. Settings: the diagnostics.ignore setting (diagnostic codes) and
|
|
10
|
+
* the diagnostics.ignorePatterns setting (globs on the workspace-relative path).
|
|
11
|
+
* 2. Inline comments: `# px:ignore <code…>` (same line) and
|
|
12
|
+
* `# px:ignore-next-line <code…>` (following line); a bare form with no
|
|
13
|
+
* codes suppresses every diagnostic on the target line. A trailing
|
|
14
|
+
* `-- <rationale>` is allowed and ignored.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Settings-driven filter. `ignore` matches a diagnostic's code (our stable
|
|
19
|
+
* codes, or tiger's `key`); `ignorePatterns` matches globs against the
|
|
20
|
+
* workspace-relative file path.
|
|
21
|
+
*/
|
|
22
|
+
export interface DiagnosticIgnoreConfig {
|
|
23
|
+
/** Diagnostic codes to drop everywhere. */
|
|
24
|
+
ignore: string[];
|
|
25
|
+
/** Glob patterns matched against the workspace-relative (forward-slash) path. */
|
|
26
|
+
ignorePatterns: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Normalize a raw settings array: strings only, trimmed, empties dropped. */
|
|
30
|
+
export function sanitizeStringList(value: unknown): string[] {
|
|
31
|
+
if (!Array.isArray(value)) return [];
|
|
32
|
+
const out: string[] = [];
|
|
33
|
+
for (const v of value) {
|
|
34
|
+
if (typeof v !== "string") continue;
|
|
35
|
+
const t = v.trim();
|
|
36
|
+
if (t !== "") out.push(t);
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Tiny `*`/`**` glob matcher (no dependency). `*` matches within a path segment,
|
|
43
|
+
* `**` matches across segments (including `/`). Matching is done on
|
|
44
|
+
* forward-slash paths and is case-insensitive (Windows-friendly). A pattern with
|
|
45
|
+
* no slash also matches against the basename, so `*.txt` works like a gitignore
|
|
46
|
+
* entry. Returns false on any malformed pattern.
|
|
47
|
+
*/
|
|
48
|
+
export function globMatch(pattern: string, filePath: string): boolean {
|
|
49
|
+
const p = pattern.replace(/\\/g, "/").toLowerCase();
|
|
50
|
+
const f = filePath.replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
|
|
51
|
+
if (p === "") return false;
|
|
52
|
+
try {
|
|
53
|
+
const re = new RegExp("^" + globToRegExpSource(p) + "$");
|
|
54
|
+
if (re.test(f)) return true;
|
|
55
|
+
// Slash-free patterns also match the basename (gitignore-style convenience).
|
|
56
|
+
if (!p.includes("/")) {
|
|
57
|
+
const base = f.slice(f.lastIndexOf("/") + 1);
|
|
58
|
+
return re.test(base);
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
} catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Translate a glob (already lowercased, forward-slashed) into a regex source. */
|
|
67
|
+
function globToRegExpSource(glob: string): string {
|
|
68
|
+
let out = "";
|
|
69
|
+
for (let i = 0; i < glob.length; i++) {
|
|
70
|
+
const c = glob[i];
|
|
71
|
+
if (c === "*") {
|
|
72
|
+
if (glob[i + 1] === "*") {
|
|
73
|
+
// `**` — cross segments, optionally swallowing a trailing slash.
|
|
74
|
+
i++;
|
|
75
|
+
if (glob[i + 1] === "/") {
|
|
76
|
+
i++;
|
|
77
|
+
out += "(?:.*/)?";
|
|
78
|
+
} else {
|
|
79
|
+
out += ".*";
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
out += "[^/]*";
|
|
83
|
+
}
|
|
84
|
+
} else if (c === "?") {
|
|
85
|
+
out += "[^/]";
|
|
86
|
+
} else if ("\\^$.|+()[]{}".includes(c)) {
|
|
87
|
+
out += "\\" + c;
|
|
88
|
+
} else {
|
|
89
|
+
out += c;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** True when a diagnostic with `code` in `filePath` should be dropped by settings. */
|
|
96
|
+
export function isIgnoredByConfig(
|
|
97
|
+
cfg: DiagnosticIgnoreConfig,
|
|
98
|
+
code: string | undefined,
|
|
99
|
+
relPath: string
|
|
100
|
+
): boolean {
|
|
101
|
+
if (code !== undefined && cfg.ignore.includes(code)) return true;
|
|
102
|
+
for (const pattern of cfg.ignorePatterns) {
|
|
103
|
+
if (globMatch(pattern, relPath)) return true;
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Inline suppression map for a file, keyed by 0-based line number. A `null`
|
|
110
|
+
* value means "suppress every code on this line"; an array means "suppress only
|
|
111
|
+
* these codes". Built by scanning comment lines once when publishing.
|
|
112
|
+
*/
|
|
113
|
+
export type InlineSuppressions = Map<number, string[] | null>;
|
|
114
|
+
|
|
115
|
+
const IGNORE_RE = /#\s*px:ignore(-next-line)?\b([^\n]*)/i;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Scan a document's text for `# px:ignore[-next-line] <code…>` comments.
|
|
119
|
+
* Cheap: only lines containing `px:ignore` are parsed. `-next-line` targets
|
|
120
|
+
* the following line; the plain form targets its own line.
|
|
121
|
+
*/
|
|
122
|
+
export function scanInlineSuppressions(text: string): InlineSuppressions {
|
|
123
|
+
const map: InlineSuppressions = new Map();
|
|
124
|
+
if (!text.includes("px:ignore")) return map;
|
|
125
|
+
const lines = text.split(/\r?\n/);
|
|
126
|
+
for (let i = 0; i < lines.length; i++) {
|
|
127
|
+
const line = lines[i];
|
|
128
|
+
// A comment can trail script on the same line; only look after the `#`.
|
|
129
|
+
const hash = line.indexOf("#");
|
|
130
|
+
if (hash < 0) continue;
|
|
131
|
+
const m = IGNORE_RE.exec(line.slice(hash));
|
|
132
|
+
if (!m) continue;
|
|
133
|
+
const target = m[1] ? i + 1 : i;
|
|
134
|
+
const codes = parseCodes(m[2]);
|
|
135
|
+
mergeSuppression(map, target, codes.length === 0 ? null : codes);
|
|
136
|
+
}
|
|
137
|
+
return map;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The codes following the marker, stopping at a `--` rationale. Writing a
|
|
142
|
+
* reason is the natural instinct, and without the cut-off every word of it
|
|
143
|
+
* parsed as a code — turning a suppression that matched everything into one
|
|
144
|
+
* that matched nothing, silently. Codes are kebab-case slugs
|
|
145
|
+
* (`unclosed-brace`, `loc-no-header`), so a leading `-` can only be the
|
|
146
|
+
* separator.
|
|
147
|
+
*/
|
|
148
|
+
function parseCodes(rest: string): string[] {
|
|
149
|
+
const out: string[] = [];
|
|
150
|
+
for (const token of rest.trim().split(/\s+/)) {
|
|
151
|
+
if (token === "") continue;
|
|
152
|
+
if (token.startsWith("-")) break; // `-- because the game allows it`
|
|
153
|
+
out.push(token);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function mergeSuppression(map: InlineSuppressions, line: number, codes: string[] | null): void {
|
|
159
|
+
const existing = map.get(line);
|
|
160
|
+
if (existing === undefined) {
|
|
161
|
+
map.set(line, codes);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// `null` (suppress-all) wins; otherwise union the code lists.
|
|
165
|
+
if (existing === null || codes === null) {
|
|
166
|
+
map.set(line, null);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
map.set(line, [...existing, ...codes]);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** True when line `line` has an inline suppression covering `code`. */
|
|
173
|
+
export function isSuppressedInline(map: InlineSuppressions, line: number, code: string | undefined): boolean {
|
|
174
|
+
if (!map.has(line)) return false;
|
|
175
|
+
const codes = map.get(line) ?? null;
|
|
176
|
+
if (codes === null) return true; // bare `# px:ignore` suppresses all
|
|
177
|
+
return code !== undefined && codes.includes(code);
|
|
178
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parser for tiger `--json` reports (the Paradox script validator family).
|
|
3
|
+
*
|
|
4
|
+
* Kept separate from the process management in tiger.ts so it stays free of
|
|
5
|
+
* `vscode` imports and defensively tolerant of format drift between tiger
|
|
6
|
+
* releases: unknown fields are ignored, malformed entries are skipped.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface TigerLocation {
|
|
10
|
+
path: string;
|
|
11
|
+
fullpath?: string;
|
|
12
|
+
/** 1-based, may be missing for file-level reports. */
|
|
13
|
+
linenr?: number;
|
|
14
|
+
/** 1-based. */
|
|
15
|
+
column?: number;
|
|
16
|
+
length?: number;
|
|
17
|
+
tag?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface TigerReport {
|
|
21
|
+
severity: string;
|
|
22
|
+
/** tiger also rates how sure it is: weak | reasonable | strong. */
|
|
23
|
+
confidence?: string;
|
|
24
|
+
key: string;
|
|
25
|
+
message: string;
|
|
26
|
+
info?: string;
|
|
27
|
+
locations: TigerLocation[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse tiger's JSON output. Returns null if no JSON array can be found at all. */
|
|
31
|
+
export function parseTigerJson(stdout: string): TigerReport[] | null {
|
|
32
|
+
let raw: unknown;
|
|
33
|
+
try {
|
|
34
|
+
raw = JSON.parse(stdout);
|
|
35
|
+
} catch {
|
|
36
|
+
// tiger may print progress noise before the JSON; try from the first '['.
|
|
37
|
+
const start = stdout.indexOf("[");
|
|
38
|
+
if (start < 0) return null;
|
|
39
|
+
try {
|
|
40
|
+
raw = JSON.parse(stdout.slice(start));
|
|
41
|
+
} catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(raw)) return null;
|
|
46
|
+
|
|
47
|
+
const reports: TigerReport[] = [];
|
|
48
|
+
for (const entry of raw) {
|
|
49
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
50
|
+
const e = entry as Record<string, unknown>;
|
|
51
|
+
const message = typeof e.message === "string" ? e.message : null;
|
|
52
|
+
const locationsRaw = Array.isArray(e.locations) ? e.locations : [];
|
|
53
|
+
if (message === null) continue;
|
|
54
|
+
const locations: TigerLocation[] = [];
|
|
55
|
+
for (const locRaw of locationsRaw) {
|
|
56
|
+
if (typeof locRaw !== "object" || locRaw === null) continue;
|
|
57
|
+
const l = locRaw as Record<string, unknown>;
|
|
58
|
+
const p = typeof l.fullpath === "string" ? l.fullpath : typeof l.path === "string" ? l.path : null;
|
|
59
|
+
if (p === null) continue;
|
|
60
|
+
const loc: TigerLocation = { path: typeof l.path === "string" ? l.path : p };
|
|
61
|
+
if (typeof l.fullpath === "string") loc.fullpath = l.fullpath;
|
|
62
|
+
const linenr = l.linenr ?? l.line;
|
|
63
|
+
if (typeof linenr === "number") loc.linenr = linenr;
|
|
64
|
+
if (typeof l.column === "number") loc.column = l.column;
|
|
65
|
+
if (typeof l.length === "number") loc.length = l.length;
|
|
66
|
+
if (typeof l.tag === "string") loc.tag = l.tag;
|
|
67
|
+
locations.push(loc);
|
|
68
|
+
}
|
|
69
|
+
reports.push({
|
|
70
|
+
severity: typeof e.severity === "string" ? e.severity : "warning",
|
|
71
|
+
confidence: typeof e.confidence === "string" ? e.confidence : undefined,
|
|
72
|
+
key: typeof e.key === "string" ? e.key : "unknown",
|
|
73
|
+
message,
|
|
74
|
+
info: typeof e.info === "string" ? e.info : undefined,
|
|
75
|
+
locations,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return reports;
|
|
79
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure logic for the translation scaffolding workflow: mirror the structure of
|
|
3
|
+
* an existing localization (usually english) into a new language so a
|
|
4
|
+
* translator only has to replace values.
|
|
5
|
+
*
|
|
6
|
+
* No `vscode` imports here: this module is unit-tested in plain Node.
|
|
7
|
+
*/
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
|
|
10
|
+
export const LOC_LANGUAGES = [
|
|
11
|
+
"english",
|
|
12
|
+
"french",
|
|
13
|
+
"german",
|
|
14
|
+
"spanish",
|
|
15
|
+
"russian",
|
|
16
|
+
"korean",
|
|
17
|
+
"simp_chinese",
|
|
18
|
+
"japanese",
|
|
19
|
+
"polish",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const BOM = "";
|
|
23
|
+
const HEADER = /^(\s*)l_([a-z_]+):/m;
|
|
24
|
+
const ENTRY = /^\s*([A-Za-z0-9_.\-']+):\d*\s*"/;
|
|
25
|
+
|
|
26
|
+
/** Language of a loc file, from its `_l_<lang>.yml` suffix or a path segment. */
|
|
27
|
+
export function detectLocFileLanguage(filePath: string): string | null {
|
|
28
|
+
const m = /_l_([a-z_]+)\.ya?ml$/i.exec(filePath);
|
|
29
|
+
if (m) return m[1].toLowerCase();
|
|
30
|
+
const segments = filePath.toLowerCase().split(/[\\/]/);
|
|
31
|
+
for (const lang of LOC_LANGUAGES) {
|
|
32
|
+
if (segments.includes(lang)) return lang;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Where the translated counterpart of `srcFile` lives: language path segments
|
|
39
|
+
* and the `_l_<lang>` filename marker are retargeted. Returns null when the
|
|
40
|
+
* path carries no language marker at all.
|
|
41
|
+
*/
|
|
42
|
+
export function retargetLocPath(srcFile: string, sourceLang: string, targetLang: string): string | null {
|
|
43
|
+
const parts = srcFile.split(/([\\/])/); // keep separators
|
|
44
|
+
let changed = false;
|
|
45
|
+
const out = parts.map((p) => {
|
|
46
|
+
if (p.toLowerCase() === sourceLang) {
|
|
47
|
+
changed = true;
|
|
48
|
+
return targetLang;
|
|
49
|
+
}
|
|
50
|
+
return p;
|
|
51
|
+
});
|
|
52
|
+
let result = out.join("");
|
|
53
|
+
const marker = new RegExp(`_l_${sourceLang}(\\.ya?ml)$`, "i");
|
|
54
|
+
if (marker.test(path.basename(result))) {
|
|
55
|
+
result = result.replace(marker, `_l_${targetLang}$1`);
|
|
56
|
+
changed = true;
|
|
57
|
+
}
|
|
58
|
+
return changed ? result : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Greedy `(.*)` closes the value at the LAST quote before any trailing
|
|
62
|
+
// comment, matching the game's parsing of inner quotes (`""speech""`).
|
|
63
|
+
const ENTRY_LINE = /^(\s*[A-Za-z0-9_.\-']+:\d*\s*)"(.*)"\s*(#.*)?$/;
|
|
64
|
+
|
|
65
|
+
/** Blank an entry's value, keeping the source text visible as a comment. */
|
|
66
|
+
function blankEntry(line: string, sourceLang: string): string {
|
|
67
|
+
const m = ENTRY_LINE.exec(line);
|
|
68
|
+
if (!m || m[2] === "") return line;
|
|
69
|
+
// The game reads the value up to the last quote on the LINE, so any quote
|
|
70
|
+
// echoed into the comment would leak back into the value; downgrade to '.
|
|
71
|
+
const comment = `# ${sourceLang}: ${m[2]}${m[3] ? ` ${m[3]}` : ""}`.replace(/"/g, "'");
|
|
72
|
+
return `${m[1]}"" ${comment}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A translation skeleton: the source file's structure (comments and blank
|
|
77
|
+
* lines preserved — they are context for the translator) with the language
|
|
78
|
+
* header switched and every value BLANKED; the source text stays visible as
|
|
79
|
+
* an inline `# english: …` comment so the translator sees it right there
|
|
80
|
+
* without it leaking into the game as a fake translation.
|
|
81
|
+
*/
|
|
82
|
+
export function buildTranslation(sourceContent: string, targetLang: string, sourceLang = "english"): string {
|
|
83
|
+
const hadBom = sourceContent.startsWith(BOM);
|
|
84
|
+
let body = hadBom ? sourceContent.slice(1) : sourceContent;
|
|
85
|
+
if (HEADER.test(body)) {
|
|
86
|
+
body = body.replace(HEADER, `$1l_${targetLang}:`);
|
|
87
|
+
} else {
|
|
88
|
+
body = `l_${targetLang}:\n` + body;
|
|
89
|
+
}
|
|
90
|
+
body = body
|
|
91
|
+
.split(/\r?\n/)
|
|
92
|
+
.map((l) => blankEntry(l, sourceLang))
|
|
93
|
+
.join("\n");
|
|
94
|
+
return BOM + body;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface MergeResult {
|
|
98
|
+
content: string;
|
|
99
|
+
added: number;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Add entries that exist in the source but not yet in the target, appended at
|
|
104
|
+
* the end under a marker comment. Existing target lines are never touched.
|
|
105
|
+
*/
|
|
106
|
+
export function mergeTranslation(
|
|
107
|
+
targetContent: string,
|
|
108
|
+
sourceContent: string,
|
|
109
|
+
sourceLang: string
|
|
110
|
+
): MergeResult {
|
|
111
|
+
const hadBom = targetContent.startsWith(BOM);
|
|
112
|
+
const target = hadBom ? targetContent.slice(1) : targetContent;
|
|
113
|
+
const eol = target.includes("\r\n") ? "\r\n" : "\n";
|
|
114
|
+
|
|
115
|
+
const existing = new Set<string>();
|
|
116
|
+
for (const line of target.split(/\r?\n/)) {
|
|
117
|
+
const m = ENTRY.exec(line);
|
|
118
|
+
if (m) existing.add(m[1]);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const missing: string[] = [];
|
|
122
|
+
for (const line of sourceContent.replace(/^/, "").split(/\r?\n/)) {
|
|
123
|
+
const m = ENTRY.exec(line);
|
|
124
|
+
if (m && !existing.has(m[1])) {
|
|
125
|
+
missing.push(blankEntry(line.replace(/\r$/, ""), sourceLang));
|
|
126
|
+
existing.add(m[1]);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (missing.length === 0) return { content: targetContent, added: 0 };
|
|
130
|
+
|
|
131
|
+
const lines = target.split(/\r?\n/);
|
|
132
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
133
|
+
lines.push(
|
|
134
|
+
"",
|
|
135
|
+
` # --- entries missing from this language; ${sourceLang} text in the comments ---`,
|
|
136
|
+
...missing,
|
|
137
|
+
""
|
|
138
|
+
);
|
|
139
|
+
return { content: (hadBom ? BOM : "") + lines.join(eol), added: missing.length };
|
|
140
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Shared data model. Keep this module free of `vscode` imports: it is used by unit-tested code. */
|
|
2
|
+
|
|
3
|
+
export interface IndexStats {
|
|
4
|
+
total: number;
|
|
5
|
+
files: number;
|
|
6
|
+
byKind: Record<string, number>;
|
|
7
|
+
bySource: Record<string, number>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type TokenKind = "trigger" | "effect" | "event_target" | "modifier";
|
|
11
|
+
|
|
12
|
+
/** One engine token parsed from a script_docs log file. */
|
|
13
|
+
export interface TokenData {
|
|
14
|
+
name: string;
|
|
15
|
+
kind: TokenKind;
|
|
16
|
+
/** Description text from the log; may be empty. */
|
|
17
|
+
doc: string;
|
|
18
|
+
/** Supported scopes as raw strings, display-only in v1. */
|
|
19
|
+
scopes: string[];
|
|
20
|
+
/** Extra metadata lines (targets, traits, categories...), display-only. */
|
|
21
|
+
traits?: string;
|
|
22
|
+
/** A syntax/usage example block (`add_hook = { type = X ... }`), preserved
|
|
23
|
+
* verbatim from a `usage:` section, an inline `name = …` line, or the wiki. */
|
|
24
|
+
usage?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Definition kinds are open strings driven by the schema table
|
|
29
|
+
* (packages/server/src/schema): "scripted_effect", "trait", "decision", ...
|
|
30
|
+
*/
|
|
31
|
+
export type DefKind = string;
|
|
32
|
+
|
|
33
|
+
/** Where a definition comes from; mod shadows parent shadows vanilla. */
|
|
34
|
+
export type DefSource = "vanilla" | "parent" | "mod";
|
|
35
|
+
|
|
36
|
+
/** One user-defined or vanilla definition found by the indexer. */
|
|
37
|
+
export interface Definition {
|
|
38
|
+
name: string;
|
|
39
|
+
kind: DefKind;
|
|
40
|
+
/** Absolute path. */
|
|
41
|
+
file: string;
|
|
42
|
+
/** 0-based line number (VS Code convention). */
|
|
43
|
+
line: number;
|
|
44
|
+
source: DefSource;
|
|
45
|
+
/** For loc_key: the localized text (truncated for memory; the edit flow re-reads the yml). */
|
|
46
|
+
value?: string;
|
|
47
|
+
/** Enclosing definition, when meaningful (e.g. the event a save_scope_as sits in). */
|
|
48
|
+
container?: string;
|
|
49
|
+
/** For scripted effects/triggers: $PARAM$ names in declaration order (signature help). */
|
|
50
|
+
params?: string[];
|
|
51
|
+
/** PdxDoc prose from a leading `#` comment block (§E); capped for memory. */
|
|
52
|
+
doc?: string;
|
|
53
|
+
/** PdxDoc structured tags (@scope, @param, @saves, @returns, @example, @deprecated, …). */
|
|
54
|
+
tags?: DocTag[];
|
|
55
|
+
/**
|
|
56
|
+
* Database entry mode stripped from the declaration key (`REPLACE:name`),
|
|
57
|
+
* for games whose profile declares entryModes. The definition is indexed
|
|
58
|
+
* under the bare name; the mode is kept for override analysis.
|
|
59
|
+
*/
|
|
60
|
+
entryMode?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** One structured PdxDoc tag line (§E1). Unknown tags render as prose, not stored here. */
|
|
64
|
+
export interface DocTag {
|
|
65
|
+
/** Tag name without the leading `@` (lowercased). */
|
|
66
|
+
tag: string;
|
|
67
|
+
/** Text after the tag word. */
|
|
68
|
+
text: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** One usage site of a name, extracted schema-driven from mod files. */
|
|
72
|
+
export interface Reference {
|
|
73
|
+
name: string;
|
|
74
|
+
/** Candidate definition kinds this usage may refer to. */
|
|
75
|
+
kinds: DefKind[];
|
|
76
|
+
/** Absolute path. */
|
|
77
|
+
file: string;
|
|
78
|
+
/** 0-based line. */
|
|
79
|
+
line: number;
|
|
80
|
+
/** Character range of the name on the line (prefix like `scope:` excluded). */
|
|
81
|
+
startChar: number;
|
|
82
|
+
endChar: number;
|
|
83
|
+
/** Key-position call site (`my_effect = yes`): shown by find-references and
|
|
84
|
+
* rename, excluded from the usage-count completion ranking signal (§C2). */
|
|
85
|
+
call?: boolean;
|
|
86
|
+
/** Call sites only: enclosing key chain below the top-level definition
|
|
87
|
+
* (dotted, outermost first) — input for call-site scope aggregation, which
|
|
88
|
+
* types un-@scope'd scripted effects/triggers from where they are called. */
|
|
89
|
+
chain?: string;
|
|
90
|
+
}
|