@dialexjs/language-server 0.7.1 → 0.7.2
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 +5 -4
- package/dist/features.d.mts +15 -3
- package/dist/features.d.mts.map +1 -1
- package/dist/features.mjs +68 -5
- package/dist/features.mjs.map +1 -1
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +4 -4
- package/dist/server.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,7 @@ A [Language Server Protocol](https://microsoft.github.io/language-server-protoco
|
|
|
4
4
|
[Dialex](https://github.com/awaiden/dialex) translations. Editors that speak LSP (Zed, Neovim,
|
|
5
5
|
Helix, Sublime and others) get Dialex's diagnostics and hover without a dedicated plugin.
|
|
6
6
|
|
|
7
|
-
> **Status: early.** Everything below works
|
|
8
|
-
> Zed extension is next.
|
|
7
|
+
> **Status: early.** Everything below works, tested against an LSP client and in Zed.
|
|
9
8
|
|
|
10
9
|
## Run it
|
|
11
10
|
|
|
@@ -18,8 +17,10 @@ npx @dialexjs/language-server --stdio
|
|
|
18
17
|
- **Diagnostics**: missing keys and locales in `.content.ts` dictionaries, `getDictionary("x")` and
|
|
19
18
|
`t("x.y")` calls that point at nothing, invalid ICU messages, `[TODO]` placeholders, and
|
|
20
19
|
translations whose source text changed since they were translated.
|
|
21
|
-
- **Hover**: hover a `t("home.title")` key
|
|
22
|
-
|
|
20
|
+
- **Hover**: hover a `t("home.title")` key, a dictionary name, or a member of a variable that holds
|
|
21
|
+
a dictionary (`s.nav.features` after `const s = useDictionary("showcase")`) to see the text in
|
|
22
|
+
every locale.
|
|
23
|
+
- **Go to definition**: jump from a key, or from `s.nav.features`, to where the dictionary writes it.
|
|
23
24
|
- **Completion**: dictionary names and key paths inside `t("…")`, `getDictionary("…")` and
|
|
24
25
|
`useDictionary("…")`, triggered by `"`, `'`, `` ` `` and `.`.
|
|
25
26
|
- **Quick fixes**: copy a missing key into a locale (marked `[TODO]`), add every missing key in a
|
package/dist/features.d.mts
CHANGED
|
@@ -12,12 +12,24 @@ export declare function modelForFile(models: Iterable<ProjectModel>, file: strin
|
|
|
12
12
|
/** Locales with the default locale first. */
|
|
13
13
|
export declare function orderedLocales(model: ProjectModel, dictionary: AnalyzedDictionary): string[];
|
|
14
14
|
//#endregion
|
|
15
|
+
//#region src/features/bindings.d.ts
|
|
16
|
+
/** Variable name -> dictionary name, for `const s = useDictionary("showcase")`. */
|
|
17
|
+
export type Bindings = ReadonlyMap<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Finds the variables in a file that hold a dictionary, so `s.nav.features` can be resolved to
|
|
20
|
+
* `showcase.nav.features`. It is a text match: a variable that is reassigned or shadowed in another
|
|
21
|
+
* scope may resolve to the wrong dictionary, which only affects editor hints.
|
|
22
|
+
*/
|
|
23
|
+
export declare function findBindings(text: string): Bindings;
|
|
24
|
+
//#endregion
|
|
15
25
|
//#region src/features/references.d.ts
|
|
16
26
|
/**
|
|
17
27
|
* The Dialex reference under the cursor on one line of code: a dictionary name, a `t("a.b.c")`
|
|
18
|
-
* path,
|
|
28
|
+
* path, a member of `getDictionary("a")` such as `.title`, or, with `bindings`, a member of a
|
|
29
|
+
* variable that holds a dictionary (`s.nav.features` after `const s = useDictionary("showcase")`).
|
|
30
|
+
* Offsets are into `lineText`.
|
|
19
31
|
*/
|
|
20
|
-
export declare function findReferenceAt(lineText: string, character: number): Reference | undefined;
|
|
32
|
+
export declare function findReferenceAt(lineText: string, character: number, bindings?: Bindings): Reference | undefined;
|
|
21
33
|
export interface ResolvedReference {
|
|
22
34
|
dictionary: AnalyzedDictionary;
|
|
23
35
|
/** Empty for a reference to the dictionary itself. */
|
|
@@ -65,7 +77,7 @@ export type CompletionContext = {
|
|
|
65
77
|
* `textBeforeCursor` is the line up to the cursor. `length` is how many characters before the
|
|
66
78
|
* cursor a completion replaces.
|
|
67
79
|
*/
|
|
68
|
-
export declare function completionContextAt(textBeforeCursor: string): CompletionContext | undefined;
|
|
80
|
+
export declare function completionContextAt(textBeforeCursor: string, bindings?: Bindings): CompletionContext | undefined;
|
|
69
81
|
export interface CompletionEntry {
|
|
70
82
|
label: string;
|
|
71
83
|
kind: "dictionary" | "group" | "leaf";
|
package/dist/features.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.d.mts","names":[],"sources":["../src/features/model.ts","../src/features/references.ts","../src/features/hover.ts","../src/features/definition.ts","../src/features/completion.ts","../src/features/quickfix.ts","../src/features/projects.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA,cAAc,YAAY;;wBAGZ,WACd,cACA,uBACA,QAAQ,KAAK,kCACZ;;wBASa,aACd,QAAQ,SAAS,eACjB,eACC;;wBAWa,eAAe,OAAO,cAAc,YAAY
|
|
1
|
+
{"version":3,"file":"features.d.mts","names":[],"sources":["../src/features/model.ts","../src/features/bindings.ts","../src/features/references.ts","../src/features/hover.ts","../src/features/definition.ts","../src/features/completion.ts","../src/features/quickfix.ts","../src/features/projects.ts"],"mappings":";;;iBAKiB;EACf;EACA;EACA,cAAc,YAAY;;wBAGZ,WACd,cACA,uBACA,QAAQ,KAAK,kCACZ;;wBASa,aACd,QAAQ,SAAS,eACjB,eACC;;wBAWa,eAAe,OAAO,cAAc,YAAY;;;;YCrCpD,WAAW;;;;;;wBAcP,aAAa,eAAe;;;;;;;;;wBCE5B,gBACd,kBACA,mBACA,WAAW,WACV;iBA8Cc;EACf,YAAY;;EAEZ;;wBAGc,iBACd,OAAO,cACP,KAAK,YACJ;;wBAWa,aACd,YAAY,oBACZ;;;;wBCjFc,eAAe;;;;;wBAwBf,WAAW,OAAO,cAAc,KAAK;;;iBC1BpC;EACf;EACA,OAAO;;;;;;wBASO,cAAc,OAAO,cAAc,KAAK,YAAY;;;YCZxD;EACN;EAAoB;EAAc;EAAe;;EACjD;EAAc;EAAoB;EAAkB;EAAe;;;;;;;wBASzD,oBACd,0BACA,WAAW,WACV;iBAiCc;EACf;EACA;EACA;;EAEA;;EAEA;EACA;;;wBA+Dc,kBACd,OAAO,cACP,SAAS,oBACR;;;iBCpHc;EACf;EACA;;EAEA;;;;;;;wBAQc,eACd,cACA,cACA,SAAS;;;;;wBAkBK,cAAc,cAAc,cAAc;;;iBCtCzC;EACf;;EAEA;;;;;;;;;;;wBAwBc,iBAAiB,iBAAiB"}
|
package/dist/features.mjs
CHANGED
|
@@ -24,14 +24,34 @@ function orderedLocales(model, dictionary) {
|
|
|
24
24
|
return [...dictionary.locales.filter((l) => l === model.defaultLocale), ...dictionary.locales.filter((l) => l !== model.defaultLocale)];
|
|
25
25
|
}
|
|
26
26
|
//#endregion
|
|
27
|
+
//#region src/features/bindings.ts
|
|
28
|
+
/**
|
|
29
|
+
* `const s = useDictionary("showcase")`, `const home = await getDictionary("home", locale)`,
|
|
30
|
+
* `let t: Home = injectDictionary<Home>("home")`: a variable that holds a dictionary.
|
|
31
|
+
*/
|
|
32
|
+
const DECLARATION = /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?=\s*(?:await\s+)?(?:[A-Za-z_$][\w$]*\.)*(?:getDictionary|useDictionary|injectDictionary)\s*(?:<[^()]*?>)?\(\s*(["'`])([^"'`$\n]+)\2/g;
|
|
33
|
+
/**
|
|
34
|
+
* Finds the variables in a file that hold a dictionary, so `s.nav.features` can be resolved to
|
|
35
|
+
* `showcase.nav.features`. It is a text match: a variable that is reassigned or shadowed in another
|
|
36
|
+
* scope may resolve to the wrong dictionary, which only affects editor hints.
|
|
37
|
+
*/
|
|
38
|
+
function findBindings(text) {
|
|
39
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
40
|
+
for (const match of text.matchAll(DECLARATION)) bindings.set(match[1], match[3]);
|
|
41
|
+
return bindings;
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
27
44
|
//#region src/features/references.ts
|
|
28
45
|
/** `getDictionary("home").nav.about` -> the `.nav.about` member chain after the call. */
|
|
29
46
|
const MEMBER_CHAIN = /(?:getDictionary|useDictionary)\(\s*(["'`])([^"'`$\n]+)\1[^()\n]*\)((?:\??\.[A-Za-z_$][\w$]*)+)/dg;
|
|
47
|
+
const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
30
48
|
/**
|
|
31
49
|
* The Dialex reference under the cursor on one line of code: a dictionary name, a `t("a.b.c")`
|
|
32
|
-
* path,
|
|
50
|
+
* path, a member of `getDictionary("a")` such as `.title`, or, with `bindings`, a member of a
|
|
51
|
+
* variable that holds a dictionary (`s.nav.features` after `const s = useDictionary("showcase")`).
|
|
52
|
+
* Offsets are into `lineText`.
|
|
33
53
|
*/
|
|
34
|
-
function findReferenceAt(lineText, character) {
|
|
54
|
+
function findReferenceAt(lineText, character, bindings) {
|
|
35
55
|
const direct = scanReferences(lineText).find((r) => character >= r.start && character <= r.end);
|
|
36
56
|
if (direct) return direct;
|
|
37
57
|
for (const match of lineText.matchAll(MEMBER_CHAIN)) {
|
|
@@ -51,6 +71,32 @@ function findReferenceAt(lineText, character) {
|
|
|
51
71
|
};
|
|
52
72
|
}
|
|
53
73
|
}
|
|
74
|
+
for (const [name, dictionary] of bindings ?? []) {
|
|
75
|
+
const chain = new RegExp(`(?<![\\w$.])${escapeRegExp(name)}(?![\\w$])((?:\\??\\.[A-Za-z_$][\\w$]*)*)`, "dg");
|
|
76
|
+
for (const match of lineText.matchAll(chain)) {
|
|
77
|
+
const nameStart = match.index;
|
|
78
|
+
const nameEnd = nameStart + name.length;
|
|
79
|
+
if (character >= nameStart && character <= nameEnd) return {
|
|
80
|
+
kind: "dictionary",
|
|
81
|
+
value: dictionary,
|
|
82
|
+
start: nameStart,
|
|
83
|
+
end: nameEnd
|
|
84
|
+
};
|
|
85
|
+
const [chainStart] = match.indices[1];
|
|
86
|
+
const keys = [];
|
|
87
|
+
for (const member of match[1].matchAll(/\??\.([A-Za-z_$][\w$]*)/g)) {
|
|
88
|
+
const start = chainStart + member.index + member[0].length - member[1].length;
|
|
89
|
+
const end = start + member[1].length;
|
|
90
|
+
keys.push(member[1]);
|
|
91
|
+
if (character >= start && character <= end) return {
|
|
92
|
+
kind: "path",
|
|
93
|
+
value: [dictionary, ...keys].join("."),
|
|
94
|
+
start,
|
|
95
|
+
end
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
54
100
|
}
|
|
55
101
|
function resolveReference(model, ref) {
|
|
56
102
|
if (ref.kind === "dictionary") {
|
|
@@ -164,9 +210,9 @@ const CALL = /(?<![\w$.])(t|getDictionary|useDictionary|DialexDictionary)\(\s*([
|
|
|
164
210
|
* `textBeforeCursor` is the line up to the cursor. `length` is how many characters before the
|
|
165
211
|
* cursor a completion replaces.
|
|
166
212
|
*/
|
|
167
|
-
function completionContextAt(textBeforeCursor) {
|
|
213
|
+
function completionContextAt(textBeforeCursor, bindings) {
|
|
168
214
|
const match = CALL.exec(textBeforeCursor);
|
|
169
|
-
if (!match) return void 0;
|
|
215
|
+
if (!match) return bindings ? memberContextAt(textBeforeCursor, bindings) : void 0;
|
|
170
216
|
const [, call, , value] = match;
|
|
171
217
|
if (call === "t" && value.includes(".")) {
|
|
172
218
|
const segments = value.split(".");
|
|
@@ -187,6 +233,23 @@ function completionContextAt(textBeforeCursor) {
|
|
|
187
233
|
length: value.length
|
|
188
234
|
};
|
|
189
235
|
}
|
|
236
|
+
/** `s.nav.fe|` where `s` holds a dictionary: the keys under `nav` that start with `fe`. */
|
|
237
|
+
function memberContextAt(textBeforeCursor, bindings) {
|
|
238
|
+
for (const [name, dictionary] of bindings) {
|
|
239
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
240
|
+
const match = new RegExp(`(?<![\\w$.])${escaped}((?:\\??\\.[A-Za-z_$][\\w$]*)*)\\??\\.([A-Za-z_$][\\w$]*)?$`).exec(textBeforeCursor);
|
|
241
|
+
if (!match) continue;
|
|
242
|
+
const parent = [...match[1].matchAll(/\??\.([A-Za-z_$][\w$]*)/g)].map((m) => m[1]);
|
|
243
|
+
const typed = match[2] ?? "";
|
|
244
|
+
return {
|
|
245
|
+
kind: "path",
|
|
246
|
+
dictionary,
|
|
247
|
+
parent,
|
|
248
|
+
typed,
|
|
249
|
+
length: typed.length
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
190
253
|
const preview = (text) => text === void 0 ? void 0 : text.length > 60 ? `${text.slice(0, 57)}...` : text;
|
|
191
254
|
function dictionaryEntries(model, context) {
|
|
192
255
|
return [...model.dictionaries.values()].filter((d) => d.name.startsWith(context.typed)).map((d) => {
|
|
@@ -293,6 +356,6 @@ function discoverProjects(folder) {
|
|
|
293
356
|
}));
|
|
294
357
|
}
|
|
295
358
|
//#endregion
|
|
296
|
-
export { addMissingKeys, addUnknownKey, buildHover, buildModel, classifyPath, completionContextAt, completionEntries, definitionFor, discoverProjects, escapeMarkdown, findReferenceAt, modelForFile, orderedLocales, resolveReference };
|
|
359
|
+
export { addMissingKeys, addUnknownKey, buildHover, buildModel, classifyPath, completionContextAt, completionEntries, definitionFor, discoverProjects, escapeMarkdown, findBindings, findReferenceAt, modelForFile, orderedLocales, resolveReference };
|
|
297
360
|
|
|
298
361
|
//# sourceMappingURL=features.mjs.map
|
package/dist/features.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"features.mjs","names":[],"sources":["../src/features/model.ts","../src/features/references.ts","../src/features/hover.ts","../src/features/definition.ts","../src/features/completion.ts","../src/features/quickfix.ts","../src/features/projects.ts"],"sourcesContent":["import path from \"node:path\";\n\nimport type { AnalysisResult, AnalyzedDictionary } from \"@dialexjs/cli/api\";\n\n/** What the editor features know about one Dialex project (a folder with its own config). */\nexport interface ProjectModel {\n root: string;\n defaultLocale: string;\n dictionaries: Map<string, AnalyzedDictionary>;\n}\n\nexport function buildModel(\n root: string,\n defaultLocale: string,\n result: Pick<AnalysisResult, \"dictionaries\">,\n): ProjectModel {\n return {\n root,\n defaultLocale,\n dictionaries: new Map(result.dictionaries.map((d) => [d.name, d])),\n };\n}\n\n/** The model whose root contains `file`, preferring the deepest (nested projects). */\nexport function modelForFile(\n models: Iterable<ProjectModel>,\n file: string,\n): ProjectModel | undefined {\n let best: ProjectModel | undefined;\n for (const model of models) {\n const rel = path.relative(model.root, file);\n if (rel.startsWith(\"..\") || path.isAbsolute(rel)) continue;\n if (!best || model.root.length > best.root.length) best = model;\n }\n return best;\n}\n\n/** Locales with the default locale first. */\nexport function orderedLocales(model: ProjectModel, dictionary: AnalyzedDictionary): string[] {\n return [\n ...dictionary.locales.filter((l) => l === model.defaultLocale),\n ...dictionary.locales.filter((l) => l !== model.defaultLocale),\n ];\n}\n","import { scanReferences, type AnalyzedDictionary, type Reference } from \"@dialexjs/cli/api\";\n\nimport type { ProjectModel } from \"./model.js\";\n\n/** `getDictionary(\"home\").nav.about` -> the `.nav.about` member chain after the call. */\nconst MEMBER_CHAIN =\n /(?:getDictionary|useDictionary)\\(\\s*([\"'`])([^\"'`$\\n]+)\\1[^()\\n]*\\)((?:\\??\\.[A-Za-z_$][\\w$]*)+)/dg;\n\n/**\n * The Dialex reference under the cursor on one line of code: a dictionary name, a `t(\"a.b.c\")`\n * path, or a member of `getDictionary(\"a\")` such as `.title`. Offsets are into `lineText`.\n */\nexport function findReferenceAt(lineText: string, character: number): Reference | undefined {\n const direct = scanReferences(lineText).find((r) => character >= r.start && character <= r.end);\n if (direct) return direct;\n\n for (const match of lineText.matchAll(MEMBER_CHAIN)) {\n const dictionary = match[2];\n const [chainStart] = match.indices![3] as [number, number];\n const members = /\\??\\.([A-Za-z_$][\\w$]*)/g;\n const keys: string[] = [];\n for (const member of match[3].matchAll(members)) {\n const start = chainStart + member.index! + member[0].length - member[1].length;\n const end = start + member[1].length;\n keys.push(member[1]);\n if (character >= start && character <= end) {\n return { kind: \"path\", value: [dictionary, ...keys].join(\".\"), start, end };\n }\n }\n }\n return undefined;\n}\n\nexport interface ResolvedReference {\n dictionary: AnalyzedDictionary;\n /** Empty for a reference to the dictionary itself. */\n keyPath: string[];\n}\n\nexport function resolveReference(\n model: ProjectModel,\n ref: Reference,\n): ResolvedReference | undefined {\n if (ref.kind === \"dictionary\") {\n const dictionary = model.dictionaries.get(ref.value);\n return dictionary ? { dictionary, keyPath: [] } : undefined;\n }\n const [name, ...keyPath] = ref.value.split(\".\");\n const dictionary = model.dictionaries.get(name);\n return dictionary ? { dictionary, keyPath } : undefined;\n}\n\n/** Whether `keyPath` is a leaf, a group of leaves, or nothing in this dictionary. */\nexport function classifyPath(\n dictionary: AnalyzedDictionary,\n keyPath: string[],\n): \"leaf\" | \"group\" | \"missing\" {\n const joined = keyPath.join(\".\");\n if (dictionary.paths.includes(joined)) return \"leaf\";\n return dictionary.paths.some((p) => p.startsWith(`${joined}.`)) ? \"group\" : \"missing\";\n}\n","import path from \"node:path\";\n\nimport { getLeafSource, type AnalyzedDictionary, type Reference } from \"@dialexjs/cli/api\";\n\nimport { orderedLocales, type ProjectModel } from \"./model.js\";\nimport { classifyPath, resolveReference } from \"./references.js\";\n\n/** Escapes Markdown so translation text cannot change the hover's formatting. */\nexport function escapeMarkdown(text: string): string {\n return text\n .replace(/[\\\\`*_{}[\\]()#+!|<>~]/g, \"\\\\$&\")\n .replace(/\\r?\\n/g, \"<br>\")\n .slice(0, 400);\n}\n\nfunction cell(dictionary: AnalyzedDictionary, locale: string, keyPath: string[]): string {\n const leaf = dictionary.leaves.get(locale)?.get(keyPath.join(\".\"));\n if (!leaf) return \"_missing_\";\n if (leaf.kind === \"string\") return escapeMarkdown(leaf.value ?? \"\");\n if (leaf.kind === \"function\") {\n const source = dictionary.df ? getLeafSource(dictionary.df, locale, keyPath) : undefined;\n return source\n ? `\\`${source.replace(/`/g, \"'\").replace(/\\s+/g, \" \").slice(0, 160)}\\``\n : \"_function_\";\n }\n return \"_value_\";\n}\n\n/**\n * Markdown for a hover over a Dialex reference: every locale's text for a key, or a summary of a\n * dictionary. Returns `undefined` for unknown references (diagnostics already flag those).\n */\nexport function buildHover(model: ProjectModel, ref: Reference): string | undefined {\n const resolved = resolveReference(model, ref);\n if (!resolved) return undefined;\n const { dictionary, keyPath } = resolved;\n const file = path.basename(dictionary.file);\n\n if (keyPath.length === 0) {\n return [\n `**${dictionary.name}** · ${file}`,\n \"\",\n `${dictionary.paths.length} key${dictionary.paths.length === 1 ? \"\" : \"s\"} · locales: ${dictionary.locales.join(\", \")}`,\n ].join(\"\\n\");\n }\n\n const kind = classifyPath(dictionary, keyPath);\n const title = `**${dictionary.name}.${keyPath.join(\".\")}** · ${file}`;\n\n if (kind === \"missing\") return undefined;\n\n if (kind === \"group\") {\n const prefix = `${keyPath.join(\".\")}.`;\n const children = new Set(\n dictionary.paths\n .filter((p) => p.startsWith(prefix))\n .map((p) => p.slice(prefix.length).split(\".\")[0]),\n );\n return `${title}\\n\\n${children.size} key${children.size === 1 ? \"\" : \"s\"}: ${[...children].map((c) => `\\`${c}\\``).join(\", \")}`;\n }\n\n const rows = orderedLocales(model, dictionary).map(\n (locale) => `| ${locale} | ${cell(dictionary, locale, keyPath)} |`,\n );\n return [title, \"\", \"| Locale | Text |\", \"| --- | --- |\", ...rows].join(\"\\n\");\n}\n","import { dictionaryLocation, locationOf, type SourceRange } from \"@dialexjs/cli/api\";\nimport type { Reference } from \"@dialexjs/cli/api\";\n\nimport { orderedLocales, type ProjectModel } from \"./model.js\";\nimport { resolveReference } from \"./references.js\";\n\nexport interface DefinitionTarget {\n file: string;\n range: SourceRange;\n}\n\nconst START: SourceRange = { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };\n\n/**\n * Where a reference is defined: the key in the default locale if it has one, otherwise in the\n * first locale that does. A dictionary reference points at the dictionary object.\n */\nexport function definitionFor(model: ProjectModel, ref: Reference): DefinitionTarget | undefined {\n const resolved = resolveReference(model, ref);\n if (!resolved) return undefined;\n const { dictionary, keyPath } = resolved;\n const { df } = dictionary;\n if (!df) return { file: dictionary.file, range: START };\n\n if (keyPath.length === 0) {\n return { file: dictionary.file, range: dictionaryLocation(df) ?? START };\n }\n\n for (const locale of orderedLocales(model, dictionary)) {\n const range = locationOf(df, locale, keyPath);\n if (range) return { file: dictionary.file, range };\n }\n return undefined;\n}\n","import type { AnalyzedDictionary } from \"@dialexjs/cli/api\";\n\nimport type { ProjectModel } from \"./model.js\";\n\nexport type CompletionContext =\n | { kind: \"dictionary\"; call: string; typed: string; length: number }\n | { kind: \"path\"; dictionary: string; parent: string[]; typed: string; length: number };\n\nconst CALL = /(?<![\\w$.])(t|getDictionary|useDictionary|DialexDictionary)\\(\\s*([\"'`])([^\"'`\\n]*)$/;\n\n/**\n * Whether the cursor is inside the string argument of a Dialex call, and what is being typed.\n * `textBeforeCursor` is the line up to the cursor. `length` is how many characters before the\n * cursor a completion replaces.\n */\nexport function completionContextAt(textBeforeCursor: string): CompletionContext | undefined {\n const match = CALL.exec(textBeforeCursor);\n if (!match) return undefined;\n const [, call, , value] = match;\n\n if (call === \"t\" && value.includes(\".\")) {\n const segments = value.split(\".\");\n const typed = segments.pop()!;\n const [dictionary, ...parent] = segments;\n return { kind: \"path\", dictionary, parent, typed, length: typed.length };\n }\n return { kind: \"dictionary\", call, typed: value, length: value.length };\n}\n\nexport interface CompletionEntry {\n label: string;\n kind: \"dictionary\" | \"group\" | \"leaf\";\n detail: string;\n /** Text to insert in place of what was typed. */\n insertText: string;\n /** Re-open the suggestion list after inserting (after `home.` or `nav.`). */\n retrigger: boolean;\n documentation?: string;\n}\n\nconst preview = (text: string | undefined) =>\n text === undefined ? undefined : text.length > 60 ? `${text.slice(0, 57)}...` : text;\n\nfunction dictionaryEntries(\n model: ProjectModel,\n context: Extract<CompletionContext, { kind: \"dictionary\" }>,\n) {\n return [...model.dictionaries.values()]\n .filter((d) => d.name.startsWith(context.typed))\n .map<CompletionEntry>((d) => {\n // t(\"home.title\") continues with a dot; getDictionary(\"home\") is complete as is\n const continues = context.call === \"t\";\n return {\n label: d.name,\n kind: \"dictionary\",\n detail: `${d.paths.length} keys · ${d.locales.join(\", \")}`,\n insertText: continues ? `${d.name}.` : d.name,\n retrigger: continues,\n };\n });\n}\n\nfunction pathEntries(\n model: ProjectModel,\n dictionary: AnalyzedDictionary,\n context: Extract<CompletionContext, { kind: \"path\" }>,\n) {\n const prefix = context.parent.length > 0 ? `${context.parent.join(\".\")}.` : \"\";\n const children = new Map<string, { leaf: boolean }>();\n\n for (const keyPath of dictionary.paths) {\n if (!keyPath.startsWith(prefix)) continue;\n const rest = keyPath.slice(prefix.length).split(\".\");\n const name = rest[0];\n if (!name.startsWith(context.typed)) continue;\n const entry = children.get(name) ?? { leaf: true };\n if (rest.length > 1) entry.leaf = false;\n children.set(name, entry);\n }\n\n return [...children].map<CompletionEntry>(([name, { leaf }]) => {\n const full = `${prefix}${name}`;\n const holder = [model.defaultLocale, ...dictionary.locales].find((l) =>\n dictionary.leaves.get(l)?.has(full),\n );\n const text = holder ? dictionary.leaves.get(holder)!.get(full)! : undefined;\n return {\n label: name,\n kind: leaf ? \"leaf\" : \"group\",\n detail: leaf\n ? (preview(text?.kind === \"string\" ? text.value : text ? `(${text.kind})` : undefined) ??\n \"\")\n : \"keys…\",\n insertText: leaf ? name : `${name}.`,\n retrigger: !leaf,\n };\n });\n}\n\n/** Suggestions for the string being typed, filtered by what is already there. */\nexport function completionEntries(\n model: ProjectModel,\n context: CompletionContext,\n): CompletionEntry[] {\n if (context.kind === \"dictionary\") return dictionaryEntries(model, context);\n const dictionary = model.dictionaries.get(context.dictionary);\n return dictionary ? pathEntries(model, dictionary, context) : [];\n}\n","import {\n TODO_PREFIX,\n copyLeaf,\n hasPath,\n listLocales,\n parseDictionaryText,\n renderDictionaryFile,\n setString,\n} from \"@dialexjs/cli/api\";\n\nexport interface MissingKey {\n locale: string;\n path: string[];\n /** A locale that has the key, to copy its value from. */\n sourceLocale?: string;\n}\n\n/**\n * New text for a dictionary file with `missing` keys added to their locales, copied from the\n * source locale (strings get the `[TODO] ` marker, like `dialex check --fix`). Returns\n * `undefined` if nothing could be added.\n */\nexport function addMissingKeys(\n file: string,\n text: string,\n missing: MissingKey[],\n): string | undefined {\n const df = parseDictionaryText(file, text);\n if (!df) return undefined;\n\n let changed = false;\n for (const { locale, path, sourceLocale } of missing) {\n if (!sourceLocale) continue;\n if (copyLeaf(df, sourceLocale, locale, path, (value) => `${TODO_PREFIX}${value}`))\n changed = true;\n }\n return changed ? renderDictionaryFile(df) : undefined;\n}\n\n/**\n * New text for a dictionary file with a key that code refers to but the dictionary lacks, added\n * to every locale as a `[TODO]` placeholder named after the key.\n */\nexport function addUnknownKey(file: string, text: string, keyPath: string[]): string | undefined {\n const df = parseDictionaryText(file, text);\n if (!df) return undefined;\n\n const placeholder = `${TODO_PREFIX}${keyPath[keyPath.length - 1]}`;\n let changed = false;\n for (const locale of listLocales(df)) {\n if (\n !hasPath(df, locale, keyPath) &&\n setString(df, locale, keyPath, placeholder) !== \"skipped\"\n ) {\n changed = true;\n }\n }\n return changed ? renderDictionaryFile(df) : undefined;\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { scanFiles } from \"@dialexjs/cli/api\";\n\nexport interface ProjectRoot {\n root: string;\n /** Globs (relative to `root`) for nested projects that belong to someone else. */\n ignore: string[];\n}\n\n// On top of the CLI's built-in list and the project's `.gitignore` files.\nconst IGNORE = [\"**/.git/**\", \"**/.nuxt/**\"];\n\n/** The nearest folder at or above `dir` (but inside `folder`) that has a package.json. */\nfunction packageRoot(folder: string, dir: string): string {\n for (let current = dir; current.startsWith(folder); current = path.dirname(current)) {\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\n if (current === folder) break;\n }\n return folder;\n}\n\n/**\n * Finds the Dialex projects inside a workspace folder.\n *\n * - Every directory with a `dialex.config.*` or `i18n.config.*` is a project.\n * - A `*.content.ts` file that no such project contains belongs to the nearest folder with a\n * `package.json` (or the workspace folder), which then uses the default settings. This covers\n * monorepos where only some packages have a config file.\n * - A project excludes the projects nested inside it, so one dictionary is never reported twice.\n */\nexport function discoverProjects(folder: string): ProjectRoot[] {\n const configs = scanFiles(folder, [\"**/dialex.config.*\", \"**/i18n.config.*\"], {\n ignore: IGNORE,\n });\n const configured = [...new Set(configs.map((f) => path.dirname(f)))];\n const roots = new Set(configured);\n\n // Judge coverage against the configured projects only; a fallback root must not hide others.\n const dictionaries = scanFiles(folder, \"**/*.content.ts\", { ignore: IGNORE });\n for (const file of dictionaries) {\n if (!configured.some((root) => file.startsWith(root + path.sep))) {\n roots.add(packageRoot(folder, path.dirname(file)));\n }\n }\n\n const all = [...roots].sort((a, b) => a.length - b.length);\n return all.map((root) => ({\n root,\n ignore: all\n .filter((other) => other !== root && other.startsWith(root + path.sep))\n .map((other) => `${path.relative(root, other).split(path.sep).join(\"/\")}/**`),\n }));\n}\n"],"mappings":";;;;AAWA,SAAgB,WACd,MACA,eACA,QACc;CACd,OAAO;EACL;EACA;EACA,cAAc,IAAI,IAAI,OAAO,aAAa,KAAK,MAAM,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;CACnE;AACF;;AAGA,SAAgB,aACd,QACA,MAC0B;CAC1B,IAAI;CACJ,KAAK,MAAM,SAAS,QAAQ;EAC1B,MAAM,MAAM,KAAK,SAAS,MAAM,MAAM,IAAI;EAC1C,IAAI,IAAI,WAAW,IAAI,KAAK,KAAK,WAAW,GAAG,GAAG;EAClD,IAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,KAAK,KAAK,QAAQ,OAAO;CAC5D;CACA,OAAO;AACT;;AAGA,SAAgB,eAAe,OAAqB,YAA0C;CAC5F,OAAO,CACL,GAAG,WAAW,QAAQ,QAAQ,MAAM,MAAM,MAAM,aAAa,GAC7D,GAAG,WAAW,QAAQ,QAAQ,MAAM,MAAM,MAAM,aAAa,CAC/D;AACF;;;;ACtCA,MAAM,eACJ;;;;;AAMF,SAAgB,gBAAgB,UAAkB,WAA0C;CAC1F,MAAM,SAAS,eAAe,QAAQ,CAAC,CAAC,MAAM,MAAM,aAAa,EAAE,SAAS,aAAa,EAAE,GAAG;CAC9F,IAAI,QAAQ,OAAO;CAEnB,KAAK,MAAM,SAAS,SAAS,SAAS,YAAY,GAAG;EACnD,MAAM,aAAa,MAAM;EACzB,MAAM,CAAC,cAAc,MAAM,QAAS;EACpC,MAAM,UAAU;EAChB,MAAM,OAAiB,CAAC;EACxB,KAAK,MAAM,UAAU,MAAM,EAAE,CAAC,SAAS,OAAO,GAAG;GAC/C,MAAM,QAAQ,aAAa,OAAO,QAAS,OAAO,EAAE,CAAC,SAAS,OAAO,EAAE,CAAC;GACxE,MAAM,MAAM,QAAQ,OAAO,EAAE,CAAC;GAC9B,KAAK,KAAK,OAAO,EAAE;GACnB,IAAI,aAAa,SAAS,aAAa,KACrC,OAAO;IAAE,MAAM;IAAQ,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,KAAK,GAAG;IAAG;IAAO;GAAI;EAE9E;CACF;AAEF;AAQA,SAAgB,iBACd,OACA,KAC+B;CAC/B,IAAI,IAAI,SAAS,cAAc;EAC7B,MAAM,aAAa,MAAM,aAAa,IAAI,IAAI,KAAK;EACnD,OAAO,aAAa;GAAE;GAAY,SAAS,CAAC;EAAE,IAAI,KAAA;CACpD;CACA,MAAM,CAAC,MAAM,GAAG,WAAW,IAAI,MAAM,MAAM,GAAG;CAC9C,MAAM,aAAa,MAAM,aAAa,IAAI,IAAI;CAC9C,OAAO,aAAa;EAAE;EAAY;CAAQ,IAAI,KAAA;AAChD;;AAGA,SAAgB,aACd,YACA,SAC8B;CAC9B,MAAM,SAAS,QAAQ,KAAK,GAAG;CAC/B,IAAI,WAAW,MAAM,SAAS,MAAM,GAAG,OAAO;CAC9C,OAAO,WAAW,MAAM,MAAM,MAAM,EAAE,WAAW,GAAG,OAAO,EAAE,CAAC,IAAI,UAAU;AAC9E;;;;ACpDA,SAAgB,eAAe,MAAsB;CACnD,OAAO,KACJ,QAAQ,0BAA0B,MAAM,CAAC,CACzC,QAAQ,UAAU,MAAM,CAAC,CACzB,MAAM,GAAG,GAAG;AACjB;AAEA,SAAS,KAAK,YAAgC,QAAgB,SAA2B;CACvF,MAAM,OAAO,WAAW,OAAO,IAAI,MAAM,CAAC,EAAE,IAAI,QAAQ,KAAK,GAAG,CAAC;CACjE,IAAI,CAAC,MAAM,OAAO;CAClB,IAAI,KAAK,SAAS,UAAU,OAAO,eAAe,KAAK,SAAS,EAAE;CAClE,IAAI,KAAK,SAAS,YAAY;EAC5B,MAAM,SAAS,WAAW,KAAK,cAAc,WAAW,IAAI,QAAQ,OAAO,IAAI,KAAA;EAC/E,OAAO,SACH,KAAK,OAAO,QAAQ,MAAM,GAAG,CAAC,CAAC,QAAQ,QAAQ,GAAG,CAAC,CAAC,MAAM,GAAG,GAAG,EAAE,MAClE;CACN;CACA,OAAO;AACT;;;;;AAMA,SAAgB,WAAW,OAAqB,KAAoC;CAClF,MAAM,WAAW,iBAAiB,OAAO,GAAG;CAC5C,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,EAAE,YAAY,YAAY;CAChC,MAAM,OAAO,KAAK,SAAS,WAAW,IAAI;CAE1C,IAAI,QAAQ,WAAW,GACrB,OAAO;EACL,KAAK,WAAW,KAAK,OAAO;EAC5B;EACA,GAAG,WAAW,MAAM,OAAO,MAAM,WAAW,MAAM,WAAW,IAAI,KAAK,IAAI,cAAc,WAAW,QAAQ,KAAK,IAAI;CACtH,CAAC,CAAC,KAAK,IAAI;CAGb,MAAM,OAAO,aAAa,YAAY,OAAO;CAC7C,MAAM,QAAQ,KAAK,WAAW,KAAK,GAAG,QAAQ,KAAK,GAAG,EAAE,OAAO;CAE/D,IAAI,SAAS,WAAW,OAAO,KAAA;CAE/B,IAAI,SAAS,SAAS;EACpB,MAAM,SAAS,GAAG,QAAQ,KAAK,GAAG,EAAE;EACpC,MAAM,WAAW,IAAI,IACnB,WAAW,MACR,QAAQ,MAAM,EAAE,WAAW,MAAM,CAAC,CAAC,CACnC,KAAK,MAAM,EAAE,MAAM,OAAO,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CACpD;EACA,OAAO,GAAG,MAAM,MAAM,SAAS,KAAK,MAAM,SAAS,SAAS,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAK,MAAM,KAAK,EAAE,GAAG,CAAC,CAAC,KAAK,IAAI;CAC7H;CAKA,OAAO;EAAC;EAAO;EAAI;EAAqB;EAAiB,GAH5C,eAAe,OAAO,UAAU,CAAC,CAAC,KAC5C,WAAW,KAAK,OAAO,KAAK,KAAK,YAAY,QAAQ,OAAO,EAAE,GAEF;CAAC,CAAC,CAAC,KAAK,IAAI;AAC7E;;;ACtDA,MAAM,QAAqB;CAAE,OAAO;EAAE,MAAM;EAAG,QAAQ;CAAE;CAAG,KAAK;EAAE,MAAM;EAAG,QAAQ;CAAE;AAAE;;;;;AAMxF,SAAgB,cAAc,OAAqB,KAA8C;CAC/F,MAAM,WAAW,iBAAiB,OAAO,GAAG;CAC5C,IAAI,CAAC,UAAU,OAAO,KAAA;CACtB,MAAM,EAAE,YAAY,YAAY;CAChC,MAAM,EAAE,OAAO;CACf,IAAI,CAAC,IAAI,OAAO;EAAE,MAAM,WAAW;EAAM,OAAO;CAAM;CAEtD,IAAI,QAAQ,WAAW,GACrB,OAAO;EAAE,MAAM,WAAW;EAAM,OAAO,mBAAmB,EAAE,KAAK;CAAM;CAGzE,KAAK,MAAM,UAAU,eAAe,OAAO,UAAU,GAAG;EACtD,MAAM,QAAQ,WAAW,IAAI,QAAQ,OAAO;EAC5C,IAAI,OAAO,OAAO;GAAE,MAAM,WAAW;GAAM;EAAM;CACnD;AAEF;;;ACzBA,MAAM,OAAO;;;;;;AAOb,SAAgB,oBAAoB,kBAAyD;CAC3F,MAAM,QAAQ,KAAK,KAAK,gBAAgB;CACxC,IAAI,CAAC,OAAO,OAAO,KAAA;CACnB,MAAM,GAAG,QAAQ,SAAS;CAE1B,IAAI,SAAS,OAAO,MAAM,SAAS,GAAG,GAAG;EACvC,MAAM,WAAW,MAAM,MAAM,GAAG;EAChC,MAAM,QAAQ,SAAS,IAAI;EAC3B,MAAM,CAAC,YAAY,GAAG,UAAU;EAChC,OAAO;GAAE,MAAM;GAAQ;GAAY;GAAQ;GAAO,QAAQ,MAAM;EAAO;CACzE;CACA,OAAO;EAAE,MAAM;EAAc;EAAM,OAAO;EAAO,QAAQ,MAAM;CAAO;AACxE;AAaA,MAAM,WAAW,SACf,SAAS,KAAA,IAAY,KAAA,IAAY,KAAK,SAAS,KAAK,GAAG,KAAK,MAAM,GAAG,EAAE,EAAE,OAAO;AAElF,SAAS,kBACP,OACA,SACA;CACA,OAAO,CAAC,GAAG,MAAM,aAAa,OAAO,CAAC,CAAC,CACpC,QAAQ,MAAM,EAAE,KAAK,WAAW,QAAQ,KAAK,CAAC,CAAC,CAC/C,KAAsB,MAAM;EAE3B,MAAM,YAAY,QAAQ,SAAS;EACnC,OAAO;GACL,OAAO,EAAE;GACT,MAAM;GACN,QAAQ,GAAG,EAAE,MAAM,OAAO,UAAU,EAAE,QAAQ,KAAK,IAAI;GACvD,YAAY,YAAY,GAAG,EAAE,KAAK,KAAK,EAAE;GACzC,WAAW;EACb;CACF,CAAC;AACL;AAEA,SAAS,YACP,OACA,YACA,SACA;CACA,MAAM,SAAS,QAAQ,OAAO,SAAS,IAAI,GAAG,QAAQ,OAAO,KAAK,GAAG,EAAE,KAAK;CAC5E,MAAM,2BAAW,IAAI,IAA+B;CAEpD,KAAK,MAAM,WAAW,WAAW,OAAO;EACtC,IAAI,CAAC,QAAQ,WAAW,MAAM,GAAG;EACjC,MAAM,OAAO,QAAQ,MAAM,OAAO,MAAM,CAAC,CAAC,MAAM,GAAG;EACnD,MAAM,OAAO,KAAK;EAClB,IAAI,CAAC,KAAK,WAAW,QAAQ,KAAK,GAAG;EACrC,MAAM,QAAQ,SAAS,IAAI,IAAI,KAAK,EAAE,MAAM,KAAK;EACjD,IAAI,KAAK,SAAS,GAAG,MAAM,OAAO;EAClC,SAAS,IAAI,MAAM,KAAK;CAC1B;CAEA,OAAO,CAAC,GAAG,QAAQ,CAAC,CAAC,KAAsB,CAAC,MAAM,EAAE,YAAY;EAC9D,MAAM,OAAO,GAAG,SAAS;EACzB,MAAM,SAAS,CAAC,MAAM,eAAe,GAAG,WAAW,OAAO,CAAC,CAAC,MAAM,MAChE,WAAW,OAAO,IAAI,CAAC,CAAC,EAAE,IAAI,IAAI,CACpC;EACA,MAAM,OAAO,SAAS,WAAW,OAAO,IAAI,MAAM,CAAC,CAAE,IAAI,IAAI,IAAK,KAAA;EAClE,OAAO;GACL,OAAO;GACP,MAAM,OAAO,SAAS;GACtB,QAAQ,OACH,QAAQ,MAAM,SAAS,WAAW,KAAK,QAAQ,OAAO,IAAI,KAAK,KAAK,KAAK,KAAA,CAAS,KACnF,KACA;GACJ,YAAY,OAAO,OAAO,GAAG,KAAK;GAClC,WAAW,CAAC;EACd;CACF,CAAC;AACH;;AAGA,SAAgB,kBACd,OACA,SACmB;CACnB,IAAI,QAAQ,SAAS,cAAc,OAAO,kBAAkB,OAAO,OAAO;CAC1E,MAAM,aAAa,MAAM,aAAa,IAAI,QAAQ,UAAU;CAC5D,OAAO,aAAa,YAAY,OAAO,YAAY,OAAO,IAAI,CAAC;AACjE;;;;;;;;ACrFA,SAAgB,eACd,MACA,MACA,SACoB;CACpB,MAAM,KAAK,oBAAoB,MAAM,IAAI;CACzC,IAAI,CAAC,IAAI,OAAO,KAAA;CAEhB,IAAI,UAAU;CACd,KAAK,MAAM,EAAE,QAAQ,MAAM,kBAAkB,SAAS;EACpD,IAAI,CAAC,cAAc;EACnB,IAAI,SAAS,IAAI,cAAc,QAAQ,OAAO,UAAU,GAAG,cAAc,OAAO,GAC9E,UAAU;CACd;CACA,OAAO,UAAU,qBAAqB,EAAE,IAAI,KAAA;AAC9C;;;;;AAMA,SAAgB,cAAc,MAAc,MAAc,SAAuC;CAC/F,MAAM,KAAK,oBAAoB,MAAM,IAAI;CACzC,IAAI,CAAC,IAAI,OAAO,KAAA;CAEhB,MAAM,cAAc,GAAG,cAAc,QAAQ,QAAQ,SAAS;CAC9D,IAAI,UAAU;CACd,KAAK,MAAM,UAAU,YAAY,EAAE,GACjC,IACE,CAAC,QAAQ,IAAI,QAAQ,OAAO,KAC5B,UAAU,IAAI,QAAQ,SAAS,WAAW,MAAM,WAEhD,UAAU;CAGd,OAAO,UAAU,qBAAqB,EAAE,IAAI,KAAA;AAC9C;;;AC9CA,MAAM,SAAS,CAAC,cAAc,aAAa;;AAG3C,SAAS,YAAY,QAAgB,KAAqB;CACxD,KAAK,IAAI,UAAU,KAAK,QAAQ,WAAW,MAAM,GAAG,UAAU,KAAK,QAAQ,OAAO,GAAG;EACnF,IAAI,GAAG,WAAW,KAAK,KAAK,SAAS,cAAc,CAAC,GAAG,OAAO;EAC9D,IAAI,YAAY,QAAQ;CAC1B;CACA,OAAO;AACT;;;;;;;;;;AAWA,SAAgB,iBAAiB,QAA+B;CAC9D,MAAM,UAAU,UAAU,QAAQ,CAAC,sBAAsB,kBAAkB,GAAG,EAC5E,QAAQ,OACV,CAAC;CACD,MAAM,aAAa,CAAC,GAAG,IAAI,IAAI,QAAQ,KAAK,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC;CACnE,MAAM,QAAQ,IAAI,IAAI,UAAU;CAGhC,MAAM,eAAe,UAAU,QAAQ,mBAAmB,EAAE,QAAQ,OAAO,CAAC;CAC5E,KAAK,MAAM,QAAQ,cACjB,IAAI,CAAC,WAAW,MAAM,SAAS,KAAK,WAAW,OAAO,KAAK,GAAG,CAAC,GAC7D,MAAM,IAAI,YAAY,QAAQ,KAAK,QAAQ,IAAI,CAAC,CAAC;CAIrD,MAAM,MAAM,CAAC,GAAG,KAAK,CAAC,CAAC,MAAM,GAAG,MAAM,EAAE,SAAS,EAAE,MAAM;CACzD,OAAO,IAAI,KAAK,UAAU;EACxB;EACA,QAAQ,IACL,QAAQ,UAAU,UAAU,QAAQ,MAAM,WAAW,OAAO,KAAK,GAAG,CAAC,CAAC,CACtE,KAAK,UAAU,GAAG,KAAK,SAAS,MAAM,KAAK,CAAC,CAAC,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,IAAI;CAChF,EAAE;AACJ"}
|
|
1
|
+
{"version":3,"file":"features.mjs","names":[],"sources":["../src/features/model.ts","../src/features/bindings.ts","../src/features/references.ts","../src/features/hover.ts","../src/features/definition.ts","../src/features/completion.ts","../src/features/quickfix.ts","../src/features/projects.ts"],"sourcesContent":["import path from \"node:path\";\n\nimport type { AnalysisResult, AnalyzedDictionary } from \"@dialexjs/cli/api\";\n\n/** What the editor features know about one Dialex project (a folder with its own config). */\nexport interface ProjectModel {\n root: string;\n defaultLocale: string;\n dictionaries: Map<string, AnalyzedDictionary>;\n}\n\nexport function buildModel(\n root: string,\n defaultLocale: string,\n result: Pick<AnalysisResult, \"dictionaries\">,\n): ProjectModel {\n return {\n root,\n defaultLocale,\n dictionaries: new Map(result.dictionaries.map((d) => [d.name, d])),\n };\n}\n\n/** The model whose root contains `file`, preferring the deepest (nested projects). */\nexport function modelForFile(\n models: Iterable<ProjectModel>,\n file: string,\n): ProjectModel | undefined {\n let best: ProjectModel | undefined;\n for (const model of models) {\n const rel = path.relative(model.root, file);\n if (rel.startsWith(\"..\") || path.isAbsolute(rel)) continue;\n if (!best || model.root.length > best.root.length) best = model;\n }\n return best;\n}\n\n/** Locales with the default locale first. */\nexport function orderedLocales(model: ProjectModel, dictionary: AnalyzedDictionary): string[] {\n return [\n ...dictionary.locales.filter((l) => l === model.defaultLocale),\n ...dictionary.locales.filter((l) => l !== model.defaultLocale),\n ];\n}\n","/** Variable name -> dictionary name, for `const s = useDictionary(\"showcase\")`. */\nexport type Bindings = ReadonlyMap<string, string>;\n\n/**\n * `const s = useDictionary(\"showcase\")`, `const home = await getDictionary(\"home\", locale)`,\n * `let t: Home = injectDictionary<Home>(\"home\")`: a variable that holds a dictionary.\n */\nconst DECLARATION =\n /\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=\\n]+)?=\\s*(?:await\\s+)?(?:[A-Za-z_$][\\w$]*\\.)*(?:getDictionary|useDictionary|injectDictionary)\\s*(?:<[^()]*?>)?\\(\\s*([\"'`])([^\"'`$\\n]+)\\2/g;\n\n/**\n * Finds the variables in a file that hold a dictionary, so `s.nav.features` can be resolved to\n * `showcase.nav.features`. It is a text match: a variable that is reassigned or shadowed in another\n * scope may resolve to the wrong dictionary, which only affects editor hints.\n */\nexport function findBindings(text: string): Bindings {\n const bindings = new Map<string, string>();\n for (const match of text.matchAll(DECLARATION)) {\n bindings.set(match[1], match[3]);\n }\n return bindings;\n}\n","import { scanReferences, type AnalyzedDictionary, type Reference } from \"@dialexjs/cli/api\";\n\nimport type { Bindings } from \"./bindings.js\";\nimport type { ProjectModel } from \"./model.js\";\n\n/** `getDictionary(\"home\").nav.about` -> the `.nav.about` member chain after the call. */\nconst MEMBER_CHAIN =\n /(?:getDictionary|useDictionary)\\(\\s*([\"'`])([^\"'`$\\n]+)\\1[^()\\n]*\\)((?:\\??\\.[A-Za-z_$][\\w$]*)+)/dg;\n\nconst escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/**\n * The Dialex reference under the cursor on one line of code: a dictionary name, a `t(\"a.b.c\")`\n * path, a member of `getDictionary(\"a\")` such as `.title`, or, with `bindings`, a member of a\n * variable that holds a dictionary (`s.nav.features` after `const s = useDictionary(\"showcase\")`).\n * Offsets are into `lineText`.\n */\nexport function findReferenceAt(\n lineText: string,\n character: number,\n bindings?: Bindings,\n): Reference | undefined {\n const direct = scanReferences(lineText).find((r) => character >= r.start && character <= r.end);\n if (direct) return direct;\n\n for (const match of lineText.matchAll(MEMBER_CHAIN)) {\n const dictionary = match[2];\n const [chainStart] = match.indices![3] as [number, number];\n const members = /\\??\\.([A-Za-z_$][\\w$]*)/g;\n const keys: string[] = [];\n for (const member of match[3].matchAll(members)) {\n const start = chainStart + member.index! + member[0].length - member[1].length;\n const end = start + member[1].length;\n keys.push(member[1]);\n if (character >= start && character <= end) {\n return { kind: \"path\", value: [dictionary, ...keys].join(\".\"), start, end };\n }\n }\n }\n\n for (const [name, dictionary] of bindings ?? []) {\n const chain = new RegExp(\n `(?<![\\\\w$.])${escapeRegExp(name)}(?![\\\\w$])((?:\\\\??\\\\.[A-Za-z_$][\\\\w$]*)*)`,\n \"dg\",\n );\n for (const match of lineText.matchAll(chain)) {\n const nameStart = match.index!;\n const nameEnd = nameStart + name.length;\n if (character >= nameStart && character <= nameEnd) {\n return { kind: \"dictionary\", value: dictionary, start: nameStart, end: nameEnd };\n }\n\n const [chainStart] = match.indices![1] as [number, number];\n const keys: string[] = [];\n for (const member of match[1].matchAll(/\\??\\.([A-Za-z_$][\\w$]*)/g)) {\n const start = chainStart + member.index! + member[0].length - member[1].length;\n const end = start + member[1].length;\n keys.push(member[1]);\n if (character >= start && character <= end) {\n return { kind: \"path\", value: [dictionary, ...keys].join(\".\"), start, end };\n }\n }\n }\n }\n return undefined;\n}\n\nexport interface ResolvedReference {\n dictionary: AnalyzedDictionary;\n /** Empty for a reference to the dictionary itself. */\n keyPath: string[];\n}\n\nexport function resolveReference(\n model: ProjectModel,\n ref: Reference,\n): ResolvedReference | undefined {\n if (ref.kind === \"dictionary\") {\n const dictionary = model.dictionaries.get(ref.value);\n return dictionary ? { dictionary, keyPath: [] } : undefined;\n }\n const [name, ...keyPath] = ref.value.split(\".\");\n const dictionary = model.dictionaries.get(name);\n return dictionary ? { dictionary, keyPath } : undefined;\n}\n\n/** Whether `keyPath` is a leaf, a group of leaves, or nothing in this dictionary. */\nexport function classifyPath(\n dictionary: AnalyzedDictionary,\n keyPath: string[],\n): \"leaf\" | \"group\" | \"missing\" {\n const joined = keyPath.join(\".\");\n if (dictionary.paths.includes(joined)) return \"leaf\";\n return dictionary.paths.some((p) => p.startsWith(`${joined}.`)) ? \"group\" : \"missing\";\n}\n","import path from \"node:path\";\n\nimport { getLeafSource, type AnalyzedDictionary, type Reference } from \"@dialexjs/cli/api\";\n\nimport { orderedLocales, type ProjectModel } from \"./model.js\";\nimport { classifyPath, resolveReference } from \"./references.js\";\n\n/** Escapes Markdown so translation text cannot change the hover's formatting. */\nexport function escapeMarkdown(text: string): string {\n return text\n .replace(/[\\\\`*_{}[\\]()#+!|<>~]/g, \"\\\\$&\")\n .replace(/\\r?\\n/g, \"<br>\")\n .slice(0, 400);\n}\n\nfunction cell(dictionary: AnalyzedDictionary, locale: string, keyPath: string[]): string {\n const leaf = dictionary.leaves.get(locale)?.get(keyPath.join(\".\"));\n if (!leaf) return \"_missing_\";\n if (leaf.kind === \"string\") return escapeMarkdown(leaf.value ?? \"\");\n if (leaf.kind === \"function\") {\n const source = dictionary.df ? getLeafSource(dictionary.df, locale, keyPath) : undefined;\n return source\n ? `\\`${source.replace(/`/g, \"'\").replace(/\\s+/g, \" \").slice(0, 160)}\\``\n : \"_function_\";\n }\n return \"_value_\";\n}\n\n/**\n * Markdown for a hover over a Dialex reference: every locale's text for a key, or a summary of a\n * dictionary. Returns `undefined` for unknown references (diagnostics already flag those).\n */\nexport function buildHover(model: ProjectModel, ref: Reference): string | undefined {\n const resolved = resolveReference(model, ref);\n if (!resolved) return undefined;\n const { dictionary, keyPath } = resolved;\n const file = path.basename(dictionary.file);\n\n if (keyPath.length === 0) {\n return [\n `**${dictionary.name}** · ${file}`,\n \"\",\n `${dictionary.paths.length} key${dictionary.paths.length === 1 ? \"\" : \"s\"} · locales: ${dictionary.locales.join(\", \")}`,\n ].join(\"\\n\");\n }\n\n const kind = classifyPath(dictionary, keyPath);\n const title = `**${dictionary.name}.${keyPath.join(\".\")}** · ${file}`;\n\n if (kind === \"missing\") return undefined;\n\n if (kind === \"group\") {\n const prefix = `${keyPath.join(\".\")}.`;\n const children = new Set(\n dictionary.paths\n .filter((p) => p.startsWith(prefix))\n .map((p) => p.slice(prefix.length).split(\".\")[0]),\n );\n return `${title}\\n\\n${children.size} key${children.size === 1 ? \"\" : \"s\"}: ${[...children].map((c) => `\\`${c}\\``).join(\", \")}`;\n }\n\n const rows = orderedLocales(model, dictionary).map(\n (locale) => `| ${locale} | ${cell(dictionary, locale, keyPath)} |`,\n );\n return [title, \"\", \"| Locale | Text |\", \"| --- | --- |\", ...rows].join(\"\\n\");\n}\n","import { dictionaryLocation, locationOf, type SourceRange } from \"@dialexjs/cli/api\";\nimport type { Reference } from \"@dialexjs/cli/api\";\n\nimport { orderedLocales, type ProjectModel } from \"./model.js\";\nimport { resolveReference } from \"./references.js\";\n\nexport interface DefinitionTarget {\n file: string;\n range: SourceRange;\n}\n\nconst START: SourceRange = { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } };\n\n/**\n * Where a reference is defined: the key in the default locale if it has one, otherwise in the\n * first locale that does. A dictionary reference points at the dictionary object.\n */\nexport function definitionFor(model: ProjectModel, ref: Reference): DefinitionTarget | undefined {\n const resolved = resolveReference(model, ref);\n if (!resolved) return undefined;\n const { dictionary, keyPath } = resolved;\n const { df } = dictionary;\n if (!df) return { file: dictionary.file, range: START };\n\n if (keyPath.length === 0) {\n return { file: dictionary.file, range: dictionaryLocation(df) ?? START };\n }\n\n for (const locale of orderedLocales(model, dictionary)) {\n const range = locationOf(df, locale, keyPath);\n if (range) return { file: dictionary.file, range };\n }\n return undefined;\n}\n","import type { AnalyzedDictionary } from \"@dialexjs/cli/api\";\n\nimport type { Bindings } from \"./bindings.js\";\nimport type { ProjectModel } from \"./model.js\";\n\nexport type CompletionContext =\n | { kind: \"dictionary\"; call: string; typed: string; length: number }\n | { kind: \"path\"; dictionary: string; parent: string[]; typed: string; length: number };\n\nconst CALL = /(?<![\\w$.])(t|getDictionary|useDictionary|DialexDictionary)\\(\\s*([\"'`])([^\"'`\\n]*)$/;\n\n/**\n * Whether the cursor is inside the string argument of a Dialex call, and what is being typed.\n * `textBeforeCursor` is the line up to the cursor. `length` is how many characters before the\n * cursor a completion replaces.\n */\nexport function completionContextAt(\n textBeforeCursor: string,\n bindings?: Bindings,\n): CompletionContext | undefined {\n const match = CALL.exec(textBeforeCursor);\n if (!match) return bindings ? memberContextAt(textBeforeCursor, bindings) : undefined;\n const [, call, , value] = match;\n\n if (call === \"t\" && value.includes(\".\")) {\n const segments = value.split(\".\");\n const typed = segments.pop()!;\n const [dictionary, ...parent] = segments;\n return { kind: \"path\", dictionary, parent, typed, length: typed.length };\n }\n return { kind: \"dictionary\", call, typed: value, length: value.length };\n}\n\n/** `s.nav.fe|` where `s` holds a dictionary: the keys under `nav` that start with `fe`. */\nfunction memberContextAt(\n textBeforeCursor: string,\n bindings: Bindings,\n): CompletionContext | undefined {\n for (const [name, dictionary] of bindings) {\n const escaped = name.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n const match = new RegExp(\n `(?<![\\\\w$.])${escaped}((?:\\\\??\\\\.[A-Za-z_$][\\\\w$]*)*)\\\\??\\\\.([A-Za-z_$][\\\\w$]*)?$`,\n ).exec(textBeforeCursor);\n if (!match) continue;\n\n const parent = [...match[1].matchAll(/\\??\\.([A-Za-z_$][\\w$]*)/g)].map((m) => m[1]);\n const typed = match[2] ?? \"\";\n return { kind: \"path\", dictionary, parent, typed, length: typed.length };\n }\n return undefined;\n}\n\nexport interface CompletionEntry {\n label: string;\n kind: \"dictionary\" | \"group\" | \"leaf\";\n detail: string;\n /** Text to insert in place of what was typed. */\n insertText: string;\n /** Re-open the suggestion list after inserting (after `home.` or `nav.`). */\n retrigger: boolean;\n documentation?: string;\n}\n\nconst preview = (text: string | undefined) =>\n text === undefined ? undefined : text.length > 60 ? `${text.slice(0, 57)}...` : text;\n\nfunction dictionaryEntries(\n model: ProjectModel,\n context: Extract<CompletionContext, { kind: \"dictionary\" }>,\n) {\n return [...model.dictionaries.values()]\n .filter((d) => d.name.startsWith(context.typed))\n .map<CompletionEntry>((d) => {\n // t(\"home.title\") continues with a dot; getDictionary(\"home\") is complete as is\n const continues = context.call === \"t\";\n return {\n label: d.name,\n kind: \"dictionary\",\n detail: `${d.paths.length} keys · ${d.locales.join(\", \")}`,\n insertText: continues ? `${d.name}.` : d.name,\n retrigger: continues,\n };\n });\n}\n\nfunction pathEntries(\n model: ProjectModel,\n dictionary: AnalyzedDictionary,\n context: Extract<CompletionContext, { kind: \"path\" }>,\n) {\n const prefix = context.parent.length > 0 ? `${context.parent.join(\".\")}.` : \"\";\n const children = new Map<string, { leaf: boolean }>();\n\n for (const keyPath of dictionary.paths) {\n if (!keyPath.startsWith(prefix)) continue;\n const rest = keyPath.slice(prefix.length).split(\".\");\n const name = rest[0];\n if (!name.startsWith(context.typed)) continue;\n const entry = children.get(name) ?? { leaf: true };\n if (rest.length > 1) entry.leaf = false;\n children.set(name, entry);\n }\n\n return [...children].map<CompletionEntry>(([name, { leaf }]) => {\n const full = `${prefix}${name}`;\n const holder = [model.defaultLocale, ...dictionary.locales].find((l) =>\n dictionary.leaves.get(l)?.has(full),\n );\n const text = holder ? dictionary.leaves.get(holder)!.get(full)! : undefined;\n return {\n label: name,\n kind: leaf ? \"leaf\" : \"group\",\n detail: leaf\n ? (preview(text?.kind === \"string\" ? text.value : text ? `(${text.kind})` : undefined) ??\n \"\")\n : \"keys…\",\n insertText: leaf ? name : `${name}.`,\n retrigger: !leaf,\n };\n });\n}\n\n/** Suggestions for the string being typed, filtered by what is already there. */\nexport function completionEntries(\n model: ProjectModel,\n context: CompletionContext,\n): CompletionEntry[] {\n if (context.kind === \"dictionary\") return dictionaryEntries(model, context);\n const dictionary = model.dictionaries.get(context.dictionary);\n return dictionary ? pathEntries(model, dictionary, context) : [];\n}\n","import {\n TODO_PREFIX,\n copyLeaf,\n hasPath,\n listLocales,\n parseDictionaryText,\n renderDictionaryFile,\n setString,\n} from \"@dialexjs/cli/api\";\n\nexport interface MissingKey {\n locale: string;\n path: string[];\n /** A locale that has the key, to copy its value from. */\n sourceLocale?: string;\n}\n\n/**\n * New text for a dictionary file with `missing` keys added to their locales, copied from the\n * source locale (strings get the `[TODO] ` marker, like `dialex check --fix`). Returns\n * `undefined` if nothing could be added.\n */\nexport function addMissingKeys(\n file: string,\n text: string,\n missing: MissingKey[],\n): string | undefined {\n const df = parseDictionaryText(file, text);\n if (!df) return undefined;\n\n let changed = false;\n for (const { locale, path, sourceLocale } of missing) {\n if (!sourceLocale) continue;\n if (copyLeaf(df, sourceLocale, locale, path, (value) => `${TODO_PREFIX}${value}`))\n changed = true;\n }\n return changed ? renderDictionaryFile(df) : undefined;\n}\n\n/**\n * New text for a dictionary file with a key that code refers to but the dictionary lacks, added\n * to every locale as a `[TODO]` placeholder named after the key.\n */\nexport function addUnknownKey(file: string, text: string, keyPath: string[]): string | undefined {\n const df = parseDictionaryText(file, text);\n if (!df) return undefined;\n\n const placeholder = `${TODO_PREFIX}${keyPath[keyPath.length - 1]}`;\n let changed = false;\n for (const locale of listLocales(df)) {\n if (\n !hasPath(df, locale, keyPath) &&\n setString(df, locale, keyPath, placeholder) !== \"skipped\"\n ) {\n changed = true;\n }\n }\n return changed ? renderDictionaryFile(df) : undefined;\n}\n","import fs from \"node:fs\";\nimport path from \"node:path\";\n\nimport { scanFiles } from \"@dialexjs/cli/api\";\n\nexport interface ProjectRoot {\n root: string;\n /** Globs (relative to `root`) for nested projects that belong to someone else. */\n ignore: string[];\n}\n\n// On top of the CLI's built-in list and the project's `.gitignore` files.\nconst IGNORE = [\"**/.git/**\", \"**/.nuxt/**\"];\n\n/** The nearest folder at or above `dir` (but inside `folder`) that has a package.json. */\nfunction packageRoot(folder: string, dir: string): string {\n for (let current = dir; current.startsWith(folder); current = path.dirname(current)) {\n if (fs.existsSync(path.join(current, \"package.json\"))) return current;\n if (current === folder) break;\n }\n return folder;\n}\n\n/**\n * Finds the Dialex projects inside a workspace folder.\n *\n * - Every directory with a `dialex.config.*` or `i18n.config.*` is a project.\n * - A `*.content.ts` file that no such project contains belongs to the nearest folder with a\n * `package.json` (or the workspace folder), which then uses the default settings. This covers\n * monorepos where only some packages have a config file.\n * - A project excludes the projects nested inside it, so one dictionary is never reported twice.\n */\nexport function discoverProjects(folder: string): ProjectRoot[] {\n const configs = scanFiles(folder, [\"**/dialex.config.*\", \"**/i18n.config.*\"], {\n ignore: IGNORE,\n });\n const configured = [...new Set(configs.map((f) => path.dirname(f)))];\n const roots = new Set(configured);\n\n // Judge coverage against the configured projects only; a fallback root must not hide others.\n const dictionaries = scanFiles(folder, \"**/*.content.ts\", { ignore: IGNORE });\n for (const file of dictionaries) {\n if (!configured.some((root) => file.startsWith(root + path.sep))) {\n roots.add(packageRoot(folder, path.dirname(file)));\n }\n }\n\n const all = [...roots].sort((a, b) => a.length - b.length);\n return all.map((root) => ({\n root,\n ignore: all\n .filter((other) => other !== root && other.startsWith(root + path.sep))\n .map((other) => `${path.relative(root, other).split(path.sep).join(\"/\")}/**`),\n }));\n}\n"],"mappings":"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"}
|
package/dist/server.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;iBAqDiB;;EAEf;;EAEA;;;;;EAKA;;EAEA;;iBAkBe;;EAEf,WAAW;;EAEX,cAAc;;EAEd,WAAW;;;;;;wBA0EG,aAAa,YAAY,aAAa"}
|
package/dist/server.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addMissingKeys, addUnknownKey, buildHover, buildModel, completionContextAt, completionEntries, definitionFor, discoverProjects, findReferenceAt, modelForFile } from "./features.mjs";
|
|
1
|
+
import { addMissingKeys, addUnknownKey, buildHover, buildModel, completionContextAt, completionEntries, definitionFor, discoverProjects, findBindings, findReferenceAt, modelForFile } from "./features.mjs";
|
|
2
2
|
import { CodeActionKind, CompletionItemKind, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesNotification, FileChangeType, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver/node.js";
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
@@ -301,7 +301,7 @@ function createServer(connection) {
|
|
|
301
301
|
const file = uriToPath(params.textDocument.uri);
|
|
302
302
|
const model = document && file ? modelFor(file) : void 0;
|
|
303
303
|
if (!document || !model) return null;
|
|
304
|
-
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character);
|
|
304
|
+
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
305
305
|
const markdown = ref ? buildHover(model, ref) : void 0;
|
|
306
306
|
if (!ref || !markdown) return null;
|
|
307
307
|
return {
|
|
@@ -326,7 +326,7 @@ function createServer(connection) {
|
|
|
326
326
|
const file = uriToPath(params.textDocument.uri);
|
|
327
327
|
const model = document && file ? modelFor(file) : void 0;
|
|
328
328
|
if (!document || !model) return null;
|
|
329
|
-
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character);
|
|
329
|
+
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
330
330
|
const target = ref ? definitionFor(model, ref) : void 0;
|
|
331
331
|
if (!target) return null;
|
|
332
332
|
return {
|
|
@@ -349,7 +349,7 @@ function createServer(connection) {
|
|
|
349
349
|
const model = document && file ? modelFor(file) : void 0;
|
|
350
350
|
if (!document || !model) return [];
|
|
351
351
|
const before = lineOf(document, params.position.line).slice(0, params.position.character);
|
|
352
|
-
const context = completionContextAt(before);
|
|
352
|
+
const context = completionContextAt(before, findBindings(document.getText()));
|
|
353
353
|
if (!context) return [];
|
|
354
354
|
const range = {
|
|
355
355
|
start: {
|
package/dist/server.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nimport {\n analyzeProject,\n clearGitignore,\n createAnalysisCache,\n generateDictionaries,\n readStaticConfig,\n renderGenerated,\n startGenerateWatcher,\n type AnalysisIssue,\n} from \"@dialexjs/cli/api\";\nimport { TextDocument } from \"vscode-languageserver-textdocument\";\nimport {\n CodeActionKind,\n CompletionItemKind,\n DiagnosticSeverity,\n DiagnosticTag,\n DidChangeWatchedFilesNotification,\n FileChangeType,\n TextDocumentSyncKind,\n TextDocuments,\n type CodeAction,\n type CompletionItem,\n type Connection,\n type Diagnostic,\n type Hover,\n type Location,\n type TextEdit,\n type InitializeParams,\n type InitializeResult,\n} from \"vscode-languageserver/node.js\";\n\nimport {\n addMissingKeys,\n addUnknownKey,\n buildHover,\n buildModel,\n completionContextAt,\n completionEntries,\n definitionFor,\n discoverProjects,\n findReferenceAt,\n modelForFile,\n type CompletionEntry,\n type MissingKey,\n type ProjectModel,\n type ProjectRoot,\n} from \"./features.js\";\n\n/** What the client can configure under the `dialex` section. */\nexport interface Settings {\n /** Turn diagnostics, hover and the other features off. */\n enable: boolean;\n /** Fade out keys and dictionaries that no source file seems to use. */\n unusedKeys: boolean;\n /**\n * Regenerate `dialex.generated.ts` and `dialex-env.d.ts` when a dictionary or config file\n * changes, in projects that already have a generated file.\n */\n autoGenerate: boolean;\n /** Path to the Dialex config file, relative to each project root. */\n configPath?: string;\n}\n\nconst DEFAULT_SETTINGS: Settings = { enable: true, unusedKeys: false, autoGenerate: true };\n\n/** Files whose creation or removal can change the set of Dialex projects or what is scanned. */\nconst WATCHED_GLOBS = [\n \"**/*.content.ts\",\n \"**/dialex.config.*\",\n \"**/i18n.config.*\",\n \"**/.gitignore\",\n];\n\ninterface ProjectState {\n model: ProjectModel;\n issues: AnalysisIssue[];\n}\n\nexport interface DialexServer {\n /** Re-analyze every project now and publish diagnostics. */\n refresh(): Promise<void>;\n /** Regenerate the generated files of every project that has them, now. */\n regenerate(): Promise<void>;\n /** Stop file watchers and timers. */\n dispose(): Promise<void>;\n}\n\nfunction severityOf(issue: AnalysisIssue): DiagnosticSeverity {\n if (issue.code === \"unused-key\" || issue.code === \"unused-dictionary\") {\n return DiagnosticSeverity.Hint;\n }\n switch (issue.level) {\n case \"error\":\n return DiagnosticSeverity.Error;\n case \"warning\":\n return DiagnosticSeverity.Warning;\n default:\n return DiagnosticSeverity.Information;\n }\n}\n\n/** What a quick fix needs from the issue that produced a diagnostic. */\ninterface IssueData {\n code: string;\n path?: string[];\n locale?: string;\n sourceLocale?: string;\n dictionary?: string;\n}\n\nconst COMPLETION_KINDS: Record<CompletionEntry[\"kind\"], CompletionItemKind> = {\n dictionary: CompletionItemKind.Module,\n group: CompletionItemKind.Folder,\n leaf: CompletionItemKind.Text,\n};\n\n/** An edit that replaces a whole document, which is how dictionary files are rewritten. */\nfunction replaceAll(text: string, newText: string): TextEdit {\n const lines = text.split(\"\\n\");\n return {\n range: {\n start: { line: 0, character: 0 },\n end: { line: lines.length - 1, character: lines[lines.length - 1].length },\n },\n newText,\n };\n}\n\nfunction toDiagnostic(issue: AnalysisIssue): Diagnostic {\n const severity = severityOf(issue);\n return {\n range: issue.range\n ? {\n start: { line: issue.range.start.line, character: issue.range.start.column },\n end: { line: issue.range.end.line, character: issue.range.end.column },\n }\n : { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },\n message: issue.message,\n severity,\n source: \"dialex\",\n code: issue.code,\n tags: severity === DiagnosticSeverity.Hint ? [DiagnosticTag.Unnecessary] : undefined,\n data: {\n code: issue.code,\n path: issue.path,\n locale: issue.locale,\n sourceLocale: issue.sourceLocale,\n dictionary: issue.dictionary,\n } satisfies IssueData,\n };\n}\n\nconst uriToPath = (uri: string) => (uri.startsWith(\"file:\") ? fileURLToPath(uri) : undefined);\n\n/**\n * Wires Dialex's editor features to an LSP connection. Nothing in the project is executed:\n * dictionaries and configs are read from the syntax tree (`runtime: false`).\n */\nexport function createServer(connection: Connection): DialexServer {\n const documents = new TextDocuments(TextDocument);\n const cache = createAnalysisCache();\n const states = new Map<string, ProjectState>();\n const projectCache = new Map<string, ProjectRoot[]>();\n\n let folders: string[] = [];\n let canConfigure = false;\n let settings: Settings = { ...DEFAULT_SETTINGS };\n let published = new Set<string>();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let generateTimer: ReturnType<typeof setTimeout> | undefined;\n /** True when the client sends file events itself; otherwise the server watches the disk. */\n let clientWatches = false;\n const watchers = new Map<string, { close(): Promise<void> }>();\n\n const projectsIn = (folder: string): ProjectRoot[] => {\n let projects = projectCache.get(folder);\n if (!projects) {\n projects = discoverProjects(folder);\n projectCache.set(folder, projects);\n }\n return projects;\n };\n\n const forgetLayout = () => {\n projectCache.clear();\n clearGitignore(cache);\n };\n\n async function loadSettings(): Promise<void> {\n settings = { ...DEFAULT_SETTINGS };\n if (!canConfigure) return;\n try {\n const value = (await connection.workspace.getConfiguration(\"dialex\")) as\n | Partial<Settings>\n | null\n | undefined;\n if (value && typeof value === \"object\") {\n if (typeof value.enable === \"boolean\") settings.enable = value.enable;\n if (typeof value.unusedKeys === \"boolean\") settings.unusedKeys = value.unusedKeys;\n if (typeof value.autoGenerate === \"boolean\") settings.autoGenerate = value.autoGenerate;\n if (typeof value.configPath === \"string\" && value.configPath) {\n settings.configPath = value.configPath;\n }\n }\n } catch {\n // the client does not answer configuration requests; keep the defaults\n }\n }\n\n function publish(): void {\n const byFile = new Map<string, Diagnostic[]>();\n for (const { issues } of states.values()) {\n for (const issue of issues) {\n const list = byFile.get(issue.file) ?? [];\n list.push(toDiagnostic(issue));\n byFile.set(issue.file, list);\n }\n }\n\n const next = new Set<string>();\n for (const [file, diagnostics] of byFile) {\n const uri = pathToFileURL(file).href;\n next.add(uri);\n void connection.sendDiagnostics({ uri, diagnostics });\n }\n // Files that had problems before and have none now need an explicit empty list.\n for (const uri of published) {\n if (!next.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });\n }\n published = next;\n }\n\n /** The projects whose generated file exists; the others do not use one. */\n function generatedProjects(): { root: string; options: { config?: string; static: true } }[] {\n const found: { root: string; options: { config?: string; static: true } }[] = [];\n for (const folder of folders) {\n for (const { root } of projectsIn(folder)) {\n const options = { config: settings.configPath, static: true as const };\n try {\n if (fs.existsSync(renderGenerated(root, options).outputPath)) {\n found.push({ root, options });\n }\n } catch (error) {\n connection.console.error(`[${root}] generate failed: ${(error as Error).message}`);\n }\n }\n }\n return found;\n }\n\n /**\n * Keeps `dialex.generated.ts` and `dialex-env.d.ts` current. The config is read from its syntax\n * tree, so no project code runs.\n */\n async function regenerate(): Promise<void> {\n if (!settings.autoGenerate || !settings.enable) return;\n for (const { root, options } of generatedProjects()) {\n try {\n const { outputPath } = renderGenerated(root, options);\n const before = fs.readFileSync(outputPath, \"utf-8\");\n const { files } = generateDictionaries(root, options);\n if (fs.readFileSync(outputPath, \"utf-8\") !== before) {\n connection.console.info(\n `[${root}] regenerated ${outputPath.slice(root.length + 1)} (${files.length} dictionaries)`,\n );\n }\n } catch (error) {\n connection.console.error(`[${root}] generate failed: ${(error as Error).message}`);\n }\n }\n }\n\n /** Without client file events, watch each project that has a generated file ourselves. */\n async function syncWatchers(): Promise<void> {\n const wanted = new Map<string, { config?: string; static: true }>();\n if (!clientWatches && settings.autoGenerate && settings.enable) {\n for (const { root, options } of generatedProjects()) wanted.set(root, options);\n }\n\n for (const [root, watcher] of watchers) {\n if (!wanted.has(root)) {\n watchers.delete(root);\n await watcher.close();\n }\n }\n for (const [root, options] of wanted) {\n if (watchers.has(root)) continue;\n try {\n watchers.set(\n root,\n startGenerateWatcher(\n root,\n options,\n () => {\n void regenerate().then(() => schedule());\n },\n 250,\n (message) => connection.console.info(`[${root}] ${message}`),\n ),\n );\n } catch (error) {\n connection.console.error(`[${root}] could not watch: ${(error as Error).message}`);\n }\n }\n }\n\n async function refresh(): Promise<void> {\n await loadSettings();\n states.clear();\n\n if (settings.enable) {\n for (const folder of folders) {\n for (const { root, ignore } of projectsIn(folder)) {\n try {\n const { config, notes } = readStaticConfig(root, settings.configPath);\n for (const note of notes) connection.console.info(`[${root}] ${note}`);\n\n const result = await analyzeProject({\n root,\n config,\n runtime: false,\n ignore,\n unused: settings.unusedKeys,\n cache,\n });\n states.set(root, {\n model: buildModel(root, config.defaultLocale, result),\n issues: result.issues,\n });\n } catch (error) {\n connection.console.error(`[${root}] analysis failed: ${(error as Error).message}`);\n }\n }\n }\n }\n publish();\n await syncWatchers();\n }\n\n function schedule(): void {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void refresh(), 300);\n }\n\n function scheduleGenerate(): void {\n if (generateTimer) clearTimeout(generateTimer);\n generateTimer = setTimeout(() => void regenerate(), 250);\n }\n\n connection.onInitialize((params: InitializeParams): InitializeResult => {\n folders = (params.workspaceFolders ?? [])\n .map((folder) => uriToPath(folder.uri))\n .filter((path): path is string => path !== undefined);\n if (folders.length === 0 && params.rootUri) {\n const root = uriToPath(params.rootUri);\n if (root) folders = [root];\n }\n canConfigure = params.capabilities.workspace?.configuration === true;\n clientWatches =\n params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;\n\n return {\n capabilities: {\n textDocumentSync: {\n openClose: true,\n change: TextDocumentSyncKind.Incremental,\n save: { includeText: false },\n },\n hoverProvider: true,\n definitionProvider: true,\n completionProvider: { triggerCharacters: ['\"', \"'\", \"`\", \".\"] },\n codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix] },\n },\n serverInfo: { name: \"dialex-language-server\" },\n };\n });\n\n connection.onInitialized(async () => {\n if (clientWatches) {\n try {\n await connection.client.register(DidChangeWatchedFilesNotification.type, {\n watchers: WATCHED_GLOBS.map((globPattern) => ({ globPattern })),\n });\n } catch {\n // the client refused; the server watches the disk itself\n clientWatches = false;\n }\n }\n schedule();\n scheduleGenerate();\n });\n\n connection.onDidChangeConfiguration(() => schedule());\n\n connection.onDidChangeWatchedFiles(({ changes }) => {\n // An edit cannot change which projects exist; a file appearing or disappearing can.\n if (changes.some((change) => change.type !== FileChangeType.Changed)) projectCache.clear();\n if (changes.some((change) => change.uri.endsWith(\".gitignore\"))) forgetLayout();\n schedule();\n scheduleGenerate();\n });\n\n documents.onDidSave(() => {\n schedule();\n scheduleGenerate();\n });\n\n connection.onShutdown(() => dispose());\n\n /** The project model for a file, unless the features are turned off. */\n const modelFor = (file: string): ProjectModel | undefined =>\n settings.enable\n ? modelForFile(\n [...states.values()].map((state) => state.model),\n file,\n )\n : undefined;\n\n /** One line of an open document, without its line break. */\n const lineOf = (document: TextDocument, line: number): string =>\n document\n .getText({ start: { line, character: 0 }, end: { line: line + 1, character: 0 } })\n .replace(/\\r?\\n$/, \"\");\n\n connection.onHover((params): Hover | null => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return null;\n\n const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character);\n const markdown = ref ? buildHover(model, ref) : undefined;\n if (!ref || !markdown) return null;\n\n return {\n contents: { kind: \"markdown\", value: markdown },\n range: {\n start: { line: params.position.line, character: ref.start },\n end: { line: params.position.line, character: ref.end },\n },\n };\n });\n\n connection.onDefinition((params): Location | null => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return null;\n\n const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character);\n const target = ref ? definitionFor(model, ref) : undefined;\n if (!target) return null;\n\n return {\n uri: pathToFileURL(target.file).href,\n range: {\n start: { line: target.range.start.line, character: target.range.start.column },\n end: { line: target.range.end.line, character: target.range.end.column },\n },\n };\n });\n\n connection.onCompletion((params): CompletionItem[] => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return [];\n\n const before = lineOf(document, params.position.line).slice(0, params.position.character);\n const context = completionContextAt(before);\n if (!context) return [];\n\n const range = {\n start: { line: params.position.line, character: params.position.character - context.length },\n end: params.position,\n };\n return completionEntries(model, context).map((entry) => ({\n label: entry.label,\n kind: COMPLETION_KINDS[entry.kind],\n detail: entry.detail,\n documentation: entry.documentation,\n filterText: entry.label,\n textEdit: { range, newText: entry.insertText },\n }));\n });\n\n connection.onCodeAction((params): CodeAction[] => {\n const file = uriToPath(params.textDocument.uri);\n const document = documents.get(params.textDocument.uri);\n if (!file || !document || !settings.enable) return [];\n\n const actions: CodeAction[] = [];\n const text = document.getText();\n const model = modelFor(file);\n\n for (const diagnostic of params.context.diagnostics) {\n const data = diagnostic.data as IssueData | undefined;\n if (diagnostic.source !== \"dialex\" || !data?.path) continue;\n\n if (data.code === \"missing-key\" && data.locale) {\n const updated = addMissingKeys(file, text, [\n { locale: data.locale, path: data.path, sourceLocale: data.sourceLocale },\n ]);\n if (!updated) continue;\n actions.push({\n title: `Add \"${data.path.join(\".\")}\" to ${data.locale} (marked [TODO])`,\n kind: CodeActionKind.QuickFix,\n diagnostics: [diagnostic],\n isPreferred: true,\n edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } },\n });\n }\n\n if (data.code === \"unknown-path\" && data.dictionary) {\n const dictionary = model?.dictionaries.get(data.dictionary);\n if (!dictionary) continue;\n\n const dictionaryUri = pathToFileURL(dictionary.file).href;\n const open = documents.get(dictionaryUri);\n let dictionaryText: string;\n try {\n dictionaryText = open ? open.getText() : fs.readFileSync(dictionary.file, \"utf-8\");\n } catch {\n continue;\n }\n const updated = addUnknownKey(dictionary.file, dictionaryText, data.path);\n if (!updated) continue;\n actions.push({\n title: `Add \"${data.path.join(\".\")}\" to the \"${dictionary.name}\" dictionary`,\n kind: CodeActionKind.QuickFix,\n diagnostics: [diagnostic],\n edit: { changes: { [dictionaryUri]: [replaceAll(dictionaryText, updated)] } },\n });\n }\n }\n\n // One action for every missing key in this dictionary file\n const missing: MissingKey[] = [...states.values()]\n .flatMap((state) => state.issues)\n .filter((issue) => issue.file === file && issue.code === \"missing-key\")\n .filter((issue) => issue.locale && issue.path)\n .map((issue) => ({\n locale: issue.locale!,\n path: issue.path!,\n sourceLocale: issue.sourceLocale,\n }));\n if (missing.length > 1) {\n const updated = addMissingKeys(file, text, missing);\n if (updated) {\n actions.push({\n title: `Add all ${missing.length} missing keys (marked [TODO])`,\n kind: CodeActionKind.QuickFix,\n edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } },\n });\n }\n }\n\n return actions;\n });\n\n documents.listen(connection);\n\n async function dispose(): Promise<void> {\n if (timer) clearTimeout(timer);\n if (generateTimer) clearTimeout(generateTimer);\n await Promise.all([...watchers.values()].map((watcher) => watcher.close()));\n watchers.clear();\n }\n\n return { refresh, regenerate, dispose };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server.mjs","names":[],"sources":["../src/server.ts"],"sourcesContent":["import fs from \"node:fs\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\n\nimport {\n analyzeProject,\n clearGitignore,\n createAnalysisCache,\n generateDictionaries,\n readStaticConfig,\n renderGenerated,\n startGenerateWatcher,\n type AnalysisIssue,\n} from \"@dialexjs/cli/api\";\nimport { TextDocument } from \"vscode-languageserver-textdocument\";\nimport {\n CodeActionKind,\n CompletionItemKind,\n DiagnosticSeverity,\n DiagnosticTag,\n DidChangeWatchedFilesNotification,\n FileChangeType,\n TextDocumentSyncKind,\n TextDocuments,\n type CodeAction,\n type CompletionItem,\n type Connection,\n type Diagnostic,\n type Hover,\n type Location,\n type TextEdit,\n type InitializeParams,\n type InitializeResult,\n} from \"vscode-languageserver/node.js\";\n\nimport {\n addMissingKeys,\n addUnknownKey,\n buildHover,\n buildModel,\n completionContextAt,\n completionEntries,\n definitionFor,\n discoverProjects,\n findBindings,\n findReferenceAt,\n modelForFile,\n type CompletionEntry,\n type MissingKey,\n type ProjectModel,\n type ProjectRoot,\n} from \"./features.js\";\n\n/** What the client can configure under the `dialex` section. */\nexport interface Settings {\n /** Turn diagnostics, hover and the other features off. */\n enable: boolean;\n /** Fade out keys and dictionaries that no source file seems to use. */\n unusedKeys: boolean;\n /**\n * Regenerate `dialex.generated.ts` and `dialex-env.d.ts` when a dictionary or config file\n * changes, in projects that already have a generated file.\n */\n autoGenerate: boolean;\n /** Path to the Dialex config file, relative to each project root. */\n configPath?: string;\n}\n\nconst DEFAULT_SETTINGS: Settings = { enable: true, unusedKeys: false, autoGenerate: true };\n\n/** Files whose creation or removal can change the set of Dialex projects or what is scanned. */\nconst WATCHED_GLOBS = [\n \"**/*.content.ts\",\n \"**/dialex.config.*\",\n \"**/i18n.config.*\",\n \"**/.gitignore\",\n];\n\ninterface ProjectState {\n model: ProjectModel;\n issues: AnalysisIssue[];\n}\n\nexport interface DialexServer {\n /** Re-analyze every project now and publish diagnostics. */\n refresh(): Promise<void>;\n /** Regenerate the generated files of every project that has them, now. */\n regenerate(): Promise<void>;\n /** Stop file watchers and timers. */\n dispose(): Promise<void>;\n}\n\nfunction severityOf(issue: AnalysisIssue): DiagnosticSeverity {\n if (issue.code === \"unused-key\" || issue.code === \"unused-dictionary\") {\n return DiagnosticSeverity.Hint;\n }\n switch (issue.level) {\n case \"error\":\n return DiagnosticSeverity.Error;\n case \"warning\":\n return DiagnosticSeverity.Warning;\n default:\n return DiagnosticSeverity.Information;\n }\n}\n\n/** What a quick fix needs from the issue that produced a diagnostic. */\ninterface IssueData {\n code: string;\n path?: string[];\n locale?: string;\n sourceLocale?: string;\n dictionary?: string;\n}\n\nconst COMPLETION_KINDS: Record<CompletionEntry[\"kind\"], CompletionItemKind> = {\n dictionary: CompletionItemKind.Module,\n group: CompletionItemKind.Folder,\n leaf: CompletionItemKind.Text,\n};\n\n/** An edit that replaces a whole document, which is how dictionary files are rewritten. */\nfunction replaceAll(text: string, newText: string): TextEdit {\n const lines = text.split(\"\\n\");\n return {\n range: {\n start: { line: 0, character: 0 },\n end: { line: lines.length - 1, character: lines[lines.length - 1].length },\n },\n newText,\n };\n}\n\nfunction toDiagnostic(issue: AnalysisIssue): Diagnostic {\n const severity = severityOf(issue);\n return {\n range: issue.range\n ? {\n start: { line: issue.range.start.line, character: issue.range.start.column },\n end: { line: issue.range.end.line, character: issue.range.end.column },\n }\n : { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },\n message: issue.message,\n severity,\n source: \"dialex\",\n code: issue.code,\n tags: severity === DiagnosticSeverity.Hint ? [DiagnosticTag.Unnecessary] : undefined,\n data: {\n code: issue.code,\n path: issue.path,\n locale: issue.locale,\n sourceLocale: issue.sourceLocale,\n dictionary: issue.dictionary,\n } satisfies IssueData,\n };\n}\n\nconst uriToPath = (uri: string) => (uri.startsWith(\"file:\") ? fileURLToPath(uri) : undefined);\n\n/**\n * Wires Dialex's editor features to an LSP connection. Nothing in the project is executed:\n * dictionaries and configs are read from the syntax tree (`runtime: false`).\n */\nexport function createServer(connection: Connection): DialexServer {\n const documents = new TextDocuments(TextDocument);\n const cache = createAnalysisCache();\n const states = new Map<string, ProjectState>();\n const projectCache = new Map<string, ProjectRoot[]>();\n\n let folders: string[] = [];\n let canConfigure = false;\n let settings: Settings = { ...DEFAULT_SETTINGS };\n let published = new Set<string>();\n let timer: ReturnType<typeof setTimeout> | undefined;\n let generateTimer: ReturnType<typeof setTimeout> | undefined;\n /** True when the client sends file events itself; otherwise the server watches the disk. */\n let clientWatches = false;\n const watchers = new Map<string, { close(): Promise<void> }>();\n\n const projectsIn = (folder: string): ProjectRoot[] => {\n let projects = projectCache.get(folder);\n if (!projects) {\n projects = discoverProjects(folder);\n projectCache.set(folder, projects);\n }\n return projects;\n };\n\n const forgetLayout = () => {\n projectCache.clear();\n clearGitignore(cache);\n };\n\n async function loadSettings(): Promise<void> {\n settings = { ...DEFAULT_SETTINGS };\n if (!canConfigure) return;\n try {\n const value = (await connection.workspace.getConfiguration(\"dialex\")) as\n | Partial<Settings>\n | null\n | undefined;\n if (value && typeof value === \"object\") {\n if (typeof value.enable === \"boolean\") settings.enable = value.enable;\n if (typeof value.unusedKeys === \"boolean\") settings.unusedKeys = value.unusedKeys;\n if (typeof value.autoGenerate === \"boolean\") settings.autoGenerate = value.autoGenerate;\n if (typeof value.configPath === \"string\" && value.configPath) {\n settings.configPath = value.configPath;\n }\n }\n } catch {\n // the client does not answer configuration requests; keep the defaults\n }\n }\n\n function publish(): void {\n const byFile = new Map<string, Diagnostic[]>();\n for (const { issues } of states.values()) {\n for (const issue of issues) {\n const list = byFile.get(issue.file) ?? [];\n list.push(toDiagnostic(issue));\n byFile.set(issue.file, list);\n }\n }\n\n const next = new Set<string>();\n for (const [file, diagnostics] of byFile) {\n const uri = pathToFileURL(file).href;\n next.add(uri);\n void connection.sendDiagnostics({ uri, diagnostics });\n }\n // Files that had problems before and have none now need an explicit empty list.\n for (const uri of published) {\n if (!next.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });\n }\n published = next;\n }\n\n /** The projects whose generated file exists; the others do not use one. */\n function generatedProjects(): { root: string; options: { config?: string; static: true } }[] {\n const found: { root: string; options: { config?: string; static: true } }[] = [];\n for (const folder of folders) {\n for (const { root } of projectsIn(folder)) {\n const options = { config: settings.configPath, static: true as const };\n try {\n if (fs.existsSync(renderGenerated(root, options).outputPath)) {\n found.push({ root, options });\n }\n } catch (error) {\n connection.console.error(`[${root}] generate failed: ${(error as Error).message}`);\n }\n }\n }\n return found;\n }\n\n /**\n * Keeps `dialex.generated.ts` and `dialex-env.d.ts` current. The config is read from its syntax\n * tree, so no project code runs.\n */\n async function regenerate(): Promise<void> {\n if (!settings.autoGenerate || !settings.enable) return;\n for (const { root, options } of generatedProjects()) {\n try {\n const { outputPath } = renderGenerated(root, options);\n const before = fs.readFileSync(outputPath, \"utf-8\");\n const { files } = generateDictionaries(root, options);\n if (fs.readFileSync(outputPath, \"utf-8\") !== before) {\n connection.console.info(\n `[${root}] regenerated ${outputPath.slice(root.length + 1)} (${files.length} dictionaries)`,\n );\n }\n } catch (error) {\n connection.console.error(`[${root}] generate failed: ${(error as Error).message}`);\n }\n }\n }\n\n /** Without client file events, watch each project that has a generated file ourselves. */\n async function syncWatchers(): Promise<void> {\n const wanted = new Map<string, { config?: string; static: true }>();\n if (!clientWatches && settings.autoGenerate && settings.enable) {\n for (const { root, options } of generatedProjects()) wanted.set(root, options);\n }\n\n for (const [root, watcher] of watchers) {\n if (!wanted.has(root)) {\n watchers.delete(root);\n await watcher.close();\n }\n }\n for (const [root, options] of wanted) {\n if (watchers.has(root)) continue;\n try {\n watchers.set(\n root,\n startGenerateWatcher(\n root,\n options,\n () => {\n void regenerate().then(() => schedule());\n },\n 250,\n (message) => connection.console.info(`[${root}] ${message}`),\n ),\n );\n } catch (error) {\n connection.console.error(`[${root}] could not watch: ${(error as Error).message}`);\n }\n }\n }\n\n async function refresh(): Promise<void> {\n await loadSettings();\n states.clear();\n\n if (settings.enable) {\n for (const folder of folders) {\n for (const { root, ignore } of projectsIn(folder)) {\n try {\n const { config, notes } = readStaticConfig(root, settings.configPath);\n for (const note of notes) connection.console.info(`[${root}] ${note}`);\n\n const result = await analyzeProject({\n root,\n config,\n runtime: false,\n ignore,\n unused: settings.unusedKeys,\n cache,\n });\n states.set(root, {\n model: buildModel(root, config.defaultLocale, result),\n issues: result.issues,\n });\n } catch (error) {\n connection.console.error(`[${root}] analysis failed: ${(error as Error).message}`);\n }\n }\n }\n }\n publish();\n await syncWatchers();\n }\n\n function schedule(): void {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => void refresh(), 300);\n }\n\n function scheduleGenerate(): void {\n if (generateTimer) clearTimeout(generateTimer);\n generateTimer = setTimeout(() => void regenerate(), 250);\n }\n\n connection.onInitialize((params: InitializeParams): InitializeResult => {\n folders = (params.workspaceFolders ?? [])\n .map((folder) => uriToPath(folder.uri))\n .filter((path): path is string => path !== undefined);\n if (folders.length === 0 && params.rootUri) {\n const root = uriToPath(params.rootUri);\n if (root) folders = [root];\n }\n canConfigure = params.capabilities.workspace?.configuration === true;\n clientWatches =\n params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;\n\n return {\n capabilities: {\n textDocumentSync: {\n openClose: true,\n change: TextDocumentSyncKind.Incremental,\n save: { includeText: false },\n },\n hoverProvider: true,\n definitionProvider: true,\n completionProvider: { triggerCharacters: ['\"', \"'\", \"`\", \".\"] },\n codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix] },\n },\n serverInfo: { name: \"dialex-language-server\" },\n };\n });\n\n connection.onInitialized(async () => {\n if (clientWatches) {\n try {\n await connection.client.register(DidChangeWatchedFilesNotification.type, {\n watchers: WATCHED_GLOBS.map((globPattern) => ({ globPattern })),\n });\n } catch {\n // the client refused; the server watches the disk itself\n clientWatches = false;\n }\n }\n schedule();\n scheduleGenerate();\n });\n\n connection.onDidChangeConfiguration(() => schedule());\n\n connection.onDidChangeWatchedFiles(({ changes }) => {\n // An edit cannot change which projects exist; a file appearing or disappearing can.\n if (changes.some((change) => change.type !== FileChangeType.Changed)) projectCache.clear();\n if (changes.some((change) => change.uri.endsWith(\".gitignore\"))) forgetLayout();\n schedule();\n scheduleGenerate();\n });\n\n documents.onDidSave(() => {\n schedule();\n scheduleGenerate();\n });\n\n connection.onShutdown(() => dispose());\n\n /** The project model for a file, unless the features are turned off. */\n const modelFor = (file: string): ProjectModel | undefined =>\n settings.enable\n ? modelForFile(\n [...states.values()].map((state) => state.model),\n file,\n )\n : undefined;\n\n /** One line of an open document, without its line break. */\n const lineOf = (document: TextDocument, line: number): string =>\n document\n .getText({ start: { line, character: 0 }, end: { line: line + 1, character: 0 } })\n .replace(/\\r?\\n$/, \"\");\n\n connection.onHover((params): Hover | null => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return null;\n\n const ref = findReferenceAt(\n lineOf(document, params.position.line),\n params.position.character,\n findBindings(document.getText()),\n );\n const markdown = ref ? buildHover(model, ref) : undefined;\n if (!ref || !markdown) return null;\n\n return {\n contents: { kind: \"markdown\", value: markdown },\n range: {\n start: { line: params.position.line, character: ref.start },\n end: { line: params.position.line, character: ref.end },\n },\n };\n });\n\n connection.onDefinition((params): Location | null => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return null;\n\n const ref = findReferenceAt(\n lineOf(document, params.position.line),\n params.position.character,\n findBindings(document.getText()),\n );\n const target = ref ? definitionFor(model, ref) : undefined;\n if (!target) return null;\n\n return {\n uri: pathToFileURL(target.file).href,\n range: {\n start: { line: target.range.start.line, character: target.range.start.column },\n end: { line: target.range.end.line, character: target.range.end.column },\n },\n };\n });\n\n connection.onCompletion((params): CompletionItem[] => {\n const document = documents.get(params.textDocument.uri);\n const file = uriToPath(params.textDocument.uri);\n const model = document && file ? modelFor(file) : undefined;\n if (!document || !model) return [];\n\n const before = lineOf(document, params.position.line).slice(0, params.position.character);\n const context = completionContextAt(before, findBindings(document.getText()));\n if (!context) return [];\n\n const range = {\n start: { line: params.position.line, character: params.position.character - context.length },\n end: params.position,\n };\n return completionEntries(model, context).map((entry) => ({\n label: entry.label,\n kind: COMPLETION_KINDS[entry.kind],\n detail: entry.detail,\n documentation: entry.documentation,\n filterText: entry.label,\n textEdit: { range, newText: entry.insertText },\n }));\n });\n\n connection.onCodeAction((params): CodeAction[] => {\n const file = uriToPath(params.textDocument.uri);\n const document = documents.get(params.textDocument.uri);\n if (!file || !document || !settings.enable) return [];\n\n const actions: CodeAction[] = [];\n const text = document.getText();\n const model = modelFor(file);\n\n for (const diagnostic of params.context.diagnostics) {\n const data = diagnostic.data as IssueData | undefined;\n if (diagnostic.source !== \"dialex\" || !data?.path) continue;\n\n if (data.code === \"missing-key\" && data.locale) {\n const updated = addMissingKeys(file, text, [\n { locale: data.locale, path: data.path, sourceLocale: data.sourceLocale },\n ]);\n if (!updated) continue;\n actions.push({\n title: `Add \"${data.path.join(\".\")}\" to ${data.locale} (marked [TODO])`,\n kind: CodeActionKind.QuickFix,\n diagnostics: [diagnostic],\n isPreferred: true,\n edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } },\n });\n }\n\n if (data.code === \"unknown-path\" && data.dictionary) {\n const dictionary = model?.dictionaries.get(data.dictionary);\n if (!dictionary) continue;\n\n const dictionaryUri = pathToFileURL(dictionary.file).href;\n const open = documents.get(dictionaryUri);\n let dictionaryText: string;\n try {\n dictionaryText = open ? open.getText() : fs.readFileSync(dictionary.file, \"utf-8\");\n } catch {\n continue;\n }\n const updated = addUnknownKey(dictionary.file, dictionaryText, data.path);\n if (!updated) continue;\n actions.push({\n title: `Add \"${data.path.join(\".\")}\" to the \"${dictionary.name}\" dictionary`,\n kind: CodeActionKind.QuickFix,\n diagnostics: [diagnostic],\n edit: { changes: { [dictionaryUri]: [replaceAll(dictionaryText, updated)] } },\n });\n }\n }\n\n // One action for every missing key in this dictionary file\n const missing: MissingKey[] = [...states.values()]\n .flatMap((state) => state.issues)\n .filter((issue) => issue.file === file && issue.code === \"missing-key\")\n .filter((issue) => issue.locale && issue.path)\n .map((issue) => ({\n locale: issue.locale!,\n path: issue.path!,\n sourceLocale: issue.sourceLocale,\n }));\n if (missing.length > 1) {\n const updated = addMissingKeys(file, text, missing);\n if (updated) {\n actions.push({\n title: `Add all ${missing.length} missing keys (marked [TODO])`,\n kind: CodeActionKind.QuickFix,\n edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } },\n });\n }\n }\n\n return actions;\n });\n\n documents.listen(connection);\n\n async function dispose(): Promise<void> {\n if (timer) clearTimeout(timer);\n if (generateTimer) clearTimeout(generateTimer);\n await Promise.all([...watchers.values()].map((watcher) => watcher.close()));\n watchers.clear();\n }\n\n return { refresh, regenerate, dispose };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dialexjs/language-server",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Language server (LSP) for Dialex translations: diagnostics, hover, go to definition, completion and quick fixes",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dialex",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"check": "vp check"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@dialexjs/cli": "0.7.
|
|
46
|
+
"@dialexjs/cli": "0.7.2",
|
|
47
47
|
"vscode-languageserver": "^9.0.1",
|
|
48
48
|
"vscode-languageserver-textdocument": "^1.0.12"
|
|
49
49
|
},
|