@wrongstack/plug-lsp 0.1.9 → 0.2.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/dist/index.js +401 -294
- package/dist/index.js.map +1 -1
- package/dist/setup.js +19 -4
- package/dist/setup.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
2
|
import * as fs2 from 'fs/promises';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { TOKENS, atomicWrite } from '@wrongstack/core';
|
|
5
5
|
import { pathToFileURL, fileURLToPath } from 'url';
|
|
6
|
-
import * as fs3 from 'fs';
|
|
7
6
|
import { EventEmitter } from 'events';
|
|
8
|
-
|
|
9
|
-
// src/config.ts
|
|
7
|
+
import * as fs3 from 'fs';
|
|
10
8
|
|
|
11
9
|
// src/presets.ts
|
|
12
10
|
var PRESETS = {
|
|
@@ -89,8 +87,62 @@ var PRESETS = {
|
|
|
89
87
|
enabled: true
|
|
90
88
|
}
|
|
91
89
|
};
|
|
90
|
+
async function resolveServerCommand(command, cwd) {
|
|
91
|
+
const local = await findLocalBinary(cwd, command);
|
|
92
|
+
if (local) return local;
|
|
93
|
+
return await commandExistsOnPath(command) ? command : null;
|
|
94
|
+
}
|
|
95
|
+
async function findLocalBinary(cwd, command) {
|
|
96
|
+
if (path.isAbsolute(command)) return await fileExists(command) ? path.normalize(command) : null;
|
|
97
|
+
let dir = path.resolve(cwd);
|
|
98
|
+
for (; ; ) {
|
|
99
|
+
const binDir = path.join(dir, "node_modules", ".bin");
|
|
100
|
+
for (const candidate of commandCandidates(command)) {
|
|
101
|
+
const full = path.join(binDir, candidate);
|
|
102
|
+
if (await fileExists(full)) return full;
|
|
103
|
+
}
|
|
104
|
+
const parent = path.dirname(dir);
|
|
105
|
+
if (parent === dir) return null;
|
|
106
|
+
dir = parent;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function commandExistsOnPath(command) {
|
|
110
|
+
const probe = process.platform === "win32" ? "where.exe" : "sh";
|
|
111
|
+
const args = process.platform === "win32" ? [command] : ["-lc", `command -v ${shellQuote(command)}`];
|
|
112
|
+
return new Promise((resolve6) => {
|
|
113
|
+
const child = spawn(probe, args, { stdio: "ignore", windowsHide: true });
|
|
114
|
+
child.on("error", () => resolve6(false));
|
|
115
|
+
child.on("close", (code) => resolve6(code === 0));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
function commandCandidates(command) {
|
|
119
|
+
if (process.platform !== "win32") return [command];
|
|
120
|
+
const ext = path.extname(command).toLowerCase();
|
|
121
|
+
if (ext) return [command];
|
|
122
|
+
return [`${command}.cmd`, `${command}.exe`, `${command}.bat`, command, `${command}.ps1`];
|
|
123
|
+
}
|
|
124
|
+
async function fileExists(filePath) {
|
|
125
|
+
try {
|
|
126
|
+
await fs2.access(filePath);
|
|
127
|
+
return true;
|
|
128
|
+
} catch {
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function shellQuote(value) {
|
|
133
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
134
|
+
}
|
|
92
135
|
|
|
93
|
-
// src/
|
|
136
|
+
// src/auto-discover.ts
|
|
137
|
+
async function autoDiscoverServers(userServers, cwd = process.cwd()) {
|
|
138
|
+
const out = { ...userServers };
|
|
139
|
+
for (const [name, cfg] of Object.entries(PRESETS)) {
|
|
140
|
+
if (out[name]) continue;
|
|
141
|
+
const command = await resolveServerCommand(cfg.command, cwd);
|
|
142
|
+
if (command) out[name] = { ...cfg, command };
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
94
146
|
var PLUGIN_NAME = "@wrongstack/plug-lsp";
|
|
95
147
|
var DEFAULT_CONFIG = {
|
|
96
148
|
autoStart: "lazy",
|
|
@@ -200,62 +252,6 @@ function isSeverityName(value) {
|
|
|
200
252
|
function isRecord(value) {
|
|
201
253
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
202
254
|
}
|
|
203
|
-
async function resolveServerCommand(command, cwd) {
|
|
204
|
-
const local = await findLocalBinary(cwd, command);
|
|
205
|
-
if (local) return local;
|
|
206
|
-
return await commandExistsOnPath(command) ? command : null;
|
|
207
|
-
}
|
|
208
|
-
async function findLocalBinary(cwd, command) {
|
|
209
|
-
if (path.isAbsolute(command)) return await fileExists(command) ? path.normalize(command) : null;
|
|
210
|
-
let dir = path.resolve(cwd);
|
|
211
|
-
for (; ; ) {
|
|
212
|
-
const binDir = path.join(dir, "node_modules", ".bin");
|
|
213
|
-
for (const candidate of commandCandidates(command)) {
|
|
214
|
-
const full = path.join(binDir, candidate);
|
|
215
|
-
if (await fileExists(full)) return full;
|
|
216
|
-
}
|
|
217
|
-
const parent = path.dirname(dir);
|
|
218
|
-
if (parent === dir) return null;
|
|
219
|
-
dir = parent;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
async function commandExistsOnPath(command) {
|
|
223
|
-
const probe = process.platform === "win32" ? "where.exe" : "sh";
|
|
224
|
-
const args = process.platform === "win32" ? [command] : ["-lc", `command -v ${shellQuote(command)}`];
|
|
225
|
-
return new Promise((resolve6) => {
|
|
226
|
-
const child = spawn(probe, args, { stdio: "ignore", windowsHide: true });
|
|
227
|
-
child.on("error", () => resolve6(false));
|
|
228
|
-
child.on("close", (code) => resolve6(code === 0));
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
function commandCandidates(command) {
|
|
232
|
-
if (process.platform !== "win32") return [command];
|
|
233
|
-
const ext = path.extname(command).toLowerCase();
|
|
234
|
-
if (ext) return [command];
|
|
235
|
-
return [`${command}.cmd`, `${command}.exe`, `${command}.bat`, command, `${command}.ps1`];
|
|
236
|
-
}
|
|
237
|
-
async function fileExists(filePath) {
|
|
238
|
-
try {
|
|
239
|
-
await fs2.access(filePath);
|
|
240
|
-
return true;
|
|
241
|
-
} catch {
|
|
242
|
-
return false;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
function shellQuote(value) {
|
|
246
|
-
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// src/auto-discover.ts
|
|
250
|
-
async function autoDiscoverServers(userServers, cwd = process.cwd()) {
|
|
251
|
-
const out = { ...userServers };
|
|
252
|
-
for (const [name, cfg] of Object.entries(PRESETS)) {
|
|
253
|
-
if (out[name]) continue;
|
|
254
|
-
const command = await resolveServerCommand(cfg.command, cwd);
|
|
255
|
-
if (command) out[name] = { ...cfg, command };
|
|
256
|
-
}
|
|
257
|
-
return out;
|
|
258
|
-
}
|
|
259
255
|
var LANGUAGE_MAP = {
|
|
260
256
|
".ts": "typescript",
|
|
261
257
|
".tsx": "typescriptreact",
|
|
@@ -430,6 +426,11 @@ function toTextDocumentItem(doc) {
|
|
|
430
426
|
};
|
|
431
427
|
}
|
|
432
428
|
|
|
429
|
+
// src/server/lifecycle.ts
|
|
430
|
+
function nextReconnectDelay(attempt) {
|
|
431
|
+
return [1e3, 4e3, 16e3][Math.max(0, Math.min(2, attempt))] ?? 16e3;
|
|
432
|
+
}
|
|
433
|
+
|
|
433
434
|
// src/types.ts
|
|
434
435
|
var LSPError = class extends Error {
|
|
435
436
|
constructor(code, message, details) {
|
|
@@ -441,29 +442,6 @@ var LSPError = class extends Error {
|
|
|
441
442
|
code;
|
|
442
443
|
details;
|
|
443
444
|
};
|
|
444
|
-
function findWorkspaceRoot(filePath, rootPatterns, fallback) {
|
|
445
|
-
const patterns = rootPatterns?.length ? rootPatterns : [];
|
|
446
|
-
if (patterns.length === 0) return path.resolve(fallback);
|
|
447
|
-
let dir = path.dirname(path.resolve(filePath));
|
|
448
|
-
const stop = path.parse(dir).root;
|
|
449
|
-
for (; ; ) {
|
|
450
|
-
for (const pattern of patterns) {
|
|
451
|
-
if (matchesAt(dir, pattern)) return dir;
|
|
452
|
-
}
|
|
453
|
-
if (dir === stop) return path.resolve(fallback);
|
|
454
|
-
dir = path.dirname(dir);
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
function matchesAt(dir, pattern) {
|
|
458
|
-
if (!pattern.includes("*")) return fs3.existsSync(path.join(dir, pattern));
|
|
459
|
-
const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
460
|
-
const re = new RegExp(`^${escaped}$`);
|
|
461
|
-
try {
|
|
462
|
-
return fs3.readdirSync(dir).some((name) => re.test(name));
|
|
463
|
-
} catch {
|
|
464
|
-
return false;
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
445
|
function safeSpawn(cfg, cwd) {
|
|
468
446
|
const shell = process.platform === "win32" && /\.(cmd|bat)$/i.test(cfg.command);
|
|
469
447
|
return spawn(cfg.command, cfg.args ?? [], {
|
|
@@ -563,6 +541,11 @@ var Connection = class {
|
|
|
563
541
|
this.events.emit("close");
|
|
564
542
|
}
|
|
565
543
|
onData(chunk) {
|
|
544
|
+
const MAX_BUFFER = 16 * 1024 * 1024;
|
|
545
|
+
if (this.buffer.length + chunk.length > MAX_BUFFER) {
|
|
546
|
+
this.close();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
566
549
|
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
567
550
|
for (; ; ) {
|
|
568
551
|
const sep = this.buffer.indexOf("\r\n\r\n");
|
|
@@ -574,6 +557,10 @@ var Connection = class {
|
|
|
574
557
|
continue;
|
|
575
558
|
}
|
|
576
559
|
const length = Number(match[1]);
|
|
560
|
+
if (!Number.isFinite(length) || length < 0 || length > MAX_BUFFER) {
|
|
561
|
+
this.buffer = this.buffer.subarray(sep + 4);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
577
564
|
const total = sep + 4 + length;
|
|
578
565
|
if (this.buffer.length < total) return;
|
|
579
566
|
const body = this.buffer.subarray(sep + 4, total).toString("utf8");
|
|
@@ -590,7 +577,8 @@ var Connection = class {
|
|
|
590
577
|
const pending = this.pending.get(id);
|
|
591
578
|
if (!pending) return;
|
|
592
579
|
this.pending.delete(id);
|
|
593
|
-
if (msg.error)
|
|
580
|
+
if (msg.error)
|
|
581
|
+
pending.reject(new LSPError("LSP_PROTOCOL_ERROR" /* ProtocolError */, msg.error.message, msg.error));
|
|
594
582
|
else pending.resolve(msg.result);
|
|
595
583
|
return;
|
|
596
584
|
}
|
|
@@ -623,7 +611,12 @@ var CLIENT_CAPABILITIES = {
|
|
|
623
611
|
executeCommand: { dynamicRegistration: false }
|
|
624
612
|
},
|
|
625
613
|
textDocument: {
|
|
626
|
-
synchronization: {
|
|
614
|
+
synchronization: {
|
|
615
|
+
didSave: false,
|
|
616
|
+
dynamicRegistration: false,
|
|
617
|
+
willSave: false,
|
|
618
|
+
willSaveWaitUntil: false
|
|
619
|
+
},
|
|
627
620
|
diagnostic: { dynamicRegistration: false },
|
|
628
621
|
definition: { linkSupport: true },
|
|
629
622
|
references: {},
|
|
@@ -642,9 +635,19 @@ async function initializeServer(connection, serverCfg, rootPath, timeoutMs, sign
|
|
|
642
635
|
rootUri,
|
|
643
636
|
capabilities: CLIENT_CAPABILITIES,
|
|
644
637
|
initializationOptions: serverCfg.initializationOptions,
|
|
645
|
-
workspaceFolders: [
|
|
638
|
+
workspaceFolders: [
|
|
639
|
+
{
|
|
640
|
+
uri: rootUri,
|
|
641
|
+
name: rootPath.split(/[\\/]/).pop() ?? rootPath
|
|
642
|
+
}
|
|
643
|
+
]
|
|
646
644
|
};
|
|
647
|
-
const result = await connection.sendRequest(
|
|
645
|
+
const result = await connection.sendRequest(
|
|
646
|
+
"initialize",
|
|
647
|
+
params,
|
|
648
|
+
timeoutMs,
|
|
649
|
+
signal
|
|
650
|
+
);
|
|
648
651
|
connection.sendNotification("initialized", {});
|
|
649
652
|
return result.capabilities;
|
|
650
653
|
}
|
|
@@ -674,7 +677,8 @@ var LSPServer = class {
|
|
|
674
677
|
return this.stderrRing.slice(-20).join("\n");
|
|
675
678
|
}
|
|
676
679
|
async start(signal = new AbortController().signal) {
|
|
677
|
-
if (this.state === "ready" || this.state === "starting" || this.state === "initializing")
|
|
680
|
+
if (this.state === "ready" || this.state === "starting" || this.state === "initializing")
|
|
681
|
+
return;
|
|
678
682
|
if (this.config.enabled === false) {
|
|
679
683
|
this.state = "disabled";
|
|
680
684
|
return;
|
|
@@ -850,14 +854,18 @@ function startupFailure(child) {
|
|
|
850
854
|
const promise = new Promise((_, reject) => {
|
|
851
855
|
const onError = (err) => {
|
|
852
856
|
cleanup();
|
|
853
|
-
reject(
|
|
857
|
+
reject(
|
|
858
|
+
new LSPError("LSP_SERVER_FAILED" /* ServerFailed */, `LSP server failed to start: ${err.message}`, err)
|
|
859
|
+
);
|
|
854
860
|
};
|
|
855
861
|
const onExit = (code, signal) => {
|
|
856
862
|
cleanup();
|
|
857
|
-
reject(
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
863
|
+
reject(
|
|
864
|
+
new LSPError(
|
|
865
|
+
"LSP_SERVER_FAILED" /* ServerFailed */,
|
|
866
|
+
`LSP server exited during startup code=${code ?? "null"} signal=${signal ?? "null"}`
|
|
867
|
+
)
|
|
868
|
+
);
|
|
861
869
|
};
|
|
862
870
|
cleanup = () => {
|
|
863
871
|
child.off("error", onError);
|
|
@@ -868,10 +876,28 @@ function startupFailure(child) {
|
|
|
868
876
|
});
|
|
869
877
|
return { promise, cancel: cleanup };
|
|
870
878
|
}
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
879
|
+
function findWorkspaceRoot(filePath, rootPatterns, fallback) {
|
|
880
|
+
const patterns = rootPatterns?.length ? rootPatterns : [];
|
|
881
|
+
if (patterns.length === 0) return path.resolve(fallback);
|
|
882
|
+
let dir = path.dirname(path.resolve(filePath));
|
|
883
|
+
const stop = path.parse(dir).root;
|
|
884
|
+
for (; ; ) {
|
|
885
|
+
for (const pattern of patterns) {
|
|
886
|
+
if (matchesAt(dir, pattern)) return dir;
|
|
887
|
+
}
|
|
888
|
+
if (dir === stop) return path.resolve(fallback);
|
|
889
|
+
dir = path.dirname(dir);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function matchesAt(dir, pattern) {
|
|
893
|
+
if (!pattern.includes("*")) return fs3.existsSync(path.join(dir, pattern));
|
|
894
|
+
const escaped = pattern.split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join(".*");
|
|
895
|
+
const re = new RegExp(`^${escaped}$`);
|
|
896
|
+
try {
|
|
897
|
+
return fs3.readdirSync(dir).some((name) => re.test(name));
|
|
898
|
+
} catch {
|
|
899
|
+
return false;
|
|
900
|
+
}
|
|
875
901
|
}
|
|
876
902
|
|
|
877
903
|
// src/registry.ts
|
|
@@ -899,14 +925,20 @@ var LSPRegistry = class {
|
|
|
899
925
|
if (autoStart === "eager") {
|
|
900
926
|
const languages = await detectProjectLanguages(cwd);
|
|
901
927
|
await Promise.all(
|
|
902
|
-
this.list().filter((s) => s.config.languages.some((lang) => languages.has(lang))).map(
|
|
928
|
+
this.list().filter((s) => s.config.languages.some((lang) => languages.has(lang))).map(
|
|
929
|
+
(s) => s.start().catch((err) => this.ctx.log.warn(`LSP ${s.name} failed to start`, err))
|
|
930
|
+
)
|
|
903
931
|
);
|
|
904
932
|
}
|
|
905
933
|
}
|
|
906
934
|
async shutdown() {
|
|
907
935
|
for (const timer of this.reconnectTimers.values()) clearTimeout(timer);
|
|
908
936
|
this.reconnectTimers.clear();
|
|
909
|
-
await Promise.all(
|
|
937
|
+
await Promise.all(
|
|
938
|
+
this.list().map(
|
|
939
|
+
(s) => s.shutdown().catch((err) => this.ctx.log.warn(`LSP ${s.name} shutdown failed`, err))
|
|
940
|
+
)
|
|
941
|
+
);
|
|
910
942
|
}
|
|
911
943
|
async findForPath(filePath, signal) {
|
|
912
944
|
if (this.servers.size === 0) this.rebuildServers();
|
|
@@ -949,7 +981,11 @@ var LSPRegistry = class {
|
|
|
949
981
|
this.languageIndex.clear();
|
|
950
982
|
for (const [name, cfg] of Object.entries(this.cfg.servers)) {
|
|
951
983
|
if (cfg.enabled === false) continue;
|
|
952
|
-
const rootPath = findWorkspaceRoot(
|
|
984
|
+
const rootPath = findWorkspaceRoot(
|
|
985
|
+
path.join(this.cwd, "__probe__"),
|
|
986
|
+
cfg.rootPatterns,
|
|
987
|
+
this.cwd
|
|
988
|
+
);
|
|
953
989
|
const server = new LSPServer(name, cfg, {
|
|
954
990
|
cwd: this.cwd,
|
|
955
991
|
rootPath,
|
|
@@ -960,7 +996,9 @@ var LSPRegistry = class {
|
|
|
960
996
|
this.servers.set(name, server);
|
|
961
997
|
for (const language of cfg.languages) {
|
|
962
998
|
if (this.languageIndex.has(language)) {
|
|
963
|
-
this.ctx.log.warn(
|
|
999
|
+
this.ctx.log.warn(
|
|
1000
|
+
`LSP language "${language}" is claimed by multiple servers; using first`
|
|
1001
|
+
);
|
|
964
1002
|
continue;
|
|
965
1003
|
}
|
|
966
1004
|
this.languageIndex.set(language, name);
|
|
@@ -1019,6 +1057,145 @@ async function detectProjectLanguages(root) {
|
|
|
1019
1057
|
return found;
|
|
1020
1058
|
}
|
|
1021
1059
|
|
|
1060
|
+
// src/formatters/diagnostics.ts
|
|
1061
|
+
var SEVERITY = {
|
|
1062
|
+
1: "error",
|
|
1063
|
+
2: "warning",
|
|
1064
|
+
3: "info",
|
|
1065
|
+
4: "hint"
|
|
1066
|
+
};
|
|
1067
|
+
var LABEL = {
|
|
1068
|
+
error: "ERROR",
|
|
1069
|
+
warning: "WARN",
|
|
1070
|
+
info: "INFO",
|
|
1071
|
+
hint: "HINT"
|
|
1072
|
+
};
|
|
1073
|
+
function formatDiagnostics(byFile, opts) {
|
|
1074
|
+
const allowed = new Set(opts.severityFilter);
|
|
1075
|
+
const sections = [];
|
|
1076
|
+
let total = 0;
|
|
1077
|
+
let files = 0;
|
|
1078
|
+
for (const [file, diagnostics] of byFile.entries()) {
|
|
1079
|
+
const filtered = diagnostics.filter((d) => allowed.has(SEVERITY[d.severity ?? 1] ?? "error")).sort(compareDiagnostics).slice(0, opts.maxPerFile);
|
|
1080
|
+
if (filtered.length === 0) continue;
|
|
1081
|
+
files++;
|
|
1082
|
+
total += filtered.length;
|
|
1083
|
+
const lines = filtered.map((d) => formatDiagnostic(d));
|
|
1084
|
+
sections.push(
|
|
1085
|
+
`${displayPath(file, opts.cwd)} (${filtered.length}):
|
|
1086
|
+
${lines.map((l) => ` ${l}`).join("\n")}`
|
|
1087
|
+
);
|
|
1088
|
+
if (total >= opts.maxTotal) break;
|
|
1089
|
+
}
|
|
1090
|
+
if (sections.length === 0) return "No LSP diagnostics.";
|
|
1091
|
+
return `${sections.join("\n\n")}
|
|
1092
|
+
|
|
1093
|
+
Total: ${total} diagnostics in ${files} files.`;
|
|
1094
|
+
}
|
|
1095
|
+
function formatDiagnostic(d) {
|
|
1096
|
+
const sev = SEVERITY[d.severity ?? 1] ?? "error";
|
|
1097
|
+
const source = d.source ? ` ${d.source}${d.code !== void 0 ? `(${String(d.code)})` : ""}` : "";
|
|
1098
|
+
const msg = d.message.replace(/\s*\r?\n\s*/g, " | ");
|
|
1099
|
+
return `L${d.range.start.line + 1}:${d.range.start.character + 1} ${LABEL[sev]}${source}: ${msg}`;
|
|
1100
|
+
}
|
|
1101
|
+
function compareDiagnostics(a, b) {
|
|
1102
|
+
return (a.severity ?? 1) - (b.severity ?? 1) || a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// src/slash-commands/diagnostics.ts
|
|
1106
|
+
function diagnosticsCommand(registry) {
|
|
1107
|
+
return {
|
|
1108
|
+
name: "diagnostics",
|
|
1109
|
+
description: "Print buffered LSP diagnostics.",
|
|
1110
|
+
async run(_args, ctx) {
|
|
1111
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
1112
|
+
for (const server of registry.list()) {
|
|
1113
|
+
for (const [uri, diagnostics] of server.diagnostics.entries()) {
|
|
1114
|
+
byFile.set(uriToPath(uri), diagnostics);
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return {
|
|
1118
|
+
message: formatDiagnostics(byFile, {
|
|
1119
|
+
cwd: ctx.cwd,
|
|
1120
|
+
severityFilter: ["error", "warning"],
|
|
1121
|
+
maxPerFile: 10,
|
|
1122
|
+
maxTotal: 100
|
|
1123
|
+
})
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/slash-commands/list.ts
|
|
1130
|
+
function listCommand(registry) {
|
|
1131
|
+
return {
|
|
1132
|
+
name: "list",
|
|
1133
|
+
description: "List configured LSP servers.",
|
|
1134
|
+
async run() {
|
|
1135
|
+
const rows = registry.list().map((s) => {
|
|
1136
|
+
const langs = s.config.languages.join(",");
|
|
1137
|
+
return `${s.name.padEnd(18)} ${s.state.padEnd(14)} ${langs} ${s.rootPath}`;
|
|
1138
|
+
});
|
|
1139
|
+
return { message: rows.length ? rows.join("\n") : "No LSP servers configured." };
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// src/slash-commands/restart.ts
|
|
1145
|
+
function restartCommand(registry) {
|
|
1146
|
+
return {
|
|
1147
|
+
name: "restart",
|
|
1148
|
+
description: "Restart an LSP server.",
|
|
1149
|
+
async run(args) {
|
|
1150
|
+
const name = args.trim();
|
|
1151
|
+
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:restart <name>" };
|
|
1152
|
+
await registry.restart(name);
|
|
1153
|
+
return { message: `Restarted LSP server "${name}".` };
|
|
1154
|
+
}
|
|
1155
|
+
};
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/slash-commands/start.ts
|
|
1159
|
+
function startCommand(registry) {
|
|
1160
|
+
return {
|
|
1161
|
+
name: "start",
|
|
1162
|
+
description: "Start an LSP server.",
|
|
1163
|
+
async run(args) {
|
|
1164
|
+
const name = args.trim();
|
|
1165
|
+
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:start <name>" };
|
|
1166
|
+
await registry.start(name);
|
|
1167
|
+
return { message: `Started LSP server "${name}".` };
|
|
1168
|
+
}
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// src/slash-commands/stop.ts
|
|
1173
|
+
function stopCommand(registry) {
|
|
1174
|
+
return {
|
|
1175
|
+
name: "stop",
|
|
1176
|
+
description: "Stop an LSP server.",
|
|
1177
|
+
async run(args) {
|
|
1178
|
+
const name = args.trim();
|
|
1179
|
+
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:stop <name>" };
|
|
1180
|
+
await registry.stop(name);
|
|
1181
|
+
return { message: `Stopped LSP server "${name}".` };
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// src/slash-commands/index.ts
|
|
1187
|
+
function registerSlashCommands(api, registry) {
|
|
1188
|
+
const commands = [
|
|
1189
|
+
listCommand(registry),
|
|
1190
|
+
startCommand(registry),
|
|
1191
|
+
stopCommand(registry),
|
|
1192
|
+
restartCommand(registry),
|
|
1193
|
+
diagnosticsCommand(registry)
|
|
1194
|
+
];
|
|
1195
|
+
for (const command of commands) api.slashCommands.register(command);
|
|
1196
|
+
return commands.map((cmd) => cmd.name);
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1022
1199
|
// src/formatters/workspace-edit.ts
|
|
1023
1200
|
function summarizeWorkspaceEdit(edit, cwd) {
|
|
1024
1201
|
const entries = editsByPath(edit);
|
|
@@ -1095,6 +1272,25 @@ function supportsCodeAction(cap) {
|
|
|
1095
1272
|
function supportsPullDiagnostics(cap) {
|
|
1096
1273
|
return !!cap.diagnosticProvider;
|
|
1097
1274
|
}
|
|
1275
|
+
function resolveInputPath(inputPath, ctx) {
|
|
1276
|
+
return path.isAbsolute(inputPath) ? path.normalize(inputPath) : path.resolve(ctx.cwd, inputPath);
|
|
1277
|
+
}
|
|
1278
|
+
async function requireServer(registry, filePath, signal) {
|
|
1279
|
+
const server = await registry.findForPath(filePath, signal);
|
|
1280
|
+
if (!server) {
|
|
1281
|
+
throw new LSPError("LSP_SERVER_NOT_FOUND" /* ServerNotFound */, `No LSP server is configured for ${filePath}`);
|
|
1282
|
+
}
|
|
1283
|
+
return server;
|
|
1284
|
+
}
|
|
1285
|
+
async function readDocumentContent(filePath, tracker) {
|
|
1286
|
+
const tracked = tracker.get(filePath);
|
|
1287
|
+
return tracked?.text ?? await fs2.readFile(filePath, "utf8");
|
|
1288
|
+
}
|
|
1289
|
+
function stringifyToolError(err) {
|
|
1290
|
+
if (err instanceof LSPError) return `[${err.code}] ${err.message}`;
|
|
1291
|
+
if (err instanceof Error) return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${err.message}`;
|
|
1292
|
+
return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${String(err)}`;
|
|
1293
|
+
}
|
|
1098
1294
|
async function applyWorkspaceEdit(edit, tracker) {
|
|
1099
1295
|
const entries = editsByPath(edit);
|
|
1100
1296
|
const ops = [];
|
|
@@ -1122,7 +1318,9 @@ async function applyWorkspaceEdit(edit, tracker) {
|
|
|
1122
1318
|
}
|
|
1123
1319
|
function applyTextEdits(original, edits) {
|
|
1124
1320
|
const lineStarts = buildLineStarts(original);
|
|
1125
|
-
const sorted = [...edits].sort(
|
|
1321
|
+
const sorted = [...edits].sort(
|
|
1322
|
+
(a, b) => offsetOf(b.range.start, lineStarts) - offsetOf(a.range.start, lineStarts)
|
|
1323
|
+
);
|
|
1126
1324
|
let out = original;
|
|
1127
1325
|
for (const edit of sorted) {
|
|
1128
1326
|
const start = offsetOf(edit.range.start, lineStarts);
|
|
@@ -1142,25 +1340,6 @@ function buildLineStarts(text) {
|
|
|
1142
1340
|
function offsetOf(pos, lineStarts) {
|
|
1143
1341
|
return (lineStarts[pos.line] ?? lineStarts[lineStarts.length - 1] ?? 0) + pos.character;
|
|
1144
1342
|
}
|
|
1145
|
-
function resolveInputPath(inputPath, ctx) {
|
|
1146
|
-
return path.isAbsolute(inputPath) ? path.normalize(inputPath) : path.resolve(ctx.cwd, inputPath);
|
|
1147
|
-
}
|
|
1148
|
-
async function requireServer(registry, filePath, signal) {
|
|
1149
|
-
const server = await registry.findForPath(filePath, signal);
|
|
1150
|
-
if (!server) {
|
|
1151
|
-
throw new LSPError("LSP_SERVER_NOT_FOUND" /* ServerNotFound */, `No LSP server is configured for ${filePath}`);
|
|
1152
|
-
}
|
|
1153
|
-
return server;
|
|
1154
|
-
}
|
|
1155
|
-
async function readDocumentContent(filePath, tracker) {
|
|
1156
|
-
const tracked = tracker.get(filePath);
|
|
1157
|
-
return tracked?.text ?? await fs2.readFile(filePath, "utf8");
|
|
1158
|
-
}
|
|
1159
|
-
function stringifyToolError(err) {
|
|
1160
|
-
if (err instanceof LSPError) return `[${err.code}] ${err.message}`;
|
|
1161
|
-
if (err instanceof Error) return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${err.message}`;
|
|
1162
|
-
return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${String(err)}`;
|
|
1163
|
-
}
|
|
1164
1343
|
|
|
1165
1344
|
// src/tools/code-actions.ts
|
|
1166
1345
|
function createCodeActionsTool(deps) {
|
|
@@ -1189,7 +1368,10 @@ function createCodeActionsTool(deps) {
|
|
|
1189
1368
|
const file = resolveInputPath(input.path, ctx);
|
|
1190
1369
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1191
1370
|
if (server.capabilities && !supportsCodeAction(server.capabilities)) {
|
|
1192
|
-
throw new LSPError(
|
|
1371
|
+
throw new LSPError(
|
|
1372
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1373
|
+
`Server "${server.name}" does not support code actions`
|
|
1374
|
+
);
|
|
1193
1375
|
}
|
|
1194
1376
|
const content = await readDocumentContent(file, deps.tracker);
|
|
1195
1377
|
const start = humanToLSP(content, { line: input.line, character: input.character ?? 1 });
|
|
@@ -1197,14 +1379,18 @@ function createCodeActionsTool(deps) {
|
|
|
1197
1379
|
line: input.end_line ?? input.line,
|
|
1198
1380
|
character: input.end_character ?? input.character ?? 1
|
|
1199
1381
|
});
|
|
1200
|
-
const actions = await server.codeAction(
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1382
|
+
const actions = await server.codeAction(
|
|
1383
|
+
{
|
|
1384
|
+
textDocument: { uri: pathToUri(file) },
|
|
1385
|
+
range: { start, end },
|
|
1386
|
+
context: {
|
|
1387
|
+
diagnostics: server.getDiagnostics(pathToUri(file)),
|
|
1388
|
+
only: input.kind_filter ? [input.kind_filter] : void 0
|
|
1389
|
+
}
|
|
1390
|
+
},
|
|
1391
|
+
1e4,
|
|
1392
|
+
opts.signal
|
|
1393
|
+
);
|
|
1208
1394
|
if (input.apply === void 0) return formatActions(actions);
|
|
1209
1395
|
const action = actions[input.apply];
|
|
1210
1396
|
if (!action) return `No code action at index ${input.apply}.`;
|
|
@@ -1250,7 +1436,11 @@ function createDefinitionTool(deps) {
|
|
|
1250
1436
|
usageHint: "Use for semantic navigation when you know the symbol position. Lines and columns are 1-based.",
|
|
1251
1437
|
inputSchema: {
|
|
1252
1438
|
type: "object",
|
|
1253
|
-
properties: {
|
|
1439
|
+
properties: {
|
|
1440
|
+
path: { type: "string" },
|
|
1441
|
+
line: { type: "integer" },
|
|
1442
|
+
character: { type: "integer" }
|
|
1443
|
+
},
|
|
1254
1444
|
required: ["path", "line", "character"]
|
|
1255
1445
|
},
|
|
1256
1446
|
permission: "auto",
|
|
@@ -1261,11 +1451,18 @@ function createDefinitionTool(deps) {
|
|
|
1261
1451
|
const file = resolveInputPath(input.path, ctx);
|
|
1262
1452
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1263
1453
|
if (server.capabilities && !supportsDefinition(server.capabilities)) {
|
|
1264
|
-
throw new LSPError(
|
|
1454
|
+
throw new LSPError(
|
|
1455
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1456
|
+
`Server "${server.name}" does not support definition`
|
|
1457
|
+
);
|
|
1265
1458
|
}
|
|
1266
1459
|
const content = await readDocumentContent(file, deps.tracker);
|
|
1267
1460
|
const position = humanToLSP(content, { line: input.line, character: input.character });
|
|
1268
|
-
const locs = await server.definition(
|
|
1461
|
+
const locs = await server.definition(
|
|
1462
|
+
{ textDocument: { uri: pathToUri(file) }, position },
|
|
1463
|
+
5e3,
|
|
1464
|
+
opts.signal
|
|
1465
|
+
);
|
|
1269
1466
|
return formatLocations(locs, ctx.cwd);
|
|
1270
1467
|
} catch (err) {
|
|
1271
1468
|
return stringifyToolError(err);
|
|
@@ -1274,56 +1471,16 @@ function createDefinitionTool(deps) {
|
|
|
1274
1471
|
};
|
|
1275
1472
|
}
|
|
1276
1473
|
|
|
1277
|
-
// src/formatters/diagnostics.ts
|
|
1278
|
-
var SEVERITY = {
|
|
1279
|
-
1: "error",
|
|
1280
|
-
2: "warning",
|
|
1281
|
-
3: "info",
|
|
1282
|
-
4: "hint"
|
|
1283
|
-
};
|
|
1284
|
-
var LABEL = {
|
|
1285
|
-
error: "ERROR",
|
|
1286
|
-
warning: "WARN",
|
|
1287
|
-
info: "INFO",
|
|
1288
|
-
hint: "HINT"
|
|
1289
|
-
};
|
|
1290
|
-
function formatDiagnostics(byFile, opts) {
|
|
1291
|
-
const allowed = new Set(opts.severityFilter);
|
|
1292
|
-
const sections = [];
|
|
1293
|
-
let total = 0;
|
|
1294
|
-
let files = 0;
|
|
1295
|
-
for (const [file, diagnostics] of byFile.entries()) {
|
|
1296
|
-
const filtered = diagnostics.filter((d) => allowed.has(SEVERITY[d.severity ?? 1] ?? "error")).sort(compareDiagnostics).slice(0, opts.maxPerFile);
|
|
1297
|
-
if (filtered.length === 0) continue;
|
|
1298
|
-
files++;
|
|
1299
|
-
total += filtered.length;
|
|
1300
|
-
const lines = filtered.map((d) => formatDiagnostic(d));
|
|
1301
|
-
sections.push(`${displayPath(file, opts.cwd)} (${filtered.length}):
|
|
1302
|
-
${lines.map((l) => ` ${l}`).join("\n")}`);
|
|
1303
|
-
if (total >= opts.maxTotal) break;
|
|
1304
|
-
}
|
|
1305
|
-
if (sections.length === 0) return "No LSP diagnostics.";
|
|
1306
|
-
return `${sections.join("\n\n")}
|
|
1307
|
-
|
|
1308
|
-
Total: ${total} diagnostics in ${files} files.`;
|
|
1309
|
-
}
|
|
1310
|
-
function formatDiagnostic(d) {
|
|
1311
|
-
const sev = SEVERITY[d.severity ?? 1] ?? "error";
|
|
1312
|
-
const source = d.source ? ` ${d.source}${d.code !== void 0 ? `(${String(d.code)})` : ""}` : "";
|
|
1313
|
-
const msg = d.message.replace(/\s*\r?\n\s*/g, " | ");
|
|
1314
|
-
return `L${d.range.start.line + 1}:${d.range.start.character + 1} ${LABEL[sev]}${source}: ${msg}`;
|
|
1315
|
-
}
|
|
1316
|
-
function compareDiagnostics(a, b) {
|
|
1317
|
-
return (a.severity ?? 1) - (b.severity ?? 1) || a.range.start.line - b.range.start.line || a.range.start.character - b.range.start.character;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
1474
|
// src/tools/diagnostics.ts
|
|
1321
1475
|
function createDiagnosticsTool(deps) {
|
|
1322
1476
|
return {
|
|
1323
1477
|
name: "lsp_diagnostics",
|
|
1324
1478
|
description: "Get diagnostics from configured language servers.",
|
|
1325
1479
|
usageHint: "Use after reading or editing a file when an LSP server is configured. Pass `path` for file diagnostics or omit it for tracked workspace diagnostics.",
|
|
1326
|
-
inputSchema: {
|
|
1480
|
+
inputSchema: {
|
|
1481
|
+
type: "object",
|
|
1482
|
+
properties: { path: { type: "string" }, limit: { type: "integer" } }
|
|
1483
|
+
},
|
|
1327
1484
|
permission: "auto",
|
|
1328
1485
|
mutating: false,
|
|
1329
1486
|
timeoutMs: 5e3,
|
|
@@ -1386,7 +1543,11 @@ function createHoverTool(deps) {
|
|
|
1386
1543
|
usageHint: "Use when you need a type/signature without opening the definition.",
|
|
1387
1544
|
inputSchema: {
|
|
1388
1545
|
type: "object",
|
|
1389
|
-
properties: {
|
|
1546
|
+
properties: {
|
|
1547
|
+
path: { type: "string" },
|
|
1548
|
+
line: { type: "integer" },
|
|
1549
|
+
character: { type: "integer" }
|
|
1550
|
+
},
|
|
1390
1551
|
required: ["path", "line", "character"]
|
|
1391
1552
|
},
|
|
1392
1553
|
permission: "auto",
|
|
@@ -1397,11 +1558,20 @@ function createHoverTool(deps) {
|
|
|
1397
1558
|
const file = resolveInputPath(input.path, ctx);
|
|
1398
1559
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1399
1560
|
if (server.capabilities && !supportsHover(server.capabilities)) {
|
|
1400
|
-
throw new LSPError(
|
|
1561
|
+
throw new LSPError(
|
|
1562
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1563
|
+
`Server "${server.name}" does not support hover`
|
|
1564
|
+
);
|
|
1401
1565
|
}
|
|
1402
1566
|
const content = await readDocumentContent(file, deps.tracker);
|
|
1403
1567
|
const position = humanToLSP(content, { line: input.line, character: input.character });
|
|
1404
|
-
return formatHover(
|
|
1568
|
+
return formatHover(
|
|
1569
|
+
await server.hover(
|
|
1570
|
+
{ textDocument: { uri: pathToUri(file) }, position },
|
|
1571
|
+
5e3,
|
|
1572
|
+
opts.signal
|
|
1573
|
+
)
|
|
1574
|
+
);
|
|
1405
1575
|
} catch (err) {
|
|
1406
1576
|
return stringifyToolError(err);
|
|
1407
1577
|
}
|
|
@@ -1434,15 +1604,22 @@ function createReferencesTool(deps) {
|
|
|
1434
1604
|
const file = resolveInputPath(input.path, ctx);
|
|
1435
1605
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1436
1606
|
if (server.capabilities && !supportsReferences(server.capabilities)) {
|
|
1437
|
-
throw new LSPError(
|
|
1607
|
+
throw new LSPError(
|
|
1608
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1609
|
+
`Server "${server.name}" does not support references`
|
|
1610
|
+
);
|
|
1438
1611
|
}
|
|
1439
1612
|
const content = await readDocumentContent(file, deps.tracker);
|
|
1440
1613
|
const position = humanToLSP(content, { line: input.line, character: input.character });
|
|
1441
|
-
const locs = await server.references(
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1614
|
+
const locs = await server.references(
|
|
1615
|
+
{
|
|
1616
|
+
textDocument: { uri: pathToUri(file) },
|
|
1617
|
+
position,
|
|
1618
|
+
context: { includeDeclaration: input.include_declaration ?? true }
|
|
1619
|
+
},
|
|
1620
|
+
1e4,
|
|
1621
|
+
opts.signal
|
|
1622
|
+
);
|
|
1446
1623
|
return formatLocations(locs, ctx.cwd, input.limit ?? 100);
|
|
1447
1624
|
} catch (err) {
|
|
1448
1625
|
return stringifyToolError(err);
|
|
@@ -1476,15 +1653,22 @@ function createRenameTool(deps) {
|
|
|
1476
1653
|
const file = resolveInputPath(input.path, ctx);
|
|
1477
1654
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1478
1655
|
if (server.capabilities && !supportsRename(server.capabilities)) {
|
|
1479
|
-
throw new LSPError(
|
|
1656
|
+
throw new LSPError(
|
|
1657
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1658
|
+
`Server "${server.name}" does not support rename`
|
|
1659
|
+
);
|
|
1480
1660
|
}
|
|
1481
1661
|
const content = await readDocumentContent(file, deps.tracker);
|
|
1482
1662
|
const position = humanToLSP(content, { line: input.line, character: input.character });
|
|
1483
|
-
const edit = await server.rename(
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1663
|
+
const edit = await server.rename(
|
|
1664
|
+
{
|
|
1665
|
+
textDocument: { uri: pathToUri(file) },
|
|
1666
|
+
position,
|
|
1667
|
+
newName: input.new_name
|
|
1668
|
+
},
|
|
1669
|
+
15e3,
|
|
1670
|
+
opts.signal
|
|
1671
|
+
);
|
|
1488
1672
|
if (!edit) return "Rename produced no edits.";
|
|
1489
1673
|
const summary = summarizeWorkspaceEdit(edit, ctx.cwd);
|
|
1490
1674
|
const applied = await applyWorkspaceEdit(edit, deps.tracker);
|
|
@@ -1508,7 +1692,9 @@ function formatWorkspaceSymbols(symbols, query, cwd, limit = 100) {
|
|
|
1508
1692
|
if (!symbols || symbols.length === 0) return `No symbols matching "${query}".`;
|
|
1509
1693
|
const lines = [`${symbols.length} symbols matching "${query}":`];
|
|
1510
1694
|
for (const sym of symbols.slice(0, limit)) {
|
|
1511
|
-
lines.push(
|
|
1695
|
+
lines.push(
|
|
1696
|
+
` ${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
|
|
1697
|
+
);
|
|
1512
1698
|
}
|
|
1513
1699
|
if (symbols.length > limit) lines.push(` ... truncated ${symbols.length - limit} more`);
|
|
1514
1700
|
return lines.join("\n");
|
|
@@ -1516,10 +1702,14 @@ function formatWorkspaceSymbols(symbols, query, cwd, limit = 100) {
|
|
|
1516
1702
|
function appendSymbol(lines, sym, depth, cwd) {
|
|
1517
1703
|
const indent = " ".repeat(depth);
|
|
1518
1704
|
if ("selectionRange" in sym) {
|
|
1519
|
-
lines.push(
|
|
1705
|
+
lines.push(
|
|
1706
|
+
`${indent}${kindName(sym.kind)} ${sym.name} (L${sym.selectionRange.start.line + 1})`
|
|
1707
|
+
);
|
|
1520
1708
|
for (const child of sym.children ?? []) appendSymbol(lines, child, depth + 1, cwd);
|
|
1521
1709
|
} else {
|
|
1522
|
-
lines.push(
|
|
1710
|
+
lines.push(
|
|
1711
|
+
`${indent}${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
|
|
1712
|
+
);
|
|
1523
1713
|
}
|
|
1524
1714
|
}
|
|
1525
1715
|
function kindName(kind) {
|
|
@@ -1561,7 +1751,11 @@ function createSymbolsTool(deps) {
|
|
|
1561
1751
|
usageHint: "Pass `path` for a file outline, or `query` for workspace symbol search.",
|
|
1562
1752
|
inputSchema: {
|
|
1563
1753
|
type: "object",
|
|
1564
|
-
properties: {
|
|
1754
|
+
properties: {
|
|
1755
|
+
path: { type: "string" },
|
|
1756
|
+
query: { type: "string" },
|
|
1757
|
+
limit: { type: "integer" }
|
|
1758
|
+
}
|
|
1565
1759
|
},
|
|
1566
1760
|
permission: "auto",
|
|
1567
1761
|
mutating: false,
|
|
@@ -1572,9 +1766,16 @@ function createSymbolsTool(deps) {
|
|
|
1572
1766
|
const file = resolveInputPath(input.path, ctx);
|
|
1573
1767
|
const server = await requireServer(deps.registry, file, opts.signal);
|
|
1574
1768
|
if (server.capabilities && !supportsDocumentSymbol(server.capabilities)) {
|
|
1575
|
-
throw new LSPError(
|
|
1769
|
+
throw new LSPError(
|
|
1770
|
+
"LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
|
|
1771
|
+
`Server "${server.name}" does not support document symbols`
|
|
1772
|
+
);
|
|
1576
1773
|
}
|
|
1577
|
-
const symbols = await server.documentSymbol(
|
|
1774
|
+
const symbols = await server.documentSymbol(
|
|
1775
|
+
{ textDocument: { uri: pathToUri(file) } },
|
|
1776
|
+
5e3,
|
|
1777
|
+
opts.signal
|
|
1778
|
+
);
|
|
1578
1779
|
return formatDocumentSymbols(file, symbols, ctx.cwd);
|
|
1579
1780
|
}
|
|
1580
1781
|
const query = input.query ?? "";
|
|
@@ -1606,100 +1807,6 @@ function makeLSPTools(deps) {
|
|
|
1606
1807
|
];
|
|
1607
1808
|
}
|
|
1608
1809
|
|
|
1609
|
-
// src/slash-commands/diagnostics.ts
|
|
1610
|
-
function diagnosticsCommand(registry) {
|
|
1611
|
-
return {
|
|
1612
|
-
name: "diagnostics",
|
|
1613
|
-
description: "Print buffered LSP diagnostics.",
|
|
1614
|
-
async run(_args, ctx) {
|
|
1615
|
-
const byFile = /* @__PURE__ */ new Map();
|
|
1616
|
-
for (const server of registry.list()) {
|
|
1617
|
-
for (const [uri, diagnostics] of server.diagnostics.entries()) {
|
|
1618
|
-
byFile.set(uriToPath(uri), diagnostics);
|
|
1619
|
-
}
|
|
1620
|
-
}
|
|
1621
|
-
return {
|
|
1622
|
-
message: formatDiagnostics(byFile, {
|
|
1623
|
-
cwd: ctx.cwd,
|
|
1624
|
-
severityFilter: ["error", "warning"],
|
|
1625
|
-
maxPerFile: 10,
|
|
1626
|
-
maxTotal: 100
|
|
1627
|
-
})
|
|
1628
|
-
};
|
|
1629
|
-
}
|
|
1630
|
-
};
|
|
1631
|
-
}
|
|
1632
|
-
|
|
1633
|
-
// src/slash-commands/list.ts
|
|
1634
|
-
function listCommand(registry) {
|
|
1635
|
-
return {
|
|
1636
|
-
name: "list",
|
|
1637
|
-
description: "List configured LSP servers.",
|
|
1638
|
-
async run() {
|
|
1639
|
-
const rows = registry.list().map((s) => {
|
|
1640
|
-
const langs = s.config.languages.join(",");
|
|
1641
|
-
return `${s.name.padEnd(18)} ${s.state.padEnd(14)} ${langs} ${s.rootPath}`;
|
|
1642
|
-
});
|
|
1643
|
-
return { message: rows.length ? rows.join("\n") : "No LSP servers configured." };
|
|
1644
|
-
}
|
|
1645
|
-
};
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// src/slash-commands/restart.ts
|
|
1649
|
-
function restartCommand(registry) {
|
|
1650
|
-
return {
|
|
1651
|
-
name: "restart",
|
|
1652
|
-
description: "Restart an LSP server.",
|
|
1653
|
-
async run(args) {
|
|
1654
|
-
const name = args.trim();
|
|
1655
|
-
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:restart <name>" };
|
|
1656
|
-
await registry.restart(name);
|
|
1657
|
-
return { message: `Restarted LSP server "${name}".` };
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
// src/slash-commands/start.ts
|
|
1663
|
-
function startCommand(registry) {
|
|
1664
|
-
return {
|
|
1665
|
-
name: "start",
|
|
1666
|
-
description: "Start an LSP server.",
|
|
1667
|
-
async run(args) {
|
|
1668
|
-
const name = args.trim();
|
|
1669
|
-
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:start <name>" };
|
|
1670
|
-
await registry.start(name);
|
|
1671
|
-
return { message: `Started LSP server "${name}".` };
|
|
1672
|
-
}
|
|
1673
|
-
};
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
// src/slash-commands/stop.ts
|
|
1677
|
-
function stopCommand(registry) {
|
|
1678
|
-
return {
|
|
1679
|
-
name: "stop",
|
|
1680
|
-
description: "Stop an LSP server.",
|
|
1681
|
-
async run(args) {
|
|
1682
|
-
const name = args.trim();
|
|
1683
|
-
if (!name) return { message: "Usage: /@wrongstack/plug-lsp:stop <name>" };
|
|
1684
|
-
await registry.stop(name);
|
|
1685
|
-
return { message: `Stopped LSP server "${name}".` };
|
|
1686
|
-
}
|
|
1687
|
-
};
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
// src/slash-commands/index.ts
|
|
1691
|
-
function registerSlashCommands(api, registry) {
|
|
1692
|
-
const commands = [
|
|
1693
|
-
listCommand(registry),
|
|
1694
|
-
startCommand(registry),
|
|
1695
|
-
stopCommand(registry),
|
|
1696
|
-
restartCommand(registry),
|
|
1697
|
-
diagnosticsCommand(registry)
|
|
1698
|
-
];
|
|
1699
|
-
for (const command of commands) api.slashCommands.register(command);
|
|
1700
|
-
return commands.map((cmd) => cmd.name);
|
|
1701
|
-
}
|
|
1702
|
-
|
|
1703
1810
|
// src/index.ts
|
|
1704
1811
|
var teardownState = null;
|
|
1705
1812
|
var plugin = {
|