@wrongstack/plug-lsp 0.264.0 → 0.267.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 CHANGED
@@ -1,13 +1,278 @@
1
- import { expectDefined, TOKENS, buildChildEnv, atomicWrite } from '@wrongstack/core';
2
1
  import { spawn } from 'child_process';
3
2
  import * as fs2 from 'fs/promises';
4
3
  import * as path from 'path';
4
+ import { buildChildEnv, expectDefined, TOKENS, atomicWrite } from '@wrongstack/core';
5
5
  import { pathToFileURL, fileURLToPath } from 'url';
6
6
  import { EventEmitter } from 'events';
7
7
  import * as fs3 from 'fs';
8
8
  import { codebaseIndexDirOverride, searchCodebaseIndex, internalKindToLspKind, lspKindToInternalKind } from '@wrongstack/tools/codebase-index/index';
9
9
 
10
- // src/index.ts
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
13
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
14
+ }) : x)(function(x) {
15
+ if (typeof require !== "undefined") return require.apply(this, arguments);
16
+ throw Error('Dynamic require of "' + x + '" is not supported');
17
+ });
18
+ var __esm = (fn, res, err) => function __init() {
19
+ if (err) throw err[0];
20
+ try {
21
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
22
+ } catch (e) {
23
+ throw err = [e], e;
24
+ }
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+ async function resolveServerCommand(command, cwd) {
31
+ const local = await findLocalBinary(cwd, command);
32
+ if (local) return local;
33
+ return await commandExistsOnPath(command) ? command : null;
34
+ }
35
+ async function findLocalBinary(cwd, command) {
36
+ if (path.isAbsolute(command)) return await fileExists(command) ? path.normalize(command) : null;
37
+ let dir = path.resolve(cwd);
38
+ for (; ; ) {
39
+ const binDir = path.join(dir, "node_modules", ".bin");
40
+ for (const candidate of commandCandidates(command)) {
41
+ const full = path.join(binDir, candidate);
42
+ if (await fileExists(full)) return full;
43
+ }
44
+ const parent = path.dirname(dir);
45
+ if (parent === dir) return null;
46
+ dir = parent;
47
+ }
48
+ }
49
+ async function commandExistsOnPath(command, timeoutMs = 2e3) {
50
+ const probe = process.platform === "win32" ? "where.exe" : "sh";
51
+ const args = process.platform === "win32" ? [command] : ["-lc", `command -v ${shellQuote(command)}`];
52
+ return new Promise((resolve7) => {
53
+ const child = spawn(probe, args, { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
54
+ const timer = setTimeout(() => {
55
+ child.kill();
56
+ resolve7(false);
57
+ }, timeoutMs);
58
+ timer.unref?.();
59
+ child.on("error", () => {
60
+ clearTimeout(timer);
61
+ resolve7(false);
62
+ });
63
+ child.on("close", (code) => {
64
+ clearTimeout(timer);
65
+ resolve7(code === 0);
66
+ });
67
+ });
68
+ }
69
+ function commandCandidates(command) {
70
+ if (process.platform !== "win32") return [command];
71
+ const ext = path.extname(command).toLowerCase();
72
+ if (ext) return [command];
73
+ return [`${command}.cmd`, `${command}.exe`, `${command}.bat`, command, `${command}.ps1`];
74
+ }
75
+ async function fileExists(filePath) {
76
+ try {
77
+ await fs2.access(filePath);
78
+ return true;
79
+ } catch {
80
+ return false;
81
+ }
82
+ }
83
+ function shellQuote(value) {
84
+ return `'${value.replace(/'/g, "'\\''")}'`;
85
+ }
86
+ var init_command_resolver = __esm({
87
+ "src/utils/command-resolver.ts"() {
88
+ }
89
+ });
90
+
91
+ // src/slash-commands/install.ts
92
+ var install_exports = {};
93
+ __export(install_exports, {
94
+ LANGUAGE_SERVERS: () => LANGUAGE_SERVERS,
95
+ SUPPORTED_LANGUAGES: () => SUPPORTED_LANGUAGES,
96
+ installLang: () => installLang
97
+ });
98
+ async function installLang(language, server, cwd, dryRun = false) {
99
+ const existing = await resolveServerCommand(server.binary, cwd);
100
+ if (existing) {
101
+ return { language, binary: server.binary, alreadyInstalled: true, dryRun: false };
102
+ }
103
+ if (server.toolchain) {
104
+ const { command, args, label } = server.toolchain;
105
+ if (!await commandExistsOnPath(command)) {
106
+ return {
107
+ language,
108
+ binary: server.binary,
109
+ alreadyInstalled: false,
110
+ dryRun,
111
+ installCommand: `${command} ${args.join(" ")}`,
112
+ error: `${label} (${command}) is not on your PATH. Install it first.`
113
+ };
114
+ }
115
+ const installCmd = `${command} ${args.join(" ")}`;
116
+ if (dryRun) {
117
+ return { language, binary: server.binary, alreadyInstalled: false, dryRun: true, installCommand: installCmd };
118
+ }
119
+ await runCommand(command, args, cwd, label);
120
+ return { language, binary: server.binary, alreadyInstalled: false, dryRun: false, packageManager: "system", installCommand: installCmd };
121
+ }
122
+ if (server.npmPackages && server.npmPackages.length > 0) {
123
+ const { command, args } = npmInstallCommand(server.npmPackages, cwd);
124
+ const installCmd = `${command} ${args.join(" ")}`;
125
+ if (dryRun) {
126
+ return { language, binary: server.binary, alreadyInstalled: false, dryRun: true, installCommand: installCmd };
127
+ }
128
+ try {
129
+ await runCommand(command, args, cwd, `Installing ${language} LSP server via npm`);
130
+ return { language, binary: server.binary, alreadyInstalled: false, dryRun: false, installCommand: installCmd };
131
+ } catch (err) {
132
+ return {
133
+ language,
134
+ binary: server.binary,
135
+ alreadyInstalled: false,
136
+ dryRun: false,
137
+ installCommand: installCmd,
138
+ error: err instanceof Error ? err.message : String(err)
139
+ };
140
+ }
141
+ }
142
+ return {
143
+ language,
144
+ binary: server.binary,
145
+ alreadyInstalled: false,
146
+ dryRun,
147
+ error: "No installation method available for this server."
148
+ };
149
+ }
150
+ function npmInstallCommand(packages, cwd) {
151
+ const pm = detectPackageManagerSync(cwd);
152
+ if (pm === "pnpm") return { command: "pnpm", args: ["add", "-D", ...packages] };
153
+ if (pm === "yarn") return { command: "yarn", args: ["add", "-D", ...packages] };
154
+ if (pm === "bun") return { command: "bun", args: ["add", "-d", ...packages] };
155
+ return { command: "npm", args: ["install", "-D", ...packages] };
156
+ }
157
+ function detectPackageManagerSync(cwd) {
158
+ if (existsSync2(path.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
159
+ if (existsSync2(path.join(cwd, "bun.lockb")) || existsSync2(path.join(cwd, "bun.lock"))) return "bun";
160
+ if (existsSync2(path.join(cwd, "yarn.lock"))) return "yarn";
161
+ return "npm";
162
+ }
163
+ function existsSync2(filePath) {
164
+ try {
165
+ __require("fs").accessSync(filePath);
166
+ return true;
167
+ } catch {
168
+ return false;
169
+ }
170
+ }
171
+ function runCommand(command, args, cwd, label) {
172
+ return new Promise((resolve7, reject) => {
173
+ const isWindowsBatch = process.platform === "win32" && /\.(cmd|bat)$/i.test(command);
174
+ const child = spawn(command, args, {
175
+ cwd,
176
+ env: buildChildEnv(),
177
+ stdio: "inherit",
178
+ shell: isWindowsBatch,
179
+ windowsHide: true
180
+ });
181
+ child.on("error", reject);
182
+ child.on("close", (code) => {
183
+ if (code === 0) resolve7();
184
+ else reject(new Error(`${label}: ${command} exited with code ${code ?? "null"}`));
185
+ });
186
+ });
187
+ }
188
+ var LANGUAGE_SERVERS, SUPPORTED_LANGUAGES;
189
+ var init_install = __esm({
190
+ "src/slash-commands/install.ts"() {
191
+ init_command_resolver();
192
+ LANGUAGE_SERVERS = {
193
+ typescript: {
194
+ binary: "typescript-language-server",
195
+ npmPackages: ["typescript", "typescript-language-server"],
196
+ args: ["--stdio"],
197
+ languages: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
198
+ rootPatterns: ["tsconfig.json", "jsconfig.json", "package.json"]
199
+ },
200
+ python: {
201
+ binary: "pyright-langserver",
202
+ npmPackages: ["pyright"],
203
+ args: ["--stdio"],
204
+ languages: ["python"],
205
+ rootPatterns: ["pyproject.toml", "pyrightconfig.json", "setup.py", "requirements.txt"]
206
+ },
207
+ json: {
208
+ binary: "vscode-json-language-server",
209
+ npmPackages: ["vscode-langservers-extracted"],
210
+ args: ["--stdio"],
211
+ languages: ["json"],
212
+ rootPatterns: ["package.json"]
213
+ },
214
+ html: {
215
+ binary: "vscode-html-language-server",
216
+ npmPackages: ["vscode-langservers-extracted"],
217
+ args: ["--stdio"],
218
+ languages: ["html"],
219
+ rootPatterns: ["package.json"]
220
+ },
221
+ css: {
222
+ binary: "vscode-css-language-server",
223
+ npmPackages: ["vscode-langservers-extracted"],
224
+ args: ["--stdio"],
225
+ languages: ["css", "scss"],
226
+ rootPatterns: ["package.json"]
227
+ },
228
+ yaml: {
229
+ binary: "yaml-language-server",
230
+ npmPackages: ["yaml-language-server"],
231
+ args: ["--stdio"],
232
+ languages: ["yaml"],
233
+ rootPatterns: ["package.json", ".git"]
234
+ },
235
+ shell: {
236
+ binary: "bash-language-server",
237
+ npmPackages: ["bash-language-server"],
238
+ args: ["start"],
239
+ languages: ["shellscript"],
240
+ rootPatterns: ["package.json", ".git"]
241
+ },
242
+ go: {
243
+ binary: "gopls",
244
+ toolchain: {
245
+ command: "go",
246
+ args: ["install", "golang.org/x/tools/gopls@latest"],
247
+ label: "Go toolchain"
248
+ },
249
+ languages: ["go"],
250
+ rootPatterns: ["go.mod", "go.work"]
251
+ },
252
+ rust: {
253
+ binary: "rust-analyzer",
254
+ toolchain: {
255
+ command: "rustup",
256
+ args: ["component", "add", "rust-analyzer"],
257
+ label: "Rust toolchain"
258
+ },
259
+ languages: ["rust"],
260
+ rootPatterns: ["Cargo.toml"]
261
+ },
262
+ ruby: {
263
+ binary: "ruby-lsp",
264
+ toolchain: {
265
+ command: "gem",
266
+ args: ["install", "ruby-lsp"],
267
+ label: "RubyGems"
268
+ },
269
+ languages: ["ruby"],
270
+ rootPatterns: ["Gemfile", ".ruby-version"]
271
+ }
272
+ };
273
+ SUPPORTED_LANGUAGES = Object.keys(LANGUAGE_SERVERS);
274
+ }
275
+ });
11
276
 
12
277
  // src/presets.ts
13
278
  var PRESETS = {
@@ -90,64 +355,9 @@ var PRESETS = {
90
355
  enabled: true
91
356
  }
92
357
  };
93
- async function resolveServerCommand(command, cwd) {
94
- const local = await findLocalBinary(cwd, command);
95
- if (local) return local;
96
- return await commandExistsOnPath(command) ? command : null;
97
- }
98
- async function findLocalBinary(cwd, command) {
99
- if (path.isAbsolute(command)) return await fileExists(command) ? path.normalize(command) : null;
100
- let dir = path.resolve(cwd);
101
- for (; ; ) {
102
- const binDir = path.join(dir, "node_modules", ".bin");
103
- for (const candidate of commandCandidates(command)) {
104
- const full = path.join(binDir, candidate);
105
- if (await fileExists(full)) return full;
106
- }
107
- const parent = path.dirname(dir);
108
- if (parent === dir) return null;
109
- dir = parent;
110
- }
111
- }
112
- async function commandExistsOnPath(command, timeoutMs = 2e3) {
113
- const probe = process.platform === "win32" ? "where.exe" : "sh";
114
- const args = process.platform === "win32" ? [command] : ["-lc", `command -v ${shellQuote(command)}`];
115
- return new Promise((resolve6) => {
116
- const child = spawn(probe, args, { env: buildChildEnv(), stdio: "ignore", windowsHide: true });
117
- const timer = setTimeout(() => {
118
- child.kill();
119
- resolve6(false);
120
- }, timeoutMs);
121
- timer.unref?.();
122
- child.on("error", () => {
123
- clearTimeout(timer);
124
- resolve6(false);
125
- });
126
- child.on("close", (code) => {
127
- clearTimeout(timer);
128
- resolve6(code === 0);
129
- });
130
- });
131
- }
132
- function commandCandidates(command) {
133
- if (process.platform !== "win32") return [command];
134
- const ext = path.extname(command).toLowerCase();
135
- if (ext) return [command];
136
- return [`${command}.cmd`, `${command}.exe`, `${command}.bat`, command, `${command}.ps1`];
137
- }
138
- async function fileExists(filePath) {
139
- try {
140
- await fs2.access(filePath);
141
- return true;
142
- } catch {
143
- return false;
144
- }
145
- }
146
- function shellQuote(value) {
147
- return `'${value.replace(/'/g, "'\\''")}'`;
148
- }
149
358
 
150
359
  // src/auto-discover.ts
360
+ init_command_resolver();
151
361
  async function autoDiscoverServers(userServers, cwd = process.cwd()) {
152
362
  const out = { ...userServers };
153
363
  const pending = Object.entries(PRESETS).filter(([name]) => !out[name]);
@@ -473,7 +683,7 @@ function safeSpawn(cfg, cwd) {
473
683
  async function promiseWithTimeout(promise, ms, signal) {
474
684
  if (signal?.aborted) throw abortError(signal);
475
685
  let timer;
476
- return await new Promise((resolve6, reject) => {
686
+ return await new Promise((resolve7, reject) => {
477
687
  const cleanup = () => {
478
688
  if (timer) clearTimeout(timer);
479
689
  signal?.removeEventListener("abort", onAbort);
@@ -490,7 +700,7 @@ async function promiseWithTimeout(promise, ms, signal) {
490
700
  promise.then(
491
701
  (value) => {
492
702
  cleanup();
493
- resolve6(value);
703
+ resolve7(value);
494
704
  },
495
705
  (err) => {
496
706
  cleanup();
@@ -521,8 +731,8 @@ var Connection = class {
521
731
  this.assertOpen();
522
732
  const id = this.nextId++;
523
733
  const request = { jsonrpc: "2.0", id, method, params };
524
- const response = new Promise((resolve6, reject) => {
525
- this.pending.set(id, { resolve: resolve6, reject });
734
+ const response = new Promise((resolve7, reject) => {
735
+ this.pending.set(id, { resolve: resolve7, reject });
526
736
  this.write(request);
527
737
  });
528
738
  try {
@@ -1209,10 +1419,444 @@ function stopCommand(registry) {
1209
1419
  }
1210
1420
  };
1211
1421
  }
1422
+ init_install();
1423
+ function parseArgs(args) {
1424
+ const parts = args.trim().split(/\s+/).filter(Boolean);
1425
+ if (parts.length === 0) return { type: "list" };
1426
+ const sub = parts[0].toLowerCase();
1427
+ if (sub === "list" || sub === "ls") return { type: "list" };
1428
+ if (sub === "status" || sub === "stat") return { type: "status" };
1429
+ if (sub === "help" || sub === "h" || sub === "--help") return { type: "help" };
1430
+ if (sub === "install" || sub === "add") {
1431
+ const lang = parts[1];
1432
+ if (!lang) return { type: "help" };
1433
+ return { type: "install", language: lang };
1434
+ }
1435
+ if (sub === "start") return { type: "start", name: parts[1] };
1436
+ if (sub === "stop") return { type: "stop", name: parts[1] };
1437
+ if (sub === "restart" || sub === "reload") return { type: "restart", name: parts[1] };
1438
+ if (sub === "diagnostics" || sub === "diag") {
1439
+ return { type: "diagnostics", file: parts[1] };
1440
+ }
1441
+ if (sub === "remove" || sub === "rm" || sub === "delete") {
1442
+ const name = parts[1];
1443
+ if (!name) return { type: "help" };
1444
+ return { type: "remove", name };
1445
+ }
1446
+ if (sub === "enable") {
1447
+ const name = parts[1];
1448
+ if (!name) return { type: "help" };
1449
+ return { type: "enable", name };
1450
+ }
1451
+ if (sub === "disable") {
1452
+ const name = parts[1];
1453
+ if (!name) return { type: "help" };
1454
+ return { type: "disable", name };
1455
+ }
1456
+ return { type: "help" };
1457
+ }
1458
+ function colorize(text, code) {
1459
+ const colors = {
1460
+ green: "\x1B[32m",
1461
+ red: "\x1B[31m",
1462
+ yellow: "\x1B[33m",
1463
+ cyan: "\x1B[36m",
1464
+ bold: "\x1B[1m",
1465
+ dim: "\x1B[2m",
1466
+ reset: "\x1B[0m"
1467
+ };
1468
+ return `${colors[code] ?? ""}${text}${colors.reset}`;
1469
+ }
1470
+ function buildLspCommand(ctx) {
1471
+ return {
1472
+ name: "lsp",
1473
+ category: "Inspect",
1474
+ aliases: ["lsplsp"],
1475
+ description: "Manage LSP language servers: /lsp [list|install <lang>|start [name]|stop [name]|restart [name]|diagnostics [file]|add|remove|enable|disable]",
1476
+ argsHint: "[list|install <lang>|start [name]|stop [name]|restart [name]|diagnostics [file]]",
1477
+ help: [
1478
+ "Usage:",
1479
+ " /lsp Show server list and status (alias for list)",
1480
+ " /lsp list List all configured servers and their states",
1481
+ " /lsp status Detailed status report for all servers",
1482
+ " /lsp install <language> Install the language server for a given language",
1483
+ " Supported: " + SUPPORTED_LANGUAGES.join(", "),
1484
+ " /lsp start [name] Start all servers, or a specific one by name",
1485
+ " /lsp stop [name] Stop all servers, or a specific one by name",
1486
+ " /lsp restart [name] Restart all servers, or a specific one by name",
1487
+ " /lsp diagnostics [file] Show diagnostics for a file or the whole workspace",
1488
+ "",
1489
+ "Examples:",
1490
+ " /lsp (shows configured servers)",
1491
+ " /lsp list",
1492
+ " /lsp install typescript",
1493
+ " /lsp install python",
1494
+ " /lsp install go",
1495
+ " /lsp start (start all enabled servers)",
1496
+ " /lsp start gopls (start a specific server)",
1497
+ " /lsp diagnostics src/index.ts",
1498
+ " /lsp status",
1499
+ "",
1500
+ "After installing, add the server to your WrongStack config under:",
1501
+ ' extensions["@wrongstack/plug-lsp"].servers',
1502
+ "Then restart your WrongStack session."
1503
+ ].join("\n"),
1504
+ async run(args) {
1505
+ const sub = parseArgs(args);
1506
+ switch (sub.type) {
1507
+ case "list":
1508
+ return runListCommand(ctx);
1509
+ case "status":
1510
+ return runStatusCommand(ctx);
1511
+ case "install":
1512
+ return runInstallCommand(ctx, sub.language);
1513
+ case "start":
1514
+ return runStartCommand(ctx, sub.name);
1515
+ case "stop":
1516
+ return runStopCommand(ctx, sub.name);
1517
+ case "restart":
1518
+ return runRestartCommand(ctx, sub.name);
1519
+ case "diagnostics":
1520
+ return runDiagnosticsCommand(ctx, sub.file);
1521
+ case "add":
1522
+ return { message: 'To add a custom server, edit your config and add an entry under\n`extensions["@wrongstack/plug-lsp"].servers`. Run `/lsp help` for usage.' };
1523
+ case "remove":
1524
+ return { message: 'To remove a server, remove its entry from `extensions["@wrongstack/plug-lsp"].servers`\nin your WrongStack config, then restart.' };
1525
+ case "enable":
1526
+ return { message: "To enable a server, ensure `enabled: true` is set (or absent \u2014 it defaults to true)\nin its config entry, then run `/lsp start <name>`." };
1527
+ case "disable":
1528
+ return { message: "To disable a server, set `enabled: false` in its config entry,\nthen run `/lsp stop <name>` to stop it." };
1529
+ case "help":
1530
+ default:
1531
+ return { message: this.help ?? this.description };
1532
+ }
1533
+ }
1534
+ };
1535
+ }
1536
+ function runListCommand(ctx) {
1537
+ const servers = ctx.registry.list();
1538
+ if (servers.length === 0) {
1539
+ return {
1540
+ message: [
1541
+ `${colorize("LSP Servers", "bold")}`,
1542
+ "No servers configured.",
1543
+ "",
1544
+ "Enable @wrongstack/plug-lsp in your config and add server definitions under",
1545
+ '`extensions["@wrongstack/plug-lsp"].servers`, or run `/lsp install <language>`',
1546
+ "to install a preset server."
1547
+ ].join("\n")
1548
+ };
1549
+ }
1550
+ const lines = [`${colorize("LSP Servers", "bold")} (${servers.length} configured)`];
1551
+ lines.push("\u2500".repeat(60));
1552
+ for (const srv of servers) {
1553
+ const state = srv.state;
1554
+ const enabled = srv.config.enabled ?? true;
1555
+ const stateColor = state === "ready" ? "green" : state === "failed" ? "red" : state === "disabled" ? "dim" : "yellow";
1556
+ const stateLabel = `[${state.toUpperCase()}]`;
1557
+ const enabledLabel = enabled ? "" : colorize(" (disabled)", "dim");
1558
+ const langs = srv.config.languages?.join(", ") ?? "";
1559
+ lines.push(
1560
+ ` ${colorize(srv.name, "cyan")} ${colorize(stateLabel, stateColor)}${enabledLabel}`
1561
+ );
1562
+ lines.push(` ${colorize("Languages:", "dim")} ${langs}`);
1563
+ lines.push(` ${colorize("Command:", "dim")} ${srv.config.command} ${(srv.config.args ?? []).join(" ")}`);
1564
+ lines.push("");
1565
+ }
1566
+ lines.push("\u2500".repeat(60));
1567
+ lines.push("Run `/lsp help` for usage, or `/lsp install <language>` to install a server.");
1568
+ return { message: lines.join("\n") };
1569
+ }
1570
+ function runStatusCommand(ctx) {
1571
+ const servers = ctx.registry.list();
1572
+ const ready = servers.filter((s) => s.state === "ready").length;
1573
+ const failed = servers.filter((s) => s.state === "failed").length;
1574
+ const starting = servers.filter((s) => s.state === "starting" || s.state === "initializing").length;
1575
+ const lines = [
1576
+ `${colorize("LSP Status Report", "bold")}`,
1577
+ "\u2500".repeat(60),
1578
+ ` ${colorize("Total servers:", "dim")} ${servers.length}`,
1579
+ ` ${colorize("Ready:", "dim")} ${colorize(String(ready), "green")}`,
1580
+ ` ${colorize("Starting:", "dim")} ${colorize(String(starting), "yellow")}`,
1581
+ ` ${colorize("Failed:", "dim")} ${colorize(String(failed), "red")}`,
1582
+ ""
1583
+ ];
1584
+ if (failed > 0) {
1585
+ lines.push(`${colorize("Failed servers:", "red")}`);
1586
+ for (const srv of servers.filter((s) => s.state === "failed")) {
1587
+ const err = srv.lastStderr || "unknown error";
1588
+ lines.push(` ${colorize(srv.name, "cyan")} \u2014 ${err}`);
1589
+ }
1590
+ lines.push("");
1591
+ }
1592
+ const activeFiles = ctx.tracker.list().length;
1593
+ lines.push(` ${colorize("Active files tracked:", "dim")} ${activeFiles}`);
1594
+ lines.push(` ${colorize("Auto-start mode:", "dim")} ${ctx.cfg.autoStart}`);
1595
+ lines.push("");
1596
+ lines.push("\u2500".repeat(60));
1597
+ lines.push("Use `/lsp diagnostics` to check for problems, or `/lsp restart <name>` to recover.");
1598
+ return { message: lines.join("\n") };
1599
+ }
1600
+ async function runInstallCommand(ctx, language) {
1601
+ const lang = language.toLowerCase().trim();
1602
+ if (!SUPPORTED_LANGUAGES.includes(lang)) {
1603
+ return {
1604
+ message: [
1605
+ `${colorize("Unknown language:", "red")} ${lang}`,
1606
+ `Supported languages: ${SUPPORTED_LANGUAGES.join(", ")}`,
1607
+ "",
1608
+ "If the language server is already installed on your system, you can add it",
1609
+ 'manually to your WrongStack config under `extensions["@wrongstack/plug-lsp"].servers`.',
1610
+ "Run `/lsp help` for instructions."
1611
+ ].join("\n")
1612
+ };
1613
+ }
1614
+ const server = LANGUAGE_SERVERS[lang];
1615
+ try {
1616
+ const { installLang: installLang2 } = await Promise.resolve().then(() => (init_install(), install_exports));
1617
+ const result = await installLang2(lang, server, ctx.cwd);
1618
+ if (result.alreadyInstalled) {
1619
+ return {
1620
+ message: [
1621
+ `${colorize("Already installed:", "green")} ${lang}`,
1622
+ ` Binary: ${colorize(server.binary, "cyan")}`,
1623
+ "",
1624
+ "The server is already available. Add it to your config to activate:",
1625
+ "",
1626
+ "```json",
1627
+ JSON.stringify(
1628
+ {
1629
+ extensions: {
1630
+ "@wrongstack/plug-lsp": {
1631
+ servers: {
1632
+ [lang]: {
1633
+ command: server.binary,
1634
+ args: server.args ?? ["--stdio"],
1635
+ languages: server.languages,
1636
+ rootPatterns: server.rootPatterns ?? []
1637
+ }
1638
+ }
1639
+ }
1640
+ }
1641
+ },
1642
+ null,
1643
+ 2
1644
+ ),
1645
+ "```",
1646
+ "",
1647
+ "Then restart your WrongStack session to load the server."
1648
+ ].join("\n")
1649
+ };
1650
+ }
1651
+ if (result.dryRun) {
1652
+ return {
1653
+ message: [
1654
+ `${colorize("Dry run \u2014 would install:", "yellow")} ${lang}`,
1655
+ ` ${result.installCommand}`,
1656
+ "",
1657
+ "Run without --dry-run to actually install."
1658
+ ].join("\n")
1659
+ };
1660
+ }
1661
+ return {
1662
+ message: [
1663
+ `${colorize("Installed:", "green")} ${lang}`,
1664
+ ` Binary: ${colorize(server.binary, "cyan")}`,
1665
+ ` Method: ${result.packageManager === "system" ? server.toolchain.label : result.installCommand}`,
1666
+ "",
1667
+ "Add this to your WrongStack config to activate the server:",
1668
+ "",
1669
+ "```json",
1670
+ JSON.stringify(
1671
+ {
1672
+ extensions: {
1673
+ "@wrongstack/plug-lsp": {
1674
+ servers: {
1675
+ [lang]: {
1676
+ command: server.binary,
1677
+ args: server.args ?? ["--stdio"],
1678
+ languages: server.languages,
1679
+ rootPatterns: server.rootPatterns ?? []
1680
+ }
1681
+ }
1682
+ }
1683
+ }
1684
+ },
1685
+ null,
1686
+ 2
1687
+ ),
1688
+ "```",
1689
+ "",
1690
+ "Restart your WrongStack session to load the server, then run `/lsp start ${lang}`."
1691
+ ].join("\n")
1692
+ };
1693
+ } catch (err) {
1694
+ const msg = err instanceof Error ? err.message : String(err);
1695
+ return {
1696
+ message: `${colorize("Installation failed:", "red")} ${lang}
1697
+ ${msg}`
1698
+ };
1699
+ }
1700
+ }
1701
+ async function runStartCommand(ctx, name) {
1702
+ const targetName = name?.trim();
1703
+ if (targetName) {
1704
+ const srv = ctx.registry.list().find((s) => s.name === targetName);
1705
+ if (!srv) {
1706
+ const available = ctx.registry.list().map((s) => s.name);
1707
+ return {
1708
+ message: [
1709
+ `${colorize("Server not found:", "red")} ${targetName}`,
1710
+ available.length > 0 ? `Available: ${available.join(", ")}` : "No servers configured.",
1711
+ "Run `/lsp install <language>` to install a server."
1712
+ ].join("\n")
1713
+ };
1714
+ }
1715
+ try {
1716
+ await ctx.registry.start(targetName);
1717
+ return { message: `${colorize("Started:", "green")} ${targetName}` };
1718
+ } catch (err) {
1719
+ const msg = err instanceof Error ? err.message : String(err);
1720
+ return { message: `${colorize("Failed to start:", "red")} ${targetName}
1721
+ ${msg}` };
1722
+ }
1723
+ }
1724
+ const allServers = ctx.registry.list();
1725
+ const started = [];
1726
+ const failed = [];
1727
+ for (const srv of allServers) {
1728
+ if (!srv.config.enabled) continue;
1729
+ if (srv.state === "ready") {
1730
+ started.push(srv.name);
1731
+ continue;
1732
+ }
1733
+ if (srv.state === "disabled") continue;
1734
+ try {
1735
+ await ctx.registry.start(srv.name);
1736
+ started.push(srv.name);
1737
+ } catch {
1738
+ failed.push(srv.name);
1739
+ }
1740
+ }
1741
+ const lines = [];
1742
+ if (started.length > 0) lines.push(`${colorize("Started:", "green")} ${started.join(", ")}`);
1743
+ if (failed.length > 0) lines.push(`${colorize("Failed to start:", "red")} ${failed.join(", ")}`);
1744
+ if (started.length === 0 && failed.length === 0) {
1745
+ lines.push("No servers to start. Run `/lsp list` to see configured servers.");
1746
+ }
1747
+ return { message: lines.join("\n") };
1748
+ }
1749
+ async function runStopCommand(ctx, name) {
1750
+ const targetName = name?.trim();
1751
+ if (targetName) {
1752
+ const srv = ctx.registry.list().find((s) => s.name === targetName);
1753
+ if (!srv) {
1754
+ return { message: `${colorize("Server not found:", "red")} ${targetName}` };
1755
+ }
1756
+ ctx.registry.stop(targetName);
1757
+ return { message: `${colorize("Stopped:", "yellow")} ${targetName}` };
1758
+ }
1759
+ const allServers = ctx.registry.list();
1760
+ for (const srv of allServers) {
1761
+ ctx.registry.stop(srv.name);
1762
+ }
1763
+ return { message: `${colorize("Stopped", "yellow")} ${allServers.length} server(s): ${allServers.map((s) => s.name).join(", ")}` };
1764
+ }
1765
+ async function runRestartCommand(ctx, name) {
1766
+ const targetName = name?.trim();
1767
+ if (targetName) {
1768
+ const srv = ctx.registry.list().find((s) => s.name === targetName);
1769
+ if (!srv) {
1770
+ return { message: `${colorize("Server not found:", "red")} ${targetName}` };
1771
+ }
1772
+ ctx.registry.stop(targetName);
1773
+ try {
1774
+ await ctx.registry.start(targetName);
1775
+ return { message: `${colorize("Restarted:", "green")} ${targetName}` };
1776
+ } catch (err) {
1777
+ const msg = err instanceof Error ? err.message : String(err);
1778
+ return { message: `${colorize("Restart failed:", "red")} ${targetName}
1779
+ ${msg}` };
1780
+ }
1781
+ }
1782
+ const allServers = ctx.registry.list().filter((s) => s.config.enabled);
1783
+ const restarted = [];
1784
+ const failed = [];
1785
+ for (const srv of allServers) {
1786
+ ctx.registry.stop(srv.name);
1787
+ try {
1788
+ await ctx.registry.start(srv.name);
1789
+ restarted.push(srv.name);
1790
+ } catch {
1791
+ failed.push(srv.name);
1792
+ }
1793
+ }
1794
+ const lines = [];
1795
+ if (restarted.length > 0) lines.push(`${colorize("Restarted:", "green")} ${restarted.join(", ")}`);
1796
+ if (failed.length > 0) lines.push(`${colorize("Failed to restart:", "red")} ${failed.join(", ")}`);
1797
+ return { message: lines.join("\n") };
1798
+ }
1799
+ async function runDiagnosticsCommand(ctx, file) {
1800
+ const lines = [`${colorize("LSP Diagnostics", "bold")}`, "\u2500".repeat(60)];
1801
+ const allDiags = collectServerDiagnostics(ctx.registry);
1802
+ if (file) {
1803
+ const resolved = path.resolve(ctx.cwd, file);
1804
+ const fileDiags = allDiags.get(resolved);
1805
+ if (!fileDiags || fileDiags.length === 0) {
1806
+ return { message: lines.join("\n") + `
1807
+ No diagnostics for ${file}` };
1808
+ }
1809
+ lines.push(`File: ${resolved}`);
1810
+ const diagMap = /* @__PURE__ */ new Map([[resolved, fileDiags]]);
1811
+ lines.push(formatDiagnostics(diagMap, {
1812
+ cwd: ctx.cwd,
1813
+ severityFilter: ctx.cfg.severityFilter,
1814
+ maxPerFile: ctx.cfg.maxDiagnosticsPerFile,
1815
+ maxTotal: ctx.cfg.maxDiagnosticsTotal
1816
+ }));
1817
+ return { message: lines.join("\n") };
1818
+ }
1819
+ if (allDiags.size === 0) {
1820
+ lines.push("No diagnostics reported by any LSP server.");
1821
+ lines.push("");
1822
+ lines.push("LSP diagnostics are reported by language servers after you open/edit files.");
1823
+ lines.push("Open a file and run `/lsp diagnostics <file>` to check specific files.");
1824
+ return { message: lines.join("\n") };
1825
+ }
1826
+ const total = Array.from(allDiags.values()).reduce((sum, d) => sum + d.length, 0);
1827
+ lines.push(`Showing diagnostics for ${allDiags.size} file(s) (${total} total)`);
1828
+ lines.push("");
1829
+ for (const [fpath, diags] of allDiags) {
1830
+ lines.push(`${colorize(fpath, "cyan")}`);
1831
+ const fdiagMap = /* @__PURE__ */ new Map([[fpath, diags]]);
1832
+ lines.push(formatDiagnostics(fdiagMap, {
1833
+ cwd: ctx.cwd,
1834
+ severityFilter: ctx.cfg.severityFilter,
1835
+ maxPerFile: ctx.cfg.maxDiagnosticsPerFile,
1836
+ maxTotal: ctx.cfg.maxDiagnosticsTotal
1837
+ }));
1838
+ lines.push("");
1839
+ }
1840
+ return { message: lines.join("\n") };
1841
+ }
1842
+ function collectServerDiagnostics(registry) {
1843
+ const result = /* @__PURE__ */ new Map();
1844
+ for (const srv of registry.list()) {
1845
+ if (srv.state !== "ready") continue;
1846
+ for (const [uri, diags] of srv.diagnostics) {
1847
+ const filePath = uri.startsWith("file://") ? uri.slice(7) : uri;
1848
+ const existing = result.get(filePath) ?? [];
1849
+ result.set(filePath, [...existing, ...diags]);
1850
+ }
1851
+ }
1852
+ return result;
1853
+ }
1212
1854
 
1213
1855
  // src/slash-commands/index.ts
1214
- function registerSlashCommands(api, registry) {
1856
+ function registerSlashCommands(api, registry, tracker, cfg, cwd) {
1857
+ const lspCommand = buildLspCommand({ registry, tracker, cfg, cwd });
1215
1858
  const commands = [
1859
+ lspCommand,
1216
1860
  listCommand(registry),
1217
1861
  startCommand(registry),
1218
1862
  stopCommand(registry),
@@ -1223,79 +1867,47 @@ function registerSlashCommands(api, registry) {
1223
1867
  return commands.map((cmd) => cmd.name);
1224
1868
  }
1225
1869
 
1226
- // src/formatters/workspace-edit.ts
1227
- function summarizeWorkspaceEdit(edit, cwd) {
1228
- const entries = editsByPath(edit);
1229
- if (entries.size === 0) return "WorkspaceEdit contains no text edits.";
1230
- let total = 0;
1231
- const lines = ["Workspace edit:"];
1232
- for (const [file, edits] of entries) {
1233
- total += edits.length;
1234
- lines.push(` ${displayPath(file, cwd)} ${edits.length} edit(s)`);
1235
- }
1236
- lines.push(`Total: ${total} edits across ${entries.size} files.`);
1237
- return lines.join("\n");
1238
- }
1239
- function editsByPath(edit) {
1240
- const out = /* @__PURE__ */ new Map();
1241
- for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
1242
- out.set(uriToPath(uri), edits);
1243
- }
1244
- for (const change of edit.documentChanges ?? []) {
1245
- if ("textDocument" in change && Array.isArray(change.edits)) {
1246
- out.set(uriToPath(change.textDocument.uri), change.edits.filter((e) => "newText" in e));
1247
- }
1248
- }
1249
- return out;
1250
- }
1870
+ // src/constants.ts
1871
+ var LSP_CONSTANTS = Object.freeze({
1872
+ /** Default timeout for LSP tool operations (5 seconds). */
1873
+ TOOL_TIMEOUT_MS: 5e3
1874
+ });
1251
1875
 
1252
- // src/position.ts
1253
- function humanToLSP(content, pos) {
1254
- const lines = splitLines(content);
1255
- const lineIdx = clamp(pos.line - 1, 0, Math.max(0, lines.length - 1));
1256
- const line = lines[lineIdx] ?? "";
1257
- const byteCol = clamp(pos.character - 1, 0, Buffer.byteLength(line, "utf8"));
1258
- let bytes = 0;
1259
- let utf16 = 0;
1260
- for (const ch of line) {
1261
- const b = Buffer.byteLength(ch, "utf8");
1262
- if (bytes + b > byteCol) break;
1263
- bytes += b;
1264
- utf16 += ch.length;
1876
+ // src/formatters/symbols.ts
1877
+ function formatCodebaseLspResults(output, cwd) {
1878
+ const { results, totalIndex, totalLsp, query, usedIndex, usedLsp } = output;
1879
+ if (results.length === 0) {
1880
+ const sources = [];
1881
+ if (usedIndex) sources.push(`index(${totalIndex})`);
1882
+ if (usedLsp) sources.push(`lsp(${totalLsp})`);
1883
+ return `No symbols matching "${query}". Searched: ${sources.join(", ") || "none"}.`;
1265
1884
  }
1266
- return { line: lineIdx, character: utf16 };
1267
- }
1268
- function splitLines(content) {
1269
- if (content.length === 0) return [""];
1270
- return content.split(/\r\n|\r|\n/);
1271
- }
1272
- function clamp(n, min, max) {
1273
- if (!Number.isFinite(n)) return min;
1274
- return Math.max(min, Math.min(max, n));
1885
+ const lines = [];
1886
+ lines.push(`${results.length} results for "${query}" (index:${totalIndex} lsp:${totalLsp}):`);
1887
+ for (const r of results) {
1888
+ const sourceTag = r.source === "index" ? "[index]" : `[lsp:${r.server ?? "?"}]`;
1889
+ const scoreTag = r.score !== void 0 ? ` score=${r.score.toFixed(2)}` : "";
1890
+ const location = `${displayPath(r.file, cwd)}:${r.line}`;
1891
+ if (r.snippet) {
1892
+ lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1893
+ lines.push(` ${r.snippet}`);
1894
+ } else {
1895
+ lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1896
+ }
1897
+ }
1898
+ return lines.join("\n");
1275
1899
  }
1276
1900
 
1277
1901
  // src/server/capabilities.ts
1278
- function supportsHover(cap) {
1279
- return !!cap.hoverProvider;
1280
- }
1281
1902
  function supportsDefinition(cap) {
1282
1903
  return !!cap.definitionProvider;
1283
1904
  }
1284
- function supportsReferences(cap) {
1285
- return !!cap.referencesProvider;
1286
- }
1287
- function supportsDocumentSymbol(cap) {
1288
- return !!cap.documentSymbolProvider;
1289
- }
1290
1905
  function supportsWorkspaceSymbol(cap) {
1291
1906
  return !!cap.workspaceSymbolProvider;
1292
1907
  }
1293
1908
  function supportsRename(cap) {
1294
1909
  return !!cap.renameProvider;
1295
1910
  }
1296
- function supportsCodeAction(cap) {
1297
- return !!cap.codeActionProvider;
1298
- }
1299
1911
  function supportsPullDiagnostics(cap) {
1300
1912
  return !!cap.diagnosticProvider;
1301
1913
  }
@@ -1318,221 +1930,6 @@ function stringifyToolError(err) {
1318
1930
  if (err instanceof Error) return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${err.message}`;
1319
1931
  return `[${"LSP_PROTOCOL_ERROR" /* ProtocolError */}] ${String(err)}`;
1320
1932
  }
1321
- async function applyWorkspaceEdit(edit, tracker) {
1322
- const entries = editsByPath(edit);
1323
- const ops = [];
1324
- for (const [file, edits] of entries) {
1325
- const original = await fs2.readFile(file, "utf8");
1326
- ops.push({ path: file, original, next: applyTextEdits(original, edits), edits: edits.length });
1327
- }
1328
- const written = [];
1329
- try {
1330
- for (const op of ops) {
1331
- await atomicWrite(op.path, op.next);
1332
- written.push(op);
1333
- }
1334
- } catch (err) {
1335
- for (const op of written) {
1336
- try {
1337
- await atomicWrite(op.path, op.original);
1338
- } catch {
1339
- }
1340
- }
1341
- throw new LSPError("LSP_APPLY_EDIT_FAILED" /* ApplyEditFailed */, "Failed to apply workspace edit", err);
1342
- }
1343
- for (const op of ops) await tracker.fileWritten(op.path);
1344
- return { files: ops.map((op) => op.path), edits: ops.reduce((sum, op) => sum + op.edits, 0) };
1345
- }
1346
- function applyTextEdits(original, edits) {
1347
- const lineStarts = buildLineStarts(original);
1348
- const sorted = [...edits].sort(
1349
- (a, b) => offsetOf(b.range.start, lineStarts) - offsetOf(a.range.start, lineStarts)
1350
- );
1351
- let out = original;
1352
- for (const edit of sorted) {
1353
- const start = offsetOf(edit.range.start, lineStarts);
1354
- const end = offsetOf(edit.range.end, lineStarts);
1355
- out = out.slice(0, start) + edit.newText + out.slice(end);
1356
- }
1357
- return out;
1358
- }
1359
- function buildLineStarts(text) {
1360
- const starts = [0];
1361
- for (let i = 0; i < text.length; i++) {
1362
- const ch = text.charCodeAt(i);
1363
- if (ch === 10) starts.push(i + 1);
1364
- }
1365
- return starts;
1366
- }
1367
- function offsetOf(pos, lineStarts) {
1368
- return (lineStarts[pos.line] ?? lineStarts[lineStarts.length - 1] ?? 0) + pos.character;
1369
- }
1370
-
1371
- // src/tools/code-actions.ts
1372
- function createCodeActionsTool(deps) {
1373
- return {
1374
- name: "lsp_code_actions",
1375
- description: "List or apply LSP code actions.",
1376
- usageHint: "Use to inspect quick fixes and refactors. This tool is confirm-gated because apply mode can mutate files.",
1377
- inputSchema: {
1378
- type: "object",
1379
- properties: {
1380
- path: { type: "string" },
1381
- line: { type: "integer" },
1382
- character: { type: "integer" },
1383
- end_line: { type: "integer" },
1384
- end_character: { type: "integer" },
1385
- apply: { type: "integer" },
1386
- kind_filter: { type: "string" }
1387
- },
1388
- required: ["path", "line"]
1389
- },
1390
- permission: "confirm",
1391
- mutating: true,
1392
- timeoutMs: 1e4,
1393
- async execute(input, ctx, opts) {
1394
- try {
1395
- const file = resolveInputPath(input.path, ctx);
1396
- const server = await requireServer(deps.registry, file, opts.signal);
1397
- if (server.capabilities && !supportsCodeAction(server.capabilities)) {
1398
- throw new LSPError(
1399
- "LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
1400
- `Server "${server.name}" does not support code actions`
1401
- );
1402
- }
1403
- const content = await readDocumentContent(file, deps.tracker);
1404
- const start = humanToLSP(content, { line: input.line, character: input.character ?? 1 });
1405
- const end = humanToLSP(content, {
1406
- line: input.end_line ?? input.line,
1407
- character: input.end_character ?? input.character ?? 1
1408
- });
1409
- const actions = await server.codeAction(
1410
- {
1411
- textDocument: { uri: pathToUri(file) },
1412
- range: { start, end },
1413
- context: {
1414
- diagnostics: server.getDiagnostics(pathToUri(file)),
1415
- ...input.kind_filter ? { only: [input.kind_filter] } : {}
1416
- }
1417
- },
1418
- 1e4,
1419
- opts.signal
1420
- );
1421
- if (input.apply === void 0) return formatActions(actions);
1422
- const action = actions[input.apply];
1423
- if (!action) return `No code action at index ${input.apply}.`;
1424
- const parts = [`Applying [${input.apply}] ${action.title}`];
1425
- if (action.edit) {
1426
- parts.push(summarizeWorkspaceEdit(action.edit, ctx.cwd));
1427
- const applied = await applyWorkspaceEdit(action.edit, deps.tracker);
1428
- parts.push(`Applied: ${applied.edits} edits across ${applied.files.length} files.`);
1429
- }
1430
- if (action.command) {
1431
- await server.executeCommand(action.command, 1e4, opts.signal);
1432
- parts.push(`Executed command: ${action.command.command}`);
1433
- }
1434
- return parts.join("\n");
1435
- } catch (err) {
1436
- return stringifyToolError(err);
1437
- }
1438
- }
1439
- };
1440
- }
1441
- function formatActions(actions) {
1442
- if (actions.length === 0) return "No code actions available.";
1443
- return actions.map((a, i) => `[${i}] ${a.kind ?? "action"} ${a.title}`).join("\n");
1444
- }
1445
-
1446
- // src/constants.ts
1447
- var LSP_CONSTANTS = Object.freeze({
1448
- /** Default timeout for LSP tool operations (5 seconds). */
1449
- TOOL_TIMEOUT_MS: 5e3
1450
- });
1451
-
1452
- // src/formatters/symbols.ts
1453
- function formatDocumentSymbols(path8, symbols, cwd) {
1454
- if (!symbols || symbols.length === 0) return "No symbols found.";
1455
- const lines = [`${displayPath(path8, cwd)}:`];
1456
- for (const sym of symbols) appendSymbol(lines, sym, 1, cwd);
1457
- return lines.join("\n");
1458
- }
1459
- function formatWorkspaceSymbols(symbols, query, cwd, limit = 100) {
1460
- if (!symbols || symbols.length === 0) return `No symbols matching "${query}".`;
1461
- const lines = [`${symbols.length} symbols matching "${query}":`];
1462
- for (const sym of symbols.slice(0, limit)) {
1463
- lines.push(
1464
- ` ${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1465
- );
1466
- }
1467
- if (symbols.length > limit) lines.push(` ... truncated ${symbols.length - limit} more`);
1468
- return lines.join("\n");
1469
- }
1470
- function appendSymbol(lines, sym, depth, cwd) {
1471
- const indent = " ".repeat(depth);
1472
- if ("selectionRange" in sym) {
1473
- lines.push(
1474
- `${indent}${kindName(sym.kind)} ${sym.name} (L${sym.selectionRange.start.line + 1})`
1475
- );
1476
- for (const child of sym.children ?? []) appendSymbol(lines, child, depth + 1, cwd);
1477
- } else {
1478
- lines.push(
1479
- `${indent}${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1480
- );
1481
- }
1482
- }
1483
- function kindName(kind) {
1484
- return [
1485
- "file",
1486
- "module",
1487
- "namespace",
1488
- "package",
1489
- "class",
1490
- "method",
1491
- "property",
1492
- "field",
1493
- "constructor",
1494
- "enum",
1495
- "interface",
1496
- "function",
1497
- "variable",
1498
- "constant",
1499
- "string",
1500
- "number",
1501
- "boolean",
1502
- "array",
1503
- "object",
1504
- "key",
1505
- "null",
1506
- "enumMember",
1507
- "struct",
1508
- "event",
1509
- "operator",
1510
- "typeParameter"
1511
- ][kind - 1] ?? "symbol";
1512
- }
1513
- function formatCodebaseLspResults(output, cwd) {
1514
- const { results, totalIndex, totalLsp, query, usedIndex, usedLsp } = output;
1515
- if (results.length === 0) {
1516
- const sources = [];
1517
- if (usedIndex) sources.push(`index(${totalIndex})`);
1518
- if (usedLsp) sources.push(`lsp(${totalLsp})`);
1519
- return `No symbols matching "${query}". Searched: ${sources.join(", ") || "none"}.`;
1520
- }
1521
- const lines = [];
1522
- lines.push(`${results.length} results for "${query}" (index:${totalIndex} lsp:${totalLsp}):`);
1523
- for (const r of results) {
1524
- const sourceTag = r.source === "index" ? "[index]" : `[lsp:${r.server ?? "?"}]`;
1525
- const scoreTag = r.score !== void 0 ? ` score=${r.score.toFixed(2)}` : "";
1526
- const location = `${displayPath(r.file, cwd)}:${r.line}`;
1527
- if (r.snippet) {
1528
- lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1529
- lines.push(` ${r.snippet}`);
1530
- } else {
1531
- lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1532
- }
1533
- }
1534
- return lines.join("\n");
1535
- }
1536
1933
 
1537
1934
  // src/tools/codebase-lsp-search.ts
1538
1935
  function createCodebaseLspSearchTool(deps) {
@@ -1732,6 +2129,31 @@ function formatLocations(locations, cwd, limit = 100) {
1732
2129
  return lines.join("\n");
1733
2130
  }
1734
2131
 
2132
+ // src/position.ts
2133
+ function humanToLSP(content, pos) {
2134
+ const lines = splitLines(content);
2135
+ const lineIdx = clamp(pos.line - 1, 0, Math.max(0, lines.length - 1));
2136
+ const line = lines[lineIdx] ?? "";
2137
+ const byteCol = clamp(pos.character - 1, 0, Buffer.byteLength(line, "utf8"));
2138
+ let bytes = 0;
2139
+ let utf16 = 0;
2140
+ for (const ch of line) {
2141
+ const b = Buffer.byteLength(ch, "utf8");
2142
+ if (bytes + b > byteCol) break;
2143
+ bytes += b;
2144
+ utf16 += ch.length;
2145
+ }
2146
+ return { line: lineIdx, character: utf16 };
2147
+ }
2148
+ function splitLines(content) {
2149
+ if (content.length === 0) return [""];
2150
+ return content.split(/\r\n|\r|\n/);
2151
+ }
2152
+ function clamp(n, min, max) {
2153
+ if (!Number.isFinite(n)) return min;
2154
+ return Math.max(min, Math.min(max, n));
2155
+ }
2156
+
1735
2157
  // src/tools/definition.ts
1736
2158
  function createDefinitionTool(deps) {
1737
2159
  return {
@@ -1818,118 +2240,79 @@ function createDiagnosticsTool(deps) {
1818
2240
  };
1819
2241
  }
1820
2242
 
1821
- // src/formatters/hover.ts
1822
- function formatHover(hover, maxChars = 4096) {
1823
- if (!hover) return "No hover information.";
1824
- const text = hoverContentsToString(hover.contents).trim();
1825
- if (!text) return "No hover information.";
1826
- return text.length > maxChars ? `${text.slice(0, maxChars)}
1827
- ...[truncated]` : text;
1828
- }
1829
- function hoverContentsToString(contents) {
1830
- if (typeof contents === "string") return contents;
1831
- if (Array.isArray(contents)) return contents.map(markedStringToString).join("\n\n");
1832
- if ("kind" in contents && "value" in contents) return contents.value;
1833
- return markedStringToString(contents);
1834
- }
1835
- function markedStringToString(value) {
1836
- if (typeof value === "string") return value;
1837
- return `\`\`\`${value.language}
1838
- ${value.value}
1839
- \`\`\``;
2243
+ // src/formatters/workspace-edit.ts
2244
+ function summarizeWorkspaceEdit(edit, cwd) {
2245
+ const entries = editsByPath(edit);
2246
+ if (entries.size === 0) return "WorkspaceEdit contains no text edits.";
2247
+ let total = 0;
2248
+ const lines = ["Workspace edit:"];
2249
+ for (const [file, edits] of entries) {
2250
+ total += edits.length;
2251
+ lines.push(` ${displayPath(file, cwd)} ${edits.length} edit(s)`);
2252
+ }
2253
+ lines.push(`Total: ${total} edits across ${entries.size} files.`);
2254
+ return lines.join("\n");
1840
2255
  }
1841
-
1842
- // src/tools/hover.ts
1843
- function createHoverTool(deps) {
1844
- return {
1845
- name: "lsp_hover",
1846
- description: "Get type information and documentation for a symbol.",
1847
- usageHint: "Use when you need a type/signature without opening the definition.",
1848
- inputSchema: {
1849
- type: "object",
1850
- properties: {
1851
- path: { type: "string" },
1852
- line: { type: "integer" },
1853
- character: { type: "integer" }
1854
- },
1855
- required: ["path", "line", "character"]
1856
- },
1857
- permission: "auto",
1858
- mutating: false,
1859
- timeoutMs: LSP_CONSTANTS.TOOL_TIMEOUT_MS,
1860
- async execute(input, ctx, opts) {
1861
- try {
1862
- const file = resolveInputPath(input.path, ctx);
1863
- const server = await requireServer(deps.registry, file, opts.signal);
1864
- if (server.capabilities && !supportsHover(server.capabilities)) {
1865
- throw new LSPError(
1866
- "LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
1867
- `Server "${server.name}" does not support hover`
1868
- );
1869
- }
1870
- const content = await readDocumentContent(file, deps.tracker);
1871
- const position = humanToLSP(content, { line: input.line, character: input.character });
1872
- return formatHover(
1873
- await server.hover(
1874
- { textDocument: { uri: pathToUri(file) }, position },
1875
- LSP_CONSTANTS.TOOL_TIMEOUT_MS,
1876
- opts.signal
1877
- )
1878
- );
1879
- } catch (err) {
1880
- return stringifyToolError(err);
1881
- }
2256
+ function editsByPath(edit) {
2257
+ const out = /* @__PURE__ */ new Map();
2258
+ for (const [uri, edits] of Object.entries(edit.changes ?? {})) {
2259
+ out.set(uriToPath(uri), edits);
2260
+ }
2261
+ for (const change of edit.documentChanges ?? []) {
2262
+ if ("textDocument" in change && Array.isArray(change.edits)) {
2263
+ out.set(uriToPath(change.textDocument.uri), change.edits.filter((e) => "newText" in e));
1882
2264
  }
1883
- };
2265
+ }
2266
+ return out;
1884
2267
  }
1885
-
1886
- // src/tools/references.ts
1887
- function createReferencesTool(deps) {
1888
- return {
1889
- name: "lsp_references",
1890
- description: "Find references to a symbol.",
1891
- usageHint: "Use instead of grep when the symbol position is known; it is syntax-aware.",
1892
- inputSchema: {
1893
- type: "object",
1894
- properties: {
1895
- path: { type: "string" },
1896
- line: { type: "integer" },
1897
- character: { type: "integer" },
1898
- include_declaration: { type: "boolean" },
1899
- limit: { type: "integer" }
1900
- },
1901
- required: ["path", "line", "character"]
1902
- },
1903
- permission: "auto",
1904
- mutating: false,
1905
- timeoutMs: 1e4,
1906
- async execute(input, ctx, opts) {
2268
+ async function applyWorkspaceEdit(edit, tracker) {
2269
+ const entries = editsByPath(edit);
2270
+ const ops = [];
2271
+ for (const [file, edits] of entries) {
2272
+ const original = await fs2.readFile(file, "utf8");
2273
+ ops.push({ path: file, original, next: applyTextEdits(original, edits), edits: edits.length });
2274
+ }
2275
+ const written = [];
2276
+ try {
2277
+ for (const op of ops) {
2278
+ await atomicWrite(op.path, op.next);
2279
+ written.push(op);
2280
+ }
2281
+ } catch (err) {
2282
+ for (const op of written) {
1907
2283
  try {
1908
- const file = resolveInputPath(input.path, ctx);
1909
- const server = await requireServer(deps.registry, file, opts.signal);
1910
- if (server.capabilities && !supportsReferences(server.capabilities)) {
1911
- throw new LSPError(
1912
- "LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
1913
- `Server "${server.name}" does not support references`
1914
- );
1915
- }
1916
- const content = await readDocumentContent(file, deps.tracker);
1917
- const position = humanToLSP(content, { line: input.line, character: input.character });
1918
- const locs = await server.references(
1919
- {
1920
- textDocument: { uri: pathToUri(file) },
1921
- position,
1922
- context: { includeDeclaration: input.include_declaration ?? true }
1923
- },
1924
- 1e4,
1925
- opts.signal
1926
- );
1927
- return formatLocations(locs, ctx.cwd, input.limit ?? 100);
1928
- } catch (err) {
1929
- return stringifyToolError(err);
2284
+ await atomicWrite(op.path, op.original);
2285
+ } catch {
1930
2286
  }
1931
2287
  }
1932
- };
2288
+ throw new LSPError("LSP_APPLY_EDIT_FAILED" /* ApplyEditFailed */, "Failed to apply workspace edit", err);
2289
+ }
2290
+ for (const op of ops) await tracker.fileWritten(op.path);
2291
+ return { files: ops.map((op) => op.path), edits: ops.reduce((sum, op) => sum + op.edits, 0) };
2292
+ }
2293
+ function applyTextEdits(original, edits) {
2294
+ const lineStarts = buildLineStarts(original);
2295
+ const sorted = [...edits].sort(
2296
+ (a, b) => offsetOf(b.range.start, lineStarts) - offsetOf(a.range.start, lineStarts)
2297
+ );
2298
+ let out = original;
2299
+ for (const edit of sorted) {
2300
+ const start = offsetOf(edit.range.start, lineStarts);
2301
+ const end = offsetOf(edit.range.end, lineStarts);
2302
+ out = out.slice(0, start) + edit.newText + out.slice(end);
2303
+ }
2304
+ return out;
2305
+ }
2306
+ function buildLineStarts(text) {
2307
+ const starts = [0];
2308
+ for (let i = 0; i < text.length; i++) {
2309
+ const ch = text.charCodeAt(i);
2310
+ if (ch === 10) starts.push(i + 1);
2311
+ }
2312
+ return starts;
2313
+ }
2314
+ function offsetOf(pos, lineStarts) {
2315
+ return (lineStarts[pos.line] ?? lineStarts[lineStarts.length - 1] ?? 0) + pos.character;
1933
2316
  }
1934
2317
 
1935
2318
  // src/tools/rename.ts
@@ -1985,68 +2368,13 @@ Applied: ${applied.edits} edits across ${applied.files.length} files.`;
1985
2368
  };
1986
2369
  }
1987
2370
 
1988
- // src/tools/symbols.ts
1989
- function createSymbolsTool(deps) {
1990
- return {
1991
- name: "lsp_symbols",
1992
- description: "List symbols in a file or search workspace symbols.",
1993
- usageHint: "Pass `path` for a file outline, or `query` for workspace symbol search.",
1994
- inputSchema: {
1995
- type: "object",
1996
- properties: {
1997
- path: { type: "string" },
1998
- query: { type: "string" },
1999
- limit: { type: "integer" }
2000
- }
2001
- },
2002
- permission: "auto",
2003
- mutating: false,
2004
- timeoutMs: LSP_CONSTANTS.TOOL_TIMEOUT_MS,
2005
- async execute(input, ctx, opts) {
2006
- try {
2007
- if (input.path) {
2008
- const file = resolveInputPath(input.path, ctx);
2009
- const server = await requireServer(deps.registry, file, opts.signal);
2010
- if (server.capabilities && !supportsDocumentSymbol(server.capabilities)) {
2011
- throw new LSPError(
2012
- "LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
2013
- `Server "${server.name}" does not support document symbols`
2014
- );
2015
- }
2016
- const symbols = await server.documentSymbol(
2017
- { textDocument: { uri: pathToUri(file) } },
2018
- LSP_CONSTANTS.TOOL_TIMEOUT_MS,
2019
- opts.signal
2020
- );
2021
- return formatDocumentSymbols(file, symbols, ctx.cwd);
2022
- }
2023
- const query = input.query ?? "";
2024
- const merged = [];
2025
- for (const server of deps.registry.list()) {
2026
- if (server.state !== "ready") continue;
2027
- if (server.capabilities && !supportsWorkspaceSymbol(server.capabilities)) continue;
2028
- const result = await server.workspaceSymbol({ query }, LSP_CONSTANTS.TOOL_TIMEOUT_MS, opts.signal);
2029
- if (result) merged.push(...result);
2030
- }
2031
- return formatWorkspaceSymbols(merged, query, ctx.cwd, input.limit ?? 100);
2032
- } catch (err) {
2033
- return stringifyToolError(err);
2034
- }
2035
- }
2036
- };
2037
- }
2038
-
2039
2371
  // src/tools/index.ts
2040
2372
  function makeLSPTools(deps) {
2041
2373
  return [
2042
2374
  createDiagnosticsTool(deps),
2043
2375
  createDefinitionTool(deps),
2044
- createReferencesTool(deps),
2045
- createHoverTool(deps),
2046
- createSymbolsTool(deps),
2047
2376
  createCodebaseLspSearchTool(deps),
2048
- createRenameTool(deps),
2049
- createCodeActionsTool(deps)
2377
+ createRenameTool(deps)
2050
2378
  ];
2051
2379
  }
2052
2380
 
@@ -2076,7 +2404,7 @@ var plugin = {
2076
2404
  await registry.bind(cwd, cfg.autoStart);
2077
2405
  const tools = makeLSPTools({ registry, tracker, cfg, log: api.log });
2078
2406
  for (const tool of tools) api.tools.register(tool);
2079
- const commandNames = registerSlashCommands(api, registry);
2407
+ const commandNames = registerSlashCommands(api, registry, tracker, cfg, cwd);
2080
2408
  const offs = [
2081
2409
  api.events.on("session.started", () => {
2082
2410
  const nextCwd = api.config.cwd ?? process.cwd();