@spences10/pi-lsp 0.0.10 → 0.0.12

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,913 +1,24 @@
1
- import { defineTool, } from '@mariozechner/pi-coding-agent';
2
- import { resolve_project_trust } from '@spences10/pi-project-trust';
3
- import { show_picker_modal, show_text_modal, } from '@spences10/pi-tui-modal';
4
- import { readFile } from 'node:fs/promises';
5
- import { isAbsolute, resolve } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
- import { Type } from 'typebox';
8
- import { file_path_to_uri, LspClient, LspClientStartError, } from './client.js';
9
- import { detect_language, find_workspace_root, get_server_config, language_id_for_file, list_supported_languages, } from './servers.js';
10
- import { create_lsp_binary_trust_subject, default_lsp_trust_store_path, is_lsp_binary_trusted, } from './trust.js';
11
- class LspToolError extends Error {
12
- details;
13
- constructor(details) {
14
- super(details.message);
15
- this.name = 'LspToolError';
16
- this.details = details;
17
- }
18
- }
19
- class LspStartupCancelledError extends Error {
20
- constructor(language, workspace_root) {
21
- super(`Startup cancelled for ${language} LSP in ${workspace_root}`);
22
- this.name = 'LspStartupCancelledError';
23
- }
24
- }
25
- const SYMBOL_KIND_LABELS = {
26
- 2: 'module',
27
- 3: 'namespace',
28
- 5: 'class',
29
- 6: 'method',
30
- 7: 'property',
31
- 8: 'field',
32
- 9: 'constructor',
33
- 11: 'interface',
34
- 12: 'function',
35
- 13: 'variable',
36
- 14: 'constant',
37
- 23: 'struct',
38
- 24: 'event',
39
- };
40
- const SYMBOL_KIND_NAMES = Object.values(SYMBOL_KIND_LABELS);
41
- const SYMBOL_KIND_SCHEMA = Type.Union(SYMBOL_KIND_NAMES.map((name) => Type.Literal(name)));
42
- const LSP_TOOL_NAMES = new Set([
43
- 'lsp_diagnostics',
44
- 'lsp_diagnostics_many',
45
- 'lsp_find_symbol',
46
- 'lsp_hover',
47
- 'lsp_definition',
48
- 'lsp_references',
49
- 'lsp_document_symbols',
50
- ]);
51
- const DIAGNOSTICS_MANY_CONCURRENCY = 8;
52
- const LSP_PROJECT_BINARY_ENV = 'MY_PI_LSP_PROJECT_BINARY';
53
- async function should_use_project_lsp_binary(server_config, ctx) {
54
- if (!server_config.is_project_local)
55
- return true;
56
- if (is_lsp_binary_trusted(server_config.command))
57
- return true;
58
- const subject = {
59
- ...create_lsp_binary_trust_subject(server_config.command),
60
- prompt_title: 'Project-local language server binaries can execute code.\nTrust this LSP binary?',
61
- summary_lines: [
62
- `Language: ${server_config.language}`,
63
- `Binary: ${server_config.command}`,
64
- ],
65
- headless_warning: `Skipping untrusted project-local LSP binary: ${server_config.command}. Set ${LSP_PROJECT_BINARY_ENV}=allow to enable it for this run.`,
66
- };
67
- const decision = await resolve_project_trust(subject, {
68
- env: process.env,
69
- has_ui: ctx?.hasUI,
70
- select: ctx?.hasUI
71
- ? async (message, choices) => (await ctx.ui.select(message, choices)) ?? ''
72
- : undefined,
73
- warn: console.warn,
74
- trust_store_path: default_lsp_trust_store_path(),
75
- });
76
- return (decision.action === 'allow-once' ||
77
- decision.action === 'trust-persisted');
78
- }
79
- export function should_inject_lsp_prompt(event) {
80
- const selected_tools = event.systemPromptOptions?.selectedTools;
81
- return (!selected_tools ||
82
- selected_tools.some((tool) => LSP_TOOL_NAMES.has(tool)));
83
- }
84
- async function map_with_concurrency(items, concurrency, mapper) {
85
- const results = [];
86
- let next_index = 0;
87
- const worker_count = Math.min(concurrency, items.length);
88
- await Promise.all(Array.from({ length: worker_count }, async () => {
89
- while (true) {
90
- const index = next_index;
91
- next_index += 1;
92
- if (index >= items.length)
93
- return;
94
- results[index] = await mapper(items[index], index);
95
- }
96
- }));
97
- return results;
98
- }
1
+ import { register_lsp_command } from './commands.js';
2
+ import { append_lsp_system_prompt, should_inject_lsp_prompt, } from './prompt.js';
3
+ import { LspServerManager, } from './server-manager.js';
4
+ import { register_lsp_tools } from './tools.js';
5
+ export { should_inject_lsp_prompt } from './prompt.js';
99
6
  export function create_lsp_extension(options = {}) {
100
- const create_client = options.create_client ??
101
- ((client_options) => new LspClient(client_options));
102
- const read_file = options.read_file ?? ((path) => readFile(path, 'utf-8'));
103
7
  return async function lsp(pi) {
104
- const cwd = options.cwd?.() ?? process.cwd();
105
- const clients_by_server = new Map();
106
- const failed_servers = new Map();
107
- const starting_servers = new Map();
108
- const resolve_abs = (file) => isAbsolute(file) ? file : resolve(cwd, file);
109
- const server_key = (language, workspace_root) => `${language}\u0000${workspace_root}`;
110
- const make_tool_result = (text, details = {}) => ({
111
- content: [{ type: 'text', text }],
112
- details,
113
- });
114
- const make_tool_error = (details) => make_tool_result(format_tool_error(details), {
115
- ok: false,
116
- error: details,
117
- });
118
- const clear_language_state = async (language) => {
119
- const states = language
120
- ? Array.from(clients_by_server.entries()).filter(([, state]) => state.language === language)
121
- : Array.from(clients_by_server.entries());
122
- const starting = language
123
- ? Array.from(starting_servers.entries()).filter(([key]) => key.startsWith(`${language}\u0000`))
124
- : Array.from(starting_servers.entries());
125
- for (const [key, startup] of starting) {
126
- startup.cancelled = true;
127
- starting_servers.delete(key);
128
- }
129
- await Promise.allSettled(states.map(([, state]) => state.client.stop()));
130
- for (const [key] of states) {
131
- clients_by_server.delete(key);
132
- }
133
- if (!language) {
134
- failed_servers.clear();
135
- return;
136
- }
137
- for (const [key, failure] of failed_servers.entries()) {
138
- if (failure.language === language) {
139
- failed_servers.delete(key);
140
- }
141
- }
142
- };
143
- const get_or_start_client = async (file_path, ctx) => {
144
- const language = detect_language(file_path);
145
- if (!language)
146
- return undefined;
147
- const workspace_root = find_workspace_root(file_path, cwd);
148
- const key = server_key(language, workspace_root);
149
- const existing = clients_by_server.get(key);
150
- if (existing)
151
- return existing;
152
- const failed = failed_servers.get(key);
153
- if (failed) {
154
- throw new LspToolError(failed);
155
- }
156
- const in_flight = starting_servers.get(key);
157
- if (in_flight)
158
- return in_flight.promise;
159
- let server_config = get_server_config(language, workspace_root);
160
- if (!server_config)
161
- return undefined;
162
- if (server_config.is_project_local &&
163
- !(await should_use_project_lsp_binary(server_config, ctx))) {
164
- server_config = get_server_config(language, '/');
165
- if (!server_config)
166
- return undefined;
167
- }
168
- const root_uri = file_path_to_uri(workspace_root);
169
- const startup = {
170
- cancelled: false,
171
- promise: Promise.resolve(undefined),
172
- };
173
- const start_promise = (async () => {
174
- const client = create_client({
175
- command: server_config.command,
176
- args: server_config.args,
177
- root_uri,
178
- language_id_for_uri: (uri) => language_id_for_file(uri),
179
- });
180
- try {
181
- await client.start();
182
- }
183
- catch (error) {
184
- if (startup.cancelled) {
185
- throw new LspStartupCancelledError(language, workspace_root);
186
- }
187
- const failure = to_lsp_tool_error(file_path, language, workspace_root, server_config.command, server_config.install_hint, error);
188
- failed_servers.set(key, failure);
189
- throw new LspToolError(failure);
190
- }
191
- if (startup.cancelled) {
192
- await Promise.allSettled([client.stop()]);
193
- throw new LspStartupCancelledError(language, workspace_root);
194
- }
195
- const state = {
196
- client,
197
- language,
198
- workspace_root,
199
- root_uri,
200
- command: server_config.command,
201
- install_hint: server_config.install_hint,
202
- };
203
- clients_by_server.set(key, state);
204
- failed_servers.delete(key);
205
- return state;
206
- })();
207
- startup.promise = start_promise;
208
- starting_servers.set(key, startup);
209
- try {
210
- return await start_promise;
211
- }
212
- finally {
213
- if (starting_servers.get(key) === startup) {
214
- starting_servers.delete(key);
215
- }
216
- }
217
- };
218
- const open_file = async (state, abs_path) => {
219
- const text = await read_file(abs_path);
220
- const uri = file_path_to_uri(abs_path);
221
- await state.client.ensure_document_open(uri, text);
222
- return uri;
223
- };
224
- const get_file_state = async (file, ctx) => {
225
- const abs = resolve_abs(file);
226
- const state = await get_or_start_client(abs, ctx);
227
- if (!state)
228
- return undefined;
229
- const uri = await open_file(state, abs);
230
- return { abs, uri, state };
231
- };
232
- const resolve_file_state = async (file, ctx) => {
233
- const abs = resolve_abs(file);
234
- try {
235
- const result = await get_file_state(abs, ctx);
236
- if (!result) {
237
- return {
238
- ok: false,
239
- error: {
240
- kind: 'unsupported_language',
241
- file: abs,
242
- message: `No language server configured for ${abs}`,
243
- },
244
- };
245
- }
246
- return {
247
- ok: true,
248
- result,
249
- };
250
- }
251
- catch (error) {
252
- if (error instanceof LspToolError) {
253
- return {
254
- ok: false,
255
- error: error.details,
256
- };
257
- }
258
- return {
259
- ok: false,
260
- error: {
261
- kind: 'tool_execution_failed',
262
- file: abs,
263
- message: error instanceof Error ? error.message : String(error),
264
- },
265
- };
266
- }
267
- };
268
- const with_file_state = async (file, ctx, run) => {
269
- const resolved = await resolve_file_state(file, ctx);
270
- if (!resolved.ok) {
271
- return make_tool_error(resolved.error);
272
- }
273
- const { result } = resolved;
274
- try {
275
- const text = await run(result);
276
- return make_tool_result(text, {
277
- ok: true,
278
- language: result.state.language,
279
- command: result.state.command,
280
- workspace_root: result.state.workspace_root,
281
- });
282
- }
283
- catch (error) {
284
- return make_tool_error(to_lsp_tool_error(result.abs, result.state.language, result.state.workspace_root, result.state.command, result.state.install_hint, error));
285
- }
286
- };
287
- pi.registerTool(defineTool({
288
- name: 'lsp_diagnostics',
289
- label: 'LSP: diagnostics',
290
- description: 'Get language server diagnostics (errors, warnings, hints) for a file. Uses the project language server and returns empty output if the file is clean.',
291
- parameters: Type.Object({
292
- file: Type.String({
293
- description: 'Path to the file to check (relative to cwd or absolute).',
294
- }),
295
- wait_ms: Type.Optional(Type.Number({
296
- description: 'Max ms to wait for diagnostics after opening the file. Default 1500.',
297
- })),
298
- }),
299
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
300
- const diagnostics = await result.state.client.wait_for_diagnostics(result.uri, params.wait_ms ?? 1500);
301
- return format_diagnostics(result.abs, diagnostics);
302
- }),
303
- }));
304
- pi.registerTool(defineTool({
305
- name: 'lsp_diagnostics_many',
306
- label: 'LSP: diagnostics many',
307
- description: 'Get language server diagnostics for multiple files in one call. Useful for package-level sweeps and summarization.',
308
- parameters: Type.Object({
309
- files: Type.Array(Type.String(), {
310
- minItems: 1,
311
- maxItems: 100,
312
- description: 'Files to check (relative to cwd or absolute).',
313
- }),
314
- wait_ms: Type.Optional(Type.Number({
315
- description: 'Max ms to wait for diagnostics after opening each file. Default 1500.',
316
- })),
317
- }),
318
- execute: async (_id, params, _signal, _on_update, ctx) => {
319
- const wait_ms = params.wait_ms ?? 1500;
320
- const lines_with_stats = await map_with_concurrency(params.files, DIAGNOSTICS_MANY_CONCURRENCY, async (file) => {
321
- const resolved = await resolve_file_state(file, ctx);
322
- if (!resolved.ok) {
323
- return {
324
- line: format_tool_error(resolved.error),
325
- diagnostics: 0,
326
- error: true,
327
- };
328
- }
329
- try {
330
- const diagnostics = await resolved.result.state.client.wait_for_diagnostics(resolved.result.uri, wait_ms);
331
- return {
332
- line: format_diagnostics(resolved.result.abs, diagnostics),
333
- diagnostics: diagnostics.length,
334
- error: false,
335
- };
336
- }
337
- catch (error) {
338
- return {
339
- line: format_tool_error(to_lsp_tool_error(resolved.result.abs, resolved.result.state.language, resolved.result.state.workspace_root, resolved.result.state.command, resolved.result.state.install_hint, error)),
340
- diagnostics: 0,
341
- error: true,
342
- };
343
- }
344
- });
345
- let diagnostic_count = 0;
346
- let clean_count = 0;
347
- let error_count = 0;
348
- const lines = [];
349
- for (const entry of lines_with_stats) {
350
- lines.push(entry.line);
351
- if (entry.error) {
352
- error_count += 1;
353
- }
354
- else {
355
- diagnostic_count += entry.diagnostics;
356
- if (entry.diagnostics === 0)
357
- clean_count += 1;
358
- }
359
- }
360
- return make_tool_result([
361
- `Checked ${params.files.length} file(s): ${diagnostic_count} diagnostic(s), ${clean_count} clean, ${error_count} error(s)`,
362
- ...lines,
363
- ].join('\n\n'), {
364
- ok: error_count === 0,
365
- checked: params.files.length,
366
- diagnostic_count,
367
- clean_count,
368
- error_count,
369
- });
370
- },
371
- }));
372
- pi.registerTool(defineTool({
373
- name: 'lsp_find_symbol',
374
- label: 'LSP: find symbol',
375
- description: 'Find symbols in a file by name or detail text using document symbols. Supports exact matching, kind filters, and top-level-only mode.',
376
- parameters: Type.Object({
377
- file: Type.String(),
378
- query: Type.String({
379
- description: 'Substring to match against symbol names/details.',
380
- }),
381
- max_results: Type.Optional(Type.Number({
382
- description: 'Max number of matches to return. Default 20.',
383
- })),
384
- top_level_only: Type.Optional(Type.Boolean({
385
- description: 'Only match top-level symbols. Default false.',
386
- })),
387
- exact_match: Type.Optional(Type.Boolean({
388
- description: 'Match whole symbol names/details exactly instead of substring matching. Default false.',
389
- })),
390
- kinds: Type.Optional(Type.Array(SYMBOL_KIND_SCHEMA, {
391
- minItems: 1,
392
- maxItems: SYMBOL_KIND_NAMES.length,
393
- description: 'Restrict matches to these symbol kinds.',
394
- })),
395
- }),
396
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
397
- const symbols = await result.state.client.document_symbols(result.uri);
398
- const matches = find_symbol_matches(symbols, params.query, {
399
- max_results: params.max_results ?? 20,
400
- top_level_only: params.top_level_only ?? false,
401
- exact_match: params.exact_match ?? false,
402
- kinds: new Set(params.kinds ?? []),
403
- });
404
- return format_symbol_matches(result.abs, params.query, matches);
405
- }),
406
- }));
407
- pi.registerTool(defineTool({
408
- name: 'lsp_hover',
409
- label: 'LSP: hover',
410
- description: 'Get hover info (types, docs) at a position in a file. Positions are zero-based.',
411
- parameters: Type.Object({
412
- file: Type.String(),
413
- line: Type.Number({
414
- description: 'Zero-based line number.',
415
- }),
416
- character: Type.Number({
417
- description: 'Zero-based character offset.',
418
- }),
419
- }),
420
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
421
- const hover = await result.state.client.hover(result.uri, {
422
- line: params.line,
423
- character: params.character,
424
- });
425
- return format_hover(hover);
426
- }),
427
- }));
428
- pi.registerTool(defineTool({
429
- name: 'lsp_definition',
430
- label: 'LSP: go to definition',
431
- description: 'Find definition locations for the symbol at a position. Positions are zero-based.',
432
- parameters: Type.Object({
433
- file: Type.String(),
434
- line: Type.Number(),
435
- character: Type.Number(),
436
- }),
437
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
438
- const locations = await result.state.client.definition(result.uri, {
439
- line: params.line,
440
- character: params.character,
441
- });
442
- return format_locations(locations, 'No definition found.');
443
- }),
444
- }));
445
- pi.registerTool(defineTool({
446
- name: 'lsp_references',
447
- label: 'LSP: find references',
448
- description: 'Find references to the symbol at a position. Positions are zero-based.',
449
- parameters: Type.Object({
450
- file: Type.String(),
451
- line: Type.Number(),
452
- character: Type.Number(),
453
- include_declaration: Type.Optional(Type.Boolean()),
454
- }),
455
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
456
- const locations = await result.state.client.references(result.uri, {
457
- line: params.line,
458
- character: params.character,
459
- }, params.include_declaration ?? true);
460
- return format_locations(locations, 'No references found.');
461
- }),
462
- }));
463
- pi.registerTool(defineTool({
464
- name: 'lsp_document_symbols',
465
- label: 'LSP: document symbols',
466
- description: 'List symbols in a file (functions, classes, variables) using the language server.',
467
- parameters: Type.Object({
468
- file: Type.String(),
469
- }),
470
- execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
471
- const symbols = await result.state.client.document_symbols(result.uri);
472
- return format_document_symbols(result.abs, symbols);
473
- }),
474
- }));
8
+ const manager = new LspServerManager(options);
9
+ register_lsp_tools(pi, manager);
475
10
  pi.on('before_agent_start', async (event) => {
476
11
  if (!should_inject_lsp_prompt(event))
477
12
  return {};
478
13
  return {
479
- systemPrompt: event.systemPrompt +
480
- `
481
-
482
- ## Language server support via LSP tools
483
-
484
- You have access to Language Server Protocol tools for diagnostics, hover/type information, definitions, references, and document symbols. Use them when:
485
- - Debugging TypeScript, JavaScript, Svelte, or other language-server-supported errors
486
- - Checking types, symbol definitions, or API documentation from code
487
- - Finding references more precisely than text search
488
- - Validating focused code changes before reporting completion
489
-
490
- Prefer LSP diagnostics over guessing from build output when a file-level check is enough. Use text search for broad discovery, then LSP tools for precise type and symbol questions.`,
14
+ systemPrompt: append_lsp_system_prompt(event.systemPrompt),
491
15
  };
492
16
  });
