@askdkc/dsh-lsp-server 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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +27 -0
- package/cordis.patch.yml +29 -0
- package/docs/usage.md +94 -0
- package/lib/cli/doctor.js +135 -0
- package/lib/cli/doctor.js.map +1 -0
- package/lib/completion-B7NPVCEN.js +79 -0
- package/lib/completion-B7NPVCEN.js.map +1 -0
- package/lib/errors-DtyWAUi0.js +16 -0
- package/lib/errors-DtyWAUi0.js.map +1 -0
- package/lib/extra-tool.js +113 -0
- package/lib/extra-tool.js.map +1 -0
- package/lib/launch-ByUvgkVv.js +308 -0
- package/lib/launch-ByUvgkVv.js.map +1 -0
- package/lib/provider.js +1164 -0
- package/lib/provider.js.map +1 -0
- package/lib/types/cli/availability.d.ts +3 -0
- package/lib/types/cli/doctor.d.ts +33 -0
- package/lib/types/config.d.ts +87 -0
- package/lib/types/errors.d.ts +7 -0
- package/lib/types/extra-tool.d.ts +13 -0
- package/lib/types/host/dsh.d.ts +10 -0
- package/lib/types/host/source.d.ts +8 -0
- package/lib/types/host/uri.d.ts +2 -0
- package/lib/types/host/workspace.d.ts +16 -0
- package/lib/types/merge/completion.d.ts +6 -0
- package/lib/types/merge/diagnostics.d.ts +6 -0
- package/lib/types/merge/hover.d.ts +5 -0
- package/lib/types/protocol/capabilities.d.ts +3 -0
- package/lib/types/protocol/initialize.d.ts +3 -0
- package/lib/types/protocol/normalize-completion.d.ts +3 -0
- package/lib/types/protocol/normalize-diagnostic.d.ts +3 -0
- package/lib/types/protocol/normalize-hover.d.ts +6 -0
- package/lib/types/protocol/normalize-location.d.ts +7 -0
- package/lib/types/protocol/types.d.ts +57 -0
- package/lib/types/provider.d.ts +35 -0
- package/lib/types/routing/classifier.d.ts +8 -0
- package/lib/types/routing/language-id.d.ts +5 -0
- package/lib/types/routing/route-table.d.ts +13 -0
- package/lib/types/routing/tailwind-candidate.d.ts +1 -0
- package/lib/types/runtime/cancellation.d.ts +4 -0
- package/lib/types/runtime/connection.d.ts +22 -0
- package/lib/types/runtime/framing.d.ts +14 -0
- package/lib/types/runtime/process-lifecycle.d.ts +21 -0
- package/lib/types/runtime/server-pool.d.ts +24 -0
- package/lib/types/runtime/server-session.d.ts +32 -0
- package/lib/types/runtime/webstack-runtime.d.ts +28 -0
- package/lib/types/servers/html.d.ts +2 -0
- package/lib/types/servers/launch.d.ts +5 -0
- package/lib/types/servers/package-bin.d.ts +2 -0
- package/lib/types/servers/phpantom.d.ts +2 -0
- package/lib/types/servers/svelte.d.ts +2 -0
- package/lib/types/servers/tailwind.d.ts +2 -0
- package/lib/types/servers/types.d.ts +26 -0
- package/lib/types/servers/typescript.d.ts +2 -0
- package/lib/types/service.d.ts +83 -0
- package/package.json +138 -0
package/lib/provider.js
ADDED
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
import { WebstackLspError } from "./errors-DtyWAUi0.js";
|
|
2
|
+
import { SERVER_IDS, createWebstackLspService, initializationOptions, resolveConfig, resolveLaunch, serverConfiguration } from "./launch-ByUvgkVv.js";
|
|
3
|
+
import { mergeCompletions, mergeDiagnostics } from "./completion-B7NPVCEN.js";
|
|
4
|
+
import { LspProviderId } from "@deepseek-ai/dsh-lsp";
|
|
5
|
+
import { TextDecoder } from "node:util";
|
|
6
|
+
|
|
7
|
+
//#region src/runtime/cancellation.ts
|
|
8
|
+
function abortError() {
|
|
9
|
+
const error = /* @__PURE__ */ new Error("The operation was aborted");
|
|
10
|
+
error.name = "AbortError";
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
function throwIfAborted(signal) {
|
|
14
|
+
if (signal?.aborted) throw abortError();
|
|
15
|
+
}
|
|
16
|
+
function onAbort(signal, callback) {
|
|
17
|
+
if (!signal) return () => {};
|
|
18
|
+
if (signal.aborted) {
|
|
19
|
+
callback();
|
|
20
|
+
return () => {};
|
|
21
|
+
}
|
|
22
|
+
signal.addEventListener("abort", callback, { once: true });
|
|
23
|
+
return () => signal.removeEventListener("abort", callback);
|
|
24
|
+
}
|
|
25
|
+
async function withAbort(promise, signal) {
|
|
26
|
+
throwIfAborted(signal);
|
|
27
|
+
if (!signal) return promise;
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
const remove = onAbort(signal, () => reject(abortError()));
|
|
30
|
+
promise.then((value) => {
|
|
31
|
+
remove();
|
|
32
|
+
resolve(value);
|
|
33
|
+
}, (error) => {
|
|
34
|
+
remove();
|
|
35
|
+
reject(error);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
//#region src/host/dsh.ts
|
|
42
|
+
async function readSource(fs, workspaceRoot, filePath, maxBytes, signal) {
|
|
43
|
+
if (!workspaceRoot?.trim() || !filePath?.trim()) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "workspaceRoot and filePath are required");
|
|
44
|
+
throwIfAborted(signal);
|
|
45
|
+
const options = signal ? { signal } : {};
|
|
46
|
+
const root = await fs.resolve(workspaceRoot, options);
|
|
47
|
+
if ((await fs.stat(root, signal))?.type !== "directory") throw new Error("workspace root is not a directory");
|
|
48
|
+
const workspace = fs.processPath(root);
|
|
49
|
+
const target = await fs.resolve(filePath, {
|
|
50
|
+
cwd: workspace,
|
|
51
|
+
...options
|
|
52
|
+
});
|
|
53
|
+
if (!fs.contains(root, target)) throw new Error("source is outside workspace");
|
|
54
|
+
if ((await fs.stat(target, signal))?.type !== "file") throw new Error("source is not a regular file");
|
|
55
|
+
let text = "", bytes = 0;
|
|
56
|
+
for await (const chunk of await fs.streamText(target, signal)) {
|
|
57
|
+
throwIfAborted(signal);
|
|
58
|
+
bytes += Buffer.byteLength(chunk);
|
|
59
|
+
if (bytes > maxBytes) throw new Error("source exceeds document limit");
|
|
60
|
+
text += chunk;
|
|
61
|
+
}
|
|
62
|
+
throwIfAborted(signal);
|
|
63
|
+
return {
|
|
64
|
+
workspace,
|
|
65
|
+
workspaceUri: fs.fileUrl(root),
|
|
66
|
+
key: root.targetKey,
|
|
67
|
+
uri: fs.fileUrl(target),
|
|
68
|
+
text
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
//#endregion
|
|
73
|
+
//#region src/routing/classifier.ts
|
|
74
|
+
const CLASSIFIERS = [
|
|
75
|
+
{
|
|
76
|
+
suffix: ".blade.php",
|
|
77
|
+
kind: "blade"
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
suffix: ".typescriptreact",
|
|
81
|
+
kind: "typescriptreact"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
suffix: ".javascriptreact",
|
|
85
|
+
kind: "javascriptreact"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
suffix: ".tsx",
|
|
89
|
+
kind: "typescriptreact"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
suffix: ".jsx",
|
|
93
|
+
kind: "javascriptreact"
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
suffix: ".mts",
|
|
97
|
+
kind: "typescript"
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
suffix: ".cts",
|
|
101
|
+
kind: "typescript"
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
suffix: ".mjs",
|
|
105
|
+
kind: "javascript"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
suffix: ".cjs",
|
|
109
|
+
kind: "javascript"
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
suffix: ".svelte",
|
|
113
|
+
kind: "svelte"
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
suffix: ".html",
|
|
117
|
+
kind: "html"
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
suffix: ".htm",
|
|
121
|
+
kind: "html"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
suffix: ".php",
|
|
125
|
+
kind: "php"
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
suffix: ".ts",
|
|
129
|
+
kind: "typescript"
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
suffix: ".js",
|
|
133
|
+
kind: "javascript"
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
suffix: ".css",
|
|
137
|
+
kind: "css"
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
suffix: ".pcss",
|
|
141
|
+
kind: "css"
|
|
142
|
+
}
|
|
143
|
+
];
|
|
144
|
+
function classifyFile(filePath, bladeSuffixes = [".blade.php"]) {
|
|
145
|
+
const normalizedPath = filePath.replaceAll("\\", "/");
|
|
146
|
+
const basename = normalizedPath.slice(normalizedPath.lastIndexOf("/") + 1).toLowerCase();
|
|
147
|
+
if (!basename || basename === "." || basename === ".." || basename.startsWith(".")) return void 0;
|
|
148
|
+
const customBlade = bladeSuffixes.map((suffix) => suffix.toLowerCase()).sort((left, right) => right.length - left.length);
|
|
149
|
+
const match = [...customBlade.map((suffix) => ({
|
|
150
|
+
suffix,
|
|
151
|
+
kind: "blade"
|
|
152
|
+
})), ...CLASSIFIERS.filter((item) => !customBlade.includes(item.suffix))].find((item) => basename.endsWith(item.suffix));
|
|
153
|
+
if (!match) return void 0;
|
|
154
|
+
return {
|
|
155
|
+
path: normalizedPath,
|
|
156
|
+
basename,
|
|
157
|
+
suffix: match.suffix,
|
|
158
|
+
kind: match.kind
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
//#endregion
|
|
163
|
+
//#region src/routing/route-table.ts
|
|
164
|
+
const ROUTES = {
|
|
165
|
+
blade: {
|
|
166
|
+
primary: {
|
|
167
|
+
server: "phpantom",
|
|
168
|
+
languageId: "blade"
|
|
169
|
+
},
|
|
170
|
+
auxiliary: {
|
|
171
|
+
server: "tailwind",
|
|
172
|
+
languageId: "html"
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
php: { primary: {
|
|
176
|
+
server: "phpantom",
|
|
177
|
+
languageId: "php"
|
|
178
|
+
} },
|
|
179
|
+
typescript: {
|
|
180
|
+
primary: {
|
|
181
|
+
server: "typescript",
|
|
182
|
+
languageId: "typescript"
|
|
183
|
+
},
|
|
184
|
+
auxiliary: {
|
|
185
|
+
server: "tailwind",
|
|
186
|
+
languageId: "typescript"
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
typescriptreact: {
|
|
190
|
+
primary: {
|
|
191
|
+
server: "typescript",
|
|
192
|
+
languageId: "typescriptreact"
|
|
193
|
+
},
|
|
194
|
+
auxiliary: {
|
|
195
|
+
server: "tailwind",
|
|
196
|
+
languageId: "typescriptreact"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
javascript: {
|
|
200
|
+
primary: {
|
|
201
|
+
server: "typescript",
|
|
202
|
+
languageId: "javascript"
|
|
203
|
+
},
|
|
204
|
+
auxiliary: {
|
|
205
|
+
server: "tailwind",
|
|
206
|
+
languageId: "javascript"
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
javascriptreact: {
|
|
210
|
+
primary: {
|
|
211
|
+
server: "typescript",
|
|
212
|
+
languageId: "javascriptreact"
|
|
213
|
+
},
|
|
214
|
+
auxiliary: {
|
|
215
|
+
server: "tailwind",
|
|
216
|
+
languageId: "javascriptreact"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
svelte: {
|
|
220
|
+
primary: {
|
|
221
|
+
server: "svelte",
|
|
222
|
+
languageId: "svelte"
|
|
223
|
+
},
|
|
224
|
+
auxiliary: {
|
|
225
|
+
server: "tailwind",
|
|
226
|
+
languageId: "svelte"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
html: {
|
|
230
|
+
primary: {
|
|
231
|
+
server: "html",
|
|
232
|
+
languageId: "html"
|
|
233
|
+
},
|
|
234
|
+
auxiliary: {
|
|
235
|
+
server: "tailwind",
|
|
236
|
+
languageId: "html"
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
css: {
|
|
240
|
+
primary: {
|
|
241
|
+
server: "css",
|
|
242
|
+
languageId: "css"
|
|
243
|
+
},
|
|
244
|
+
auxiliary: {
|
|
245
|
+
server: "tailwind",
|
|
246
|
+
languageId: "css"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
function routeFor(file) {
|
|
251
|
+
const route = ROUTES[file.kind];
|
|
252
|
+
return {
|
|
253
|
+
primary: { ...route.primary },
|
|
254
|
+
...route.auxiliary ? { auxiliary: { ...route.auxiliary } } : {}
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//#endregion
|
|
259
|
+
//#region src/routing/tailwind-candidate.ts
|
|
260
|
+
const DEFAULT_PATTERNS = [
|
|
261
|
+
/\bclass\s*=\s*["'][^"']*$/i,
|
|
262
|
+
/\bclassName\s*=\s*["'][^"']*$/i,
|
|
263
|
+
/\bclass:list\s*=\s*["'][^"']*$/i,
|
|
264
|
+
/@apply\s+[\w-]*$/i,
|
|
265
|
+
/\b(?:clsx|cva)\s*\([^)]*["'][^"']*$/i
|
|
266
|
+
];
|
|
267
|
+
function hasTailwindCandidate(source, line, classRegex = [], character, classAttributes = [
|
|
268
|
+
"class",
|
|
269
|
+
"className",
|
|
270
|
+
"class:list"
|
|
271
|
+
]) {
|
|
272
|
+
const lines = source.split(/\r?\n/);
|
|
273
|
+
const context = [...lines.slice(Math.max(0, line - 2), line), (lines[line] ?? "").slice(0, character)].join("\n");
|
|
274
|
+
if (classAttributes.some((attribute) => {
|
|
275
|
+
const index = context.lastIndexOf(attribute + "=");
|
|
276
|
+
return index >= 0 && /^[\"'][^\"']*$/.test(context.slice(index + attribute.length + 1).trimStart());
|
|
277
|
+
})) return true;
|
|
278
|
+
const configured = classRegex.flatMap((item) => Array.isArray(item) ? item : [item]).map((pattern) => new RegExp(pattern));
|
|
279
|
+
return [...DEFAULT_PATTERNS, ...configured].some((pattern) => pattern.test(context));
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
//#endregion
|
|
283
|
+
//#region src/protocol/initialize.ts
|
|
284
|
+
async function initializeServer(connection, params, signal) {
|
|
285
|
+
throwIfAborted(signal);
|
|
286
|
+
const value = await connection.request("initialize", params, signal);
|
|
287
|
+
if (!isInitializeResult(value)) throw new Error("malformed initialize response");
|
|
288
|
+
await connection.notify("initialized", {});
|
|
289
|
+
return value;
|
|
290
|
+
}
|
|
291
|
+
function isInitializeResult(value) {
|
|
292
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.capabilities === "object" && value.capabilities !== null;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/runtime/framing.ts
|
|
297
|
+
var JsonRpcFrameError = class extends Error {
|
|
298
|
+
code = "LSP_MALFORMED_RESPONSE";
|
|
299
|
+
constructor(message) {
|
|
300
|
+
super(message);
|
|
301
|
+
this.name = "JsonRpcFrameError";
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
var ContentLengthDecoder = class {
|
|
305
|
+
buffer = Buffer.alloc(0);
|
|
306
|
+
decoder = new TextDecoder("utf-8", { fatal: true });
|
|
307
|
+
constructor(maxMessageBytes) {
|
|
308
|
+
this.maxMessageBytes = maxMessageBytes;
|
|
309
|
+
if (!Number.isSafeInteger(maxMessageBytes) || maxMessageBytes <= 0) throw new RangeError("maxMessageBytes must be positive");
|
|
310
|
+
}
|
|
311
|
+
feed(chunk) {
|
|
312
|
+
this.buffer = Buffer.concat([this.buffer, Buffer.from(chunk)]);
|
|
313
|
+
const messages = [];
|
|
314
|
+
while (true) {
|
|
315
|
+
const headerEnd = this.buffer.indexOf(Buffer.from("\r\n\r\n"));
|
|
316
|
+
if (headerEnd < 0) {
|
|
317
|
+
if (this.buffer.length > 8192) throw new JsonRpcFrameError("header exceeds limit");
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
321
|
+
const length = this.parseLength(header);
|
|
322
|
+
const bodyStart = headerEnd + 4;
|
|
323
|
+
if (length > this.maxMessageBytes) throw new JsonRpcFrameError("message exceeds configured limit");
|
|
324
|
+
if (this.buffer.length < bodyStart + length) break;
|
|
325
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length);
|
|
326
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
327
|
+
let text;
|
|
328
|
+
try {
|
|
329
|
+
text = this.decoder.decode(body);
|
|
330
|
+
} catch {
|
|
331
|
+
throw new JsonRpcFrameError("message is not valid UTF-8");
|
|
332
|
+
}
|
|
333
|
+
try {
|
|
334
|
+
messages.push(JSON.parse(text));
|
|
335
|
+
} catch {
|
|
336
|
+
throw new JsonRpcFrameError("message body is not valid JSON");
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return messages;
|
|
340
|
+
}
|
|
341
|
+
end() {
|
|
342
|
+
if (this.buffer.length > 0) throw new JsonRpcFrameError("incomplete JSON-RPC frame");
|
|
343
|
+
}
|
|
344
|
+
parseLength(header) {
|
|
345
|
+
const lines = header.split("\r\n");
|
|
346
|
+
let length;
|
|
347
|
+
for (const line of lines) {
|
|
348
|
+
const separator = line.indexOf(":");
|
|
349
|
+
if (separator < 1) throw new JsonRpcFrameError("malformed header");
|
|
350
|
+
const key = line.slice(0, separator).toLowerCase();
|
|
351
|
+
const value = line.slice(separator + 1).trim();
|
|
352
|
+
if (key === "content-length") {
|
|
353
|
+
if (length !== void 0 || !/^\d+$/.test(value)) throw new JsonRpcFrameError("invalid Content-Length");
|
|
354
|
+
length = Number(value);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (length === void 0 || !Number.isSafeInteger(length)) throw new JsonRpcFrameError("Content-Length is required");
|
|
358
|
+
return length;
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
function encodeJsonRpc(message) {
|
|
362
|
+
const body = Buffer.from(JSON.stringify(message), "utf8");
|
|
363
|
+
return Buffer.concat([Buffer.from(`Content-Length: ${body.length}\r\n\r\n`, "ascii"), body]);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/runtime/connection.ts
|
|
368
|
+
var JsonRpcConnection = class {
|
|
369
|
+
decoder;
|
|
370
|
+
pending = /* @__PURE__ */ new Map();
|
|
371
|
+
notificationListeners = /* @__PURE__ */ new Set();
|
|
372
|
+
nextId = 1;
|
|
373
|
+
closed = false;
|
|
374
|
+
requestHandler;
|
|
375
|
+
constructor(input, output, options) {
|
|
376
|
+
this.input = input;
|
|
377
|
+
this.output = output;
|
|
378
|
+
this.decoder = new ContentLengthDecoder(options.maxMessageBytes);
|
|
379
|
+
this.requestHandler = options.onServerRequest ?? (() => null);
|
|
380
|
+
if (options.onNotification) this.notificationListeners.add(options.onNotification);
|
|
381
|
+
input.on("data", (chunk) => this.receive(chunk));
|
|
382
|
+
input.once("end", () => this.close(/* @__PURE__ */ new Error("LSP process closed stdout")));
|
|
383
|
+
input.once("error", (error) => this.close(error));
|
|
384
|
+
output.on("error", (error) => this.close(error));
|
|
385
|
+
}
|
|
386
|
+
get isOpen() {
|
|
387
|
+
return !this.closed;
|
|
388
|
+
}
|
|
389
|
+
onNotification(listener) {
|
|
390
|
+
this.notificationListeners.add(listener);
|
|
391
|
+
return () => this.notificationListeners.delete(listener);
|
|
392
|
+
}
|
|
393
|
+
setRequestHandler(handler) {
|
|
394
|
+
this.requestHandler = handler;
|
|
395
|
+
}
|
|
396
|
+
async request(method, params, signal) {
|
|
397
|
+
throwIfAborted(signal);
|
|
398
|
+
if (this.closed) throw new Error("connection is closed");
|
|
399
|
+
const id = this.nextId++;
|
|
400
|
+
const promise = new Promise((resolve, reject) => this.pending.set(id, {
|
|
401
|
+
resolve,
|
|
402
|
+
reject,
|
|
403
|
+
method
|
|
404
|
+
}));
|
|
405
|
+
promise.catch(() => {});
|
|
406
|
+
const remove = onAbort(signal, () => {
|
|
407
|
+
this.notify("$/cancelRequest", { id }).catch(() => {});
|
|
408
|
+
this.pending.get(id)?.reject(abortError());
|
|
409
|
+
this.pending.delete(id);
|
|
410
|
+
});
|
|
411
|
+
try {
|
|
412
|
+
this.write({
|
|
413
|
+
jsonrpc: "2.0",
|
|
414
|
+
id,
|
|
415
|
+
method,
|
|
416
|
+
...params === void 0 ? {} : { params }
|
|
417
|
+
}).catch((error) => this.close(error));
|
|
418
|
+
return await promise;
|
|
419
|
+
} finally {
|
|
420
|
+
remove();
|
|
421
|
+
this.pending.delete(id);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
async notify(method, params) {
|
|
425
|
+
if (this.closed) throw new Error("connection is closed");
|
|
426
|
+
await this.write({
|
|
427
|
+
jsonrpc: "2.0",
|
|
428
|
+
method,
|
|
429
|
+
...params === void 0 ? {} : { params }
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
close(reason = /* @__PURE__ */ new Error("connection closed")) {
|
|
433
|
+
if (this.closed) return;
|
|
434
|
+
this.closed = true;
|
|
435
|
+
for (const pending of this.pending.values()) pending.reject(reason);
|
|
436
|
+
this.pending.clear();
|
|
437
|
+
}
|
|
438
|
+
receive(chunk) {
|
|
439
|
+
if (this.closed) return;
|
|
440
|
+
let messages;
|
|
441
|
+
try {
|
|
442
|
+
messages = this.decoder.feed(chunk);
|
|
443
|
+
} catch (error) {
|
|
444
|
+
this.close(error instanceof Error ? error : new JsonRpcFrameError("invalid frame"));
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
for (const message of messages) this.dispatch(message);
|
|
448
|
+
}
|
|
449
|
+
dispatch(value) {
|
|
450
|
+
if (!isRecord$4(value) || value.jsonrpc !== "2.0") return this.close(new JsonRpcFrameError("invalid JSON-RPC message"));
|
|
451
|
+
if ("method" in value && typeof value.method === "string") {
|
|
452
|
+
if ("id" in value) {
|
|
453
|
+
const request = value;
|
|
454
|
+
Promise.resolve().then(() => this.requestHandler(request.method, request.params)).then((result) => this.write({
|
|
455
|
+
jsonrpc: "2.0",
|
|
456
|
+
id: request.id,
|
|
457
|
+
result
|
|
458
|
+
}), (error) => this.write({
|
|
459
|
+
jsonrpc: "2.0",
|
|
460
|
+
id: request.id,
|
|
461
|
+
error: {
|
|
462
|
+
code: -32603,
|
|
463
|
+
message: error instanceof Error ? error.message : "server request failed"
|
|
464
|
+
}
|
|
465
|
+
})).catch(() => {});
|
|
466
|
+
} else for (const listener of this.notificationListeners) listener(value);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
if ("id" in value) {
|
|
470
|
+
const response = value;
|
|
471
|
+
const pending = this.pending.get(response.id);
|
|
472
|
+
if (!pending) return;
|
|
473
|
+
if (response.error) pending.reject(/* @__PURE__ */ new Error(`${response.error.code}: ${response.error.message}`));
|
|
474
|
+
else pending.resolve(response.result);
|
|
475
|
+
this.pending.delete(response.id);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
write(message) {
|
|
479
|
+
return new Promise((resolve, reject) => {
|
|
480
|
+
this.output.write(encodeJsonRpc(message), (error) => error ? reject(error) : resolve());
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
};
|
|
484
|
+
function isRecord$4(value) {
|
|
485
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
//#endregion
|
|
489
|
+
//#region src/runtime/server-session.ts
|
|
490
|
+
var ServerSession = class {
|
|
491
|
+
state = "NEW";
|
|
492
|
+
capabilities = {};
|
|
493
|
+
child;
|
|
494
|
+
connection;
|
|
495
|
+
stopping;
|
|
496
|
+
lifetime = new AbortController();
|
|
497
|
+
constructor(options) {
|
|
498
|
+
this.options = options;
|
|
499
|
+
}
|
|
500
|
+
async start(signal) {
|
|
501
|
+
throwIfAborted(signal);
|
|
502
|
+
this.state = "STARTING";
|
|
503
|
+
try {
|
|
504
|
+
const child = this.options.spawn();
|
|
505
|
+
this.child = child;
|
|
506
|
+
child.done.catch(() => {});
|
|
507
|
+
if (!child.stdout || !child.stdin) throw new Error("server stdio is unavailable");
|
|
508
|
+
const connection = new JsonRpcConnection(child.stdout, child.stdin, {
|
|
509
|
+
maxMessageBytes: this.options.maxMessageBytes,
|
|
510
|
+
onServerRequest: (method, params) => this.answer(method, params)
|
|
511
|
+
});
|
|
512
|
+
this.connection = connection;
|
|
513
|
+
child.done.then(() => connection.close(/* @__PURE__ */ new Error("LSP process exited")), (error) => connection.close(error instanceof Error ? error : /* @__PURE__ */ new Error("LSP spawn failed"))).then(() => {
|
|
514
|
+
if (this.state !== "STOPPING" && this.state !== "STOPPED") this.state = "FAILED";
|
|
515
|
+
});
|
|
516
|
+
const result = await initializeServer(connection, this.options.initialize, this.signal(signal));
|
|
517
|
+
if (result.capabilities.positionEncoding && result.capabilities.positionEncoding !== "utf-16") throw new Error("server requires unsupported position encoding");
|
|
518
|
+
const sync = result.capabilities.textDocumentSync;
|
|
519
|
+
if (sync === void 0 || sync === 0 || typeof sync === "object" && sync !== null && !sync.openClose) throw new Error("server does not support document open/close");
|
|
520
|
+
this.capabilities = result.capabilities;
|
|
521
|
+
await connection.notify("workspace/didChangeConfiguration", { settings: this.options.configuration });
|
|
522
|
+
throwIfAborted(this.signal(signal));
|
|
523
|
+
this.state = "READY";
|
|
524
|
+
return result;
|
|
525
|
+
} catch (error) {
|
|
526
|
+
this.state = "FAILED";
|
|
527
|
+
await this.stop();
|
|
528
|
+
throw error;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
get usable() {
|
|
532
|
+
return this.state === "READY" && !!this.connection?.isOpen;
|
|
533
|
+
}
|
|
534
|
+
isUsable() {
|
|
535
|
+
return this.usable;
|
|
536
|
+
}
|
|
537
|
+
async run(task, signal) {
|
|
538
|
+
if (!this.usable || !this.connection) throw new Error("session is not ready");
|
|
539
|
+
try {
|
|
540
|
+
return await withAbort(task(this.connection), this.signal(signal));
|
|
541
|
+
} catch (error) {
|
|
542
|
+
if (signal?.aborted || !this.connection.isOpen) await this.stop();
|
|
543
|
+
throw error;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
stop() {
|
|
547
|
+
return this.stopping ??= this.teardown();
|
|
548
|
+
}
|
|
549
|
+
async teardown() {
|
|
550
|
+
this.state = "STOPPING";
|
|
551
|
+
this.lifetime.abort();
|
|
552
|
+
const child = this.child, connection = this.connection;
|
|
553
|
+
const timeout = AbortSignal.timeout(this.options.shutdown.shutdownMs);
|
|
554
|
+
if (connection?.isOpen) try {
|
|
555
|
+
await connection.request("shutdown", null, timeout);
|
|
556
|
+
await connection.notify("exit");
|
|
557
|
+
} catch {}
|
|
558
|
+
connection?.close(/* @__PURE__ */ new Error("session stopped"));
|
|
559
|
+
if (child) {
|
|
560
|
+
child.terminate();
|
|
561
|
+
if (!await child.waitForExit(AbortSignal.timeout(this.options.shutdown.killGraceMs + 1e3))) throw new Error("language server process tree did not exit within the cleanup deadline");
|
|
562
|
+
}
|
|
563
|
+
this.state = "STOPPED";
|
|
564
|
+
}
|
|
565
|
+
signal(signal) {
|
|
566
|
+
return signal ? AbortSignal.any([signal, this.lifetime.signal]) : this.lifetime.signal;
|
|
567
|
+
}
|
|
568
|
+
answer(method, params) {
|
|
569
|
+
if (method === "workspace/applyEdit") return {
|
|
570
|
+
applied: false,
|
|
571
|
+
failureReason: "read-only provider"
|
|
572
|
+
};
|
|
573
|
+
if (method === "workspace/workspaceFolders") return this.options.initialize.workspaceFolders ?? [];
|
|
574
|
+
if (method === "workspace/configuration") return (params?.items ?? []).map((item) => {
|
|
575
|
+
let value = this.options.configuration;
|
|
576
|
+
for (const key of item.section?.split(".") ?? []) value = typeof value === "object" && value !== null ? value[key] : void 0;
|
|
577
|
+
return value ?? null;
|
|
578
|
+
});
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
};
|
|
582
|
+
|
|
583
|
+
//#endregion
|
|
584
|
+
//#region src/runtime/server-pool.ts
|
|
585
|
+
var ServerPool = class {
|
|
586
|
+
sessions = /* @__PURE__ */ new Map();
|
|
587
|
+
creation = /* @__PURE__ */ new Map();
|
|
588
|
+
queues = /* @__PURE__ */ new Map();
|
|
589
|
+
disposing = false;
|
|
590
|
+
constructor(factory) {
|
|
591
|
+
this.factory = factory;
|
|
592
|
+
}
|
|
593
|
+
async getOrCreate(serverId, workspace, signal) {
|
|
594
|
+
throwIfAborted(signal);
|
|
595
|
+
if (this.disposing) throw new Error("server pool is disposing");
|
|
596
|
+
const key = keyOf(serverId, workspace);
|
|
597
|
+
const existing = this.sessions.get(key);
|
|
598
|
+
if (existing?.isUsable()) return existing;
|
|
599
|
+
const pending = this.creation.get(key);
|
|
600
|
+
if (pending) return withAbort(pending, signal);
|
|
601
|
+
const creation = this.create(key, serverId, workspace, existing, signal);
|
|
602
|
+
this.creation.set(key, creation);
|
|
603
|
+
try {
|
|
604
|
+
return await creation;
|
|
605
|
+
} finally {
|
|
606
|
+
if (this.creation.get(key) === creation) this.creation.delete(key);
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
run(serverId, workspace, task, signal) {
|
|
610
|
+
return this.serialize(keyOf(serverId, workspace), async () => task(await this.getOrCreate(serverId, workspace, signal)), signal);
|
|
611
|
+
}
|
|
612
|
+
runWithRetry(serverId, workspace, task, signal) {
|
|
613
|
+
return this.serialize(keyOf(serverId, workspace), async () => {
|
|
614
|
+
for (let attempt = 0;; attempt++) {
|
|
615
|
+
const session = await this.getOrCreate(serverId, workspace, signal);
|
|
616
|
+
try {
|
|
617
|
+
return await task(session);
|
|
618
|
+
} catch (error) {
|
|
619
|
+
throwIfAborted(signal);
|
|
620
|
+
if (attempt > 0 || session.isUsable()) throw error;
|
|
621
|
+
if (this.sessions.get(keyOf(serverId, workspace)) === session) await this.evict(serverId, workspace);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}, signal);
|
|
625
|
+
}
|
|
626
|
+
async evict(serverId, workspace) {
|
|
627
|
+
const key = keyOf(serverId, workspace);
|
|
628
|
+
const session = this.sessions.get(key);
|
|
629
|
+
this.sessions.delete(key);
|
|
630
|
+
if (session) await session.stop();
|
|
631
|
+
}
|
|
632
|
+
async disposeAll() {
|
|
633
|
+
this.disposing = true;
|
|
634
|
+
const results = await Promise.allSettled([...this.sessions.values()].map((session) => session.stop()));
|
|
635
|
+
this.sessions.clear();
|
|
636
|
+
await Promise.all([...this.creation.values()].map((creation) => creation.then((session) => session.stop(), () => {})));
|
|
637
|
+
this.creation.clear();
|
|
638
|
+
const failures = results.filter((result) => result.status === "rejected");
|
|
639
|
+
if (failures.length) throw new AggregateError(failures.map((result) => result.reason), "language server cleanup failed");
|
|
640
|
+
}
|
|
641
|
+
get size() {
|
|
642
|
+
return this.sessions.size;
|
|
643
|
+
}
|
|
644
|
+
async serialize(key, task, signal) {
|
|
645
|
+
const previous = this.queues.get(key) ?? Promise.resolve();
|
|
646
|
+
let release;
|
|
647
|
+
const current = new Promise((resolve) => {
|
|
648
|
+
release = resolve;
|
|
649
|
+
});
|
|
650
|
+
this.queues.set(key, current);
|
|
651
|
+
try {
|
|
652
|
+
await withAbort(previous, signal);
|
|
653
|
+
throwIfAborted(signal);
|
|
654
|
+
return await task();
|
|
655
|
+
} finally {
|
|
656
|
+
previous.then(release, release);
|
|
657
|
+
current.then(() => {
|
|
658
|
+
if (this.queues.get(key) === current) this.queues.delete(key);
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
async create(key, serverId, workspace, previous, signal) {
|
|
663
|
+
if (previous) {
|
|
664
|
+
this.sessions.delete(key);
|
|
665
|
+
await previous.stop();
|
|
666
|
+
}
|
|
667
|
+
throwIfAborted(signal);
|
|
668
|
+
const session = await this.factory(serverId, workspace, signal);
|
|
669
|
+
try {
|
|
670
|
+
await session.start(signal);
|
|
671
|
+
} catch (error) {
|
|
672
|
+
await session.stop();
|
|
673
|
+
throw error;
|
|
674
|
+
}
|
|
675
|
+
if (this.disposing) {
|
|
676
|
+
await session.stop();
|
|
677
|
+
throw new Error("server pool is disposing");
|
|
678
|
+
}
|
|
679
|
+
this.sessions.set(key, session);
|
|
680
|
+
return session;
|
|
681
|
+
}
|
|
682
|
+
};
|
|
683
|
+
function keyOf(serverId, workspace) {
|
|
684
|
+
return `${serverId}\u0000${workspace}`;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
//#endregion
|
|
688
|
+
//#region src/protocol/capabilities.ts
|
|
689
|
+
function supportsOperation(capabilities, operation) {
|
|
690
|
+
if (operation === "goToDefinition") return capabilities.definitionProvider === true || isProvider(capabilities.definitionProvider);
|
|
691
|
+
if (operation === "findReferences") return capabilities.referencesProvider === true || isProvider(capabilities.referencesProvider);
|
|
692
|
+
if (operation === "goToImplementation") return capabilities.implementationProvider === true || isProvider(capabilities.implementationProvider);
|
|
693
|
+
return capabilities.hoverProvider === true || isProvider(capabilities.hoverProvider);
|
|
694
|
+
}
|
|
695
|
+
function isProvider(value) {
|
|
696
|
+
return typeof value === "object" && value !== null || value === true;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/protocol/normalize-hover.ts
|
|
701
|
+
function normalizeHover(value) {
|
|
702
|
+
if (value === null || value === void 0) return null;
|
|
703
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
704
|
+
const record = value;
|
|
705
|
+
const contents = normalizeContents(record.contents);
|
|
706
|
+
if (contents === void 0) return void 0;
|
|
707
|
+
return {
|
|
708
|
+
contents,
|
|
709
|
+
...isRange$2(record.range) ? { range: record.range } : {}
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
function normalizeContents(value) {
|
|
713
|
+
if (typeof value === "string") return value;
|
|
714
|
+
if (Array.isArray(value)) {
|
|
715
|
+
const parts = value.map((item) => typeof item === "string" ? item : isRecord$3(item) && typeof item.value === "string" ? item.value : void 0);
|
|
716
|
+
return parts.every((item) => item !== void 0) ? parts.join("\n") : void 0;
|
|
717
|
+
}
|
|
718
|
+
if (isRecord$3(value) && typeof value.value === "string") return value.value;
|
|
719
|
+
}
|
|
720
|
+
function isRange$2(value) {
|
|
721
|
+
return isRecord$3(value) && isPosition$2(value.start) && isPosition$2(value.end);
|
|
722
|
+
}
|
|
723
|
+
function isPosition$2(value) {
|
|
724
|
+
return isRecord$3(value) && Number.isInteger(value.line) && Number.isInteger(value.character) && value.line >= 0 && value.character >= 0;
|
|
725
|
+
}
|
|
726
|
+
function isRecord$3(value) {
|
|
727
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
//#endregion
|
|
731
|
+
//#region src/protocol/normalize-location.ts
|
|
732
|
+
function normalizeLocation(value) {
|
|
733
|
+
if (isRecord$2(value) && typeof value.targetUri === "string" && isRange$1(value.targetSelectionRange)) return {
|
|
734
|
+
uri: value.targetUri,
|
|
735
|
+
range: value.targetSelectionRange
|
|
736
|
+
};
|
|
737
|
+
if (!isRecord$2(value) || typeof value.uri !== "string" || !isRange$1(value.range)) return void 0;
|
|
738
|
+
return {
|
|
739
|
+
uri: value.uri,
|
|
740
|
+
range: value.range
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
function normalizeLocations(value) {
|
|
744
|
+
return (Array.isArray(value) ? value : value === null || value === void 0 ? [] : [value]).map(normalizeLocation).filter((location) => location !== void 0);
|
|
745
|
+
}
|
|
746
|
+
function isRange$1(value) {
|
|
747
|
+
return isRecord$2(value) && isPosition$1(value.start) && isPosition$1(value.end);
|
|
748
|
+
}
|
|
749
|
+
function isPosition$1(value) {
|
|
750
|
+
return isRecord$2(value) && Number.isInteger(value.line) && Number.isInteger(value.character) && value.line >= 0 && value.character >= 0;
|
|
751
|
+
}
|
|
752
|
+
function isRecord$2(value) {
|
|
753
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/protocol/normalize-diagnostic.ts
|
|
758
|
+
function normalizeDiagnostic(value, server, fallbackUri) {
|
|
759
|
+
if (!isRecord$1(value) || !isRange(value.range) || typeof value.message !== "string") return void 0;
|
|
760
|
+
const uri = typeof value.uri === "string" ? value.uri : fallbackUri;
|
|
761
|
+
const severity = typeof value.severity === "number" && value.severity >= 1 && value.severity <= 4 ? value.severity : void 0;
|
|
762
|
+
const code = typeof value.code === "string" || typeof value.code === "number" ? value.code : void 0;
|
|
763
|
+
return {
|
|
764
|
+
server,
|
|
765
|
+
uri,
|
|
766
|
+
range: value.range,
|
|
767
|
+
message: value.message,
|
|
768
|
+
...severity === void 0 ? {} : { severity },
|
|
769
|
+
...code === void 0 ? {} : { code },
|
|
770
|
+
...typeof value.source === "string" ? { source: value.source } : {}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
function normalizeDiagnostics(value, server, fallbackUri) {
|
|
774
|
+
return (Array.isArray(value) ? value : []).map((item) => normalizeDiagnostic(item, server, fallbackUri)).filter((item) => item !== void 0);
|
|
775
|
+
}
|
|
776
|
+
function isRange(value) {
|
|
777
|
+
return isRecord$1(value) && isPosition(value.start) && isPosition(value.end);
|
|
778
|
+
}
|
|
779
|
+
function isPosition(value) {
|
|
780
|
+
return isRecord$1(value) && Number.isInteger(value.line) && Number.isInteger(value.character) && value.line >= 0 && value.character >= 0;
|
|
781
|
+
}
|
|
782
|
+
function isRecord$1(value) {
|
|
783
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
//#endregion
|
|
787
|
+
//#region src/protocol/normalize-completion.ts
|
|
788
|
+
function normalizeCompletionItem(value, server) {
|
|
789
|
+
if (!isRecord(value) || typeof value.label !== "string" || value.label.length === 0) return void 0;
|
|
790
|
+
return {
|
|
791
|
+
server,
|
|
792
|
+
label: value.label,
|
|
793
|
+
...typeof value.kind === "number" ? { kind: value.kind } : {},
|
|
794
|
+
...typeof value.detail === "string" ? { detail: value.detail } : {},
|
|
795
|
+
...typeof value.documentation === "string" ? { documentation: value.documentation } : {},
|
|
796
|
+
...typeof value.sortText === "string" ? { sortText: value.sortText } : {},
|
|
797
|
+
...typeof value.filterText === "string" ? { filterText: value.filterText } : {},
|
|
798
|
+
...typeof value.insertText === "string" ? { insertText: value.insertText } : {},
|
|
799
|
+
...typeof value.insertTextFormat === "number" ? { insertTextFormat: value.insertTextFormat } : {},
|
|
800
|
+
...typeof value.deprecated === "boolean" ? { deprecated: value.deprecated } : {}
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
function normalizeCompletion(value, server) {
|
|
804
|
+
return (isRecord(value) && Array.isArray(value.items) ? value.items : Array.isArray(value) ? value : []).map((item) => normalizeCompletionItem(item, server)).filter((item) => item !== void 0);
|
|
805
|
+
}
|
|
806
|
+
function isRecord(value) {
|
|
807
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
//#endregion
|
|
811
|
+
//#region src/merge/hover.ts
|
|
812
|
+
function mergeHover(primary, auxiliary) {
|
|
813
|
+
if (!primary && !auxiliary) return null;
|
|
814
|
+
if (!primary) return auxiliary ? { ...auxiliary } : null;
|
|
815
|
+
if (!auxiliary) return { ...primary };
|
|
816
|
+
const contents = [primary.contents, auxiliary.contents].filter((value, index, values) => values.indexOf(value) === index);
|
|
817
|
+
const range = sameRange(primary.range, auxiliary.range) ? primary.range : void 0;
|
|
818
|
+
return {
|
|
819
|
+
server: primary.server,
|
|
820
|
+
contents: contents.join("\n\n"),
|
|
821
|
+
...range ? { range } : {}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
function sameRange(left, right) {
|
|
825
|
+
return JSON.stringify(left) === JSON.stringify(right);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/runtime/webstack-runtime.ts
|
|
830
|
+
const methods = {
|
|
831
|
+
hover: "textDocument/hover",
|
|
832
|
+
goToDefinition: "textDocument/definition",
|
|
833
|
+
findReferences: "textDocument/references",
|
|
834
|
+
goToImplementation: "textDocument/implementation"
|
|
835
|
+
};
|
|
836
|
+
const clientCapabilities = {
|
|
837
|
+
general: { positionEncodings: ["utf-16"] },
|
|
838
|
+
workspace: {
|
|
839
|
+
configuration: true,
|
|
840
|
+
workspaceFolders: true,
|
|
841
|
+
applyEdit: false
|
|
842
|
+
},
|
|
843
|
+
textDocument: {
|
|
844
|
+
synchronization: {
|
|
845
|
+
dynamicRegistration: false,
|
|
846
|
+
didSave: false
|
|
847
|
+
},
|
|
848
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
849
|
+
completion: { completionItem: {
|
|
850
|
+
snippetSupport: true,
|
|
851
|
+
documentationFormat: ["markdown", "plaintext"]
|
|
852
|
+
} },
|
|
853
|
+
publishDiagnostics: { versionSupport: true },
|
|
854
|
+
diagnostic: {}
|
|
855
|
+
}
|
|
856
|
+
};
|
|
857
|
+
var WebstackLspRuntime = class {
|
|
858
|
+
pool;
|
|
859
|
+
workspaces = /* @__PURE__ */ new Map();
|
|
860
|
+
lifetime = new AbortController();
|
|
861
|
+
live = /* @__PURE__ */ new Map();
|
|
862
|
+
starts = /* @__PURE__ */ new Map();
|
|
863
|
+
restarts = /* @__PURE__ */ new Map();
|
|
864
|
+
constructor(config, fs, subprocess) {
|
|
865
|
+
this.config = config;
|
|
866
|
+
this.fs = fs;
|
|
867
|
+
this.subprocess = subprocess;
|
|
868
|
+
this.pool = new ServerPool(async (rawId, key, signal) => {
|
|
869
|
+
const id = rawId, source = this.workspaces.get(key);
|
|
870
|
+
const spec = await resolveLaunch(id, config, source.workspace);
|
|
871
|
+
let executable;
|
|
872
|
+
try {
|
|
873
|
+
executable = await subprocess.resolveExecutable(spec.command, spec.env, signal);
|
|
874
|
+
} catch {
|
|
875
|
+
throwIfAborted(signal);
|
|
876
|
+
throw new WebstackLspError("LSP_UNAVAILABLE", `${id} executable is unavailable; run dsh-lsp-webstack --json`);
|
|
877
|
+
}
|
|
878
|
+
const session = new ServerSession({
|
|
879
|
+
spawn: () => subprocess.spawn({
|
|
880
|
+
argv: [executable, ...spec.args],
|
|
881
|
+
cwd: source.workspace,
|
|
882
|
+
env: spec.env,
|
|
883
|
+
stdio: {
|
|
884
|
+
stdin: "pipe",
|
|
885
|
+
stdout: "pipe",
|
|
886
|
+
stderr: { maxBytes: config.limits.maxStderrBytes }
|
|
887
|
+
},
|
|
888
|
+
graceMs: config.timeouts.killGraceMs
|
|
889
|
+
}),
|
|
890
|
+
initialize: {
|
|
891
|
+
processId: null,
|
|
892
|
+
rootUri: source.workspaceUri,
|
|
893
|
+
workspaceFolders: [{
|
|
894
|
+
uri: source.workspaceUri,
|
|
895
|
+
name: "workspace"
|
|
896
|
+
}],
|
|
897
|
+
capabilities: clientCapabilities,
|
|
898
|
+
initializationOptions: initializationOptions(id, config)
|
|
899
|
+
},
|
|
900
|
+
configuration: serverConfiguration(id, config),
|
|
901
|
+
maxMessageBytes: config.limits.maxMessageBytes,
|
|
902
|
+
shutdown: {
|
|
903
|
+
shutdownMs: config.timeouts.shutdownMs,
|
|
904
|
+
killGraceMs: config.timeouts.killGraceMs
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
const slot = `${id}\u0000${key}`;
|
|
908
|
+
if (this.starts.has(slot)) this.restarts.set(id, (this.restarts.get(id) ?? 0) + 1);
|
|
909
|
+
this.starts.set(slot, (this.starts.get(slot) ?? 0) + 1);
|
|
910
|
+
for (const [old] of this.live) if (old.state === "STOPPED" || old.state === "FAILED") this.live.delete(old);
|
|
911
|
+
this.live.set(session, id);
|
|
912
|
+
return session;
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
async navigation(request, signal) {
|
|
916
|
+
const deadline = this.signal(request.operation === "hover" ? this.config.timeouts.primaryHoverMs : this.config.timeouts.navigationMs, signal);
|
|
917
|
+
const [source, route] = await this.prepare(request, deadline);
|
|
918
|
+
this.validatePosition(source.text, request.position);
|
|
919
|
+
const result = await this.document(route.primary.server, source, route.primary.languageId, deadline, async (session, connection) => {
|
|
920
|
+
if (!supportsOperation(session.capabilities, request.operation)) throw new WebstackLspError("LSP_UNSUPPORTED_OPERATION", `${route.primary.server} does not support ${request.operation}`);
|
|
921
|
+
return connection.request(methods[request.operation], {
|
|
922
|
+
textDocument: { uri: source.uri },
|
|
923
|
+
position: request.position,
|
|
924
|
+
...request.operation === "findReferences" ? { context: { includeDeclaration: true } } : {}
|
|
925
|
+
}, deadline);
|
|
926
|
+
});
|
|
927
|
+
if (request.operation !== "hover") return {
|
|
928
|
+
kind: "locations",
|
|
929
|
+
locations: normalizeLocations(result),
|
|
930
|
+
resolvedWorkspaceUri: source.workspaceUri
|
|
931
|
+
};
|
|
932
|
+
const primary = normalizeHover(result);
|
|
933
|
+
if (primary === void 0) throw new WebstackLspError("LSP_MALFORMED_RESPONSE", "invalid hover result");
|
|
934
|
+
let auxiliary = null;
|
|
935
|
+
if (this.useTailwind(route, source, request.position)) {
|
|
936
|
+
const auxSignal = this.signal(this.config.timeouts.auxiliaryHoverMs, signal);
|
|
937
|
+
try {
|
|
938
|
+
const value = await this.document("tailwind", source, route.auxiliary.languageId, auxSignal, (_session, connection) => connection.request("textDocument/hover", {
|
|
939
|
+
textDocument: { uri: source.uri },
|
|
940
|
+
position: request.position
|
|
941
|
+
}, auxSignal));
|
|
942
|
+
auxiliary = normalizeHover(value) ?? null;
|
|
943
|
+
} catch {
|
|
944
|
+
throwIfAborted(signal);
|
|
945
|
+
throwIfAborted(this.lifetime.signal);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
const hover = mergeHover(primary && {
|
|
949
|
+
...primary,
|
|
950
|
+
server: route.primary.server
|
|
951
|
+
}, auxiliary && {
|
|
952
|
+
...auxiliary,
|
|
953
|
+
server: "tailwind"
|
|
954
|
+
});
|
|
955
|
+
return {
|
|
956
|
+
kind: "hover",
|
|
957
|
+
hover: hover ? {
|
|
958
|
+
contents: hover.contents,
|
|
959
|
+
...hover.range ? { range: hover.range } : {}
|
|
960
|
+
} : null
|
|
961
|
+
};
|
|
962
|
+
}
|
|
963
|
+
async completion(request, signal) {
|
|
964
|
+
const deadline = this.signal(this.config.timeouts.completionMs, signal);
|
|
965
|
+
const [source, route] = await this.prepare(request, deadline);
|
|
966
|
+
this.validatePosition(source.text, request.position);
|
|
967
|
+
const query = (id, language) => this.document(id, source, language, deadline, (session, connection) => {
|
|
968
|
+
if (!session.capabilities.completionProvider) throw new WebstackLspError("LSP_UNSUPPORTED_OPERATION", `${id} does not support completion`);
|
|
969
|
+
return connection.request("textDocument/completion", {
|
|
970
|
+
textDocument: { uri: source.uri },
|
|
971
|
+
position: request.position,
|
|
972
|
+
context: {
|
|
973
|
+
triggerKind: request.triggerKind ?? 1,
|
|
974
|
+
...request.triggerCharacter ? { triggerCharacter: request.triggerCharacter } : {}
|
|
975
|
+
}
|
|
976
|
+
}, deadline);
|
|
977
|
+
}).then((value) => {
|
|
978
|
+
const prefix = source.text.split(/\r?\n/)[request.position.line].slice(0, request.position.character).match(/[\w$:@/!\[\]-]+$/)?.[0]?.toLowerCase() ?? "";
|
|
979
|
+
return normalizeCompletion(value, id).sort((a, b) => Number((b.filterText ?? b.label).toLowerCase().startsWith(prefix)) - Number((a.filterText ?? a.label).toLowerCase().startsWith(prefix)));
|
|
980
|
+
});
|
|
981
|
+
const primary = await query(route.primary.server, route.primary.languageId);
|
|
982
|
+
let auxiliary = [];
|
|
983
|
+
if (this.useTailwind(route, source, request.position)) try {
|
|
984
|
+
auxiliary = await query("tailwind", route.auxiliary.languageId);
|
|
985
|
+
} catch {
|
|
986
|
+
throwIfAborted(signal);
|
|
987
|
+
throwIfAborted(this.lifetime.signal);
|
|
988
|
+
}
|
|
989
|
+
return mergeCompletions([primary, auxiliary], {
|
|
990
|
+
maxCompletions: Number.MAX_SAFE_INTEGER,
|
|
991
|
+
maxResultChars: Number.MAX_SAFE_INTEGER
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
async diagnostics(request, signal) {
|
|
995
|
+
const deadline = this.signal(this.config.timeouts.diagnosticsMs, signal);
|
|
996
|
+
const [source, route] = await this.prepare(request, deadline);
|
|
997
|
+
const selected = [route.primary, ...route.auxiliary && this.config.tailwind.enabled ? [route.auxiliary] : []].filter((item) => this.config.servers[item.server].enabled && (!request.servers || request.servers.includes(item.server)));
|
|
998
|
+
if (!selected.length) throw new WebstackLspError("LSP_UNAVAILABLE", "no selected diagnostic server supports this file");
|
|
999
|
+
const omittedServers = [];
|
|
1000
|
+
const groups = await Promise.all(selected.map(async (item) => {
|
|
1001
|
+
try {
|
|
1002
|
+
const value = await this.document(item.server, source, item.languageId, deadline, async (session, connection, opened) => {
|
|
1003
|
+
if (session.capabilities.diagnosticProvider) {
|
|
1004
|
+
await opened();
|
|
1005
|
+
return (await connection.request("textDocument/diagnostic", { textDocument: { uri: source.uri } }, deadline))?.items ?? [];
|
|
1006
|
+
}
|
|
1007
|
+
let remove = () => {};
|
|
1008
|
+
const published = new Promise((resolve) => {
|
|
1009
|
+
remove = connection.onNotification((message) => {
|
|
1010
|
+
const p = message.params;
|
|
1011
|
+
if (message.method === "textDocument/publishDiagnostics" && p?.uri === source.uri && (p.version === void 0 || p.version === 1)) resolve(p.diagnostics);
|
|
1012
|
+
});
|
|
1013
|
+
});
|
|
1014
|
+
try {
|
|
1015
|
+
await opened();
|
|
1016
|
+
return await withAbort(published, deadline);
|
|
1017
|
+
} finally {
|
|
1018
|
+
remove();
|
|
1019
|
+
}
|
|
1020
|
+
}, true);
|
|
1021
|
+
return normalizeDiagnostics(value, item.server, source.uri).filter((d) => !request.severity || request.severity === "all" || d.severity === (request.severity === "error" ? 1 : 2));
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
throwIfAborted(signal);
|
|
1024
|
+
throwIfAborted(this.lifetime.signal);
|
|
1025
|
+
if (item.server === route.primary.server || this.config.missingServerPolicy === "error") throw error;
|
|
1026
|
+
omittedServers.push(item.server);
|
|
1027
|
+
return [];
|
|
1028
|
+
}
|
|
1029
|
+
}));
|
|
1030
|
+
return {
|
|
1031
|
+
...mergeDiagnostics(groups, {
|
|
1032
|
+
maxDiagnostics: Number.MAX_SAFE_INTEGER,
|
|
1033
|
+
maxResultChars: Number.MAX_SAFE_INTEGER
|
|
1034
|
+
}),
|
|
1035
|
+
...omittedServers.length ? { omittedServers: omittedServers.sort() } : {}
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
async status() {
|
|
1039
|
+
return {
|
|
1040
|
+
providerId: this.config.providerId,
|
|
1041
|
+
servers: await Promise.all(SERVER_IDS.map(async (id) => {
|
|
1042
|
+
const config = this.config.servers[id];
|
|
1043
|
+
let available = false;
|
|
1044
|
+
try {
|
|
1045
|
+
const launch = await resolveLaunch(id, this.config, ".");
|
|
1046
|
+
await this.subprocess.resolveExecutable(launch.command, launch.env, this.signal(5e3));
|
|
1047
|
+
available = true;
|
|
1048
|
+
} catch {}
|
|
1049
|
+
return {
|
|
1050
|
+
id,
|
|
1051
|
+
enabled: config.enabled && (id !== "tailwind" || this.config.tailwind.enabled),
|
|
1052
|
+
available,
|
|
1053
|
+
source: config.mode,
|
|
1054
|
+
liveWorkspaces: [...this.live].filter(([s, server]) => server === id && s.isUsable()).length,
|
|
1055
|
+
restarts: this.restarts.get(id) ?? 0
|
|
1056
|
+
};
|
|
1057
|
+
}))
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
async dispose() {
|
|
1061
|
+
this.lifetime.abort();
|
|
1062
|
+
await this.pool.disposeAll();
|
|
1063
|
+
this.workspaces.clear();
|
|
1064
|
+
this.live.clear();
|
|
1065
|
+
}
|
|
1066
|
+
signal(ms, signal) {
|
|
1067
|
+
return AbortSignal.any([
|
|
1068
|
+
this.lifetime.signal,
|
|
1069
|
+
AbortSignal.timeout(ms),
|
|
1070
|
+
...signal ? [signal] : []
|
|
1071
|
+
]);
|
|
1072
|
+
}
|
|
1073
|
+
async prepare(request, signal) {
|
|
1074
|
+
throwIfAborted(signal);
|
|
1075
|
+
const file = classifyFile(request.filePath, this.config.routing.bladeSuffixes);
|
|
1076
|
+
if (!file) throw new WebstackLspError("LSP_UNAVAILABLE", "unsupported file extension");
|
|
1077
|
+
const source = await readSource(this.fs, request.workspaceRoot, request.filePath, this.config.limits.maxDocumentBytes, signal).catch((error) => {
|
|
1078
|
+
throwIfAborted(signal);
|
|
1079
|
+
throw error;
|
|
1080
|
+
});
|
|
1081
|
+
this.workspaces.set(source.key, {
|
|
1082
|
+
...source,
|
|
1083
|
+
text: ""
|
|
1084
|
+
});
|
|
1085
|
+
return [source, routeFor(file)];
|
|
1086
|
+
}
|
|
1087
|
+
validatePosition(text, position) {
|
|
1088
|
+
const line = text.split(/\r?\n/)[position.line];
|
|
1089
|
+
if (!Number.isSafeInteger(position.line) || !Number.isSafeInteger(position.character) || position.line < 0 || position.character < 0 || line === void 0 || position.character > line.length) throw new RangeError("position is outside document (zero-based UTF-16 required)");
|
|
1090
|
+
}
|
|
1091
|
+
useTailwind(route, source, position) {
|
|
1092
|
+
return !!route.auxiliary && this.config.tailwind.enabled && this.config.servers.tailwind.enabled && (route.primary.languageId === "blade" || this.config.routing.tailwindLanguages.includes(route.primary.languageId) || route.primary.languageId === "css") && (this.config.tailwind.queryStrategy === "always" || hasTailwindCandidate(source.text, position.line, this.config.tailwind.classRegex, position.character, this.config.tailwind.classAttributes));
|
|
1093
|
+
}
|
|
1094
|
+
async document(id, source, language, signal, task, manualOpen = false) {
|
|
1095
|
+
if (!this.config.servers[id].enabled) throw new WebstackLspError("LSP_UNAVAILABLE", `${id} is disabled`);
|
|
1096
|
+
return this.pool.runWithRetry(id, source.key, (session) => session.run(async (connection) => {
|
|
1097
|
+
let opened = false;
|
|
1098
|
+
const open = async () => {
|
|
1099
|
+
opened = true;
|
|
1100
|
+
await connection.notify("textDocument/didOpen", { textDocument: {
|
|
1101
|
+
uri: source.uri,
|
|
1102
|
+
languageId: language,
|
|
1103
|
+
version: 1,
|
|
1104
|
+
text: source.text
|
|
1105
|
+
} });
|
|
1106
|
+
};
|
|
1107
|
+
try {
|
|
1108
|
+
if (!manualOpen) await open();
|
|
1109
|
+
return await task(session, connection, open);
|
|
1110
|
+
} finally {
|
|
1111
|
+
if (opened && connection.isOpen) await connection.notify("textDocument/didClose", { textDocument: { uri: source.uri } }).catch(() => {});
|
|
1112
|
+
}
|
|
1113
|
+
}, signal), signal);
|
|
1114
|
+
}
|
|
1115
|
+
};
|
|
1116
|
+
|
|
1117
|
+
//#endregion
|
|
1118
|
+
//#region src/provider.ts
|
|
1119
|
+
const name = "lsp-webstack-provider";
|
|
1120
|
+
const inject = [
|
|
1121
|
+
"fs",
|
|
1122
|
+
"lsp",
|
|
1123
|
+
"subprocess"
|
|
1124
|
+
];
|
|
1125
|
+
const OUTER_EXTENSION_MAP = {
|
|
1126
|
+
".php": "php",
|
|
1127
|
+
".ts": "typescript",
|
|
1128
|
+
".tsx": "typescriptreact",
|
|
1129
|
+
".mts": "typescript",
|
|
1130
|
+
".cts": "typescript",
|
|
1131
|
+
".js": "javascript",
|
|
1132
|
+
".jsx": "javascriptreact",
|
|
1133
|
+
".mjs": "javascript",
|
|
1134
|
+
".cjs": "javascript",
|
|
1135
|
+
".svelte": "svelte",
|
|
1136
|
+
".html": "html",
|
|
1137
|
+
".htm": "html",
|
|
1138
|
+
".css": "css",
|
|
1139
|
+
".pcss": "css"
|
|
1140
|
+
};
|
|
1141
|
+
function createCompositeProvider(runtime, config = { providerId: "webstack" }) {
|
|
1142
|
+
return {
|
|
1143
|
+
id: LspProviderId(config.providerId),
|
|
1144
|
+
extensionToLanguage: { ...OUTER_EXTENSION_MAP },
|
|
1145
|
+
query: (request, signal) => runtime.navigation(request, signal)
|
|
1146
|
+
};
|
|
1147
|
+
}
|
|
1148
|
+
async function apply(ctx, config = {}) {
|
|
1149
|
+
const resolved = resolveConfig(config);
|
|
1150
|
+
const runtime = new WebstackLspRuntime(resolved, ctx.fs, ctx.subprocess);
|
|
1151
|
+
await ctx.effect(async function* () {
|
|
1152
|
+
yield () => runtime.dispose();
|
|
1153
|
+
if (resolved.missingServerPolicy === "error") {
|
|
1154
|
+
const missing = (await runtime.status()).servers.filter((server) => server.enabled && !server.available);
|
|
1155
|
+
if (missing.length) throw new Error(`unavailable language servers: ${missing.map((server) => server.id).join(", ")}`);
|
|
1156
|
+
}
|
|
1157
|
+
yield ctx.lsp.registerProvider(createCompositeProvider(runtime, resolved));
|
|
1158
|
+
yield ctx.provide("webstackLsp", createWebstackLspService(runtime));
|
|
1159
|
+
}, "webstack LSP provider");
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
//#endregion
|
|
1163
|
+
export { OUTER_EXTENSION_MAP, apply, createCompositeProvider, inject, name };
|
|
1164
|
+
//# sourceMappingURL=provider.js.map
|