@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/README.md +6 -5
- package/dist/features-Bpcwl0Es.mjs +361 -0
- package/dist/features-Bpcwl0Es.mjs.map +1 -0
- package/dist/features.mjs +1 -360
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/server.d.mts +10 -2
- package/dist/server.d.mts.map +1 -1
- package/dist/server.mjs +281 -224
- package/dist/server.mjs.map +1 -1
- package/package.json +2 -2
- package/dist/features.mjs.map +0 -1
package/dist/server.mjs
CHANGED
|
@@ -1,30 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { a as completionEntries, f as findBindings, i as completionContextAt, 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";
|
|
2
|
+
import { analyzeProject, clearGitignore, createAnalysisCache, generateDictionaries, readStaticConfig, renderGenerated, startGenerateWatcher } from "@dialexjs/cli/api";
|
|
3
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
2
4
|
import { CodeActionKind, CompletionItemKind, DiagnosticSeverity, DiagnosticTag, DidChangeWatchedFilesNotification, FileChangeType, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver/node.js";
|
|
3
5
|
import fs from "node:fs";
|
|
4
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
5
|
-
|
|
6
|
-
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
7
|
-
//#region src/server.ts
|
|
8
|
-
const DEFAULT_SETTINGS = {
|
|
9
|
-
enable: true,
|
|
10
|
-
unusedKeys: false,
|
|
11
|
-
autoGenerate: true
|
|
12
|
-
};
|
|
13
|
-
/** Files whose creation or removal can change the set of Dialex projects or what is scanned. */
|
|
14
|
-
const WATCHED_GLOBS = [
|
|
15
|
-
"**/*.content.ts",
|
|
16
|
-
"**/dialex.config.*",
|
|
17
|
-
"**/i18n.config.*",
|
|
18
|
-
"**/.gitignore"
|
|
19
|
-
];
|
|
20
|
-
function severityOf(issue) {
|
|
21
|
-
if (issue.code === "unused-key" || issue.code === "unused-dictionary") return DiagnosticSeverity.Hint;
|
|
22
|
-
switch (issue.level) {
|
|
23
|
-
case "error": return DiagnosticSeverity.Error;
|
|
24
|
-
case "warning": return DiagnosticSeverity.Warning;
|
|
25
|
-
default: return DiagnosticSeverity.Information;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
7
|
+
//#region src/server/convert.ts
|
|
28
8
|
const COMPLETION_KINDS = {
|
|
29
9
|
dictionary: CompletionItemKind.Module,
|
|
30
10
|
group: CompletionItemKind.Folder,
|
|
@@ -47,6 +27,28 @@ function replaceAll(text, newText) {
|
|
|
47
27
|
newText
|
|
48
28
|
};
|
|
49
29
|
}
|
|
30
|
+
const uriToPath = (uri) => uri.startsWith("file:") ? fileURLToPath(uri) : void 0;
|
|
31
|
+
/** One line of an open document, without its line break. */
|
|
32
|
+
const lineOf = (document, line) => document.getText({
|
|
33
|
+
start: {
|
|
34
|
+
line,
|
|
35
|
+
character: 0
|
|
36
|
+
},
|
|
37
|
+
end: {
|
|
38
|
+
line: line + 1,
|
|
39
|
+
character: 0
|
|
40
|
+
}
|
|
41
|
+
}).replace(/\r?\n$/, "");
|
|
42
|
+
//#endregion
|
|
43
|
+
//#region src/server/diagnostics.ts
|
|
44
|
+
function severityOf(issue) {
|
|
45
|
+
if (issue.code === "unused-key" || issue.code === "unused-dictionary") return DiagnosticSeverity.Hint;
|
|
46
|
+
switch (issue.level) {
|
|
47
|
+
case "error": return DiagnosticSeverity.Error;
|
|
48
|
+
case "warning": return DiagnosticSeverity.Warning;
|
|
49
|
+
default: return DiagnosticSeverity.Information;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
50
52
|
function toDiagnostic(issue) {
|
|
51
53
|
const severity = severityOf(issue);
|
|
52
54
|
return {
|
|
@@ -83,53 +85,15 @@ function toDiagnostic(issue) {
|
|
|
83
85
|
}
|
|
84
86
|
};
|
|
85
87
|
}
|
|
86
|
-
const uriToPath = (uri) => uri.startsWith("file:") ? fileURLToPath(uri) : void 0;
|
|
87
88
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
89
|
+
* Publishes the issues of every project, one list per file, and an explicit empty list for
|
|
90
|
+
* files that had problems before and have none now.
|
|
90
91
|
*/
|
|
91
|
-
function
|
|
92
|
-
const documents = new TextDocuments(TextDocument);
|
|
93
|
-
const cache = createAnalysisCache();
|
|
94
|
-
const states = /* @__PURE__ */ new Map();
|
|
95
|
-
const projectCache = /* @__PURE__ */ new Map();
|
|
96
|
-
let folders = [];
|
|
97
|
-
let canConfigure = false;
|
|
98
|
-
let settings = { ...DEFAULT_SETTINGS };
|
|
92
|
+
function createPublisher(connection) {
|
|
99
93
|
let published = /* @__PURE__ */ new Set();
|
|
100
|
-
|
|
101
|
-
let generateTimer;
|
|
102
|
-
/** True when the client sends file events itself; otherwise the server watches the disk. */
|
|
103
|
-
let clientWatches = false;
|
|
104
|
-
const watchers = /* @__PURE__ */ new Map();
|
|
105
|
-
const projectsIn = (folder) => {
|
|
106
|
-
let projects = projectCache.get(folder);
|
|
107
|
-
if (!projects) {
|
|
108
|
-
projects = discoverProjects(folder);
|
|
109
|
-
projectCache.set(folder, projects);
|
|
110
|
-
}
|
|
111
|
-
return projects;
|
|
112
|
-
};
|
|
113
|
-
const forgetLayout = () => {
|
|
114
|
-
projectCache.clear();
|
|
115
|
-
clearGitignore(cache);
|
|
116
|
-
};
|
|
117
|
-
async function loadSettings() {
|
|
118
|
-
settings = { ...DEFAULT_SETTINGS };
|
|
119
|
-
if (!canConfigure) return;
|
|
120
|
-
try {
|
|
121
|
-
const value = await connection.workspace.getConfiguration("dialex");
|
|
122
|
-
if (value && typeof value === "object") {
|
|
123
|
-
if (typeof value.enable === "boolean") settings.enable = value.enable;
|
|
124
|
-
if (typeof value.unusedKeys === "boolean") settings.unusedKeys = value.unusedKeys;
|
|
125
|
-
if (typeof value.autoGenerate === "boolean") settings.autoGenerate = value.autoGenerate;
|
|
126
|
-
if (typeof value.configPath === "string" && value.configPath) settings.configPath = value.configPath;
|
|
127
|
-
}
|
|
128
|
-
} catch {}
|
|
129
|
-
}
|
|
130
|
-
function publish() {
|
|
94
|
+
return (issues) => {
|
|
131
95
|
const byFile = /* @__PURE__ */ new Map();
|
|
132
|
-
for (const
|
|
96
|
+
for (const issue of issues) {
|
|
133
97
|
const list = byFile.get(issue.file) ?? [];
|
|
134
98
|
list.push(toDiagnostic(issue));
|
|
135
99
|
byFile.set(issue.file, list);
|
|
@@ -148,13 +112,20 @@ function createServer(connection) {
|
|
|
148
112
|
diagnostics: []
|
|
149
113
|
});
|
|
150
114
|
published = next;
|
|
151
|
-
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
//#region src/server/generation.ts
|
|
119
|
+
/** Keeps the generated files of the projects that have them current, and watches the disk. */
|
|
120
|
+
function createGeneration(context) {
|
|
121
|
+
const { connection } = context;
|
|
122
|
+
const watchers = /* @__PURE__ */ new Map();
|
|
152
123
|
/** The projects whose generated file exists; the others do not use one. */
|
|
153
124
|
function generatedProjects() {
|
|
154
125
|
const found = [];
|
|
155
|
-
for (const folder of folders) for (const { root } of projectsIn(folder)) {
|
|
126
|
+
for (const folder of context.folders()) for (const { root } of context.projectsIn(folder)) {
|
|
156
127
|
const options = {
|
|
157
|
-
config: settings.configPath,
|
|
128
|
+
config: context.settings().configPath,
|
|
158
129
|
static: true
|
|
159
130
|
};
|
|
160
131
|
try {
|
|
@@ -173,6 +144,7 @@ function createServer(connection) {
|
|
|
173
144
|
* tree, so no project code runs.
|
|
174
145
|
*/
|
|
175
146
|
async function regenerate() {
|
|
147
|
+
const settings = context.settings();
|
|
176
148
|
if (!settings.autoGenerate || !settings.enable) return;
|
|
177
149
|
for (const { root, options } of generatedProjects()) try {
|
|
178
150
|
const { outputPath } = renderGenerated(root, options);
|
|
@@ -186,7 +158,8 @@ function createServer(connection) {
|
|
|
186
158
|
/** Without client file events, watch each project that has a generated file ourselves. */
|
|
187
159
|
async function syncWatchers() {
|
|
188
160
|
const wanted = /* @__PURE__ */ new Map();
|
|
189
|
-
|
|
161
|
+
const settings = context.settings();
|
|
162
|
+
if (!context.clientWatches() && settings.autoGenerate && settings.enable) for (const { root, options } of generatedProjects()) wanted.set(root, options);
|
|
190
163
|
for (const [root, watcher] of watchers) if (!wanted.has(root)) {
|
|
191
164
|
watchers.delete(root);
|
|
192
165
|
await watcher.close();
|
|
@@ -195,13 +168,232 @@ function createServer(connection) {
|
|
|
195
168
|
if (watchers.has(root)) continue;
|
|
196
169
|
try {
|
|
197
170
|
watchers.set(root, startGenerateWatcher(root, options, () => {
|
|
198
|
-
regenerate().then(() =>
|
|
171
|
+
regenerate().then(() => context.afterWatchRegenerate());
|
|
199
172
|
}, 250, (message) => connection.console.info(`[${root}] ${message}`)));
|
|
200
173
|
} catch (error) {
|
|
201
174
|
connection.console.error(`[${root}] could not watch: ${error.message}`);
|
|
202
175
|
}
|
|
203
176
|
}
|
|
204
177
|
}
|
|
178
|
+
async function dispose() {
|
|
179
|
+
await Promise.all([...watchers.values()].map((watcher) => watcher.close()));
|
|
180
|
+
watchers.clear();
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
regenerate,
|
|
184
|
+
syncWatchers,
|
|
185
|
+
dispose
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//#endregion
|
|
189
|
+
//#region src/server/handlers.ts
|
|
190
|
+
const hoverHandler = (ctx) => (params) => {
|
|
191
|
+
const document = ctx.documents.get(params.textDocument.uri);
|
|
192
|
+
const file = uriToPath(params.textDocument.uri);
|
|
193
|
+
const model = document && file ? ctx.modelFor(file) : void 0;
|
|
194
|
+
if (!document || !model) return null;
|
|
195
|
+
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
196
|
+
const markdown = ref ? buildHover(model, ref) : void 0;
|
|
197
|
+
if (!ref || !markdown) return null;
|
|
198
|
+
return {
|
|
199
|
+
contents: {
|
|
200
|
+
kind: "markdown",
|
|
201
|
+
value: markdown
|
|
202
|
+
},
|
|
203
|
+
range: {
|
|
204
|
+
start: {
|
|
205
|
+
line: params.position.line,
|
|
206
|
+
character: ref.start
|
|
207
|
+
},
|
|
208
|
+
end: {
|
|
209
|
+
line: params.position.line,
|
|
210
|
+
character: ref.end
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
const definitionHandler = (ctx) => (params) => {
|
|
216
|
+
const document = ctx.documents.get(params.textDocument.uri);
|
|
217
|
+
const file = uriToPath(params.textDocument.uri);
|
|
218
|
+
const model = document && file ? ctx.modelFor(file) : void 0;
|
|
219
|
+
if (!document || !model) return null;
|
|
220
|
+
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
221
|
+
const target = ref ? definitionFor(model, ref) : void 0;
|
|
222
|
+
if (!target) return null;
|
|
223
|
+
return {
|
|
224
|
+
uri: pathToFileURL(target.file).href,
|
|
225
|
+
range: {
|
|
226
|
+
start: {
|
|
227
|
+
line: target.range.start.line,
|
|
228
|
+
character: target.range.start.column
|
|
229
|
+
},
|
|
230
|
+
end: {
|
|
231
|
+
line: target.range.end.line,
|
|
232
|
+
character: target.range.end.column
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
};
|
|
237
|
+
const completionHandler = (ctx) => (params) => {
|
|
238
|
+
const document = ctx.documents.get(params.textDocument.uri);
|
|
239
|
+
const file = uriToPath(params.textDocument.uri);
|
|
240
|
+
const model = document && file ? ctx.modelFor(file) : void 0;
|
|
241
|
+
if (!document || !model) return [];
|
|
242
|
+
const before = lineOf(document, params.position.line).slice(0, params.position.character);
|
|
243
|
+
const context = completionContextAt(before, findBindings(document.getText()));
|
|
244
|
+
if (!context) return [];
|
|
245
|
+
const range = {
|
|
246
|
+
start: {
|
|
247
|
+
line: params.position.line,
|
|
248
|
+
character: params.position.character - context.length
|
|
249
|
+
},
|
|
250
|
+
end: params.position
|
|
251
|
+
};
|
|
252
|
+
return completionEntries(model, context).map((entry) => ({
|
|
253
|
+
label: entry.label,
|
|
254
|
+
kind: COMPLETION_KINDS[entry.kind],
|
|
255
|
+
detail: entry.detail,
|
|
256
|
+
documentation: entry.documentation,
|
|
257
|
+
filterText: entry.label,
|
|
258
|
+
textEdit: {
|
|
259
|
+
range,
|
|
260
|
+
newText: entry.insertText
|
|
261
|
+
}
|
|
262
|
+
}));
|
|
263
|
+
};
|
|
264
|
+
const codeActionHandler = (ctx) => (params) => {
|
|
265
|
+
const file = uriToPath(params.textDocument.uri);
|
|
266
|
+
const document = ctx.documents.get(params.textDocument.uri);
|
|
267
|
+
if (!file || !document || !ctx.enabled()) return [];
|
|
268
|
+
const actions = [];
|
|
269
|
+
const text = document.getText();
|
|
270
|
+
const model = ctx.modelFor(file);
|
|
271
|
+
for (const diagnostic of params.context.diagnostics) {
|
|
272
|
+
const data = diagnostic.data;
|
|
273
|
+
if (diagnostic.source !== "dialex" || !data?.path) continue;
|
|
274
|
+
if (data.code === "missing-key" && data.locale) {
|
|
275
|
+
const updated = addMissingKeys(file, text, [{
|
|
276
|
+
locale: data.locale,
|
|
277
|
+
path: data.path,
|
|
278
|
+
sourceLocale: data.sourceLocale
|
|
279
|
+
}]);
|
|
280
|
+
if (!updated) continue;
|
|
281
|
+
actions.push({
|
|
282
|
+
title: `Add "${data.path.join(".")}" to ${data.locale} (marked [TODO])`,
|
|
283
|
+
kind: CodeActionKind.QuickFix,
|
|
284
|
+
diagnostics: [diagnostic],
|
|
285
|
+
isPreferred: true,
|
|
286
|
+
edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } }
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (data.code === "unknown-path" && data.dictionary) {
|
|
290
|
+
const dictionary = model?.dictionaries.get(data.dictionary);
|
|
291
|
+
if (!dictionary) continue;
|
|
292
|
+
const dictionaryUri = pathToFileURL(dictionary.file).href;
|
|
293
|
+
const open = ctx.documents.get(dictionaryUri);
|
|
294
|
+
let dictionaryText;
|
|
295
|
+
try {
|
|
296
|
+
dictionaryText = open ? open.getText() : fs.readFileSync(dictionary.file, "utf-8");
|
|
297
|
+
} catch {
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const updated = addUnknownKey(dictionary.file, dictionaryText, data.path);
|
|
301
|
+
if (!updated) continue;
|
|
302
|
+
actions.push({
|
|
303
|
+
title: `Add "${data.path.join(".")}" to the "${dictionary.name}" dictionary`,
|
|
304
|
+
kind: CodeActionKind.QuickFix,
|
|
305
|
+
diagnostics: [diagnostic],
|
|
306
|
+
edit: { changes: { [dictionaryUri]: [replaceAll(dictionaryText, updated)] } }
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const missing = ctx.issues().filter((issue) => issue.file === file && issue.code === "missing-key").filter((issue) => issue.locale && issue.path).map((issue) => ({
|
|
311
|
+
locale: issue.locale,
|
|
312
|
+
path: issue.path,
|
|
313
|
+
sourceLocale: issue.sourceLocale
|
|
314
|
+
}));
|
|
315
|
+
if (missing.length > 1) {
|
|
316
|
+
const updated = addMissingKeys(file, text, missing);
|
|
317
|
+
if (updated) actions.push({
|
|
318
|
+
title: `Add all ${missing.length} missing keys (marked [TODO])`,
|
|
319
|
+
kind: CodeActionKind.QuickFix,
|
|
320
|
+
edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } }
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
return actions;
|
|
324
|
+
};
|
|
325
|
+
//#endregion
|
|
326
|
+
//#region src/server/settings.ts
|
|
327
|
+
const DEFAULT_SETTINGS = {
|
|
328
|
+
enable: true,
|
|
329
|
+
diagnostics: true,
|
|
330
|
+
unusedKeys: false,
|
|
331
|
+
autoGenerate: true
|
|
332
|
+
};
|
|
333
|
+
/** Files whose creation or removal can change the set of Dialex projects or what is scanned. */
|
|
334
|
+
const WATCHED_GLOBS = [
|
|
335
|
+
"**/*.content.ts",
|
|
336
|
+
"**/dialex.config.*",
|
|
337
|
+
"**/i18n.config.*",
|
|
338
|
+
"**/.gitignore"
|
|
339
|
+
];
|
|
340
|
+
/** Copies the values the client sent for the `dialex` section onto `settings`. */
|
|
341
|
+
function applyClientSettings(settings, value) {
|
|
342
|
+
const sent = value;
|
|
343
|
+
if (sent && typeof sent === "object") {
|
|
344
|
+
if (typeof sent.enable === "boolean") settings.enable = sent.enable;
|
|
345
|
+
if (typeof sent.diagnostics === "boolean") settings.diagnostics = sent.diagnostics;
|
|
346
|
+
if (typeof sent.unusedKeys === "boolean") settings.unusedKeys = sent.unusedKeys;
|
|
347
|
+
if (typeof sent.autoGenerate === "boolean") settings.autoGenerate = sent.autoGenerate;
|
|
348
|
+
if (typeof sent.configPath === "string" && sent.configPath) settings.configPath = sent.configPath;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/server.ts
|
|
353
|
+
/**
|
|
354
|
+
* Wires Dialex's editor features to an LSP connection. Nothing in the project is executed:
|
|
355
|
+
* dictionaries and configs are read from the syntax tree (`runtime: false`).
|
|
356
|
+
*/
|
|
357
|
+
function createServer(connection) {
|
|
358
|
+
const documents = new TextDocuments(TextDocument);
|
|
359
|
+
const cache = createAnalysisCache();
|
|
360
|
+
const states = /* @__PURE__ */ new Map();
|
|
361
|
+
const projectCache = /* @__PURE__ */ new Map();
|
|
362
|
+
const publish = createPublisher(connection);
|
|
363
|
+
let folders = [];
|
|
364
|
+
let canConfigure = false;
|
|
365
|
+
let settings = { ...DEFAULT_SETTINGS };
|
|
366
|
+
let timer;
|
|
367
|
+
let generateTimer;
|
|
368
|
+
/** True when the client sends file events itself; otherwise the server watches the disk. */
|
|
369
|
+
let clientWatches = false;
|
|
370
|
+
const projectsIn = (folder) => {
|
|
371
|
+
let projects = projectCache.get(folder);
|
|
372
|
+
if (!projects) {
|
|
373
|
+
projects = discoverProjects(folder);
|
|
374
|
+
projectCache.set(folder, projects);
|
|
375
|
+
}
|
|
376
|
+
return projects;
|
|
377
|
+
};
|
|
378
|
+
const forgetLayout = () => {
|
|
379
|
+
projectCache.clear();
|
|
380
|
+
clearGitignore(cache);
|
|
381
|
+
};
|
|
382
|
+
const generation = createGeneration({
|
|
383
|
+
connection,
|
|
384
|
+
folders: () => folders,
|
|
385
|
+
projectsIn,
|
|
386
|
+
settings: () => settings,
|
|
387
|
+
clientWatches: () => clientWatches,
|
|
388
|
+
afterWatchRegenerate: () => schedule()
|
|
389
|
+
});
|
|
390
|
+
async function loadSettings() {
|
|
391
|
+
settings = { ...DEFAULT_SETTINGS };
|
|
392
|
+
if (!canConfigure) return;
|
|
393
|
+
try {
|
|
394
|
+
applyClientSettings(settings, await connection.workspace.getConfiguration("dialex"));
|
|
395
|
+
} catch {}
|
|
396
|
+
}
|
|
205
397
|
async function refresh() {
|
|
206
398
|
await loadSettings();
|
|
207
399
|
states.clear();
|
|
@@ -223,8 +415,8 @@ function createServer(connection) {
|
|
|
223
415
|
} catch (error) {
|
|
224
416
|
connection.console.error(`[${root}] analysis failed: ${error.message}`);
|
|
225
417
|
}
|
|
226
|
-
publish();
|
|
227
|
-
await syncWatchers();
|
|
418
|
+
publish(settings.diagnostics ? allIssues() : []);
|
|
419
|
+
await generation.syncWatchers();
|
|
228
420
|
}
|
|
229
421
|
function schedule() {
|
|
230
422
|
if (timer) clearTimeout(timer);
|
|
@@ -232,7 +424,7 @@ function createServer(connection) {
|
|
|
232
424
|
}
|
|
233
425
|
function scheduleGenerate() {
|
|
234
426
|
if (generateTimer) clearTimeout(generateTimer);
|
|
235
|
-
generateTimer = setTimeout(() => void regenerate(), 250);
|
|
427
|
+
generateTimer = setTimeout(() => void generation.regenerate(), 250);
|
|
236
428
|
}
|
|
237
429
|
connection.onInitialize((params) => {
|
|
238
430
|
folders = (params.workspaceFolders ?? []).map((folder) => uriToPath(folder.uri)).filter((path) => path !== void 0);
|
|
@@ -285,162 +477,27 @@ function createServer(connection) {
|
|
|
285
477
|
connection.onShutdown(() => dispose());
|
|
286
478
|
/** The project model for a file, unless the features are turned off. */
|
|
287
479
|
const modelFor = (file) => settings.enable ? modelForFile([...states.values()].map((state) => state.model), file) : void 0;
|
|
288
|
-
/**
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
connection.
|
|
300
|
-
const document = documents.get(params.textDocument.uri);
|
|
301
|
-
const file = uriToPath(params.textDocument.uri);
|
|
302
|
-
const model = document && file ? modelFor(file) : void 0;
|
|
303
|
-
if (!document || !model) return null;
|
|
304
|
-
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
305
|
-
const markdown = ref ? buildHover(model, ref) : void 0;
|
|
306
|
-
if (!ref || !markdown) return null;
|
|
307
|
-
return {
|
|
308
|
-
contents: {
|
|
309
|
-
kind: "markdown",
|
|
310
|
-
value: markdown
|
|
311
|
-
},
|
|
312
|
-
range: {
|
|
313
|
-
start: {
|
|
314
|
-
line: params.position.line,
|
|
315
|
-
character: ref.start
|
|
316
|
-
},
|
|
317
|
-
end: {
|
|
318
|
-
line: params.position.line,
|
|
319
|
-
character: ref.end
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
};
|
|
323
|
-
});
|
|
324
|
-
connection.onDefinition((params) => {
|
|
325
|
-
const document = documents.get(params.textDocument.uri);
|
|
326
|
-
const file = uriToPath(params.textDocument.uri);
|
|
327
|
-
const model = document && file ? modelFor(file) : void 0;
|
|
328
|
-
if (!document || !model) return null;
|
|
329
|
-
const ref = findReferenceAt(lineOf(document, params.position.line), params.position.character, findBindings(document.getText()));
|
|
330
|
-
const target = ref ? definitionFor(model, ref) : void 0;
|
|
331
|
-
if (!target) return null;
|
|
332
|
-
return {
|
|
333
|
-
uri: pathToFileURL(target.file).href,
|
|
334
|
-
range: {
|
|
335
|
-
start: {
|
|
336
|
-
line: target.range.start.line,
|
|
337
|
-
character: target.range.start.column
|
|
338
|
-
},
|
|
339
|
-
end: {
|
|
340
|
-
line: target.range.end.line,
|
|
341
|
-
character: target.range.end.column
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
connection.onCompletion((params) => {
|
|
347
|
-
const document = documents.get(params.textDocument.uri);
|
|
348
|
-
const file = uriToPath(params.textDocument.uri);
|
|
349
|
-
const model = document && file ? modelFor(file) : void 0;
|
|
350
|
-
if (!document || !model) return [];
|
|
351
|
-
const before = lineOf(document, params.position.line).slice(0, params.position.character);
|
|
352
|
-
const context = completionContextAt(before, findBindings(document.getText()));
|
|
353
|
-
if (!context) return [];
|
|
354
|
-
const range = {
|
|
355
|
-
start: {
|
|
356
|
-
line: params.position.line,
|
|
357
|
-
character: params.position.character - context.length
|
|
358
|
-
},
|
|
359
|
-
end: params.position
|
|
360
|
-
};
|
|
361
|
-
return completionEntries(model, context).map((entry) => ({
|
|
362
|
-
label: entry.label,
|
|
363
|
-
kind: COMPLETION_KINDS[entry.kind],
|
|
364
|
-
detail: entry.detail,
|
|
365
|
-
documentation: entry.documentation,
|
|
366
|
-
filterText: entry.label,
|
|
367
|
-
textEdit: {
|
|
368
|
-
range,
|
|
369
|
-
newText: entry.insertText
|
|
370
|
-
}
|
|
371
|
-
}));
|
|
372
|
-
});
|
|
373
|
-
connection.onCodeAction((params) => {
|
|
374
|
-
const file = uriToPath(params.textDocument.uri);
|
|
375
|
-
const document = documents.get(params.textDocument.uri);
|
|
376
|
-
if (!file || !document || !settings.enable) return [];
|
|
377
|
-
const actions = [];
|
|
378
|
-
const text = document.getText();
|
|
379
|
-
const model = modelFor(file);
|
|
380
|
-
for (const diagnostic of params.context.diagnostics) {
|
|
381
|
-
const data = diagnostic.data;
|
|
382
|
-
if (diagnostic.source !== "dialex" || !data?.path) continue;
|
|
383
|
-
if (data.code === "missing-key" && data.locale) {
|
|
384
|
-
const updated = addMissingKeys(file, text, [{
|
|
385
|
-
locale: data.locale,
|
|
386
|
-
path: data.path,
|
|
387
|
-
sourceLocale: data.sourceLocale
|
|
388
|
-
}]);
|
|
389
|
-
if (!updated) continue;
|
|
390
|
-
actions.push({
|
|
391
|
-
title: `Add "${data.path.join(".")}" to ${data.locale} (marked [TODO])`,
|
|
392
|
-
kind: CodeActionKind.QuickFix,
|
|
393
|
-
diagnostics: [diagnostic],
|
|
394
|
-
isPreferred: true,
|
|
395
|
-
edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } }
|
|
396
|
-
});
|
|
397
|
-
}
|
|
398
|
-
if (data.code === "unknown-path" && data.dictionary) {
|
|
399
|
-
const dictionary = model?.dictionaries.get(data.dictionary);
|
|
400
|
-
if (!dictionary) continue;
|
|
401
|
-
const dictionaryUri = pathToFileURL(dictionary.file).href;
|
|
402
|
-
const open = documents.get(dictionaryUri);
|
|
403
|
-
let dictionaryText;
|
|
404
|
-
try {
|
|
405
|
-
dictionaryText = open ? open.getText() : fs.readFileSync(dictionary.file, "utf-8");
|
|
406
|
-
} catch {
|
|
407
|
-
continue;
|
|
408
|
-
}
|
|
409
|
-
const updated = addUnknownKey(dictionary.file, dictionaryText, data.path);
|
|
410
|
-
if (!updated) continue;
|
|
411
|
-
actions.push({
|
|
412
|
-
title: `Add "${data.path.join(".")}" to the "${dictionary.name}" dictionary`,
|
|
413
|
-
kind: CodeActionKind.QuickFix,
|
|
414
|
-
diagnostics: [diagnostic],
|
|
415
|
-
edit: { changes: { [dictionaryUri]: [replaceAll(dictionaryText, updated)] } }
|
|
416
|
-
});
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
const missing = [...states.values()].flatMap((state) => state.issues).filter((issue) => issue.file === file && issue.code === "missing-key").filter((issue) => issue.locale && issue.path).map((issue) => ({
|
|
420
|
-
locale: issue.locale,
|
|
421
|
-
path: issue.path,
|
|
422
|
-
sourceLocale: issue.sourceLocale
|
|
423
|
-
}));
|
|
424
|
-
if (missing.length > 1) {
|
|
425
|
-
const updated = addMissingKeys(file, text, missing);
|
|
426
|
-
if (updated) actions.push({
|
|
427
|
-
title: `Add all ${missing.length} missing keys (marked [TODO])`,
|
|
428
|
-
kind: CodeActionKind.QuickFix,
|
|
429
|
-
edit: { changes: { [params.textDocument.uri]: [replaceAll(text, updated)] } }
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
return actions;
|
|
433
|
-
});
|
|
480
|
+
/** The issues of every project, in project order. */
|
|
481
|
+
const allIssues = () => [...states.values()].flatMap((state) => state.issues);
|
|
482
|
+
const handlers = {
|
|
483
|
+
documents,
|
|
484
|
+
modelFor,
|
|
485
|
+
issues: allIssues,
|
|
486
|
+
enabled: () => settings.enable
|
|
487
|
+
};
|
|
488
|
+
connection.onHover(hoverHandler(handlers));
|
|
489
|
+
connection.onDefinition(definitionHandler(handlers));
|
|
490
|
+
connection.onCompletion(completionHandler(handlers));
|
|
491
|
+
connection.onCodeAction(codeActionHandler(handlers));
|
|
434
492
|
documents.listen(connection);
|
|
435
493
|
async function dispose() {
|
|
436
494
|
if (timer) clearTimeout(timer);
|
|
437
495
|
if (generateTimer) clearTimeout(generateTimer);
|
|
438
|
-
await
|
|
439
|
-
watchers.clear();
|
|
496
|
+
await generation.dispose();
|
|
440
497
|
}
|
|
441
498
|
return {
|
|
442
499
|
refresh,
|
|
443
|
-
regenerate,
|
|
500
|
+
regenerate: generation.regenerate,
|
|
444
501
|
dispose
|
|
445
502
|
};
|
|
446
503
|
}
|