493
- pi.registerCommand('lsp', {
494
- description: 'Show or manage language server state',
495
- getArgumentCompletions: (prefix) => {
496
- const parts = prefix.trim().split(/\s+/);
497
- const subcommands = ['status', 'list', 'restart'];
498
- if (!prefix.trim()) {
499
- return subcommands.map((value) => ({
500
- value,
501
- label: value,
502
- }));
503
- }
504
- if (parts.length <= 1) {
505
- return subcommands
506
- .filter((value) => value.startsWith(parts[0]))
507
- .map((value) => ({ value, label: value }));
508
- }
509
- if (parts[0] === 'restart') {
510
- const candidate = parts[1] ?? '';
511
- return ['all', ...list_supported_languages()]
512
- .filter((value) => value.startsWith(candidate))
513
- .map((value) => ({
514
- value: `restart ${value}`,
515
- label: value,
516
- }));
517
- }
518
- return null;
519
- },
520
- handler: async (args, ctx) => {
521
- await handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state);
522
- },
523
- });
17
+ register_lsp_command(pi, manager);
524
18
  pi.on('session_shutdown', async () => {
525
- await clear_language_state();
19
+ await manager.clear_language_state();
526
20
  });
527
21
  };
528
22
  }
529
23
  export default create_lsp_extension();
