@wrongstack/plug-lsp 0.1.9 → 0.1.10

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 CHANGED
@@ -1,12 +1,10 @@
1
- import { TOKENS, atomicWrite } from '@wrongstack/core';
1
+ import { spawn } from 'child_process';
2
2
  import * as fs2 from 'fs/promises';
3
3
  import * as path from 'path';
4
- import { spawn } from 'child_process';
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/config.ts
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) pending.reject(new LSPError("LSP_PROTOCOL_ERROR" /* ProtocolError */, msg.error.message, 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: { didSave: false, dynamicRegistration: false, willSave: false, willSaveWaitUntil: false },
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: [{ uri: rootUri, name: rootPath.split(/[\\/]/).pop() ?? rootPath }]
638
+ workspaceFolders: [
639
+ {
640
+ uri: rootUri,
641
+ name: rootPath.split(/[\\/]/).pop() ?? rootPath
642
+ }
643
+ ]
646
644
  };
647
- const result = await connection.sendRequest("initialize", params, timeoutMs, signal);
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") return;
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(new LSPError("LSP_SERVER_FAILED" /* ServerFailed */, `LSP server failed to start: ${err.message}`, err));
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(new LSPError(
858
- "LSP_SERVER_FAILED" /* ServerFailed */,
859
- `LSP server exited during startup code=${code ?? "null"} signal=${signal ?? "null"}`
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
- // src/server/lifecycle.ts
873
- function nextReconnectDelay(attempt) {
874
- return [1e3, 4e3, 16e3][Math.max(0, Math.min(2, attempt))] ?? 16e3;
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((s) => s.start().catch((err) => this.ctx.log.warn(`LSP ${s.name} failed to start`, err)))
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(this.list().map((s) => s.shutdown().catch((err) => this.ctx.log.warn(`LSP ${s.name} shutdown failed`, err))));
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(path.join(this.cwd, "__probe__"), cfg.rootPatterns, this.cwd);
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(`LSP language "${language}" is claimed by multiple servers; using first`);
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((a, b) => offsetOf(b.range.start, lineStarts) - offsetOf(a.range.start, lineStarts));
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support code actions`);
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
- textDocument: { uri: pathToUri(file) },
1202
- range: { start, end },
1203
- context: {
1204
- diagnostics: server.getDiagnostics(pathToUri(file)),
1205
- only: input.kind_filter ? [input.kind_filter] : void 0
1206
- }
1207
- }, 1e4, opts.signal);
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: { path: { type: "string" }, line: { type: "integer" }, character: { type: "integer" } },
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support definition`);
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({ textDocument: { uri: pathToUri(file) }, position }, 5e3, opts.signal);
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: { type: "object", properties: { path: { type: "string" }, limit: { type: "integer" } } },
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: { path: { type: "string" }, line: { type: "integer" }, character: { type: "integer" } },
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support hover`);
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(await server.hover({ textDocument: { uri: pathToUri(file) }, position }, 5e3, opts.signal));
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support references`);
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
- textDocument: { uri: pathToUri(file) },
1443
- position,
1444
- context: { includeDeclaration: input.include_declaration ?? true }
1445
- }, 1e4, opts.signal);
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support rename`);
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
- textDocument: { uri: pathToUri(file) },
1485
- position,
1486
- newName: input.new_name
1487
- }, 15e3, opts.signal);
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(` ${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`);
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(`${indent}${kindName(sym.kind)} ${sym.name} (L${sym.selectionRange.start.line + 1})`);
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(`${indent}${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`);
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: { path: { type: "string" }, query: { type: "string" }, limit: { type: "integer" } }
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("LSP_CAPABILITY_MISSING" /* CapabilityMissing */, `Server "${server.name}" does not support document symbols`);
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({ textDocument: { uri: pathToUri(file) } }, 5e3, opts.signal);
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 = {