@dialexjs/language-server 0.7.2 → 0.9.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/dist/features.mjs CHANGED
@@ -1,361 +1,2 @@
1
- import fs from "node:fs";
2
- import { TODO_PREFIX, copyLeaf, dictionaryLocation, getLeafSource, hasPath, listLocales, locationOf, parseDictionaryText, renderDictionaryFile, scanFiles, scanReferences, setString } from "@dialexjs/cli/api";
3
- import path from "node:path";
4
- //#region src/features/model.ts
5
- function buildModel(root, defaultLocale, result) {
6
- return {
7
- root,
8
- defaultLocale,
9
- dictionaries: new Map(result.dictionaries.map((d) => [d.name, d]))
10
- };
11
- }
12
- /** The model whose root contains `file`, preferring the deepest (nested projects). */
13
- function modelForFile(models, file) {
14
- let best;
15
- for (const model of models) {
16
- const rel = path.relative(model.root, file);
17
- if (rel.startsWith("..") || path.isAbsolute(rel)) continue;
18
- if (!best || model.root.length > best.root.length) best = model;
19
- }
20
- return best;
21
- }
22
- /** Locales with the default locale first. */
23
- function orderedLocales(model, dictionary) {
24
- return [...dictionary.locales.filter((l) => l === model.defaultLocale), ...dictionary.locales.filter((l) => l !== model.defaultLocale)];
25
- }
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
44
- //#region src/features/references.ts
45
- /** `getDictionary("home").nav.about` -> the `.nav.about` member chain after the call. */
46
- const MEMBER_CHAIN = /(?:getDictionary|useDictionary)\(\s*(["'`])([^"'`$\n]+)\1[^()\n]*\)((?:\??\.[A-Za-z_$][\w$]*)+)/dg;
47
- const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
48
- /**
49
- * The Dialex reference under the cursor on one line of code: a dictionary name, a `t("a.b.c")`
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`.
53
- */
54
- function findReferenceAt(lineText, character, bindings) {
55
- const direct = scanReferences(lineText).find((r) => character >= r.start && character <= r.end);
56
- if (direct) return direct;
57
- for (const match of lineText.matchAll(MEMBER_CHAIN)) {
58
- const dictionary = match[2];
59
- const [chainStart] = match.indices[3];
60
- const members = /\??\.([A-Za-z_$][\w$]*)/g;
61
- const keys = [];
62
- for (const member of match[3].matchAll(members)) {
63
- const start = chainStart + member.index + member[0].length - member[1].length;
64
- const end = start + member[1].length;
65
- keys.push(member[1]);
66
- if (character >= start && character <= end) return {
67
- kind: "path",
68
- value: [dictionary, ...keys].join("."),
69
- start,
70
- end
71
- };
72
- }
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
- }
100
- }
101
- function resolveReference(model, ref) {
102
- if (ref.kind === "dictionary") {
103
- const dictionary = model.dictionaries.get(ref.value);
104
- return dictionary ? {
105
- dictionary,
106
- keyPath: []
107
- } : void 0;
108
- }
109
- const [name, ...keyPath] = ref.value.split(".");
110
- const dictionary = model.dictionaries.get(name);
111
- return dictionary ? {
112
- dictionary,
113
- keyPath
114
- } : void 0;
115
- }
116
- /** Whether `keyPath` is a leaf, a group of leaves, or nothing in this dictionary. */
117
- function classifyPath(dictionary, keyPath) {
118
- const joined = keyPath.join(".");
119
- if (dictionary.paths.includes(joined)) return "leaf";
120
- return dictionary.paths.some((p) => p.startsWith(`${joined}.`)) ? "group" : "missing";
121
- }
122
- //#endregion
123
- //#region src/features/hover.ts
124
- /** Escapes Markdown so translation text cannot change the hover's formatting. */
125
- function escapeMarkdown(text) {
126
- return text.replace(/[\\`*_{}[\]()#+!|<>~]/g, "\\$&").replace(/\r?\n/g, "<br>").slice(0, 400);
127
- }
128
- function cell(dictionary, locale, keyPath) {
129
- const leaf = dictionary.leaves.get(locale)?.get(keyPath.join("."));
130
- if (!leaf) return "_missing_";
131
- if (leaf.kind === "string") return escapeMarkdown(leaf.value ?? "");
132
- if (leaf.kind === "function") {
133
- const source = dictionary.df ? getLeafSource(dictionary.df, locale, keyPath) : void 0;
134
- return source ? `\`${source.replace(/`/g, "'").replace(/\s+/g, " ").slice(0, 160)}\`` : "_function_";
135
- }
136
- return "_value_";
137
- }
138
- /**
139
- * Markdown for a hover over a Dialex reference: every locale's text for a key, or a summary of a
140
- * dictionary. Returns `undefined` for unknown references (diagnostics already flag those).
141
- */
142
- function buildHover(model, ref) {
143
- const resolved = resolveReference(model, ref);
144
- if (!resolved) return void 0;
145
- const { dictionary, keyPath } = resolved;
146
- const file = path.basename(dictionary.file);
147
- if (keyPath.length === 0) return [
148
- `**${dictionary.name}** · ${file}`,
149
- "",
150
- `${dictionary.paths.length} key${dictionary.paths.length === 1 ? "" : "s"} · locales: ${dictionary.locales.join(", ")}`
151
- ].join("\n");
152
- const kind = classifyPath(dictionary, keyPath);
153
- const title = `**${dictionary.name}.${keyPath.join(".")}** · ${file}`;
154
- if (kind === "missing") return void 0;
155
- if (kind === "group") {
156
- const prefix = `${keyPath.join(".")}.`;
157
- const children = new Set(dictionary.paths.filter((p) => p.startsWith(prefix)).map((p) => p.slice(prefix.length).split(".")[0]));
158
- return `${title}\n\n${children.size} key${children.size === 1 ? "" : "s"}: ${[...children].map((c) => `\`${c}\``).join(", ")}`;
159
- }
160
- return [
161
- title,
162
- "",
163
- "| Locale | Text |",
164
- "| --- | --- |",
165
- ...orderedLocales(model, dictionary).map((locale) => `| ${locale} | ${cell(dictionary, locale, keyPath)} |`)
166
- ].join("\n");
167
- }
168
- //#endregion
169
- //#region src/features/definition.ts
170
- const START = {
171
- start: {
172
- line: 0,
173
- column: 0
174
- },
175
- end: {
176
- line: 0,
177
- column: 0
178
- }
179
- };
180
- /**
181
- * Where a reference is defined: the key in the default locale if it has one, otherwise in the
182
- * first locale that does. A dictionary reference points at the dictionary object.
183
- */
184
- function definitionFor(model, ref) {
185
- const resolved = resolveReference(model, ref);
186
- if (!resolved) return void 0;
187
- const { dictionary, keyPath } = resolved;
188
- const { df } = dictionary;
189
- if (!df) return {
190
- file: dictionary.file,
191
- range: START
192
- };
193
- if (keyPath.length === 0) return {
194
- file: dictionary.file,
195
- range: dictionaryLocation(df) ?? START
196
- };
197
- for (const locale of orderedLocales(model, dictionary)) {
198
- const range = locationOf(df, locale, keyPath);
199
- if (range) return {
200
- file: dictionary.file,
201
- range
202
- };
203
- }
204
- }
205
- //#endregion
206
- //#region src/features/completion.ts
207
- const CALL = /(?<![\w$.])(t|getDictionary|useDictionary|DialexDictionary)\(\s*(["'`])([^"'`\n]*)$/;
208
- /**
209
- * Whether the cursor is inside the string argument of a Dialex call, and what is being typed.
210
- * `textBeforeCursor` is the line up to the cursor. `length` is how many characters before the
211
- * cursor a completion replaces.
212
- */
213
- function completionContextAt(textBeforeCursor, bindings) {
214
- const match = CALL.exec(textBeforeCursor);
215
- if (!match) return bindings ? memberContextAt(textBeforeCursor, bindings) : void 0;
216
- const [, call, , value] = match;
217
- if (call === "t" && value.includes(".")) {
218
- const segments = value.split(".");
219
- const typed = segments.pop();
220
- const [dictionary, ...parent] = segments;
221
- return {
222
- kind: "path",
223
- dictionary,
224
- parent,
225
- typed,
226
- length: typed.length
227
- };
228
- }
229
- return {
230
- kind: "dictionary",
231
- call,
232
- typed: value,
233
- length: value.length
234
- };
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
- }
253
- const preview = (text) => text === void 0 ? void 0 : text.length > 60 ? `${text.slice(0, 57)}...` : text;
254
- function dictionaryEntries(model, context) {
255
- return [...model.dictionaries.values()].filter((d) => d.name.startsWith(context.typed)).map((d) => {
256
- const continues = context.call === "t";
257
- return {
258
- label: d.name,
259
- kind: "dictionary",
260
- detail: `${d.paths.length} keys · ${d.locales.join(", ")}`,
261
- insertText: continues ? `${d.name}.` : d.name,
262
- retrigger: continues
263
- };
264
- });
265
- }
266
- function pathEntries(model, dictionary, context) {
267
- const prefix = context.parent.length > 0 ? `${context.parent.join(".")}.` : "";
268
- const children = /* @__PURE__ */ new Map();
269
- for (const keyPath of dictionary.paths) {
270
- if (!keyPath.startsWith(prefix)) continue;
271
- const rest = keyPath.slice(prefix.length).split(".");
272
- const name = rest[0];
273
- if (!name.startsWith(context.typed)) continue;
274
- const entry = children.get(name) ?? { leaf: true };
275
- if (rest.length > 1) entry.leaf = false;
276
- children.set(name, entry);
277
- }
278
- return [...children].map(([name, { leaf }]) => {
279
- const full = `${prefix}${name}`;
280
- const holder = [model.defaultLocale, ...dictionary.locales].find((l) => dictionary.leaves.get(l)?.has(full));
281
- const text = holder ? dictionary.leaves.get(holder).get(full) : void 0;
282
- return {
283
- label: name,
284
- kind: leaf ? "leaf" : "group",
285
- detail: leaf ? preview(text?.kind === "string" ? text.value : text ? `(${text.kind})` : void 0) ?? "" : "keys…",
286
- insertText: leaf ? name : `${name}.`,
287
- retrigger: !leaf
288
- };
289
- });
290
- }
291
- /** Suggestions for the string being typed, filtered by what is already there. */
292
- function completionEntries(model, context) {
293
- if (context.kind === "dictionary") return dictionaryEntries(model, context);
294
- const dictionary = model.dictionaries.get(context.dictionary);
295
- return dictionary ? pathEntries(model, dictionary, context) : [];
296
- }
297
- //#endregion
298
- //#region src/features/quickfix.ts
299
- /**
300
- * New text for a dictionary file with `missing` keys added to their locales, copied from the
301
- * source locale (strings get the `[TODO] ` marker, like `dialex check --fix`). Returns
302
- * `undefined` if nothing could be added.
303
- */
304
- function addMissingKeys(file, text, missing) {
305
- const df = parseDictionaryText(file, text);
306
- if (!df) return void 0;
307
- let changed = false;
308
- for (const { locale, path, sourceLocale } of missing) {
309
- if (!sourceLocale) continue;
310
- if (copyLeaf(df, sourceLocale, locale, path, (value) => `${TODO_PREFIX}${value}`)) changed = true;
311
- }
312
- return changed ? renderDictionaryFile(df) : void 0;
313
- }
314
- /**
315
- * New text for a dictionary file with a key that code refers to but the dictionary lacks, added
316
- * to every locale as a `[TODO]` placeholder named after the key.
317
- */
318
- function addUnknownKey(file, text, keyPath) {
319
- const df = parseDictionaryText(file, text);
320
- if (!df) return void 0;
321
- const placeholder = `${TODO_PREFIX}${keyPath[keyPath.length - 1]}`;
322
- let changed = false;
323
- for (const locale of listLocales(df)) if (!hasPath(df, locale, keyPath) && setString(df, locale, keyPath, placeholder) !== "skipped") changed = true;
324
- return changed ? renderDictionaryFile(df) : void 0;
325
- }
326
- //#endregion
327
- //#region src/features/projects.ts
328
- const IGNORE = ["**/.git/**", "**/.nuxt/**"];
329
- /** The nearest folder at or above `dir` (but inside `folder`) that has a package.json. */
330
- function packageRoot(folder, dir) {
331
- for (let current = dir; current.startsWith(folder); current = path.dirname(current)) {
332
- if (fs.existsSync(path.join(current, "package.json"))) return current;
333
- if (current === folder) break;
334
- }
335
- return folder;
336
- }
337
- /**
338
- * Finds the Dialex projects inside a workspace folder.
339
- *
340
- * - Every directory with a `dialex.config.*` or `i18n.config.*` is a project.
341
- * - A `*.content.ts` file that no such project contains belongs to the nearest folder with a
342
- * `package.json` (or the workspace folder), which then uses the default settings. This covers
343
- * monorepos where only some packages have a config file.
344
- * - A project excludes the projects nested inside it, so one dictionary is never reported twice.
345
- */
346
- function discoverProjects(folder) {
347
- const configs = scanFiles(folder, ["**/dialex.config.*", "**/i18n.config.*"], { ignore: IGNORE });
348
- const configured = [...new Set(configs.map((f) => path.dirname(f)))];
349
- const roots = new Set(configured);
350
- const dictionaries = scanFiles(folder, "**/*.content.ts", { ignore: IGNORE });
351
- for (const file of dictionaries) if (!configured.some((root) => file.startsWith(root + path.sep))) roots.add(packageRoot(folder, path.dirname(file)));
352
- const all = [...roots].sort((a, b) => a.length - b.length);
353
- return all.map((root) => ({
354
- root,
355
- ignore: all.filter((other) => other !== root && other.startsWith(root + path.sep)).map((other) => `${path.relative(root, other).split(path.sep).join("/")}/**`)
356
- }));
357
- }
358
- //#endregion
1
+ import { a as completionEntries, c as escapeMarkdown, d as resolveReference, f as findBindings, h as orderedLocales, i as completionContextAt, l as classifyPath, m as modelForFile, n as addMissingKeys, o as definitionFor, p as buildModel, r as addUnknownKey, s as buildHover, t as discoverProjects, u as findReferenceAt } from "./features-Bpcwl0Es.mjs";
359
2
  export { addMissingKeys, addUnknownKey, buildHover, buildModel, classifyPath, completionContextAt, completionEntries, definitionFor, discoverProjects, escapeMarkdown, findBindings, findReferenceAt, modelForFile, orderedLocales, resolveReference };
360
-
361
- //# sourceMappingURL=features.mjs.map
package/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { createServer } from "./server.mjs";
3
- import { ProposedFeatures, createConnection } from "vscode-languageserver/node.js";
4
2
  //#region src/index.ts
5
3
  console.log = (...args) => console.error(...args);
4
+ const { createConnection, ProposedFeatures } = await import("vscode-languageserver/node.js");
5
+ const { createServer } = await import("./server.mjs");
6
6
  const connection = createConnection(ProposedFeatures.all);
7
7
  createServer(connection);
8
8
  connection.listen();
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { createConnection, ProposedFeatures } from \"vscode-languageserver/node.js\";\n\nimport { createServer } from \"./server.js\";\n\n// stdout carries the protocol; anything that logs there would corrupt it.\nconsole.log = (...args: unknown[]) => console.error(...args);\n\n// `--stdio` (what editors pass), `--node-ipc` and `--socket=<port>` are handled by the library.\nconst connection = createConnection(ProposedFeatures.all);\ncreateServer(connection);\nconnection.listen();\n"],"mappings":";;;;AAMA,QAAQ,OAAO,GAAG,SAAoB,QAAQ,MAAM,GAAG,IAAI;AAG3D,MAAM,aAAa,iBAAiB,iBAAiB,GAAG;AACxD,aAAa,UAAU;AACvB,WAAW,OAAO"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\n\n// stdout carries the protocol; anything that logs there would corrupt it. This runs before the\n// server modules are loaded, hence the dynamic imports.\nconsole.log = (...args: unknown[]) => console.error(...args);\n\nconst { createConnection, ProposedFeatures } = await import(\"vscode-languageserver/node.js\");\nconst { createServer } = await import(\"./server.js\");\n\n// `--stdio` (what editors pass), `--node-ipc` and `--socket=<port>` are handled by the library.\nconst connection = createConnection(ProposedFeatures.all);\ncreateServer(connection);\nconnection.listen();\n\n// Makes this file a module, which top-level await needs.\nexport {};\n"],"mappings":";;AAIA,QAAQ,OAAO,GAAG,SAAoB,QAAQ,MAAM,GAAG,IAAI;AAE3D,MAAM,EAAE,kBAAkB,qBAAqB,MAAM,OAAO;AAC5D,MAAM,EAAE,iBAAiB,MAAM,OAAO;AAGtC,MAAM,aAAa,iBAAiB,iBAAiB,GAAG;AACxD,aAAa,UAAU;AACvB,WAAW,OAAO"}
package/dist/server.d.mts CHANGED
@@ -1,9 +1,14 @@
1
1
  import { Connection } from "vscode-languageserver/node.js";
2
- //#region src/server.d.ts
2
+ //#region src/server/settings.d.ts
3
3
  /** What the client can configure under the `dialex` section. */
4
- export interface Settings {
4
+ interface Settings {
5
5
  /** Turn diagnostics, hover and the other features off. */
6
6
  enable: boolean;
7
+ /**
8
+ * Publish problems. Turn off when the ESLint or oxlint plugin reports them, to avoid showing
9
+ * each twice; hover, completion and quick fixes keep working.
10
+ */
11
+ diagnostics: boolean;
7
12
  /** Fade out keys and dictionaries that no source file seems to use. */
8
13
  unusedKeys: boolean;
9
14
  /**
@@ -14,6 +19,8 @@ export interface Settings {
14
19
  /** Path to the Dialex config file, relative to each project root. */
15
20
  configPath?: string;
16
21
  }
22
+ //#endregion
23
+ //#region src/server.d.ts
17
24
  export interface DialexServer {
18
25
  /** Re-analyze every project now and publish diagnostics. */
19
26
  refresh(): Promise<void>;
@@ -28,4 +35,5 @@ export interface DialexServer {
28
35
  */
29
36
  export declare function createServer(connection: Connection): DialexServer;
30
37
  //#endregion
38
+ export type { Settings };
31
39
  //# sourceMappingURL=server.d.mts.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../src/server/settings.ts","../src/server.ts"],"mappings":";;;UACiB;;EAEf;;;;;EAKA;;EAEA;;;;;EAKA;;EAEA;;;;iBCgCe;;EAEf,WAAW;;EAEX,cAAc;;EAEd,WAAW;;;;;;wBAOG,aAAa,YAAY,aAAa"}