coc-ltex-plus 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +197 -0
- package/lib/index.js +2319 -0
- package/package.json +700 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,2319 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
__testHooks: () => __testHooks,
|
|
24
|
+
activate: () => activate
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(src_exports);
|
|
27
|
+
var import_fs4 = require("fs");
|
|
28
|
+
|
|
29
|
+
// src/extension.ts
|
|
30
|
+
var import_coc8 = require("coc.nvim");
|
|
31
|
+
|
|
32
|
+
// src/constants.ts
|
|
33
|
+
var DEFAULT_LANGUAGES = [
|
|
34
|
+
"bibtex",
|
|
35
|
+
"context",
|
|
36
|
+
"context.tex",
|
|
37
|
+
"html",
|
|
38
|
+
"latex",
|
|
39
|
+
"markdown",
|
|
40
|
+
"mdx",
|
|
41
|
+
"typst",
|
|
42
|
+
"org",
|
|
43
|
+
"quarto",
|
|
44
|
+
"rsweave",
|
|
45
|
+
"restructuredtext",
|
|
46
|
+
"mail",
|
|
47
|
+
"neorg",
|
|
48
|
+
"norg",
|
|
49
|
+
"help"
|
|
50
|
+
];
|
|
51
|
+
var LTEX_LANGUAGE_CODES = [
|
|
52
|
+
"auto",
|
|
53
|
+
"ar",
|
|
54
|
+
"ast-ES",
|
|
55
|
+
"be-BY",
|
|
56
|
+
"br-FR",
|
|
57
|
+
"ca-ES",
|
|
58
|
+
"ca-ES-valencia",
|
|
59
|
+
"da-DK",
|
|
60
|
+
"de",
|
|
61
|
+
"de-AT",
|
|
62
|
+
"de-CH",
|
|
63
|
+
"de-DE",
|
|
64
|
+
"de-DE-x-simple-language",
|
|
65
|
+
"el-GR",
|
|
66
|
+
"en",
|
|
67
|
+
"en-AU",
|
|
68
|
+
"en-CA",
|
|
69
|
+
"en-GB",
|
|
70
|
+
"en-NZ",
|
|
71
|
+
"en-US",
|
|
72
|
+
"en-ZA",
|
|
73
|
+
"eo",
|
|
74
|
+
"es",
|
|
75
|
+
"es-AR",
|
|
76
|
+
"fa",
|
|
77
|
+
"fr",
|
|
78
|
+
"ga-IE",
|
|
79
|
+
"gl-ES",
|
|
80
|
+
"it",
|
|
81
|
+
"ja-JP",
|
|
82
|
+
"km-KH",
|
|
83
|
+
"nl",
|
|
84
|
+
"nl-BE",
|
|
85
|
+
"pl-PL",
|
|
86
|
+
"pt",
|
|
87
|
+
"pt-AO",
|
|
88
|
+
"pt-BR",
|
|
89
|
+
"pt-MZ",
|
|
90
|
+
"pt-PT",
|
|
91
|
+
"ro-RO",
|
|
92
|
+
"ru-RU",
|
|
93
|
+
"sk-SK",
|
|
94
|
+
"sl-SI",
|
|
95
|
+
"sv",
|
|
96
|
+
"ta-IN",
|
|
97
|
+
"tl-PH",
|
|
98
|
+
"uk-UA",
|
|
99
|
+
"zh-CN"
|
|
100
|
+
];
|
|
101
|
+
var PROCESS_SETTING_KEYS = [
|
|
102
|
+
"ltex.enabled",
|
|
103
|
+
"ltex.ltex-ls.path",
|
|
104
|
+
"ltex.ltex-ls.args",
|
|
105
|
+
"ltex.java.path",
|
|
106
|
+
"ltex.java.initialHeapSize",
|
|
107
|
+
"ltex.java.maximumHeapSize"
|
|
108
|
+
];
|
|
109
|
+
var MANAGED_SETTING_NAMES = [
|
|
110
|
+
"dictionary",
|
|
111
|
+
"disabledRules",
|
|
112
|
+
"hiddenFalsePositives"
|
|
113
|
+
];
|
|
114
|
+
var LTEX_RELEASES_URL = "https://github.com/ltex-plus/ltex-ls-plus/releases";
|
|
115
|
+
var LTEX_LATEST_RELEASE_API_URL = "https://api.github.com/repos/ltex-plus/ltex-ls-plus/releases/latest";
|
|
116
|
+
var HTTP_REDIRECT_LIMIT = 5;
|
|
117
|
+
var RELEASE_METADATA_LIMIT_BYTES = 10 * 1024 * 1024;
|
|
118
|
+
var INSTALL_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
119
|
+
var DOWNLOAD_IDLE_TIMEOUT_MS = 30 * 1e3;
|
|
120
|
+
var VERIFY_TIMEOUT_MS = 30 * 1e3;
|
|
121
|
+
var MANAGED_RELEASE_KEY = "managedRelease";
|
|
122
|
+
|
|
123
|
+
// src/client.ts
|
|
124
|
+
var import_coc3 = require("coc.nvim");
|
|
125
|
+
|
|
126
|
+
// src/config.ts
|
|
127
|
+
var import_coc2 = require("coc.nvim");
|
|
128
|
+
var import_path2 = require("path");
|
|
129
|
+
|
|
130
|
+
// src/paths.ts
|
|
131
|
+
var import_coc = require("coc.nvim");
|
|
132
|
+
var import_fs = require("fs");
|
|
133
|
+
var import_path = require("path");
|
|
134
|
+
var import_os = require("os");
|
|
135
|
+
function ltexConfig(resource) {
|
|
136
|
+
return import_coc.workspace.getConfiguration("ltex", resource);
|
|
137
|
+
}
|
|
138
|
+
function expandHome(pathValue) {
|
|
139
|
+
return pathValue.replace(/^~(?=$|[\\/])/, (0, import_os.homedir)());
|
|
140
|
+
}
|
|
141
|
+
function normalizePath(pathValue) {
|
|
142
|
+
if (!pathValue) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
const trimmed = pathValue.trim();
|
|
146
|
+
return trimmed ? expandHome(trimmed) : null;
|
|
147
|
+
}
|
|
148
|
+
function executableNames(base) {
|
|
149
|
+
if (process.platform !== "win32") {
|
|
150
|
+
return [base];
|
|
151
|
+
}
|
|
152
|
+
return [`${base}.cmd`, `${base}.bat`, `${base}.exe`, base];
|
|
153
|
+
}
|
|
154
|
+
function isExecutableFile(pathValue) {
|
|
155
|
+
try {
|
|
156
|
+
const stat = (0, import_fs.statSync)(pathValue);
|
|
157
|
+
if (!stat.isFile()) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
(0, import_fs.accessSync)(pathValue, import_fs.constants.X_OK);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
function findInPath(base) {
|
|
167
|
+
const pathValue = process.env.PATH ?? "";
|
|
168
|
+
for (const dir of pathValue.split(import_path.delimiter)) {
|
|
169
|
+
if (!dir) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
for (const name of executableNames(base)) {
|
|
173
|
+
const candidate = (0, import_path.join)(dir, name);
|
|
174
|
+
if (isExecutableFile(candidate)) {
|
|
175
|
+
return candidate;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
function ltexExecutableCandidates(directory) {
|
|
182
|
+
const candidates = [];
|
|
183
|
+
for (const base of ["ltex-ls-plus", "ltex-ls"]) {
|
|
184
|
+
for (const name of executableNames(base)) {
|
|
185
|
+
candidates.push((0, import_path.join)(directory, "bin", name), (0, import_path.join)(directory, name));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return candidates;
|
|
189
|
+
}
|
|
190
|
+
function existingLtexExecutable(directory) {
|
|
191
|
+
for (const candidate of ltexExecutableCandidates(directory)) {
|
|
192
|
+
try {
|
|
193
|
+
if ((0, import_fs.statSync)(candidate).isFile()) {
|
|
194
|
+
return candidate;
|
|
195
|
+
}
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
function resolveExecutableFromConfiguredPath(pathValue) {
|
|
202
|
+
const expanded = expandHome(pathValue);
|
|
203
|
+
if (!(0, import_path.isAbsolute)(expanded) && !expanded.includes("/") && !expanded.includes("\\")) {
|
|
204
|
+
return findInPath(expanded);
|
|
205
|
+
}
|
|
206
|
+
if (!(0, import_fs.existsSync)(expanded)) {
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
const stat = (0, import_fs.statSync)(expanded);
|
|
210
|
+
if (stat.isFile()) {
|
|
211
|
+
return expanded;
|
|
212
|
+
}
|
|
213
|
+
if (!stat.isDirectory()) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
return ltexExecutableCandidates(expanded).find(isExecutableFile) ?? null;
|
|
217
|
+
}
|
|
218
|
+
function managedServerDirectory(storagePath) {
|
|
219
|
+
return (0, import_path.join)(storagePath, "ltex-ls-plus");
|
|
220
|
+
}
|
|
221
|
+
function managedServerExecutable(storagePath) {
|
|
222
|
+
return resolveExecutableFromConfiguredPath(managedServerDirectory(storagePath));
|
|
223
|
+
}
|
|
224
|
+
function resolveLtexServer(storagePath) {
|
|
225
|
+
const configuredPath = normalizePath(ltexConfig().get("ltex-ls.path", ""));
|
|
226
|
+
if (configuredPath) {
|
|
227
|
+
const command = resolveExecutableFromConfiguredPath(configuredPath);
|
|
228
|
+
return command ? { command, source: "configured" } : null;
|
|
229
|
+
}
|
|
230
|
+
if (storagePath) {
|
|
231
|
+
const managed = managedServerExecutable(storagePath);
|
|
232
|
+
if (managed) {
|
|
233
|
+
return { command: managed, source: "managed" };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const system = findInPath("ltex-ls-plus") ?? findInPath("ltex-ls");
|
|
237
|
+
return system ? { command: system, source: "system" } : null;
|
|
238
|
+
}
|
|
239
|
+
function resolveLtexExecutable(storagePath) {
|
|
240
|
+
return resolveLtexServer(storagePath)?.command ?? null;
|
|
241
|
+
}
|
|
242
|
+
function ltexPlatformName(platform = process.platform) {
|
|
243
|
+
switch (platform) {
|
|
244
|
+
case "darwin":
|
|
245
|
+
return "mac";
|
|
246
|
+
case "linux":
|
|
247
|
+
return "linux";
|
|
248
|
+
case "win32":
|
|
249
|
+
return "windows";
|
|
250
|
+
default:
|
|
251
|
+
return void 0;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function ltexArchName(arch = process.arch) {
|
|
255
|
+
switch (arch) {
|
|
256
|
+
case "arm64":
|
|
257
|
+
return "aarch64";
|
|
258
|
+
case "x64":
|
|
259
|
+
return "x64";
|
|
260
|
+
default:
|
|
261
|
+
return void 0;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function releaseVersionFromTag(tag) {
|
|
265
|
+
return tag.replace(/^v/i, "");
|
|
266
|
+
}
|
|
267
|
+
function ltexAssetNameForPlatform(version, platform = process.platform, arch = process.arch) {
|
|
268
|
+
const platformName = ltexPlatformName(platform);
|
|
269
|
+
const archName = ltexArchName(arch);
|
|
270
|
+
if (!platformName || !archName) {
|
|
271
|
+
return void 0;
|
|
272
|
+
}
|
|
273
|
+
const archiveType = platformName === "windows" ? "zip" : "tar.gz";
|
|
274
|
+
return `ltex-ls-plus-${version}-${platformName}-${archName}.${archiveType}`;
|
|
275
|
+
}
|
|
276
|
+
function assetMatchesPlatform(assetName, version, platform = process.platform, arch = process.arch) {
|
|
277
|
+
const exact = ltexAssetNameForPlatform(version, platform, arch);
|
|
278
|
+
if (exact && assetName === exact) {
|
|
279
|
+
return true;
|
|
280
|
+
}
|
|
281
|
+
const platformName = ltexPlatformName(platform);
|
|
282
|
+
const archName = ltexArchName(arch);
|
|
283
|
+
return !!platformName && !!archName && new RegExp(`^ltex-ls-plus-.+-${platformName}-${archName}\\.(?:tar\\.gz|zip)$`).test(assetName);
|
|
284
|
+
}
|
|
285
|
+
function sha256FromDigest(digest) {
|
|
286
|
+
const match = digest?.match(/^sha256:([a-f0-9]{64})$/i);
|
|
287
|
+
return match?.[1].toLowerCase();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// src/config.ts
|
|
291
|
+
function workspaceRoot() {
|
|
292
|
+
const folders = import_coc2.workspace.workspaceFolders ?? [];
|
|
293
|
+
if (folders.length > 0) {
|
|
294
|
+
return import_coc2.Uri.parse(folders[0].uri).fsPath;
|
|
295
|
+
}
|
|
296
|
+
if (import_coc2.workspace.root) {
|
|
297
|
+
return import_coc2.workspace.root;
|
|
298
|
+
}
|
|
299
|
+
if (import_coc2.workspace.folderPaths?.[0]) {
|
|
300
|
+
return import_coc2.workspace.folderPaths[0];
|
|
301
|
+
}
|
|
302
|
+
return process.cwd();
|
|
303
|
+
}
|
|
304
|
+
function enabledLanguages() {
|
|
305
|
+
const enabled = ltexConfig().get("enabled", DEFAULT_LANGUAGES);
|
|
306
|
+
if (enabled === true) {
|
|
307
|
+
return [...DEFAULT_LANGUAGES];
|
|
308
|
+
}
|
|
309
|
+
if (enabled === false) {
|
|
310
|
+
return [];
|
|
311
|
+
}
|
|
312
|
+
if (!Array.isArray(enabled)) {
|
|
313
|
+
return [...DEFAULT_LANGUAGES];
|
|
314
|
+
}
|
|
315
|
+
return enabled.filter((language) => typeof language === "string" && language.length > 0);
|
|
316
|
+
}
|
|
317
|
+
function documentSelector() {
|
|
318
|
+
return enabledLanguages().flatMap((language) => [
|
|
319
|
+
{ scheme: "file", language },
|
|
320
|
+
{ scheme: "untitled", language }
|
|
321
|
+
]);
|
|
322
|
+
}
|
|
323
|
+
function checkableWorkspaceDocuments(documents = import_coc2.workspace.documents, languages = enabledLanguages()) {
|
|
324
|
+
const enabled = new Set(languages);
|
|
325
|
+
const seen = /* @__PURE__ */ new Set();
|
|
326
|
+
return documents.filter((document) => {
|
|
327
|
+
const language = document.filetype || document.languageId;
|
|
328
|
+
if (!enabled.has(language) || seen.has(document.uri) || !isCheckableWorkspaceUri(document.uri)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
seen.add(document.uri);
|
|
332
|
+
return true;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function isCheckableWorkspaceUri(uriString) {
|
|
336
|
+
const uri = import_coc2.Uri.parse(uriString);
|
|
337
|
+
if (uri.scheme === "untitled") {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (uri.scheme !== "file") {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const folders = import_coc2.workspace.workspaceFolders ?? [];
|
|
344
|
+
if (folders.length > 0) {
|
|
345
|
+
return import_coc2.workspace.getWorkspaceFolder(uriString) != null;
|
|
346
|
+
}
|
|
347
|
+
const roots = [import_coc2.workspace.root, ...import_coc2.workspace.folderPaths ?? []].filter((root) => typeof root === "string" && root.length > 0);
|
|
348
|
+
if (roots.length === 0) {
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
351
|
+
return roots.some((root) => pathContains(root, uri.fsPath));
|
|
352
|
+
}
|
|
353
|
+
function pathContains(root, filePath) {
|
|
354
|
+
const relativePath = (0, import_path2.relative)((0, import_path2.resolve)(root), (0, import_path2.resolve)(filePath));
|
|
355
|
+
return relativePath === "" || !!relativePath && !relativePath.startsWith("..") && !(0, import_path2.isAbsolute)(relativePath);
|
|
356
|
+
}
|
|
357
|
+
function serverEnvironment() {
|
|
358
|
+
const { ELECTRON_RUN_AS_NODE, ...env } = process.env;
|
|
359
|
+
const javaPath = normalizePath(ltexConfig().get("java.path", ""));
|
|
360
|
+
if (javaPath) {
|
|
361
|
+
env.JAVA_HOME = javaPath;
|
|
362
|
+
env.PATH = `${(0, import_path2.join)(javaPath, "bin")}${import_path2.delimiter}${env.PATH ?? ""}`;
|
|
363
|
+
}
|
|
364
|
+
const javaOptions = [];
|
|
365
|
+
const initialHeapSize = ltexConfig().get("java.initialHeapSize", void 0);
|
|
366
|
+
const maximumHeapSize = ltexConfig().get("java.maximumHeapSize", void 0);
|
|
367
|
+
if (typeof initialHeapSize === "number" && Number.isFinite(initialHeapSize) && initialHeapSize > 0) {
|
|
368
|
+
javaOptions.push(`-Xms${initialHeapSize}m`);
|
|
369
|
+
}
|
|
370
|
+
if (typeof maximumHeapSize === "number" && Number.isFinite(maximumHeapSize) && maximumHeapSize > 0) {
|
|
371
|
+
javaOptions.push(`-Xmx${maximumHeapSize}m`);
|
|
372
|
+
}
|
|
373
|
+
if (javaOptions.length > 0) {
|
|
374
|
+
env.JAVA_OPTS = [env.JAVA_OPTS, ...javaOptions].filter(Boolean).join(" ");
|
|
375
|
+
}
|
|
376
|
+
return env;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/client.ts
|
|
380
|
+
function createServerOptions(command) {
|
|
381
|
+
const args = ltexConfig().get("ltex-ls.args", []);
|
|
382
|
+
const run = {
|
|
383
|
+
command,
|
|
384
|
+
args: Array.isArray(args) ? args : [],
|
|
385
|
+
options: {
|
|
386
|
+
cwd: workspaceRoot(),
|
|
387
|
+
env: serverEnvironment(),
|
|
388
|
+
shell: process.platform === "win32"
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
return { run, debug: run };
|
|
392
|
+
}
|
|
393
|
+
function createClient(command, externalSettings, diagnosticsState = { enabled: true }) {
|
|
394
|
+
const clientOptions = {
|
|
395
|
+
documentSelector: documentSelector(),
|
|
396
|
+
synchronize: {
|
|
397
|
+
configurationSection: "ltex"
|
|
398
|
+
},
|
|
399
|
+
initializationOptions: {
|
|
400
|
+
customCapabilities: {
|
|
401
|
+
workspaceSpecificConfiguration: true
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
outputChannelName: "LTeX+",
|
|
405
|
+
revealOutputChannelOn: import_coc3.RevealOutputChannelOn.Never,
|
|
406
|
+
progressOnInitialization: true,
|
|
407
|
+
middleware: {
|
|
408
|
+
handleDiagnostics(uri, diagnostics, next) {
|
|
409
|
+
next(uri, diagnosticsState.enabled ? diagnostics : []);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
const client = new import_coc3.LanguageClient("ltex", "LTeX+ Language Server", createServerOptions(command), clientOptions);
|
|
414
|
+
client.onRequest("ltex/workspaceSpecificConfiguration", (params) => {
|
|
415
|
+
return externalSettings.handleWorkspaceSpecificConfiguration(params);
|
|
416
|
+
});
|
|
417
|
+
return client;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// src/diagnostics.ts
|
|
421
|
+
function diagnosticsFromCollection(collection) {
|
|
422
|
+
const records = [];
|
|
423
|
+
collection?.forEach((uri, diagnostics) => {
|
|
424
|
+
for (const diagnostic of diagnostics) {
|
|
425
|
+
records.push({ uri, diagnostic });
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
return records;
|
|
429
|
+
}
|
|
430
|
+
function diagnosticCode(diagnostic) {
|
|
431
|
+
if (diagnostic.code == null) {
|
|
432
|
+
return void 0;
|
|
433
|
+
}
|
|
434
|
+
return String(diagnostic.code);
|
|
435
|
+
}
|
|
436
|
+
function diagnosticsContextForEntry(entry, diagnostics, document) {
|
|
437
|
+
if (entry.settingName === "hiddenFalsePositives") {
|
|
438
|
+
return hiddenFalsePositiveContext(entry, diagnostics, document);
|
|
439
|
+
}
|
|
440
|
+
if (entry.settingName === "disabledRules") {
|
|
441
|
+
return disabledRuleContext(entry, diagnostics);
|
|
442
|
+
}
|
|
443
|
+
return {
|
|
444
|
+
status: "unknown",
|
|
445
|
+
label: "unknown"
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function parseHiddenFalsePositiveEntry(value) {
|
|
449
|
+
const trimmed = value.trim();
|
|
450
|
+
if (!trimmed) {
|
|
451
|
+
return {};
|
|
452
|
+
}
|
|
453
|
+
const parsed = parseHiddenFalsePositiveJson(trimmed);
|
|
454
|
+
if (parsed.ruleId || parsed.sentence) {
|
|
455
|
+
return parsed;
|
|
456
|
+
}
|
|
457
|
+
const logged = trimmed.match(/\brule\s+['"]([^'"]+)['"].*\bsentence\s+['"]([^'"]+)['"]/i);
|
|
458
|
+
if (logged) {
|
|
459
|
+
return {
|
|
460
|
+
ruleId: logged[1],
|
|
461
|
+
sentence: logged[2]
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const delimited = trimmed.match(/^([A-Z][A-Z0-9_]+)\s*(?:\t|\||:)\s*(.+)$/);
|
|
465
|
+
if (delimited) {
|
|
466
|
+
return {
|
|
467
|
+
ruleId: delimited[1],
|
|
468
|
+
sentence: cleanStoredSentence(delimited[2])
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
const sentence = cleanStoredSentence(trimmed);
|
|
472
|
+
return looksLikeSentence(sentence) ? { sentence } : {};
|
|
473
|
+
}
|
|
474
|
+
function parseHiddenFalsePositiveJson(value) {
|
|
475
|
+
if (!value.startsWith("{")) {
|
|
476
|
+
return {};
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const parsed = JSON.parse(value);
|
|
480
|
+
const ruleId = firstString(parsed, ["ruleId", "rule", "id"]);
|
|
481
|
+
const sentence = firstString(parsed, ["sentence", "text", "message", "falsePositive"]);
|
|
482
|
+
return { ruleId, sentence: sentence ? cleanStoredSentence(sentence) : void 0 };
|
|
483
|
+
} catch {
|
|
484
|
+
return {};
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
function firstString(record, keys) {
|
|
488
|
+
for (const key of keys) {
|
|
489
|
+
const value = record[key];
|
|
490
|
+
if (typeof value === "string" && value.trim()) {
|
|
491
|
+
return value.trim();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return void 0;
|
|
495
|
+
}
|
|
496
|
+
function cleanStoredSentence(value) {
|
|
497
|
+
return value.trim().replace(/^['"]|['"]$/g, "").trim();
|
|
498
|
+
}
|
|
499
|
+
function looksLikeSentence(value) {
|
|
500
|
+
return !/^[A-Z][A-Z0-9_]+$/.test(value) && /[\s.!?;:,]/.test(value);
|
|
501
|
+
}
|
|
502
|
+
function hiddenFalsePositiveContext(entry, diagnostics, document) {
|
|
503
|
+
const parsed = parseHiddenFalsePositiveEntry(entry.value);
|
|
504
|
+
if (!parsed.sentence) {
|
|
505
|
+
const matchedDiagnostic = diagnosticForRule(parsed.ruleId, diagnostics);
|
|
506
|
+
if (matchedDiagnostic) {
|
|
507
|
+
return contextFromDiagnostic("matchedDiagnostic", "diagnostic", matchedDiagnostic, parsed.ruleId);
|
|
508
|
+
}
|
|
509
|
+
return {
|
|
510
|
+
status: "unknown",
|
|
511
|
+
label: "unknown",
|
|
512
|
+
ruleId: parsed.ruleId
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
if (!document) {
|
|
516
|
+
return {
|
|
517
|
+
status: "unknown",
|
|
518
|
+
label: "unknown",
|
|
519
|
+
ruleId: parsed.ruleId,
|
|
520
|
+
sentence: parsed.sentence
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
const range = sentenceRange(document.content, parsed.sentence);
|
|
524
|
+
if (!range) {
|
|
525
|
+
return {
|
|
526
|
+
status: "obsolete",
|
|
527
|
+
label: "obsolete",
|
|
528
|
+
ruleId: parsed.ruleId,
|
|
529
|
+
sentence: parsed.sentence
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
return {
|
|
533
|
+
status: "active",
|
|
534
|
+
label: "active",
|
|
535
|
+
uri: document.uri,
|
|
536
|
+
range,
|
|
537
|
+
ruleId: parsed.ruleId,
|
|
538
|
+
sentence: parsed.sentence,
|
|
539
|
+
line: document.lines[range.start.line]
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
function disabledRuleContext(entry, diagnostics) {
|
|
543
|
+
const matchedDiagnostic = diagnosticForRule(entry.value, diagnostics);
|
|
544
|
+
if (matchedDiagnostic) {
|
|
545
|
+
return contextFromDiagnostic("matchedDiagnostic", "diagnostic", matchedDiagnostic, entry.value);
|
|
546
|
+
}
|
|
547
|
+
return {
|
|
548
|
+
status: "unknown",
|
|
549
|
+
label: "no current diagnostic",
|
|
550
|
+
ruleId: entry.value
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function diagnosticForRule(ruleId, diagnostics) {
|
|
554
|
+
if (!ruleId) {
|
|
555
|
+
return void 0;
|
|
556
|
+
}
|
|
557
|
+
return diagnostics.find((record) => diagnosticCode(record.diagnostic) === ruleId);
|
|
558
|
+
}
|
|
559
|
+
function contextFromDiagnostic(status, label, record, ruleId, sentence) {
|
|
560
|
+
return {
|
|
561
|
+
status,
|
|
562
|
+
label,
|
|
563
|
+
uri: record.uri,
|
|
564
|
+
range: record.diagnostic.range,
|
|
565
|
+
ruleId,
|
|
566
|
+
sentence,
|
|
567
|
+
message: record.diagnostic.message
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function sentenceRange(content, sentence) {
|
|
571
|
+
const exactIndex = content.indexOf(sentence);
|
|
572
|
+
if (exactIndex >= 0) {
|
|
573
|
+
return rangeFromOffsets(content, exactIndex, exactIndex + sentence.length);
|
|
574
|
+
}
|
|
575
|
+
const pattern = escapeRegExp(sentence).replace(/\s+/g, "\\s+");
|
|
576
|
+
const match = new RegExp(pattern).exec(content);
|
|
577
|
+
if (!match) {
|
|
578
|
+
return void 0;
|
|
579
|
+
}
|
|
580
|
+
return rangeFromOffsets(content, match.index, match.index + match[0].length);
|
|
581
|
+
}
|
|
582
|
+
function rangeFromOffsets(content, startOffset, endOffset) {
|
|
583
|
+
return {
|
|
584
|
+
start: positionAt(content, startOffset),
|
|
585
|
+
end: positionAt(content, endOffset)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function positionAt(content, offset) {
|
|
589
|
+
let line = 0;
|
|
590
|
+
let lineStart = 0;
|
|
591
|
+
const cappedOffset = Math.max(0, Math.min(offset, content.length));
|
|
592
|
+
for (let index = 0; index < cappedOffset; index += 1) {
|
|
593
|
+
if (content.charCodeAt(index) === 10) {
|
|
594
|
+
line += 1;
|
|
595
|
+
lineStart = index + 1;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
line,
|
|
600
|
+
character: cappedOffset - lineStart
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function escapeRegExp(value) {
|
|
604
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/externalSettings.ts
|
|
608
|
+
var import_coc5 = require("coc.nvim");
|
|
609
|
+
var import_path3 = require("path");
|
|
610
|
+
var import_fs2 = require("fs");
|
|
611
|
+
|
|
612
|
+
// src/text.ts
|
|
613
|
+
var import_coc4 = require("coc.nvim");
|
|
614
|
+
function uniqueSorted(values) {
|
|
615
|
+
return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b, void 0, { sensitivity: "base" }));
|
|
616
|
+
}
|
|
617
|
+
function cleanLanguageSpecificEntries(entries) {
|
|
618
|
+
const negatives = /* @__PURE__ */ new Set();
|
|
619
|
+
const positives = /* @__PURE__ */ new Set();
|
|
620
|
+
for (const rawEntry of entries) {
|
|
621
|
+
let entry = rawEntry.trim();
|
|
622
|
+
if (!entry) {
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
if (entry.startsWith("-")) {
|
|
626
|
+
entry = entry.slice(1);
|
|
627
|
+
if (positives.has(entry)) {
|
|
628
|
+
positives.delete(entry);
|
|
629
|
+
} else {
|
|
630
|
+
negatives.add(entry);
|
|
631
|
+
}
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
positives.add(entry);
|
|
635
|
+
negatives.delete(entry);
|
|
636
|
+
}
|
|
637
|
+
return uniqueSorted([...Array.from(negatives, (entry) => `-${entry}`), ...positives]);
|
|
638
|
+
}
|
|
639
|
+
function mergeLanguageSpecific(target, source) {
|
|
640
|
+
if (!source) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
for (const [language, entries] of Object.entries(source)) {
|
|
644
|
+
if (!Array.isArray(entries)) {
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
target[language] = [...target[language] ?? [], ...entries.filter((entry) => typeof entry === "string")];
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
function languageFileSuffix(language) {
|
|
651
|
+
return language.replace(/[^A-Za-z0-9_-]/g, "");
|
|
652
|
+
}
|
|
653
|
+
function workspaceFolderForUri(uri) {
|
|
654
|
+
const folder = import_coc4.workspace.getWorkspaceFolder(uri.toString());
|
|
655
|
+
if (!folder) {
|
|
656
|
+
return null;
|
|
657
|
+
}
|
|
658
|
+
const folderUri = import_coc4.Uri.parse(folder.uri);
|
|
659
|
+
return folderUri.scheme === "file" ? folderUri.fsPath : null;
|
|
660
|
+
}
|
|
661
|
+
function commentLine(commentString, text) {
|
|
662
|
+
let template = typeof commentString === "string" ? commentString : "";
|
|
663
|
+
if (template === "%%s") {
|
|
664
|
+
template = "% %s";
|
|
665
|
+
}
|
|
666
|
+
if (template.includes("%s")) {
|
|
667
|
+
return template.replace("%s", text);
|
|
668
|
+
}
|
|
669
|
+
return `# ${text}`;
|
|
670
|
+
}
|
|
671
|
+
function selectedLineRange(range, cursor) {
|
|
672
|
+
if (!range) {
|
|
673
|
+
return { startLine: cursor.line, endLine: cursor.line };
|
|
674
|
+
}
|
|
675
|
+
const startLine = Math.min(range.start.line, range.end.line);
|
|
676
|
+
let endLine = Math.max(range.start.line, range.end.line);
|
|
677
|
+
if (range.end.character === 0 && endLine > startLine) {
|
|
678
|
+
endLine -= 1;
|
|
679
|
+
}
|
|
680
|
+
return { startLine, endLine };
|
|
681
|
+
}
|
|
682
|
+
function disableHereReplacement(lines, startLine, endLine, commentString) {
|
|
683
|
+
const indent = lines[startLine]?.match(/^\s*/)?.[0] ?? "";
|
|
684
|
+
const opening = `${indent}${commentLine(commentString, "LTeX: enabled=false")}`;
|
|
685
|
+
const closing = `${indent}${commentLine(commentString, "LTeX: enabled=true")}`;
|
|
686
|
+
const selectedText = lines.slice(startLine, endLine + 1).join("\n");
|
|
687
|
+
return `${opening}
|
|
688
|
+
${selectedText}
|
|
689
|
+
${closing}
|
|
690
|
+
`;
|
|
691
|
+
}
|
|
692
|
+
function parseExternalFileEntries(contents) {
|
|
693
|
+
return contents.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
694
|
+
}
|
|
695
|
+
function documentForUri(uri) {
|
|
696
|
+
const uriString = uri.toString();
|
|
697
|
+
return import_coc4.workspace.documents.find((document) => document.uri === uriString);
|
|
698
|
+
}
|
|
699
|
+
function splitCommaList(value) {
|
|
700
|
+
return String(value ?? "").split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
701
|
+
}
|
|
702
|
+
function normalizeSpellLanguage(language) {
|
|
703
|
+
const normalized = language.replace("_", "-");
|
|
704
|
+
const [base, region] = normalized.split("-", 2);
|
|
705
|
+
return region ? `${base.toLowerCase()}-${region.toUpperCase()}` : base.toLowerCase();
|
|
706
|
+
}
|
|
707
|
+
function settingTitle(settingName) {
|
|
708
|
+
switch (settingName) {
|
|
709
|
+
case "dictionary":
|
|
710
|
+
return "Dictionary";
|
|
711
|
+
case "disabledRules":
|
|
712
|
+
return "Disabled rules";
|
|
713
|
+
case "hiddenFalsePositives":
|
|
714
|
+
return "Hidden false positives";
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function scopeSource(scope) {
|
|
718
|
+
switch (scope) {
|
|
719
|
+
case import_coc4.ConfigurationTarget.Global:
|
|
720
|
+
return "user";
|
|
721
|
+
case import_coc4.ConfigurationTarget.Workspace:
|
|
722
|
+
return "workspace";
|
|
723
|
+
case import_coc4.ConfigurationTarget.WorkspaceFolder:
|
|
724
|
+
return "workspaceFolder";
|
|
725
|
+
default:
|
|
726
|
+
return "workspace";
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
function scopeLabel(scope) {
|
|
730
|
+
switch (scope) {
|
|
731
|
+
case import_coc4.ConfigurationTarget.Global:
|
|
732
|
+
return "user settings";
|
|
733
|
+
case import_coc4.ConfigurationTarget.Workspace:
|
|
734
|
+
return "workspace settings";
|
|
735
|
+
case import_coc4.ConfigurationTarget.WorkspaceFolder:
|
|
736
|
+
return "workspace folder settings";
|
|
737
|
+
default:
|
|
738
|
+
return "settings";
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
function settingsEntryKey(entry) {
|
|
742
|
+
return [
|
|
743
|
+
entry.settingName,
|
|
744
|
+
entry.language,
|
|
745
|
+
entry.value,
|
|
746
|
+
entry.source,
|
|
747
|
+
entry.filePath ?? "",
|
|
748
|
+
entry.scope ?? ""
|
|
749
|
+
].join("\0");
|
|
750
|
+
}
|
|
751
|
+
function singleListEntry(item) {
|
|
752
|
+
if (Array.isArray(item)) {
|
|
753
|
+
return void 0;
|
|
754
|
+
}
|
|
755
|
+
return item.data?.entry;
|
|
756
|
+
}
|
|
757
|
+
function settingsEntryLabel(entry) {
|
|
758
|
+
const source = (entry.source === "externalFile" || entry.source === "runtimeDictionary") && entry.filePath ? entry.filePath : entry.sourceLabel;
|
|
759
|
+
return `${settingTitle(entry.settingName)} ${entry.language} ${entry.value} ${source}`;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// src/externalSettings.ts
|
|
763
|
+
var ExternalSettings = class {
|
|
764
|
+
constructor(context) {
|
|
765
|
+
this.context = context;
|
|
766
|
+
this.contents = /* @__PURE__ */ new Map();
|
|
767
|
+
this.watchers = /* @__PURE__ */ new Map();
|
|
768
|
+
}
|
|
769
|
+
dispose() {
|
|
770
|
+
for (const watcher of this.watchers.values()) {
|
|
771
|
+
watcher.close();
|
|
772
|
+
}
|
|
773
|
+
this.watchers.clear();
|
|
774
|
+
this.contents.clear();
|
|
775
|
+
}
|
|
776
|
+
async handleWorkspaceSpecificConfiguration(params) {
|
|
777
|
+
return Promise.all(params.items.map(async (item) => {
|
|
778
|
+
const uri = import_coc5.Uri.parse(item.scopeUri);
|
|
779
|
+
const result = {
|
|
780
|
+
dictionary: await this.mergedSetting(uri, "dictionary"),
|
|
781
|
+
disabledRules: await this.mergedSetting(uri, "disabledRules"),
|
|
782
|
+
enabledRules: await this.mergedSetting(uri, "enabledRules"),
|
|
783
|
+
hiddenFalsePositives: await this.mergedSetting(uri, "hiddenFalsePositives")
|
|
784
|
+
};
|
|
785
|
+
const spellLanguage = await this.spellLanguageOverride(uri);
|
|
786
|
+
if (spellLanguage) {
|
|
787
|
+
result.language = spellLanguage;
|
|
788
|
+
}
|
|
789
|
+
return result;
|
|
790
|
+
}));
|
|
791
|
+
}
|
|
792
|
+
firstExternalFilePath(uri, settingName, scope, language) {
|
|
793
|
+
const defaultDir = this.defaultDirectory(uri, scope);
|
|
794
|
+
const configured = this.settingValueForScope(uri, settingName, scope);
|
|
795
|
+
const explicit = this.explicitFilesForLanguage(configured, language, defaultDir);
|
|
796
|
+
if (explicit.length > 0) {
|
|
797
|
+
return explicit[0];
|
|
798
|
+
}
|
|
799
|
+
return defaultDir ? (0, import_path3.join)(defaultDir, `ltex.${settingName}.${languageFileSuffix(language)}.txt`) : null;
|
|
800
|
+
}
|
|
801
|
+
listEntries(uri) {
|
|
802
|
+
const entries = /* @__PURE__ */ new Map();
|
|
803
|
+
for (const settingName of MANAGED_SETTING_NAMES) {
|
|
804
|
+
for (const scope of [import_coc5.ConfigurationTarget.Global, import_coc5.ConfigurationTarget.Workspace, import_coc5.ConfigurationTarget.WorkspaceFolder]) {
|
|
805
|
+
for (const entry of [
|
|
806
|
+
...this.listExternalEntries(uri, settingName, scope),
|
|
807
|
+
...this.listInternalEntries(uri, settingName, scope)
|
|
808
|
+
]) {
|
|
809
|
+
entries.set(settingsEntryKey(entry), entry);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
for (const entry of this.listRuntimeDictionaryEntries(uri)) {
|
|
814
|
+
entries.set(settingsEntryKey(entry), entry);
|
|
815
|
+
}
|
|
816
|
+
return Array.from(entries.values()).sort((a, b) => {
|
|
817
|
+
const settingCompare = settingTitle(a.settingName).localeCompare(settingTitle(b.settingName));
|
|
818
|
+
if (settingCompare !== 0) {
|
|
819
|
+
return settingCompare;
|
|
820
|
+
}
|
|
821
|
+
const languageCompare = a.language.localeCompare(b.language);
|
|
822
|
+
if (languageCompare !== 0) {
|
|
823
|
+
return languageCompare;
|
|
824
|
+
}
|
|
825
|
+
return a.value.localeCompare(b.value);
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
async deleteEntry(entry) {
|
|
829
|
+
if (entry.readOnly) {
|
|
830
|
+
return;
|
|
831
|
+
}
|
|
832
|
+
if (entry.source === "externalFile" && entry.filePath) {
|
|
833
|
+
this.updateExternalFileEntry(entry.filePath, entry.value, null);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
if (entry.scope != null) {
|
|
837
|
+
await this.updateInternalEntry(entry, null);
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
async editEntry(entry, nextValue) {
|
|
841
|
+
if (entry.readOnly) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const cleanValue = nextValue.trim();
|
|
845
|
+
if (!cleanValue) {
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (entry.source === "externalFile" && entry.filePath) {
|
|
849
|
+
this.updateExternalFileEntry(entry.filePath, entry.value, cleanValue);
|
|
850
|
+
return;
|
|
851
|
+
}
|
|
852
|
+
if (entry.scope != null) {
|
|
853
|
+
await this.updateInternalEntry(entry, cleanValue);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
reloadFromOpenDocuments() {
|
|
857
|
+
this.clearCache();
|
|
858
|
+
let loaded = 0;
|
|
859
|
+
const seen = /* @__PURE__ */ new Set();
|
|
860
|
+
for (const document of import_coc5.workspace.documents) {
|
|
861
|
+
const uri = import_coc5.Uri.parse(document.uri);
|
|
862
|
+
for (const settingName of MANAGED_SETTING_NAMES) {
|
|
863
|
+
for (const filePath of this.externalFilePaths(uri, settingName)) {
|
|
864
|
+
if (seen.has(filePath)) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
seen.add(filePath);
|
|
868
|
+
if (this.loadFileContents(filePath) != null) {
|
|
869
|
+
loaded += 1;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
return loaded;
|
|
875
|
+
}
|
|
876
|
+
appendToFile(filePath, entries) {
|
|
877
|
+
const existing = this.contents.get(filePath) ?? this.loadFileContents(filePath) ?? "";
|
|
878
|
+
const knownEntries = new Set(parseExternalFileEntries(existing));
|
|
879
|
+
const entriesToAppend = [];
|
|
880
|
+
for (const entry of entries) {
|
|
881
|
+
const cleanEntry = entry.trim();
|
|
882
|
+
if (!cleanEntry || knownEntries.has(cleanEntry)) {
|
|
883
|
+
continue;
|
|
884
|
+
}
|
|
885
|
+
knownEntries.add(cleanEntry);
|
|
886
|
+
entriesToAppend.push(cleanEntry);
|
|
887
|
+
}
|
|
888
|
+
if (entriesToAppend.length === 0) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const prefix = existing.length > 0 && !existing.endsWith("\n") ? "\n" : "";
|
|
892
|
+
const nextContents = `${existing}${prefix}${entriesToAppend.join("\n")}
|
|
893
|
+
`;
|
|
894
|
+
(0, import_fs2.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
|
|
895
|
+
(0, import_fs2.writeFileSync)(filePath, nextContents, "utf8");
|
|
896
|
+
this.contents.set(filePath, nextContents);
|
|
897
|
+
this.watchFile(filePath);
|
|
898
|
+
}
|
|
899
|
+
clearCache() {
|
|
900
|
+
for (const watcher of this.watchers.values()) {
|
|
901
|
+
watcher.close();
|
|
902
|
+
}
|
|
903
|
+
this.watchers.clear();
|
|
904
|
+
this.contents.clear();
|
|
905
|
+
}
|
|
906
|
+
listExternalEntries(uri, settingName, scope) {
|
|
907
|
+
const result = [];
|
|
908
|
+
const configured = this.settingValueForScope(uri, settingName, scope);
|
|
909
|
+
const defaultDir = this.defaultDirectory(uri, scope);
|
|
910
|
+
const languages = this.languagesForExternalFiles(configured);
|
|
911
|
+
for (const language of languages) {
|
|
912
|
+
for (const filePath of this.externalFilesForLanguage(configured, settingName, language, defaultDir)) {
|
|
913
|
+
for (const value of this.readEntries(filePath)) {
|
|
914
|
+
result.push({
|
|
915
|
+
settingName,
|
|
916
|
+
language,
|
|
917
|
+
value,
|
|
918
|
+
source: "externalFile",
|
|
919
|
+
sourceLabel: filePath,
|
|
920
|
+
uri: uri.toString(),
|
|
921
|
+
filePath
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
return result;
|
|
927
|
+
}
|
|
928
|
+
listInternalEntries(uri, settingName, scope) {
|
|
929
|
+
const configured = this.settingValueForScope(uri, settingName, scope);
|
|
930
|
+
const result = [];
|
|
931
|
+
for (const [language, rawEntries] of Object.entries(configured ?? {})) {
|
|
932
|
+
if (!Array.isArray(rawEntries)) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
for (const value of rawEntries) {
|
|
936
|
+
if (typeof value !== "string" || value.startsWith(":")) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
result.push({
|
|
940
|
+
settingName,
|
|
941
|
+
language,
|
|
942
|
+
value,
|
|
943
|
+
source: scopeSource(scope),
|
|
944
|
+
sourceLabel: scopeLabel(scope),
|
|
945
|
+
uri: uri.toString(),
|
|
946
|
+
scope
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return result;
|
|
951
|
+
}
|
|
952
|
+
externalFilePaths(uri, settingName) {
|
|
953
|
+
const paths = [];
|
|
954
|
+
for (const scope of [import_coc5.ConfigurationTarget.Global, import_coc5.ConfigurationTarget.Workspace, import_coc5.ConfigurationTarget.WorkspaceFolder]) {
|
|
955
|
+
const configured = this.settingValueForScope(uri, settingName, scope);
|
|
956
|
+
const defaultDir = this.defaultDirectory(uri, scope);
|
|
957
|
+
for (const language of this.languagesForExternalFiles(configured)) {
|
|
958
|
+
paths.push(...this.externalFilesForLanguage(configured, settingName, language, defaultDir));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (settingName === "dictionary") {
|
|
962
|
+
paths.push(...this.runtimeDictionaryFilesForReload(uri));
|
|
963
|
+
}
|
|
964
|
+
return uniqueSorted(paths);
|
|
965
|
+
}
|
|
966
|
+
externalFilesForLanguage(configured, settingName, language, defaultDir) {
|
|
967
|
+
const files = this.explicitFilesForLanguage(configured, language, defaultDir);
|
|
968
|
+
if (defaultDir) {
|
|
969
|
+
files.push((0, import_path3.join)(defaultDir, `ltex.${settingName}.${languageFileSuffix(language)}.txt`));
|
|
970
|
+
}
|
|
971
|
+
return uniqueSorted(files);
|
|
972
|
+
}
|
|
973
|
+
updateExternalFileEntry(filePath, oldValue, nextValue) {
|
|
974
|
+
const existing = this.contents.get(filePath) ?? this.loadFileContents(filePath) ?? "";
|
|
975
|
+
let changed = false;
|
|
976
|
+
const nextEntries = [];
|
|
977
|
+
const seen = /* @__PURE__ */ new Set();
|
|
978
|
+
for (const entry of parseExternalFileEntries(existing)) {
|
|
979
|
+
const replacement = entry === oldValue ? nextValue : entry;
|
|
980
|
+
if (entry === oldValue) {
|
|
981
|
+
changed = true;
|
|
982
|
+
}
|
|
983
|
+
if (!replacement || seen.has(replacement)) {
|
|
984
|
+
continue;
|
|
985
|
+
}
|
|
986
|
+
seen.add(replacement);
|
|
987
|
+
nextEntries.push(replacement);
|
|
988
|
+
}
|
|
989
|
+
if (!changed && nextValue) {
|
|
990
|
+
nextEntries.push(nextValue);
|
|
991
|
+
}
|
|
992
|
+
const nextContents = nextEntries.length > 0 ? `${nextEntries.join("\n")}
|
|
993
|
+
` : "";
|
|
994
|
+
(0, import_fs2.mkdirSync)((0, import_path3.dirname)(filePath), { recursive: true });
|
|
995
|
+
(0, import_fs2.writeFileSync)(filePath, nextContents, "utf8");
|
|
996
|
+
this.contents.set(filePath, nextContents);
|
|
997
|
+
this.watchFile(filePath);
|
|
998
|
+
}
|
|
999
|
+
async updateInternalEntry(entry, nextValue) {
|
|
1000
|
+
if (entry.scope == null) {
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
const uri = import_coc5.Uri.parse(entry.uri);
|
|
1004
|
+
const config = ltexConfig(uri.toString());
|
|
1005
|
+
const current = this.settingValueForScope(uri, entry.settingName, entry.scope) ?? {};
|
|
1006
|
+
const next = { ...current };
|
|
1007
|
+
const languageEntries = Array.isArray(next[entry.language]) ? [...next[entry.language]] : [];
|
|
1008
|
+
const updated = languageEntries.map((value) => value === entry.value ? nextValue : value).filter((value) => typeof value === "string" && value.length > 0);
|
|
1009
|
+
next[entry.language] = cleanLanguageSpecificEntries(updated);
|
|
1010
|
+
if (next[entry.language].length === 0) {
|
|
1011
|
+
delete next[entry.language];
|
|
1012
|
+
}
|
|
1013
|
+
await config.update(entry.settingName, next, entry.scope);
|
|
1014
|
+
}
|
|
1015
|
+
async mergedSetting(uri, settingName) {
|
|
1016
|
+
const result = {};
|
|
1017
|
+
for (const scope of [import_coc5.ConfigurationTarget.Global, import_coc5.ConfigurationTarget.Workspace, import_coc5.ConfigurationTarget.WorkspaceFolder]) {
|
|
1018
|
+
mergeLanguageSpecific(result, this.settingForScopeWithExternalFiles(uri, settingName, scope));
|
|
1019
|
+
}
|
|
1020
|
+
if (settingName === "dictionary") {
|
|
1021
|
+
mergeLanguageSpecific(result, await this.runtimeDictionary(uri));
|
|
1022
|
+
if (ltexConfig(uri.toString()).get("dictionary.useSpellFile", false)) {
|
|
1023
|
+
mergeLanguageSpecific(result, await this.spellFileDictionary(uri));
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
for (const language of Object.keys(result)) {
|
|
1027
|
+
result[language] = cleanLanguageSpecificEntries(result[language]);
|
|
1028
|
+
}
|
|
1029
|
+
return result;
|
|
1030
|
+
}
|
|
1031
|
+
async runtimeDictionary(uri) {
|
|
1032
|
+
const result = {};
|
|
1033
|
+
for (const language of await this.runtimeDictionaryLanguages(uri)) {
|
|
1034
|
+
for (const file of this.runtimeDictionaryFilesForLanguage(uri, language)) {
|
|
1035
|
+
const entries = this.readEntries(file.filePath);
|
|
1036
|
+
if (entries.length > 0) {
|
|
1037
|
+
result[language] = [...result[language] ?? [], ...entries];
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
return result;
|
|
1042
|
+
}
|
|
1043
|
+
listRuntimeDictionaryEntries(uri) {
|
|
1044
|
+
const result = [];
|
|
1045
|
+
for (const file of this.runtimeDictionaryFilesForList(uri)) {
|
|
1046
|
+
for (const value of this.readEntries(file.filePath)) {
|
|
1047
|
+
result.push({
|
|
1048
|
+
settingName: "dictionary",
|
|
1049
|
+
language: file.appliesToAllLanguages ? "default" : file.language,
|
|
1050
|
+
value,
|
|
1051
|
+
source: "runtimeDictionary",
|
|
1052
|
+
sourceLabel: `${file.filePath} (runtime dictionary)`,
|
|
1053
|
+
uri: uri.toString(),
|
|
1054
|
+
filePath: file.filePath,
|
|
1055
|
+
readOnly: true
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
return result;
|
|
1060
|
+
}
|
|
1061
|
+
runtimeDictionaryFilesForReload(uri) {
|
|
1062
|
+
return this.runtimeDictionaryFilesForList(uri).map((file) => file.filePath);
|
|
1063
|
+
}
|
|
1064
|
+
runtimeDictionaryFilesForList(uri) {
|
|
1065
|
+
const files = [];
|
|
1066
|
+
for (const configuredPath of this.configuredDictionaryExternalFiles(uri)) {
|
|
1067
|
+
const filePath = this.resolveDictionaryExternalPath(uri, configuredPath);
|
|
1068
|
+
if (isDirectory(filePath)) {
|
|
1069
|
+
files.push(...this.runtimeDictionaryDirectoryFiles(filePath, void 0));
|
|
1070
|
+
continue;
|
|
1071
|
+
}
|
|
1072
|
+
const language = languageFromDictionaryFile(filePath);
|
|
1073
|
+
files.push({
|
|
1074
|
+
language: language ?? "default",
|
|
1075
|
+
filePath,
|
|
1076
|
+
appliesToAllLanguages: language == null
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
return uniqueRuntimeDictionaryFiles(files);
|
|
1080
|
+
}
|
|
1081
|
+
runtimeDictionaryFilesForLanguage(uri, language) {
|
|
1082
|
+
const files = [];
|
|
1083
|
+
for (const configuredPath of this.configuredDictionaryExternalFiles(uri)) {
|
|
1084
|
+
const filePath = this.resolveDictionaryExternalPath(uri, configuredPath);
|
|
1085
|
+
if (isDirectory(filePath)) {
|
|
1086
|
+
files.push(...this.runtimeDictionaryDirectoryFiles(filePath, language));
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
const fileLanguage = languageFromDictionaryFile(filePath);
|
|
1090
|
+
if (fileLanguage == null || fileLanguage === language) {
|
|
1091
|
+
files.push({
|
|
1092
|
+
language: fileLanguage ?? language,
|
|
1093
|
+
filePath,
|
|
1094
|
+
appliesToAllLanguages: fileLanguage == null
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return uniqueRuntimeDictionaryFiles(files);
|
|
1099
|
+
}
|
|
1100
|
+
runtimeDictionaryDirectoryFiles(directory, language) {
|
|
1101
|
+
const files = [];
|
|
1102
|
+
const directories = uniqueSorted([directory, (0, import_path3.join)(directory, "dict")]);
|
|
1103
|
+
for (const dictionaryDirectory of directories) {
|
|
1104
|
+
files.push({
|
|
1105
|
+
language: language ?? "default",
|
|
1106
|
+
filePath: (0, import_path3.join)(dictionaryDirectory, "default.txt"),
|
|
1107
|
+
appliesToAllLanguages: true
|
|
1108
|
+
});
|
|
1109
|
+
for (const targetLanguage of language ? [language] : LTEX_LANGUAGE_CODES) {
|
|
1110
|
+
files.push({
|
|
1111
|
+
language: targetLanguage,
|
|
1112
|
+
filePath: (0, import_path3.join)(dictionaryDirectory, `${languageFileSuffix(targetLanguage)}.txt`),
|
|
1113
|
+
appliesToAllLanguages: false
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return uniqueRuntimeDictionaryFiles(files);
|
|
1118
|
+
}
|
|
1119
|
+
configuredDictionaryExternalFiles(uri) {
|
|
1120
|
+
const configured = ltexConfig(uri.toString()).get("dictionary.externalFiles", []);
|
|
1121
|
+
if (!Array.isArray(configured)) {
|
|
1122
|
+
return [];
|
|
1123
|
+
}
|
|
1124
|
+
return configured.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
1125
|
+
}
|
|
1126
|
+
resolveDictionaryExternalPath(uri, filePath) {
|
|
1127
|
+
const expanded = expandHome(filePath.trim());
|
|
1128
|
+
if ((0, import_path3.isAbsolute)(expanded)) {
|
|
1129
|
+
return expanded;
|
|
1130
|
+
}
|
|
1131
|
+
return (0, import_path3.resolve)(workspaceFolderForUri(uri) ?? workspaceRoot(), expanded);
|
|
1132
|
+
}
|
|
1133
|
+
async runtimeDictionaryLanguages(uri) {
|
|
1134
|
+
const configuredLanguage = ltexConfig(uri.toString()).get("language", "en-US");
|
|
1135
|
+
if (configuredLanguage === "auto") {
|
|
1136
|
+
return LTEX_LANGUAGE_CODES;
|
|
1137
|
+
}
|
|
1138
|
+
const languages = [configuredLanguage];
|
|
1139
|
+
const motherTongue = ltexConfig(uri.toString()).get("additionalRules.motherTongue", "");
|
|
1140
|
+
if (motherTongue) {
|
|
1141
|
+
languages.push(motherTongue);
|
|
1142
|
+
}
|
|
1143
|
+
const document = documentForUri(uri);
|
|
1144
|
+
if (document) {
|
|
1145
|
+
languages.push(...await this.spellLanguagesForDocument(document));
|
|
1146
|
+
}
|
|
1147
|
+
return uniqueSorted(languages.filter((language) => LTEX_LANGUAGE_CODES.includes(language)));
|
|
1148
|
+
}
|
|
1149
|
+
async spellFileDictionary(uri) {
|
|
1150
|
+
const document = documentForUri(uri);
|
|
1151
|
+
if (!document) {
|
|
1152
|
+
return {};
|
|
1153
|
+
}
|
|
1154
|
+
const spellFileOption = await document.buffer.getOption("spellfile");
|
|
1155
|
+
const spellFiles = splitCommaList(spellFileOption).map((filePath) => {
|
|
1156
|
+
const expanded = expandHome(filePath);
|
|
1157
|
+
return (0, import_path3.isAbsolute)(expanded) ? expanded : (0, import_path3.resolve)(workspaceRoot(), expanded);
|
|
1158
|
+
});
|
|
1159
|
+
if (spellFiles.length === 0) {
|
|
1160
|
+
return {};
|
|
1161
|
+
}
|
|
1162
|
+
const languages = await this.spellLanguagesForDocument(document);
|
|
1163
|
+
const configuredLanguage = ltexConfig(uri.toString()).get("language", "en-US");
|
|
1164
|
+
const targetLanguages = uniqueSorted(languages.length > 0 ? languages : [configuredLanguage]);
|
|
1165
|
+
const words = uniqueSorted(spellFiles.flatMap((filePath) => this.readEntries(filePath)));
|
|
1166
|
+
if (words.length === 0) {
|
|
1167
|
+
return {};
|
|
1168
|
+
}
|
|
1169
|
+
const result = {};
|
|
1170
|
+
for (const language of targetLanguages) {
|
|
1171
|
+
result[language] = words;
|
|
1172
|
+
}
|
|
1173
|
+
return result;
|
|
1174
|
+
}
|
|
1175
|
+
async spellLanguageOverride(uri) {
|
|
1176
|
+
if (!ltexConfig(uri.toString()).get("languageFromSpellLang", false)) {
|
|
1177
|
+
return void 0;
|
|
1178
|
+
}
|
|
1179
|
+
const document = documentForUri(uri);
|
|
1180
|
+
if (!document) {
|
|
1181
|
+
return void 0;
|
|
1182
|
+
}
|
|
1183
|
+
const languages = await this.spellLanguagesForDocument(document);
|
|
1184
|
+
return languages.find((language) => LTEX_LANGUAGE_CODES.includes(language));
|
|
1185
|
+
}
|
|
1186
|
+
async spellLanguagesForDocument(document) {
|
|
1187
|
+
const spellLangOption = await document.buffer.getOption("spelllang");
|
|
1188
|
+
return uniqueSorted(splitCommaList(spellLangOption).map(normalizeSpellLanguage));
|
|
1189
|
+
}
|
|
1190
|
+
settingForScopeWithExternalFiles(uri, settingName, scope) {
|
|
1191
|
+
const result = {};
|
|
1192
|
+
const defaultDir = this.defaultDirectory(uri, scope);
|
|
1193
|
+
const configured = this.settingValueForScope(uri, settingName, scope);
|
|
1194
|
+
const languages = this.languagesForExternalFiles(configured);
|
|
1195
|
+
for (const language of languages) {
|
|
1196
|
+
const files = this.explicitFilesForLanguage(configured, language, defaultDir);
|
|
1197
|
+
if (defaultDir) {
|
|
1198
|
+
files.push((0, import_path3.join)(defaultDir, `ltex.${settingName}.${languageFileSuffix(language)}.txt`));
|
|
1199
|
+
}
|
|
1200
|
+
for (const file of files) {
|
|
1201
|
+
const entries = this.readEntries(file);
|
|
1202
|
+
if (entries.length > 0) {
|
|
1203
|
+
result[language] = [...result[language] ?? [], ...entries];
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
for (const [language, entries] of Object.entries(configured ?? {})) {
|
|
1208
|
+
if (!Array.isArray(entries)) {
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
const inlineEntries = entries.filter((entry) => typeof entry === "string" && !entry.startsWith(":"));
|
|
1212
|
+
if (inlineEntries.length > 0) {
|
|
1213
|
+
result[language] = [...result[language] ?? [], ...inlineEntries];
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return result;
|
|
1217
|
+
}
|
|
1218
|
+
settingValueForScope(uri, settingName, scope) {
|
|
1219
|
+
const inspect = ltexConfig(uri.toString()).inspect(settingName);
|
|
1220
|
+
if (!inspect) {
|
|
1221
|
+
return void 0;
|
|
1222
|
+
}
|
|
1223
|
+
switch (scope) {
|
|
1224
|
+
case import_coc5.ConfigurationTarget.Global:
|
|
1225
|
+
return inspect.globalValue;
|
|
1226
|
+
case import_coc5.ConfigurationTarget.Workspace:
|
|
1227
|
+
return inspect.workspaceValue;
|
|
1228
|
+
case import_coc5.ConfigurationTarget.WorkspaceFolder:
|
|
1229
|
+
return inspect.workspaceFolderValue;
|
|
1230
|
+
default:
|
|
1231
|
+
return void 0;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
explicitFilesForLanguage(configured, language, defaultDir) {
|
|
1235
|
+
const rawEntries = configured?.[language];
|
|
1236
|
+
const entries = Array.isArray(rawEntries) ? rawEntries : [];
|
|
1237
|
+
return entries.filter((entry) => typeof entry === "string" && entry.startsWith(":")).map((entry) => {
|
|
1238
|
+
const explicitPath = expandHome(entry.slice(1));
|
|
1239
|
+
return (0, import_path3.isAbsolute)(explicitPath) || !defaultDir ? explicitPath : (0, import_path3.resolve)(defaultDir, explicitPath);
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
defaultDirectory(uri, scope) {
|
|
1243
|
+
switch (scope) {
|
|
1244
|
+
case import_coc5.ConfigurationTarget.Global:
|
|
1245
|
+
return this.context.storagePath ? (0, import_path3.resolve)(this.context.storagePath) : null;
|
|
1246
|
+
case import_coc5.ConfigurationTarget.Workspace: {
|
|
1247
|
+
const folders = import_coc5.workspace.workspaceFolders ?? [];
|
|
1248
|
+
if (folders.length === 0) {
|
|
1249
|
+
return null;
|
|
1250
|
+
}
|
|
1251
|
+
const folderUri = import_coc5.Uri.parse(folders[0].uri);
|
|
1252
|
+
return folderUri.scheme === "file" ? (0, import_path3.resolve)(folderUri.fsPath, ".vim") : null;
|
|
1253
|
+
}
|
|
1254
|
+
case import_coc5.ConfigurationTarget.WorkspaceFolder: {
|
|
1255
|
+
const folderPath = workspaceFolderForUri(uri);
|
|
1256
|
+
return folderPath ? (0, import_path3.resolve)(folderPath, ".vim") : null;
|
|
1257
|
+
}
|
|
1258
|
+
default:
|
|
1259
|
+
return null;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
languagesForExternalFiles(configured) {
|
|
1263
|
+
const configuredLanguages = configured ? Object.keys(configured).filter((language) => Array.isArray(configured[language])) : [];
|
|
1264
|
+
const activeLanguage = ltexConfig().get("language", "en-US");
|
|
1265
|
+
const motherTongue = ltexConfig().get("additionalRules.motherTongue", "");
|
|
1266
|
+
return uniqueSorted([
|
|
1267
|
+
...LTEX_LANGUAGE_CODES,
|
|
1268
|
+
...configuredLanguages,
|
|
1269
|
+
...activeLanguage ? [activeLanguage] : [],
|
|
1270
|
+
...motherTongue ? [motherTongue] : []
|
|
1271
|
+
]);
|
|
1272
|
+
}
|
|
1273
|
+
readEntries(filePath) {
|
|
1274
|
+
const contents = this.contents.get(filePath) ?? this.loadFileContents(filePath);
|
|
1275
|
+
return contents == null ? [] : parseExternalFileEntries(contents);
|
|
1276
|
+
}
|
|
1277
|
+
loadFileContents(filePath) {
|
|
1278
|
+
if (!(0, import_fs2.existsSync)(filePath)) {
|
|
1279
|
+
return null;
|
|
1280
|
+
}
|
|
1281
|
+
try {
|
|
1282
|
+
const contents = (0, import_fs2.readFileSync)(filePath, "utf8");
|
|
1283
|
+
this.contents.set(filePath, contents);
|
|
1284
|
+
this.watchFile(filePath);
|
|
1285
|
+
return contents;
|
|
1286
|
+
} catch (err) {
|
|
1287
|
+
console.error(`Failed to read LTEX external setting file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1288
|
+
return null;
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
watchFile(filePath) {
|
|
1292
|
+
if (this.watchers.has(filePath)) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
try {
|
|
1296
|
+
const watcher = (0, import_fs2.watch)(filePath, () => this.reloadWatchedFile(filePath));
|
|
1297
|
+
this.watchers.set(filePath, watcher);
|
|
1298
|
+
} catch (err) {
|
|
1299
|
+
console.error(`Failed to watch LTEX external setting file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
reloadWatchedFile(filePath) {
|
|
1303
|
+
if (!(0, import_fs2.existsSync)(filePath)) {
|
|
1304
|
+
this.unwatchFile(filePath);
|
|
1305
|
+
this.contents.delete(filePath);
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1308
|
+
try {
|
|
1309
|
+
this.contents.set(filePath, (0, import_fs2.readFileSync)(filePath, "utf8"));
|
|
1310
|
+
} catch (err) {
|
|
1311
|
+
console.error(`Failed to reload LTEX external setting file ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
unwatchFile(filePath) {
|
|
1315
|
+
const watcher = this.watchers.get(filePath);
|
|
1316
|
+
if (watcher) {
|
|
1317
|
+
watcher.close();
|
|
1318
|
+
this.watchers.delete(filePath);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
};
|
|
1322
|
+
function isDirectory(filePath) {
|
|
1323
|
+
try {
|
|
1324
|
+
return (0, import_fs2.statSync)(filePath).isDirectory();
|
|
1325
|
+
} catch {
|
|
1326
|
+
return false;
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
function languageFromDictionaryFile(filePath) {
|
|
1330
|
+
const name = (0, import_path3.basename)(filePath);
|
|
1331
|
+
if (!name.endsWith(".txt")) {
|
|
1332
|
+
return void 0;
|
|
1333
|
+
}
|
|
1334
|
+
const language = name.slice(0, -".txt".length);
|
|
1335
|
+
return language !== "default" && LTEX_LANGUAGE_CODES.includes(language) ? language : void 0;
|
|
1336
|
+
}
|
|
1337
|
+
function uniqueRuntimeDictionaryFiles(files) {
|
|
1338
|
+
const result = /* @__PURE__ */ new Map();
|
|
1339
|
+
for (const file of files) {
|
|
1340
|
+
result.set(`${file.language}\0${file.filePath}\0${file.appliesToAllLanguages ? "1" : "0"}`, file);
|
|
1341
|
+
}
|
|
1342
|
+
return Array.from(result.values()).sort((a, b) => {
|
|
1343
|
+
const languageCompare = a.language.localeCompare(b.language);
|
|
1344
|
+
return languageCompare === 0 ? a.filePath.localeCompare(b.filePath) : languageCompare;
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// src/format.ts
|
|
1349
|
+
function seconds(value) {
|
|
1350
|
+
return typeof value === "number" && Number.isFinite(value) ? `${value.toFixed(1)}s` : "n/a";
|
|
1351
|
+
}
|
|
1352
|
+
function percent(value) {
|
|
1353
|
+
return typeof value === "number" && Number.isFinite(value) ? `${(value * 100).toFixed(1)}%` : "n/a";
|
|
1354
|
+
}
|
|
1355
|
+
function bytes(value) {
|
|
1356
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
1357
|
+
return "n/a";
|
|
1358
|
+
}
|
|
1359
|
+
const units = ["B", "KiB", "MiB", "GiB"];
|
|
1360
|
+
let normalized = value;
|
|
1361
|
+
let unitIndex = 0;
|
|
1362
|
+
while (normalized >= 1024 && unitIndex < units.length - 1) {
|
|
1363
|
+
normalized /= 1024;
|
|
1364
|
+
unitIndex += 1;
|
|
1365
|
+
}
|
|
1366
|
+
return `${normalized.toFixed(unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// src/installer.ts
|
|
1370
|
+
var import_coc6 = require("coc.nvim");
|
|
1371
|
+
var import_child_process = require("child_process");
|
|
1372
|
+
var import_crypto = require("crypto");
|
|
1373
|
+
var import_fs3 = require("fs");
|
|
1374
|
+
var import_http = require("http");
|
|
1375
|
+
var import_https = require("https");
|
|
1376
|
+
var import_path4 = require("path");
|
|
1377
|
+
var import_promises = require("stream/promises");
|
|
1378
|
+
function isRedirect(statusCode) {
|
|
1379
|
+
return statusCode === 301 || statusCode === 302 || statusCode === 303 || statusCode === 307 || statusCode === 308;
|
|
1380
|
+
}
|
|
1381
|
+
function getUrl(url, callback) {
|
|
1382
|
+
const options = {
|
|
1383
|
+
headers: {
|
|
1384
|
+
"User-Agent": "coc-ltex-plus",
|
|
1385
|
+
Accept: "application/vnd.github+json, application/octet-stream"
|
|
1386
|
+
}
|
|
1387
|
+
};
|
|
1388
|
+
return url.protocol === "http:" ? (0, import_http.get)(url, options, callback) : (0, import_https.get)(url, options, callback);
|
|
1389
|
+
}
|
|
1390
|
+
async function fetchText(urlString, redirectCount = 0) {
|
|
1391
|
+
return new Promise((resolvePromise, reject) => {
|
|
1392
|
+
const url = new URL(urlString);
|
|
1393
|
+
const request = getUrl(url, (response) => {
|
|
1394
|
+
if (isRedirect(response.statusCode)) {
|
|
1395
|
+
response.resume();
|
|
1396
|
+
const location = response.headers.location;
|
|
1397
|
+
if (!location) {
|
|
1398
|
+
reject(new Error(`Request to ${urlString} redirected without a Location header.`));
|
|
1399
|
+
return;
|
|
1400
|
+
}
|
|
1401
|
+
if (redirectCount >= HTTP_REDIRECT_LIMIT) {
|
|
1402
|
+
reject(new Error(`Request to ${urlString} exceeded ${HTTP_REDIRECT_LIMIT} redirects.`));
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
fetchText(new URL(location, url).toString(), redirectCount + 1).then(resolvePromise, reject);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
const chunks = [];
|
|
1409
|
+
let received = 0;
|
|
1410
|
+
response.on("data", (chunk) => {
|
|
1411
|
+
received += chunk.length;
|
|
1412
|
+
if (received > RELEASE_METADATA_LIMIT_BYTES) {
|
|
1413
|
+
request.destroy(new Error(`Response from ${urlString} exceeded ${RELEASE_METADATA_LIMIT_BYTES} bytes.`));
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
chunks.push(Buffer.from(chunk));
|
|
1417
|
+
});
|
|
1418
|
+
response.on("end", () => {
|
|
1419
|
+
resolvePromise({
|
|
1420
|
+
statusCode: response.statusCode ?? 0,
|
|
1421
|
+
statusMessage: response.statusMessage ?? "",
|
|
1422
|
+
text: Buffer.concat(chunks).toString("utf8")
|
|
1423
|
+
});
|
|
1424
|
+
});
|
|
1425
|
+
});
|
|
1426
|
+
request.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => request.destroy(new Error(`Request to ${urlString} timed out.`)));
|
|
1427
|
+
request.on("error", reject);
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
async function downloadFile(urlString, filePath, onProgress, redirectCount = 0) {
|
|
1431
|
+
return new Promise((resolvePromise, reject) => {
|
|
1432
|
+
const url = new URL(urlString);
|
|
1433
|
+
const request = getUrl(url, (response) => {
|
|
1434
|
+
if (isRedirect(response.statusCode)) {
|
|
1435
|
+
response.resume();
|
|
1436
|
+
const location = response.headers.location;
|
|
1437
|
+
if (!location) {
|
|
1438
|
+
reject(new Error(`Download from ${urlString} redirected without a Location header.`));
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
if (redirectCount >= HTTP_REDIRECT_LIMIT) {
|
|
1442
|
+
reject(new Error(`Download from ${urlString} exceeded ${HTTP_REDIRECT_LIMIT} redirects.`));
|
|
1443
|
+
return;
|
|
1444
|
+
}
|
|
1445
|
+
downloadFile(new URL(location, url).toString(), filePath, onProgress, redirectCount + 1).then(resolvePromise, reject);
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
1448
|
+
if (response.statusCode !== 200) {
|
|
1449
|
+
response.resume();
|
|
1450
|
+
reject(new Error(`Download from ${urlString} failed with HTTP ${response.statusCode ?? "unknown"}.`));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
const total = response.headers["content-length"] ? Number(response.headers["content-length"]) : void 0;
|
|
1454
|
+
let received = 0;
|
|
1455
|
+
response.on("data", (chunk) => {
|
|
1456
|
+
received += chunk.length;
|
|
1457
|
+
onProgress?.(received, Number.isFinite(total) ? total : void 0);
|
|
1458
|
+
});
|
|
1459
|
+
(0, import_promises.pipeline)(response, (0, import_fs3.createWriteStream)(filePath, { mode: 420 })).then(resolvePromise, reject);
|
|
1460
|
+
});
|
|
1461
|
+
request.setTimeout(DOWNLOAD_IDLE_TIMEOUT_MS, () => request.destroy(new Error(`Download from ${urlString} timed out.`)));
|
|
1462
|
+
request.on("error", reject);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
async function runProcess(command, args, timeoutMs) {
|
|
1466
|
+
return new Promise((resolvePromise) => {
|
|
1467
|
+
const child = (0, import_child_process.spawn)(command, args, { windowsHide: true });
|
|
1468
|
+
const stdout = [];
|
|
1469
|
+
const stderr = [];
|
|
1470
|
+
let settled = false;
|
|
1471
|
+
const timer = setTimeout(() => {
|
|
1472
|
+
if (!settled) {
|
|
1473
|
+
child.kill();
|
|
1474
|
+
}
|
|
1475
|
+
}, timeoutMs);
|
|
1476
|
+
child.stdout?.on("data", (chunk) => stdout.push(Buffer.from(chunk)));
|
|
1477
|
+
child.stderr?.on("data", (chunk) => stderr.push(Buffer.from(chunk)));
|
|
1478
|
+
child.on("error", (error) => {
|
|
1479
|
+
if (settled) {
|
|
1480
|
+
return;
|
|
1481
|
+
}
|
|
1482
|
+
settled = true;
|
|
1483
|
+
clearTimeout(timer);
|
|
1484
|
+
resolvePromise({ code: null, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8"), error });
|
|
1485
|
+
});
|
|
1486
|
+
child.on("close", (code) => {
|
|
1487
|
+
if (settled) {
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
settled = true;
|
|
1491
|
+
clearTimeout(timer);
|
|
1492
|
+
resolvePromise({ code, stdout: Buffer.concat(stdout).toString("utf8"), stderr: Buffer.concat(stderr).toString("utf8") });
|
|
1493
|
+
});
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
function processError(result, label) {
|
|
1497
|
+
if (result.code === 0) {
|
|
1498
|
+
return void 0;
|
|
1499
|
+
}
|
|
1500
|
+
const detail = result.error?.message || result.stderr.trim() || result.stdout.trim() || `exit code ${result.code ?? "unknown"}`;
|
|
1501
|
+
return new Error(`${label} failed: ${detail}`);
|
|
1502
|
+
}
|
|
1503
|
+
async function runProcessOrThrow(command, args, timeoutMs, label) {
|
|
1504
|
+
const result = await runProcess(command, args, timeoutMs);
|
|
1505
|
+
const error = processError(result, label);
|
|
1506
|
+
if (error) {
|
|
1507
|
+
throw error;
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
function parseSemver(value) {
|
|
1511
|
+
const match = value.match(/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?/);
|
|
1512
|
+
if (!match) {
|
|
1513
|
+
return void 0;
|
|
1514
|
+
}
|
|
1515
|
+
const raw = match[0];
|
|
1516
|
+
return {
|
|
1517
|
+
raw,
|
|
1518
|
+
core: raw.split(/[-+]/, 1)[0]
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async function verifyLtexExecutable(command, releaseTag) {
|
|
1522
|
+
const result = await runProcess(command, ["--version"], VERIFY_TIMEOUT_MS);
|
|
1523
|
+
const error = processError(result, "ltex-ls-plus --version");
|
|
1524
|
+
if (error) {
|
|
1525
|
+
throw error;
|
|
1526
|
+
}
|
|
1527
|
+
const output = `${result.stdout}
|
|
1528
|
+
${result.stderr}`.trim();
|
|
1529
|
+
const expected = parseSemver(releaseVersionFromTag(releaseTag));
|
|
1530
|
+
const actual = parseSemver(output);
|
|
1531
|
+
if (expected && actual && expected.core !== actual.core) {
|
|
1532
|
+
throw new Error(`Downloaded ltex-ls-plus version mismatch: expected ${expected.core}, got ${actual.raw}.`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
async function sha256File(filePath) {
|
|
1536
|
+
return new Promise((resolvePromise, reject) => {
|
|
1537
|
+
const hash = (0, import_crypto.createHash)("sha256");
|
|
1538
|
+
const stream = (0, import_fs3.createReadStream)(filePath);
|
|
1539
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1540
|
+
stream.on("error", reject);
|
|
1541
|
+
stream.on("end", () => resolvePromise(hash.digest("hex")));
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
async function verifyDownloadedArchive(filePath, release) {
|
|
1545
|
+
if (!release.sha256) {
|
|
1546
|
+
return;
|
|
1547
|
+
}
|
|
1548
|
+
const actual = await sha256File(filePath);
|
|
1549
|
+
if (actual !== release.sha256) {
|
|
1550
|
+
throw new Error(`Downloaded ltex-ls-plus checksum mismatch: expected ${release.sha256}, got ${actual}.`);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function extractArchive(archivePath, targetDir, assetName) {
|
|
1554
|
+
await import_fs3.promises.mkdir(targetDir, { recursive: true });
|
|
1555
|
+
if (assetName.endsWith(".zip")) {
|
|
1556
|
+
if (process.platform === "win32") {
|
|
1557
|
+
await runProcessOrThrow(
|
|
1558
|
+
"powershell.exe",
|
|
1559
|
+
["-NoProfile", "-NonInteractive", "-Command", "Expand-Archive -LiteralPath $args[0] -DestinationPath $args[1] -Force", archivePath, targetDir],
|
|
1560
|
+
INSTALL_TIMEOUT_MS,
|
|
1561
|
+
"Extracting ltex-ls-plus zip archive"
|
|
1562
|
+
);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
const unzip = await runProcess("unzip", ["-q", archivePath, "-d", targetDir], INSTALL_TIMEOUT_MS);
|
|
1566
|
+
if (!processError(unzip, "unzip")) {
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
await runProcessOrThrow("tar", ["-xf", archivePath, "-C", targetDir], INSTALL_TIMEOUT_MS, "Extracting ltex-ls-plus zip archive");
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
await runProcessOrThrow("tar", ["-xzf", archivePath, "-C", targetDir], INSTALL_TIMEOUT_MS, "Extracting ltex-ls-plus tar archive");
|
|
1573
|
+
}
|
|
1574
|
+
async function findExtractedServerDirectory(rootDir) {
|
|
1575
|
+
const rootExecutable = existingLtexExecutable(rootDir);
|
|
1576
|
+
if (rootExecutable) {
|
|
1577
|
+
await import_fs3.promises.chmod(rootExecutable, 493).catch(() => {
|
|
1578
|
+
});
|
|
1579
|
+
return rootDir;
|
|
1580
|
+
}
|
|
1581
|
+
const entries = await import_fs3.promises.readdir(rootDir);
|
|
1582
|
+
for (const entry of entries) {
|
|
1583
|
+
const candidate = (0, import_path4.join)(rootDir, entry);
|
|
1584
|
+
const stat = await import_fs3.promises.stat(candidate).catch(() => void 0);
|
|
1585
|
+
if (!stat?.isDirectory()) {
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
const executable = existingLtexExecutable(candidate);
|
|
1589
|
+
if (executable) {
|
|
1590
|
+
await import_fs3.promises.chmod(executable, 493).catch(() => {
|
|
1591
|
+
});
|
|
1592
|
+
return candidate;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
throw new Error("Extracted ltex-ls-plus archive did not contain a server directory.");
|
|
1596
|
+
}
|
|
1597
|
+
async function removePath(pathValue) {
|
|
1598
|
+
if (!pathValue) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
await import_fs3.promises.rm(pathValue, { recursive: true, force: true }).catch((err) => {
|
|
1602
|
+
console.error(`Failed to remove ${pathValue}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
function selectReleaseAsset(release) {
|
|
1606
|
+
const version = releaseVersionFromTag(release.tag_name);
|
|
1607
|
+
const exactName = ltexAssetNameForPlatform(version);
|
|
1608
|
+
return release.assets.find((asset) => asset.name === exactName) ?? release.assets.find((asset) => assetMatchesPlatform(asset.name, version));
|
|
1609
|
+
}
|
|
1610
|
+
async function fetchLatestLtexRelease() {
|
|
1611
|
+
const platformName = ltexPlatformName();
|
|
1612
|
+
const archName = ltexArchName();
|
|
1613
|
+
if (!platformName || !archName) {
|
|
1614
|
+
throw new Error(`Managed ltex-ls-plus install is not available for ${process.platform}/${process.arch}.`);
|
|
1615
|
+
}
|
|
1616
|
+
const response = await fetchText(LTEX_LATEST_RELEASE_API_URL);
|
|
1617
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
1618
|
+
throw new Error(`GitHub release request failed with HTTP ${response.statusCode}: ${response.text}`);
|
|
1619
|
+
}
|
|
1620
|
+
const release = JSON.parse(response.text);
|
|
1621
|
+
const asset = selectReleaseAsset(release);
|
|
1622
|
+
if (!asset) {
|
|
1623
|
+
const expected = ltexAssetNameForPlatform(releaseVersionFromTag(release.tag_name)) ?? `${platformName}-${archName}`;
|
|
1624
|
+
throw new Error(`Release ${release.tag_name} does not include a ltex-ls-plus asset for ${process.platform}/${process.arch} (${expected}).`);
|
|
1625
|
+
}
|
|
1626
|
+
return {
|
|
1627
|
+
tag: release.tag_name,
|
|
1628
|
+
assetName: asset.name,
|
|
1629
|
+
url: asset.browser_download_url,
|
|
1630
|
+
sha256: sha256FromDigest(asset.digest)
|
|
1631
|
+
};
|
|
1632
|
+
}
|
|
1633
|
+
var LtexServerInstaller = class {
|
|
1634
|
+
constructor(context) {
|
|
1635
|
+
this.context = context;
|
|
1636
|
+
}
|
|
1637
|
+
managedRelease() {
|
|
1638
|
+
return this.context.globalState.get(MANAGED_RELEASE_KEY);
|
|
1639
|
+
}
|
|
1640
|
+
async installLatest() {
|
|
1641
|
+
const release = await fetchLatestLtexRelease();
|
|
1642
|
+
await this.installRelease(release);
|
|
1643
|
+
return release;
|
|
1644
|
+
}
|
|
1645
|
+
async updateLatest() {
|
|
1646
|
+
const release = await fetchLatestLtexRelease();
|
|
1647
|
+
if (this.managedRelease() === release.tag && managedServerExecutable(this.context.storagePath)) {
|
|
1648
|
+
return null;
|
|
1649
|
+
}
|
|
1650
|
+
await this.installRelease(release);
|
|
1651
|
+
return release;
|
|
1652
|
+
}
|
|
1653
|
+
async installRelease(release) {
|
|
1654
|
+
if (!(0, import_fs3.existsSync)(this.context.storagePath)) {
|
|
1655
|
+
await import_fs3.promises.mkdir(this.context.storagePath, { recursive: true });
|
|
1656
|
+
}
|
|
1657
|
+
const statusItem = import_coc6.window.createStatusBarItem(0, { progress: true });
|
|
1658
|
+
const tempId = (0, import_crypto.randomBytes)(5).toString("hex");
|
|
1659
|
+
const archivePath = (0, import_path4.join)(this.context.storagePath, `${release.assetName}-${tempId}`);
|
|
1660
|
+
const extractDir = (0, import_path4.join)(this.context.storagePath, `extract-${tempId}`);
|
|
1661
|
+
const backupDir = (0, import_path4.join)(this.context.storagePath, `ltex-ls-plus.backup-${tempId}`);
|
|
1662
|
+
const finalDir = managedServerDirectory(this.context.storagePath);
|
|
1663
|
+
let backupCreated = false;
|
|
1664
|
+
let installed = false;
|
|
1665
|
+
try {
|
|
1666
|
+
statusItem.text = `Downloading ltex-ls-plus ${release.tag}`;
|
|
1667
|
+
statusItem.show();
|
|
1668
|
+
await downloadFile(release.url, archivePath, (received, total) => {
|
|
1669
|
+
statusItem.text = total ? `${(received / total * 100).toFixed(1)}% Downloading ltex-ls-plus ${release.tag}` : `Downloading ltex-ls-plus ${release.tag}`;
|
|
1670
|
+
});
|
|
1671
|
+
statusItem.text = `Verifying ltex-ls-plus ${release.tag}`;
|
|
1672
|
+
await verifyDownloadedArchive(archivePath, release);
|
|
1673
|
+
statusItem.text = `Extracting ltex-ls-plus ${release.tag}`;
|
|
1674
|
+
await extractArchive(archivePath, extractDir, release.assetName);
|
|
1675
|
+
const extractedDir = await findExtractedServerDirectory(extractDir);
|
|
1676
|
+
const executable = existingLtexExecutable(extractedDir);
|
|
1677
|
+
if (!executable) {
|
|
1678
|
+
throw new Error("Extracted ltex-ls-plus server directory is missing bin/ltex-ls-plus.");
|
|
1679
|
+
}
|
|
1680
|
+
statusItem.text = `Testing ltex-ls-plus ${release.tag}`;
|
|
1681
|
+
await verifyLtexExecutable(executable, release.tag);
|
|
1682
|
+
await import_fs3.promises.rename(finalDir, backupDir).then(
|
|
1683
|
+
() => {
|
|
1684
|
+
backupCreated = true;
|
|
1685
|
+
},
|
|
1686
|
+
(err) => {
|
|
1687
|
+
if (err.code !== "ENOENT") {
|
|
1688
|
+
throw err;
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
);
|
|
1692
|
+
await import_fs3.promises.rename(extractedDir, finalDir);
|
|
1693
|
+
installed = true;
|
|
1694
|
+
await this.context.globalState.update(MANAGED_RELEASE_KEY, release.tag).catch((err) => {
|
|
1695
|
+
import_coc6.window.showWarningMessage(`ltex-ls-plus ${release.tag} installed, but coc-ltex-plus could not record release metadata.`);
|
|
1696
|
+
console.error(err);
|
|
1697
|
+
});
|
|
1698
|
+
} catch (err) {
|
|
1699
|
+
if (backupCreated) {
|
|
1700
|
+
await removePath(finalDir);
|
|
1701
|
+
await import_fs3.promises.rename(backupDir, finalDir).catch((restoreErr) => {
|
|
1702
|
+
console.error(`Failed to restore previous ltex-ls-plus install: ${restoreErr instanceof Error ? restoreErr.message : String(restoreErr)}`);
|
|
1703
|
+
});
|
|
1704
|
+
backupCreated = false;
|
|
1705
|
+
}
|
|
1706
|
+
throw err;
|
|
1707
|
+
} finally {
|
|
1708
|
+
statusItem.hide();
|
|
1709
|
+
statusItem.dispose();
|
|
1710
|
+
await removePath(archivePath);
|
|
1711
|
+
await removePath(extractDir);
|
|
1712
|
+
if (backupCreated && installed) {
|
|
1713
|
+
await removePath(backupDir);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
};
|
|
1718
|
+
|
|
1719
|
+
// src/settingsList.ts
|
|
1720
|
+
var import_coc7 = require("coc.nvim");
|
|
1721
|
+
var LtexSettingsList = class extends import_coc7.BasicList {
|
|
1722
|
+
constructor(externalSettings, onChanged, diagnosticsProvider = () => []) {
|
|
1723
|
+
super();
|
|
1724
|
+
this.externalSettings = externalSettings;
|
|
1725
|
+
this.onChanged = onChanged;
|
|
1726
|
+
this.diagnosticsProvider = diagnosticsProvider;
|
|
1727
|
+
this.name = "ltexSettings";
|
|
1728
|
+
this.defaultAction = "inspect";
|
|
1729
|
+
this.description = "LTeX dictionaries, disabled rules, and hidden false positives";
|
|
1730
|
+
this.detail = "Inspect, edit, delete, jump to diagnostics, or clean obsolete LTeX settings.";
|
|
1731
|
+
this.addAction("inspect", (item) => this.inspect(item));
|
|
1732
|
+
this.addAction("jump", (item) => this.jump(item));
|
|
1733
|
+
this.addAction("edit", (item) => this.edit(item), { reload: true });
|
|
1734
|
+
this.addAction("delete", (item) => this.delete(item), { reload: true });
|
|
1735
|
+
this.addAction("cleanupObsoleteHiddenFalsePositives", () => this.cleanupObsoleteHiddenFalsePositives(), { reload: true });
|
|
1736
|
+
}
|
|
1737
|
+
get alignColumns() {
|
|
1738
|
+
return true;
|
|
1739
|
+
}
|
|
1740
|
+
async loadItems(_context) {
|
|
1741
|
+
const document = await import_coc7.workspace.document;
|
|
1742
|
+
const diagnostics = this.diagnosticsProvider();
|
|
1743
|
+
const entries = this.externalSettings.listEntries(import_coc7.Uri.parse(document.uri));
|
|
1744
|
+
return entries.map((entry) => {
|
|
1745
|
+
const context = diagnosticsContextForEntry(entry, diagnostics, document);
|
|
1746
|
+
return {
|
|
1747
|
+
label: `${context.label} ${settingsEntryLabel(entry)}`,
|
|
1748
|
+
filterText: `${context.label} ${settingTitle(entry.settingName)} ${entry.language} ${entry.value} ${entry.sourceLabel} ${entry.filePath ?? ""} ${context.message ?? ""} ${context.line ?? ""}`,
|
|
1749
|
+
sortText: `${entry.settingName}:${context.label}:${entry.language}:${entry.value}`,
|
|
1750
|
+
data: { entry, context }
|
|
1751
|
+
};
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
inspect(item) {
|
|
1755
|
+
const data = singleListItemData(item);
|
|
1756
|
+
const entry = data?.entry ?? singleListEntry(item);
|
|
1757
|
+
if (!entry) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
const context = data?.context;
|
|
1761
|
+
const lines = [
|
|
1762
|
+
`${settingTitle(entry.settingName)} entry`,
|
|
1763
|
+
`Language: ${entry.language}`,
|
|
1764
|
+
`Value: ${entry.value}`,
|
|
1765
|
+
`Source: ${entry.sourceLabel}`
|
|
1766
|
+
];
|
|
1767
|
+
if (entry.readOnly) {
|
|
1768
|
+
lines.push("Read-only: true");
|
|
1769
|
+
}
|
|
1770
|
+
if (entry.filePath) {
|
|
1771
|
+
lines.push(`File: ${entry.filePath}`);
|
|
1772
|
+
}
|
|
1773
|
+
lines.push(...diagnosticDetailLines(context));
|
|
1774
|
+
import_coc7.window.showInformationMessage(lines.join("\n"));
|
|
1775
|
+
}
|
|
1776
|
+
async jump(item) {
|
|
1777
|
+
const context = singleListItemData(item)?.context;
|
|
1778
|
+
if (!context?.uri || !context.range) {
|
|
1779
|
+
import_coc7.window.showWarningMessage("No current LTeX diagnostic or active hidden false positive location is available for this entry.");
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
await import_coc7.workspace.jumpTo(context.uri, context.range.start);
|
|
1783
|
+
}
|
|
1784
|
+
async edit(item) {
|
|
1785
|
+
const entry = singleListEntry(item);
|
|
1786
|
+
if (!entry) {
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
if (entry.readOnly) {
|
|
1790
|
+
import_coc7.window.showWarningMessage("This LTeX settings entry is read-only.");
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
const nextValue = await import_coc7.window.requestInput(`Edit ${settingTitle(entry.settingName)} entry`, entry.value);
|
|
1794
|
+
if (!nextValue || nextValue.trim() === entry.value) {
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
await this.externalSettings.editEntry(entry, nextValue);
|
|
1798
|
+
await this.onChanged();
|
|
1799
|
+
}
|
|
1800
|
+
async delete(item) {
|
|
1801
|
+
const entry = singleListEntry(item);
|
|
1802
|
+
if (!entry) {
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1805
|
+
if (entry.readOnly) {
|
|
1806
|
+
import_coc7.window.showWarningMessage("This LTeX settings entry is read-only.");
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const confirmed = await import_coc7.window.showPrompt(`Delete ${settingTitle(entry.settingName)} entry "${entry.value}"?`);
|
|
1810
|
+
if (!confirmed) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
await this.externalSettings.deleteEntry(entry);
|
|
1814
|
+
await this.onChanged();
|
|
1815
|
+
}
|
|
1816
|
+
async cleanupObsoleteHiddenFalsePositives() {
|
|
1817
|
+
const document = await import_coc7.workspace.document;
|
|
1818
|
+
const diagnostics = this.diagnosticsProvider();
|
|
1819
|
+
const obsoleteEntries = this.externalSettings.listEntries(import_coc7.Uri.parse(document.uri)).filter((entry) => entry.settingName === "hiddenFalsePositives").filter((entry) => diagnosticsContextForEntry(entry, diagnostics, document).status === "obsolete");
|
|
1820
|
+
if (obsoleteEntries.length === 0) {
|
|
1821
|
+
import_coc7.window.showInformationMessage("No obsolete hidden false positive entries found for the current document.");
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1824
|
+
const confirmed = await import_coc7.window.showPrompt(obsoleteCleanupPrompt(obsoleteEntries));
|
|
1825
|
+
if (!confirmed) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
for (const entry of obsoleteEntries) {
|
|
1829
|
+
await this.externalSettings.deleteEntry(entry);
|
|
1830
|
+
}
|
|
1831
|
+
await this.onChanged();
|
|
1832
|
+
import_coc7.window.showInformationMessage(`Deleted ${obsoleteEntries.length} obsolete hidden false positive entr${obsoleteEntries.length === 1 ? "y" : "ies"}.`);
|
|
1833
|
+
}
|
|
1834
|
+
};
|
|
1835
|
+
function obsoleteCleanupPrompt(entries) {
|
|
1836
|
+
const preview = entries.slice(0, 8).map((entry) => {
|
|
1837
|
+
const source = entry.filePath ?? entry.sourceLabel;
|
|
1838
|
+
return `- ${entry.language}: ${entry.value} (${source})`;
|
|
1839
|
+
});
|
|
1840
|
+
const remaining = entries.length > preview.length ? `
|
|
1841
|
+
...and ${entries.length - preview.length} more.` : "";
|
|
1842
|
+
return [
|
|
1843
|
+
`Delete ${entries.length} hidden false positive entr${entries.length === 1 ? "y" : "ies"} not found in the current buffer?`,
|
|
1844
|
+
"Shared entries may still apply to other files.",
|
|
1845
|
+
...preview,
|
|
1846
|
+
remaining
|
|
1847
|
+
].filter(Boolean).join("\n");
|
|
1848
|
+
}
|
|
1849
|
+
function singleListItemData(item) {
|
|
1850
|
+
if (Array.isArray(item)) {
|
|
1851
|
+
return void 0;
|
|
1852
|
+
}
|
|
1853
|
+
return item.data;
|
|
1854
|
+
}
|
|
1855
|
+
function diagnosticDetailLines(context) {
|
|
1856
|
+
if (!context) {
|
|
1857
|
+
return ["Diagnostic context: unknown"];
|
|
1858
|
+
}
|
|
1859
|
+
const lines = [`Diagnostic context: ${context.label}`];
|
|
1860
|
+
if (context.ruleId) {
|
|
1861
|
+
lines.push(`Rule: ${context.ruleId}`);
|
|
1862
|
+
}
|
|
1863
|
+
if (context.sentence) {
|
|
1864
|
+
lines.push(`Sentence: ${context.sentence}`);
|
|
1865
|
+
}
|
|
1866
|
+
if (context.message) {
|
|
1867
|
+
lines.push(`Diagnostic: ${context.message}`);
|
|
1868
|
+
}
|
|
1869
|
+
if (context.uri && context.range) {
|
|
1870
|
+
lines.push(`Location: ${context.uri}:${context.range.start.line + 1}:${context.range.start.character + 1}`);
|
|
1871
|
+
}
|
|
1872
|
+
if (context.line) {
|
|
1873
|
+
lines.push(`Line: ${context.line}`);
|
|
1874
|
+
}
|
|
1875
|
+
return lines;
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
// src/extension.ts
|
|
1879
|
+
var LtexPlusExtension = class {
|
|
1880
|
+
constructor(context) {
|
|
1881
|
+
this.context = context;
|
|
1882
|
+
this.diagnosticsState = { enabled: true };
|
|
1883
|
+
this.externalSettings = new ExternalSettings(context);
|
|
1884
|
+
this.serverInstaller = new LtexServerInstaller(context);
|
|
1885
|
+
}
|
|
1886
|
+
register() {
|
|
1887
|
+
this.context.subscriptions.push(
|
|
1888
|
+
import_coc8.workspace.onDidChangeConfiguration((event) => this.onConfigurationChange(event)),
|
|
1889
|
+
import_coc8.commands.registerCommand("ltex.activateExtension", () => this.start()),
|
|
1890
|
+
import_coc8.commands.registerCommand("ltex.restart", () => this.restart()),
|
|
1891
|
+
import_coc8.commands.registerCommand("ltex.resetAndRestart", () => this.restart()),
|
|
1892
|
+
import_coc8.commands.registerCommand("ltex.checkCurrentDocument", () => this.checkCurrentDocument()),
|
|
1893
|
+
import_coc8.commands.registerCommand("ltex.checkSelection", () => this.checkSelection()),
|
|
1894
|
+
import_coc8.commands.registerCommand("ltex.checkAllDocumentsInWorkspace", () => this.checkAllDocumentsInWorkspace()),
|
|
1895
|
+
import_coc8.commands.registerCommand("ltex.clearDiagnosticsInCurrentDocument", () => this.clearDiagnosticsInCurrentDocument()),
|
|
1896
|
+
import_coc8.commands.registerCommand("ltex.clearAllDiagnostics", () => this.clearAllDiagnostics()),
|
|
1897
|
+
import_coc8.commands.registerCommand("ltex.toggleDiagnostics", () => this.toggleDiagnostics()),
|
|
1898
|
+
import_coc8.commands.registerCommand("ltex.showStatusInformation", () => this.showStatusInformation()),
|
|
1899
|
+
import_coc8.commands.registerCommand("ltex.serverStatus", () => this.showServerStatus()),
|
|
1900
|
+
import_coc8.commands.registerCommand("ltex.showOutputChannel", () => this.client?.outputChannel.show()),
|
|
1901
|
+
import_coc8.commands.registerCommand("ltex.disableHere", () => this.disableHere()),
|
|
1902
|
+
import_coc8.commands.registerCommand("ltex.reloadExternalSettings", () => this.reloadExternalSettings()),
|
|
1903
|
+
import_coc8.commands.registerCommand("ltex.installServer", () => this.installServer()),
|
|
1904
|
+
import_coc8.commands.registerCommand("ltex.updateServer", () => this.updateServer()),
|
|
1905
|
+
import_coc8.commands.registerCommand("ltex.checkDuplicateServer", () => this.checkDuplicateServerConfig()),
|
|
1906
|
+
import_coc8.commands.registerCommand("_ltex.addToDictionary", (params) => this.addToLanguageSpecificSetting(params, "dictionary", "words")),
|
|
1907
|
+
import_coc8.commands.registerCommand("_ltex.disableRules", (params) => this.addToLanguageSpecificSetting(params, "disabledRules", "ruleIds")),
|
|
1908
|
+
import_coc8.commands.registerCommand(
|
|
1909
|
+
"_ltex.hideFalsePositives",
|
|
1910
|
+
(params) => this.addToLanguageSpecificSetting(params, "hiddenFalsePositives", "falsePositives")
|
|
1911
|
+
),
|
|
1912
|
+
import_coc8.listManager.registerList(
|
|
1913
|
+
new LtexSettingsList(
|
|
1914
|
+
this.externalSettings,
|
|
1915
|
+
() => this.reloadExternalSettings(false),
|
|
1916
|
+
() => diagnosticsFromCollection(this.client?.diagnostics)
|
|
1917
|
+
)
|
|
1918
|
+
),
|
|
1919
|
+
import_coc8.Disposable.create(() => {
|
|
1920
|
+
this.externalSettings.dispose();
|
|
1921
|
+
void this.stop().catch((err) => console.error(err));
|
|
1922
|
+
})
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
async activate() {
|
|
1926
|
+
this.checkDuplicateServerConfig(false);
|
|
1927
|
+
if (enabledLanguages().length === 0) {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
await this.start();
|
|
1931
|
+
}
|
|
1932
|
+
async start() {
|
|
1933
|
+
if (this.client?.isRunning()) {
|
|
1934
|
+
return;
|
|
1935
|
+
}
|
|
1936
|
+
const resolved = resolveLtexServer(this.context.storagePath);
|
|
1937
|
+
if (!resolved) {
|
|
1938
|
+
import_coc8.window.showWarningMessage(`ltex-ls-plus was not found. Run :CocCommand ltex.installServer or set "ltex.ltex-ls.path". Releases: ${LTEX_RELEASES_URL}`);
|
|
1939
|
+
return;
|
|
1940
|
+
}
|
|
1941
|
+
this.client = createClient(resolved.command, this.externalSettings, this.diagnosticsState);
|
|
1942
|
+
this.clientRegistration = import_coc8.services.registLanguageClient(this.client);
|
|
1943
|
+
await this.client.onReady();
|
|
1944
|
+
}
|
|
1945
|
+
async restart() {
|
|
1946
|
+
await this.stop();
|
|
1947
|
+
await this.start();
|
|
1948
|
+
}
|
|
1949
|
+
async stop() {
|
|
1950
|
+
const client = this.client;
|
|
1951
|
+
this.client = void 0;
|
|
1952
|
+
this.clientRegistration?.dispose();
|
|
1953
|
+
this.clientRegistration = void 0;
|
|
1954
|
+
if (!client) {
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
if (client.isRunning()) {
|
|
1958
|
+
await client.stop();
|
|
1959
|
+
}
|
|
1960
|
+
await client.dispose();
|
|
1961
|
+
}
|
|
1962
|
+
async onConfigurationChange(event) {
|
|
1963
|
+
const changedProcessSetting = PROCESS_SETTING_KEYS.find((key) => event.affectsConfiguration(key));
|
|
1964
|
+
if (!changedProcessSetting) {
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
if (enabledLanguages().length === 0) {
|
|
1968
|
+
await this.stop();
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
if (!this.client) {
|
|
1972
|
+
await this.start();
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const restart = await import_coc8.window.showPrompt(`Changing "${changedProcessSetting}" requires restarting LTeX+. Restart now?`);
|
|
1976
|
+
if (restart) {
|
|
1977
|
+
await this.restart();
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
async checkCurrentDocument() {
|
|
1981
|
+
const document = await import_coc8.workspace.document;
|
|
1982
|
+
return this.checkDocument(document);
|
|
1983
|
+
}
|
|
1984
|
+
async checkSelection() {
|
|
1985
|
+
const document = await import_coc8.workspace.document;
|
|
1986
|
+
const range = await import_coc8.window.getSelectedRange("v");
|
|
1987
|
+
if (!range) {
|
|
1988
|
+
return false;
|
|
1989
|
+
}
|
|
1990
|
+
return this.checkDocument(document, range);
|
|
1991
|
+
}
|
|
1992
|
+
async checkAllDocumentsInWorkspace() {
|
|
1993
|
+
if (!this.client) {
|
|
1994
|
+
import_coc8.window.showErrorMessage("LTeX+ is not initialized.");
|
|
1995
|
+
return false;
|
|
1996
|
+
}
|
|
1997
|
+
const documents = checkableWorkspaceDocuments(import_coc8.workspace.documents);
|
|
1998
|
+
if (documents.length === 0) {
|
|
1999
|
+
import_coc8.window.showWarningMessage("No open workspace documents match ltex.enabled.");
|
|
2000
|
+
return false;
|
|
2001
|
+
}
|
|
2002
|
+
let checked = 0;
|
|
2003
|
+
let failed = 0;
|
|
2004
|
+
for (const document of documents) {
|
|
2005
|
+
if (await this.checkDocument(document)) {
|
|
2006
|
+
checked += 1;
|
|
2007
|
+
} else {
|
|
2008
|
+
failed += 1;
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
if (failed > 0) {
|
|
2012
|
+
import_coc8.window.showWarningMessage(`Checked ${checked} open workspace document${checked === 1 ? "" : "s"} with ${failed} failure${failed === 1 ? "" : "s"}.`);
|
|
2013
|
+
return false;
|
|
2014
|
+
}
|
|
2015
|
+
import_coc8.window.showInformationMessage(`Checked ${checked} open workspace document${checked === 1 ? "" : "s"}.`);
|
|
2016
|
+
return true;
|
|
2017
|
+
}
|
|
2018
|
+
async checkDocument(document, range) {
|
|
2019
|
+
if (!this.client) {
|
|
2020
|
+
import_coc8.window.showErrorMessage("LTeX+ is not initialized.");
|
|
2021
|
+
return false;
|
|
2022
|
+
}
|
|
2023
|
+
const params = {
|
|
2024
|
+
uri: document.uri,
|
|
2025
|
+
codeLanguageId: document.filetype,
|
|
2026
|
+
text: document.content,
|
|
2027
|
+
range
|
|
2028
|
+
};
|
|
2029
|
+
try {
|
|
2030
|
+
await this.client.onReady();
|
|
2031
|
+
const result = await this.client.sendRequest("workspace/executeCommand", {
|
|
2032
|
+
command: "_ltex.checkDocument",
|
|
2033
|
+
arguments: [params]
|
|
2034
|
+
});
|
|
2035
|
+
if (result?.success === false) {
|
|
2036
|
+
import_coc8.window.showErrorMessage(`LTeX+ could not check the document: ${result.errorMessage ?? "unknown error"}`);
|
|
2037
|
+
return false;
|
|
2038
|
+
}
|
|
2039
|
+
return true;
|
|
2040
|
+
} catch (err) {
|
|
2041
|
+
import_coc8.window.showErrorMessage(`LTeX+ could not check the document: ${err instanceof Error ? err.message : String(err)}`);
|
|
2042
|
+
return false;
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
async clearDiagnosticsInCurrentDocument() {
|
|
2046
|
+
if (!this.client) {
|
|
2047
|
+
import_coc8.window.showErrorMessage("LTeX+ is not initialized.");
|
|
2048
|
+
return false;
|
|
2049
|
+
}
|
|
2050
|
+
const document = await import_coc8.workspace.document;
|
|
2051
|
+
this.client.diagnostics?.set(document.uri, null);
|
|
2052
|
+
return true;
|
|
2053
|
+
}
|
|
2054
|
+
clearAllDiagnostics() {
|
|
2055
|
+
if (!this.client) {
|
|
2056
|
+
import_coc8.window.showErrorMessage("LTeX+ is not initialized.");
|
|
2057
|
+
return false;
|
|
2058
|
+
}
|
|
2059
|
+
this.client.diagnostics?.clear();
|
|
2060
|
+
return true;
|
|
2061
|
+
}
|
|
2062
|
+
async toggleDiagnostics() {
|
|
2063
|
+
this.diagnosticsState.enabled = !this.diagnosticsState.enabled;
|
|
2064
|
+
if (!this.diagnosticsState.enabled) {
|
|
2065
|
+
this.client?.diagnostics?.clear();
|
|
2066
|
+
import_coc8.window.showInformationMessage("LTeX+ diagnostics disabled for this session.");
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
import_coc8.window.showInformationMessage("LTeX+ diagnostics enabled for this session.");
|
|
2070
|
+
if (this.client?.isRunning()) {
|
|
2071
|
+
await this.checkCurrentDocument();
|
|
2072
|
+
}
|
|
2073
|
+
return true;
|
|
2074
|
+
}
|
|
2075
|
+
showStatusInformation() {
|
|
2076
|
+
const resolved = resolveLtexServer(this.context.storagePath);
|
|
2077
|
+
const env = serverEnvironment();
|
|
2078
|
+
const languages = enabledLanguages();
|
|
2079
|
+
const status = [
|
|
2080
|
+
`LTeX+ client: ${this.client?.isRunning() ? "running" : "stopped"}`,
|
|
2081
|
+
`Diagnostics: ${this.diagnosticsState.enabled ? "enabled" : "disabled"}`,
|
|
2082
|
+
`Server command: ${resolved?.command ?? "not found"}`,
|
|
2083
|
+
`Server source: ${resolved?.source ?? "none"}`,
|
|
2084
|
+
`Java home override: ${env.JAVA_HOME ?? "none"}`,
|
|
2085
|
+
`JAVA_OPTS: ${env.JAVA_OPTS ?? "none"}`,
|
|
2086
|
+
`Managed release: ${this.serverInstaller.managedRelease() ?? "none"}`,
|
|
2087
|
+
`Enabled filetypes: ${languages.length > 0 ? languages.join(", ") : "none"}`
|
|
2088
|
+
];
|
|
2089
|
+
const message = status.join("\n");
|
|
2090
|
+
if (this.client) {
|
|
2091
|
+
this.client.outputChannel.appendLine(message);
|
|
2092
|
+
this.client.outputChannel.show();
|
|
2093
|
+
}
|
|
2094
|
+
import_coc8.window.showInformationMessage(message);
|
|
2095
|
+
}
|
|
2096
|
+
async showServerStatus() {
|
|
2097
|
+
if (!this.client) {
|
|
2098
|
+
import_coc8.window.showErrorMessage("LTeX+ is not initialized.");
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
try {
|
|
2102
|
+
await this.client.onReady();
|
|
2103
|
+
const result = await this.client.sendRequest("workspace/executeCommand", {
|
|
2104
|
+
command: "_ltex.getServerStatus",
|
|
2105
|
+
arguments: []
|
|
2106
|
+
});
|
|
2107
|
+
const lines = [
|
|
2108
|
+
"LTeX+ Server Status",
|
|
2109
|
+
`Process ID: ${result.processId ?? "n/a"}`,
|
|
2110
|
+
`Wall-clock duration: ${seconds(result.wallClockDuration)}`,
|
|
2111
|
+
`CPU duration: ${seconds(result.cpuDuration)}`,
|
|
2112
|
+
`CPU usage: ${percent(result.cpuUsage)}`,
|
|
2113
|
+
`Used memory: ${bytes(result.usedMemory)}`,
|
|
2114
|
+
`Total JVM memory: ${bytes(result.totalMemory)}`,
|
|
2115
|
+
`Is checking: ${result.isChecking ? "true" : "false"}`
|
|
2116
|
+
];
|
|
2117
|
+
if (result.documentUriBeingChecked) {
|
|
2118
|
+
lines.push(`Document URI being checked: ${result.documentUriBeingChecked}`);
|
|
2119
|
+
}
|
|
2120
|
+
this.client.outputChannel.appendLine(lines.join("\n"));
|
|
2121
|
+
this.client.outputChannel.show();
|
|
2122
|
+
} catch (err) {
|
|
2123
|
+
import_coc8.window.showErrorMessage(`Could not get LTeX+ server status: ${err instanceof Error ? err.message : String(err)}`);
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
async disableHere() {
|
|
2127
|
+
const document = await import_coc8.workspace.document;
|
|
2128
|
+
const cursor = await import_coc8.window.getCursorPosition();
|
|
2129
|
+
const range = await import_coc8.window.getSelectedRange("v");
|
|
2130
|
+
const { startLine, endLine } = selectedLineRange(range, cursor);
|
|
2131
|
+
const commentString = await document.buffer.getOption("commentstring");
|
|
2132
|
+
await document.applyEdits([
|
|
2133
|
+
import_coc8.TextEdit.replace(
|
|
2134
|
+
import_coc8.Range.create(startLine, 0, Math.min(endLine + 1, document.lineCount), 0),
|
|
2135
|
+
disableHereReplacement(document.lines, startLine, endLine, commentString)
|
|
2136
|
+
)
|
|
2137
|
+
]);
|
|
2138
|
+
import_coc8.window.showInformationMessage("Inserted LTeX disable markers.");
|
|
2139
|
+
}
|
|
2140
|
+
async reloadExternalSettings(showMessage = true) {
|
|
2141
|
+
const loaded = this.externalSettings.reloadFromOpenDocuments();
|
|
2142
|
+
await this.notifyConfigurationChanged();
|
|
2143
|
+
if (showMessage) {
|
|
2144
|
+
import_coc8.window.showInformationMessage(`Reloaded ${loaded} LTeX external setting file${loaded === 1 ? "" : "s"}.`);
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
async installServer() {
|
|
2148
|
+
try {
|
|
2149
|
+
const release = await this.serverInstaller.installLatest();
|
|
2150
|
+
import_coc8.window.showInformationMessage(`ltex-ls-plus ${release.tag} installed.`);
|
|
2151
|
+
await this.restartIfManagedServerIsActive();
|
|
2152
|
+
} catch (err) {
|
|
2153
|
+
import_coc8.window.showErrorMessage(`ltex-ls-plus install failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
async updateServer() {
|
|
2157
|
+
try {
|
|
2158
|
+
const release = await this.serverInstaller.updateLatest();
|
|
2159
|
+
if (!release) {
|
|
2160
|
+
import_coc8.window.showInformationMessage(`Managed ltex-ls-plus is already up to date (${this.serverInstaller.managedRelease()}).`);
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
import_coc8.window.showInformationMessage(`ltex-ls-plus ${release.tag} installed.`);
|
|
2164
|
+
await this.restartIfManagedServerIsActive();
|
|
2165
|
+
} catch (err) {
|
|
2166
|
+
import_coc8.window.showErrorMessage(`ltex-ls-plus update failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
async restartIfManagedServerIsActive() {
|
|
2170
|
+
const configuredPath = normalizePath(ltexConfig().get("ltex-ls.path", ""));
|
|
2171
|
+
if (configuredPath) {
|
|
2172
|
+
import_coc8.window.showInformationMessage("Managed ltex-ls-plus was installed, but ltex.ltex-ls.path is still configured and remains active.");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
await this.restart();
|
|
2176
|
+
}
|
|
2177
|
+
async notifyConfigurationChanged() {
|
|
2178
|
+
if (!this.client) {
|
|
2179
|
+
return;
|
|
2180
|
+
}
|
|
2181
|
+
try {
|
|
2182
|
+
await this.client.onReady();
|
|
2183
|
+
await this.client.sendNotification("workspace/didChangeConfiguration", { settings: { ltex: {} } });
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
import_coc8.window.showWarningMessage(`Could not notify LTeX+ about settings changes: ${err instanceof Error ? err.message : String(err)}`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
checkDuplicateServerConfig(showSuccess = true) {
|
|
2189
|
+
const languageServerConfig = import_coc8.workspace.getConfiguration("languageserver");
|
|
2190
|
+
const duplicate = languageServerConfig.get("ltex-plus", void 0) ?? languageServerConfig.get("ltex", void 0);
|
|
2191
|
+
if (duplicate) {
|
|
2192
|
+
import_coc8.window.showWarningMessage(
|
|
2193
|
+
"Detected languageserver.ltex-plus/ltex in Coc settings. Remove it to avoid running a second LTEX server beside coc-ltex-plus."
|
|
2194
|
+
);
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
if (showSuccess) {
|
|
2198
|
+
import_coc8.window.showInformationMessage("No duplicate LTEX languageserver configuration detected.");
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
addToLanguageSpecificSetting(params, settingName, payloadKey) {
|
|
2202
|
+
const typedParams = params;
|
|
2203
|
+
if (!typedParams?.uri) {
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
const entries = typedParams[payloadKey];
|
|
2207
|
+
if (!entries) {
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
const uri = import_coc8.Uri.parse(typedParams.uri);
|
|
2211
|
+
const configurationTarget = import_coc8.workspace.getConfiguration("ltex.configurationTarget", uri.toString());
|
|
2212
|
+
const target = configurationTarget.get(settingName, "workspaceFolderExternalFile");
|
|
2213
|
+
const scopes = this.scopesForConfigurationTarget(target);
|
|
2214
|
+
if (target.endsWith("ExternalFile")) {
|
|
2215
|
+
this.addToExternalFiles(uri, settingName, scopes, entries);
|
|
2216
|
+
} else {
|
|
2217
|
+
void this.addToInternalSetting(uri, settingName, scopes, entries);
|
|
2218
|
+
}
|
|
2219
|
+
void this.checkCurrentDocument();
|
|
2220
|
+
}
|
|
2221
|
+
scopesForConfigurationTarget(target) {
|
|
2222
|
+
if (target.startsWith("user")) {
|
|
2223
|
+
return [import_coc8.ConfigurationTarget.Global];
|
|
2224
|
+
}
|
|
2225
|
+
if (target.startsWith("workspaceFolder")) {
|
|
2226
|
+
return [import_coc8.ConfigurationTarget.WorkspaceFolder, import_coc8.ConfigurationTarget.Workspace, import_coc8.ConfigurationTarget.Global];
|
|
2227
|
+
}
|
|
2228
|
+
if (target.startsWith("workspace")) {
|
|
2229
|
+
return [import_coc8.ConfigurationTarget.Workspace, import_coc8.ConfigurationTarget.Global];
|
|
2230
|
+
}
|
|
2231
|
+
return [import_coc8.ConfigurationTarget.WorkspaceFolder, import_coc8.ConfigurationTarget.Workspace, import_coc8.ConfigurationTarget.Global];
|
|
2232
|
+
}
|
|
2233
|
+
addToExternalFiles(uri, settingName, scopes, entries) {
|
|
2234
|
+
for (const [language, values] of Object.entries(entries)) {
|
|
2235
|
+
const cleanValues = values.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
|
|
2236
|
+
if (cleanValues.length === 0) {
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
let written = false;
|
|
2240
|
+
for (const scope of scopes) {
|
|
2241
|
+
const filePath = this.externalSettings.firstExternalFilePath(uri, settingName, scope, language);
|
|
2242
|
+
if (!filePath) {
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
this.externalSettings.appendToFile(filePath, cleanValues);
|
|
2246
|
+
written = true;
|
|
2247
|
+
break;
|
|
2248
|
+
}
|
|
2249
|
+
if (!written) {
|
|
2250
|
+
void this.addToInternalSetting(uri, settingName, scopes, { [language]: cleanValues });
|
|
2251
|
+
}
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
async addToInternalSetting(uri, settingName, scopes, entries) {
|
|
2255
|
+
const config = ltexConfig(uri.toString());
|
|
2256
|
+
const current = config.get(settingName, {});
|
|
2257
|
+
const next = { ...current };
|
|
2258
|
+
for (const [language, values] of Object.entries(entries)) {
|
|
2259
|
+
next[language] = cleanLanguageSpecificEntries([...next[language] ?? [], ...values]);
|
|
2260
|
+
}
|
|
2261
|
+
for (const scope of scopes) {
|
|
2262
|
+
try {
|
|
2263
|
+
await config.update(settingName, next, scope);
|
|
2264
|
+
return;
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
console.error(`Failed to update ltex.${settingName} at scope ${scope}: ${err instanceof Error ? err.message : String(err)}`);
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
import_coc8.window.showErrorMessage(`Could not update ltex.${settingName}.`);
|
|
2270
|
+
}
|
|
2271
|
+
};
|
|
2272
|
+
|
|
2273
|
+
// src/testHooks.ts
|
|
2274
|
+
var __testHooks = {
|
|
2275
|
+
ExternalSettings,
|
|
2276
|
+
assetMatchesPlatform,
|
|
2277
|
+
checkableWorkspaceDocuments,
|
|
2278
|
+
cleanLanguageSpecificEntries,
|
|
2279
|
+
commentLine,
|
|
2280
|
+
diagnosticCode,
|
|
2281
|
+
diagnosticsContextForEntry,
|
|
2282
|
+
disableHereReplacement,
|
|
2283
|
+
expandHome,
|
|
2284
|
+
ltexArchName,
|
|
2285
|
+
ltexAssetNameForPlatform,
|
|
2286
|
+
ltexPlatformName,
|
|
2287
|
+
managedServerDirectory,
|
|
2288
|
+
managedServerExecutable,
|
|
2289
|
+
normalizePath,
|
|
2290
|
+
normalizeSpellLanguage,
|
|
2291
|
+
parseExternalFileEntries,
|
|
2292
|
+
parseHiddenFalsePositiveEntry,
|
|
2293
|
+
releaseVersionFromTag,
|
|
2294
|
+
resolveExecutableFromConfiguredPath,
|
|
2295
|
+
resolveLtexExecutable,
|
|
2296
|
+
resolveLtexServer,
|
|
2297
|
+
selectedLineRange,
|
|
2298
|
+
selectReleaseAsset,
|
|
2299
|
+
serverEnvironment,
|
|
2300
|
+
async mergedSetting(settings, uri, settingName) {
|
|
2301
|
+
const internal = settings;
|
|
2302
|
+
return internal.mergedSetting(uri, settingName);
|
|
2303
|
+
}
|
|
2304
|
+
};
|
|
2305
|
+
|
|
2306
|
+
// src/index.ts
|
|
2307
|
+
async function activate(context) {
|
|
2308
|
+
if (!(0, import_fs4.existsSync)(context.storagePath)) {
|
|
2309
|
+
(0, import_fs4.mkdirSync)(context.storagePath, { recursive: true });
|
|
2310
|
+
}
|
|
2311
|
+
const extension = new LtexPlusExtension(context);
|
|
2312
|
+
extension.register();
|
|
2313
|
+
await extension.activate();
|
|
2314
|
+
}
|
|
2315
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
2316
|
+
0 && (module.exports = {
|
|
2317
|
+
__testHooks,
|
|
2318
|
+
activate
|
|
2319
|
+
});
|