530
- async function handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state) {
531
- const parts = args.trim() ? args.trim().split(/\s+/, 2) : [];
532
- if (parts.length === 0 && has_modal_ui(ctx)) {
533
- while (true) {
534
- const selected = await show_lsp_home_modal(ctx, cwd, clients_by_server, failed_servers);
535
- if (!selected)
536
- return;
537
- if (selected === 'restart') {
538
- await handle_lsp_restart_modal(ctx, clear_language_state);
539
- continue;
540
- }
541
- if (selected === 'restart-all') {
542
- await restart_all_lsp_servers(ctx, clear_language_state);
543
- continue;
544
- }
545
- await show_lsp_text_modal(ctx, selected === 'running'
546
- ? 'Running LSP servers'
547
- : selected === 'failed'
548
- ? 'Failed LSP servers'
549
- : 'LSP status', format_lsp_view(selected, cwd, clients_by_server, failed_servers));
550
- }
551
- }
552
- const [subcommand = 'status', target] = parts;
553
- switch (subcommand) {
554
- case 'status':
555
- case 'list':
556
- await present_lsp_text(ctx, 'LSP status', format_status_lines(cwd, clients_by_server, failed_servers).join('\n'));
557
- return;
558
- case 'restart': {
559
- if (!target && has_modal_ui(ctx)) {
560
- await handle_lsp_restart_modal(ctx, clear_language_state);
561
- return;
562
- }
563
- if (!target || target === 'all') {
564
- await clear_language_state();
565
- ctx.ui.notify('Restarted all language server state.');
566
- return;
567
- }
568
- if (!list_supported_languages().includes(target)) {
569
- ctx.ui.notify(`Unknown language: ${target}. Use one of: ${list_supported_languages().join(', ')}`, 'warning');
570
- return;
571
- }
572
- await clear_language_state(target);
573
- ctx.ui.notify(`Restarted ${target} language server state.`);
574
- return;
575
- }
576
- default:
577
- ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: status, list, restart`, 'warning');
578
- }
579
- }
580
- function has_modal_ui(ctx) {
581
- return ctx.hasUI && typeof ctx.ui.custom === 'function';
582
- }
583
- async function present_lsp_text(ctx, title, text) {
584
- if (has_modal_ui(ctx)) {
585
- await show_lsp_text_modal(ctx, title, text);
586
- return;
587
- }
588
- ctx.ui.notify(text);
589
- }
590
- async function show_lsp_home_modal(ctx, cwd, clients_by_server, failed_servers) {
591
- const running_count = clients_by_server.size;
592
- const failed_count = failed_servers.size;
593
- return await show_picker_modal(ctx, {
594
- title: 'Language servers',
595
- subtitle: `${running_count} running • ${failed_count} failed • ${list_supported_languages().length} supported`,
596
- items: [
597
- {
598
- value: 'status',
599
- label: 'Status',
600
- description: `All configured language servers for ${cwd}`,
601
- },
602
- {
603
- value: 'running',
604
- label: 'Running servers',
605
- description: `${running_count} active workspace server(s)`,
606
- },
607
- {
608
- value: 'failed',
609
- label: 'Failed servers',
610
- description: `${failed_count} failed server(s)`,
611
- },
612
- {
613
- value: 'restart',
614
- label: 'Restart server',
615
- description: 'Pick a supported language to restart',
616
- },
617
- {
618
- value: 'restart-all',
619
- label: 'Restart all',
620
- description: 'Stop every running language server',
621
- },
622
- ],
623
- footer: 'enter opens • esc close/back',
624
- });
625
- }
626
- async function show_lsp_text_modal(ctx, title, text) {
627
- await show_text_modal(ctx, {
628
- title,
629
- text,
630
- max_visible_lines: 20,
631
- overlay_options: { width: '90%', minWidth: 72 },
632
- });
633
- }
634
- async function handle_lsp_restart_modal(ctx, clear_language_state) {
635
- const selected = await show_picker_modal(ctx, {
636
- title: 'Restart LSP server',
637
- subtitle: 'Clear cached language server state',
638
- items: [
639
- {
640
- value: 'all',
641
- label: 'All servers',
642
- description: 'Stop every running language server',
643
- },
644
- ...list_supported_languages().map((language) => ({
645
- value: language,
646
- label: language,
647
- description: `Restart ${language} language server state`,
648
- })),
649
- ],
650
- footer: 'enter restarts • esc back',
651
- });
652
- if (!selected)
653
- return;
654
- if (selected === 'all') {
655
- await restart_all_lsp_servers(ctx, clear_language_state);
656
- return;
657
- }
658
- await clear_language_state(selected);
659
- ctx.ui.notify(`Restarted ${selected} language server state.`);
660
- }
661
- async function restart_all_lsp_servers(ctx, clear_language_state) {
662
- await clear_language_state();
663
- ctx.ui.notify('Restarted all language server state.');
664
- }
665
- function format_lsp_view(view, cwd, clients_by_server, failed_servers) {
666
- if (view === 'running') {
667
- const lines = format_running_server_lines(clients_by_server);
668
- return lines.length > 0
669
- ? lines.join('\n')
670
- : 'No running language servers.';
671
- }
672
- if (view === 'failed') {
673
- const lines = format_failed_server_lines(failed_servers);
674
- return lines.length > 0
675
- ? lines.join('\n')
676
- : 'No failed language servers.';
677
- }
678
- return format_status_lines(cwd, clients_by_server, failed_servers).join('\n');
679
- }
680
- function format_running_server_lines(clients_by_server) {
681
- return Array.from(clients_by_server.values())
682
- .sort((a, b) => a.language.localeCompare(b.language) ||
683
- a.workspace_root.localeCompare(b.workspace_root))
684
- .map((state) => `${state.language}: running (ready=${state.client.is_ready()}) — ${state.command} [workspace ${state.workspace_root}]`);
685
- }
686
- function format_failed_server_lines(failed_servers) {
687
- return Array.from(failed_servers.values())
688
- .sort((a, b) => (a.language ?? '').localeCompare(b.language ?? '') ||
689
- (a.workspace_root ?? '').localeCompare(b.workspace_root ?? ''))
690
- .map((failure) => {
691
- const workspace = failure.workspace_root
692
- ? ` [workspace ${failure.workspace_root}]`
693
- : '';
694
- return `${failure.language ?? 'unknown'}: failed — ${failure.message}${workspace}`;
695
- });
696
- }
697
- function format_status_lines(cwd, clients_by_server, failed_servers) {
698
- const lines = [];
699
- const active_languages = new Set();
700
- const running_states = Array.from(clients_by_server.values()).sort((a, b) => a.language.localeCompare(b.language) ||
701
- a.workspace_root.localeCompare(b.workspace_root));
702
- for (const running of running_states) {
703
- active_languages.add(running.language);
704
- lines.push(`${running.language}: running (ready=${running.client.is_ready()}) — ${running.command} [workspace ${running.workspace_root}]`);
705
- }
706
- const failures = Array.from(failed_servers.values()).sort((a, b) => (a.language ?? '').localeCompare(b.language ?? '') ||
707
- (a.workspace_root ?? '').localeCompare(b.workspace_root ?? ''));
708
- for (const failure of failures) {
709
- if (failure.language) {
710
- active_languages.add(failure.language);
711
- }
712
- const workspace = failure.workspace_root
713
- ? ` [workspace ${failure.workspace_root}]`
714
- : '';
715
- const language = failure.language ?? 'unknown';
716
- lines.push(`${language}: failed — ${failure.message}${workspace}`);
717
- }
718
- for (const language of list_supported_languages()) {
719
- if (active_languages.has(language))
720
- continue;
721
- const config = get_server_config(language, cwd);
722
- if (config) {
723
- lines.push(`${language}: idle — ${config.command}`);
724
- }
725
- }
726
- return lines.length > 0
727
- ? lines
728
- : ['No language servers configured for this project.'];
729
- }
730
- function to_lsp_tool_error(file, language, workspace_root, command, install_hint, error) {
731
- if (error instanceof LspToolError) {
732
- return error.details;
733
- }
734
- if (error instanceof LspClientStartError) {
735
- const missing_binary = error.code === 'ENOENT';
736
- return {
737
- kind: 'server_start_failed',
738
- file,
739
- language,
740
- workspace_root,
741
- command,
742
- install_hint,
743
- code: error.code,
744
- message: missing_binary
745
- ? `command "${command}" not found`
746
- : error.message,
747
- };
748
- }
749
- return {
750
- kind: 'tool_execution_failed',
751
- file,
752
- language,
753
- workspace_root,
754
- command,
755
- install_hint,
756
- message: error instanceof Error ? error.message : String(error),
757
- code: typeof error === 'object' &&
758
- error !== null &&
759
- 'code' in error &&
760
- typeof error.code === 'string'
761
- ? error.code
762
- : undefined,
763
- };
764
- }
765
- function format_tool_error(details) {
766
- if (details.kind === 'unsupported_language') {
767
- return details.message;
768
- }
769
- const lines = [
770
- details.language
771
- ? `${details.language} LSP unavailable for ${details.file}`
772
- : `LSP request failed for ${details.file}`,
773
- `Reason: ${details.message}`,
774
- ];
775
- if (details.command) {
776
- lines.push(`Command: ${details.command}`);
777
- }
778
- if (details.workspace_root) {
779
- lines.push(`Workspace: ${details.workspace_root}`);
780
- }
781
- if (details.install_hint) {
782
- lines.push(`Hint: ${details.install_hint}`);
783
- }
784
- return lines.join('\n');
785
- }
786
- function severity_label(severity) {
787
- switch (severity) {
788
- case 1:
789
- return 'error';
790
- case 2:
791
- return 'warning';
792
- case 3:
793
- return 'info';
794
- case 4:
795
- return 'hint';
796
- default:
797
- return 'info';
798
- }
799
- }
800
- function format_diagnostics(file, diagnostics) {
801
- if (diagnostics.length === 0) {
802
- return `${file}: no diagnostics`;
803
- }
804
- const lines = [`${file}: ${diagnostics.length} diagnostic(s)`];
805
- for (const d of diagnostics) {
806
- const position = `${d.range.start.line + 1}:${d.range.start.character + 1}`;
807
- const source = d.source ? ` [${d.source}]` : '';
808
- const code = d.code != null ? ` (${d.code})` : '';
809
- lines.push(` ${position} ${severity_label(d.severity)}${source}${code}: ${d.message}`);
810
- }
811
- return lines.join('\n');
812
- }
813
- function format_hover(hover) {
814
- if (!hover)
815
- return 'No hover info.';
816
- const contents = hover.contents;
817
- const extract = (item) => (typeof item === 'string' ? item : (item.value ?? ''));
818
- if (Array.isArray(contents)) {
819
- return (contents.map(extract).join('\n\n').trim() || 'No hover info.');
820
- }
821
- return extract(contents).trim() || 'No hover info.';
822
- }
823
- function format_locations(locations, empty_message) {
824
- if (locations.length === 0)
825
- return empty_message;
826
- return locations
827
- .map((loc) => {
828
- const path = file_url_to_path_or_value(loc.uri);
829
- return `${path}:${loc.range.start.line + 1}:${loc.range.start.character + 1}`;
830
- })
831
- .join('\n');
832
- }
833
- function format_document_symbols(file, symbols) {
834
- if (symbols.length === 0) {
835
- return `${file}: no symbols`;
836
- }
837
- const lines = [`${file}: ${symbols.length} top-level symbol(s)`];
838
- append_symbol_lines(lines, symbols, 1);
839
- return lines.join('\n');
840
- }
841
- function find_symbol_matches(symbols, query, options) {
842
- const normalized = query.trim().toLowerCase();
843
- if (!normalized)
844
- return [];
845
- const matches = [];
846
- const matches_query = (symbol) => {
847
- const values = [symbol.name, symbol.detail ?? ''].map((value) => value.trim().toLowerCase());
848
- return options.exact_match
849
- ? values.some((value) => value === normalized)
850
- : values.some((value) => value.includes(normalized));
851
- };
852
- const matches_kind = (symbol) => {
853
- if (options.kinds.size === 0)
854
- return true;
855
- return options.kinds.has(symbol_kind_label(symbol.kind));
856
- };
857
- const visit = (entries, depth) => {
858
- for (const symbol of entries) {
859
- if (matches_kind(symbol) && matches_query(symbol)) {
860
- matches.push({ symbol, depth });
861
- if (matches.length >= options.max_results) {
862
- return;
863
- }
864
- }
865
- if (!options.top_level_only && symbol.children?.length) {
866
- visit(symbol.children, depth + 1);
867
- if (matches.length >= options.max_results) {
868
- return;
869
- }
870
- }
871
- }
872
- };
873
- visit(symbols, 1);
874
- return matches;
875
- }
876
- function format_symbol_matches(file, query, matches) {
877
- if (matches.length === 0) {
878
- return `${file}: no symbols matching "${query}"`;
879
- }
880
- const lines = [
881
- `${file}: ${matches.length} symbol match(es) for "${query}"`,
882
- ];
883
- for (const { symbol, depth } of matches) {
884
- const indent = ' '.repeat(depth);
885
- const detail = symbol.detail ? ` — ${symbol.detail}` : '';
886
- const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
887
- lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
888
- }
889
- return lines.join('\n');
890
- }
891
- function append_symbol_lines(lines, symbols, depth) {
892
- for (const symbol of symbols) {
893
- const indent = ' '.repeat(depth);
894
- const detail = symbol.detail ? ` — ${symbol.detail}` : '';
895
- const range = `${symbol.range.start.line + 1}:${symbol.range.start.character + 1}`;
896
- lines.push(`${indent}${symbol_kind_label(symbol.kind)} ${symbol.name}${detail} @ ${range}`);
897
- if (symbol.children?.length) {
898
- append_symbol_lines(lines, symbol.children, depth + 1);
899
- }
900
- }
901
- }
902
- function symbol_kind_label(kind) {
903
- return SYMBOL_KIND_LABELS[kind] ?? 'symbol';
904
- }
905
- function file_url_to_path_or_value(uri) {
906
- try {
907
- return uri.startsWith('file:') ? fileURLToPath(uri) : uri;
908
- }
909
- catch {
910
- return uri;
911
- }
912
- }
913
24
  //# sourceMappingURL=index.js.map