@spences10/pi-lsp 0.0.11 → 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/README.md +3 -1
- package/dist/commands.d.ts +4 -0
- package/dist/commands.js +172 -0
- package/dist/commands.js.map +1 -0
- package/dist/index.d.ts +5 -18
- package/dist/index.js +10 -626
- package/dist/index.js.map +1 -1
- package/dist/prompt.d.ts +5 -0
- package/dist/prompt.js +31 -0
- package/dist/prompt.js.map +1 -0
- package/dist/server-manager.d.ts +49 -0
- package/dist/server-manager.js +205 -0
- package/dist/server-manager.js.map +1 -0
- package/dist/tools.d.ts +3 -0
- package/dist/tools.js +236 -0
- package/dist/tools.js.map +1 -0
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,640 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
import { Type } from 'typebox';
|
|
7
|
-
import { file_path_to_uri, LspClient, } from './client.js';
|
|
8
|
-
import { find_symbol_matches, format_diagnostics, format_document_symbols, format_hover, format_locations, format_lsp_view, format_status_lines, format_symbol_matches, format_tool_error, LspToolError, SYMBOL_KIND_NAMES, SYMBOL_KIND_SCHEMA, to_lsp_tool_error, } from './format.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 LspStartupCancelledError extends Error {
|
|
12
|
-
constructor(language, workspace_root) {
|
|
13
|
-
super(`Startup cancelled for ${language} LSP in ${workspace_root}`);
|
|
14
|
-
this.name = 'LspStartupCancelledError';
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
const LSP_TOOL_NAMES = new Set([
|
|
18
|
-
'lsp_diagnostics',
|
|
19
|
-
'lsp_diagnostics_many',
|
|
20
|
-
'lsp_find_symbol',
|
|
21
|
-
'lsp_hover',
|
|
22
|
-
'lsp_definition',
|
|
23
|
-
'lsp_references',
|
|
24
|
-
'lsp_document_symbols',
|
|
25
|
-
]);
|
|
26
|
-
const DIAGNOSTICS_MANY_CONCURRENCY = 8;
|
|
27
|
-
const LSP_PROJECT_BINARY_ENV = 'MY_PI_LSP_PROJECT_BINARY';
|
|
28
|
-
async function should_use_project_lsp_binary(server_config, ctx) {
|
|
29
|
-
if (!server_config.is_project_local)
|
|
30
|
-
return true;
|
|
31
|
-
if (is_lsp_binary_trusted(server_config.command))
|
|
32
|
-
return true;
|
|
33
|
-
const subject = {
|
|
34
|
-
...create_lsp_binary_trust_subject(server_config.command),
|
|
35
|
-
prompt_title: 'Project-local language server binaries can execute code.\nTrust this LSP binary?',
|
|
36
|
-
summary_lines: [
|
|
37
|
-
`Language: ${server_config.language}`,
|
|
38
|
-
`Binary: ${server_config.command}`,
|
|
39
|
-
],
|
|
40
|
-
headless_warning: `Skipping untrusted project-local LSP binary: ${server_config.command}. Set ${LSP_PROJECT_BINARY_ENV}=allow to enable it for this run.`,
|
|
41
|
-
};
|
|
42
|
-
const decision = await resolve_project_trust(subject, {
|
|
43
|
-
env: process.env,
|
|
44
|
-
has_ui: ctx?.hasUI,
|
|
45
|
-
select: ctx?.hasUI
|
|
46
|
-
? async (message, choices) => (await ctx.ui.select(message, choices)) ?? ''
|
|
47
|
-
: undefined,
|
|
48
|
-
warn: console.warn,
|
|
49
|
-
trust_store_path: default_lsp_trust_store_path(),
|
|
50
|
-
});
|
|
51
|
-
return (decision.action === 'allow-once' ||
|
|
52
|
-
decision.action === 'trust-persisted');
|
|
53
|
-
}
|
|
54
|
-
export function should_inject_lsp_prompt(event) {
|
|
55
|
-
const selected_tools = event.systemPromptOptions?.selectedTools;
|
|
56
|
-
return (!selected_tools ||
|
|
57
|
-
selected_tools.some((tool) => LSP_TOOL_NAMES.has(tool)));
|
|
58
|
-
}
|
|
59
|
-
async function map_with_concurrency(items, concurrency, mapper) {
|
|
60
|
-
const results = [];
|
|
61
|
-
let next_index = 0;
|
|
62
|
-
const worker_count = Math.min(concurrency, items.length);
|
|
63
|
-
await Promise.all(Array.from({ length: worker_count }, async () => {
|
|
64
|
-
while (true) {
|
|
65
|
-
const index = next_index;
|
|
66
|
-
next_index += 1;
|
|
67
|
-
if (index >= items.length)
|
|
68
|
-
return;
|
|
69
|
-
results[index] = await mapper(items[index], index);
|
|
70
|
-
}
|
|
71
|
-
}));
|
|
72
|
-
return results;
|
|
73
|
-
}
|
|
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';
|
|
74
6
|
export function create_lsp_extension(options = {}) {
|
|
75
|
-
const create_client = options.create_client ??
|
|
76
|
-
((client_options) => new LspClient(client_options));
|
|
77
|
-
const read_file = options.read_file ?? ((path) => readFile(path, 'utf-8'));
|
|
78
7
|
return async function lsp(pi) {
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
const failed_servers = new Map();
|
|
82
|
-
const starting_servers = new Map();
|
|
83
|
-
const resolve_abs = (file) => isAbsolute(file) ? file : resolve(cwd, file);
|
|
84
|
-
const server_key = (language, workspace_root) => `${language}\u0000${workspace_root}`;
|
|
85
|
-
const make_tool_result = (text, details = {}) => ({
|
|
86
|
-
content: [{ type: 'text', text }],
|
|
87
|
-
details,
|
|
88
|
-
});
|
|
89
|
-
const make_tool_error = (details) => make_tool_result(format_tool_error(details), {
|
|
90
|
-
ok: false,
|
|
91
|
-
error: details,
|
|
92
|
-
});
|
|
93
|
-
const clear_language_state = async (language) => {
|
|
94
|
-
const states = language
|
|
95
|
-
? Array.from(clients_by_server.entries()).filter(([, state]) => state.language === language)
|
|
96
|
-
: Array.from(clients_by_server.entries());
|
|
97
|
-
const starting = language
|
|
98
|
-
? Array.from(starting_servers.entries()).filter(([key]) => key.startsWith(`${language}\u0000`))
|
|
99
|
-
: Array.from(starting_servers.entries());
|
|
100
|
-
for (const [key, startup] of starting) {
|
|
101
|
-
startup.cancelled = true;
|
|
102
|
-
starting_servers.delete(key);
|
|
103
|
-
}
|
|
104
|
-
await Promise.allSettled(states.map(([, state]) => state.client.stop()));
|
|
105
|
-
for (const [key] of states) {
|
|
106
|
-
clients_by_server.delete(key);
|
|
107
|
-
}
|
|
108
|
-
if (!language) {
|
|
109
|
-
failed_servers.clear();
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
for (const [key, failure] of failed_servers.entries()) {
|
|
113
|
-
if (failure.language === language) {
|
|
114
|
-
failed_servers.delete(key);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
const get_or_start_client = async (file_path, ctx) => {
|
|
119
|
-
const language = detect_language(file_path);
|
|
120
|
-
if (!language)
|
|
121
|
-
return undefined;
|
|
122
|
-
const workspace_root = find_workspace_root(file_path, cwd);
|
|
123
|
-
const key = server_key(language, workspace_root);
|
|
124
|
-
const existing = clients_by_server.get(key);
|
|
125
|
-
if (existing)
|
|
126
|
-
return existing;
|
|
127
|
-
const failed = failed_servers.get(key);
|
|
128
|
-
if (failed) {
|
|
129
|
-
throw new LspToolError(failed);
|
|
130
|
-
}
|
|
131
|
-
const in_flight = starting_servers.get(key);
|
|
132
|
-
if (in_flight)
|
|
133
|
-
return in_flight.promise;
|
|
134
|
-
let server_config = get_server_config(language, workspace_root);
|
|
135
|
-
if (!server_config)
|
|
136
|
-
return undefined;
|
|
137
|
-
if (server_config.is_project_local &&
|
|
138
|
-
!(await should_use_project_lsp_binary(server_config, ctx))) {
|
|
139
|
-
server_config = get_server_config(language, '/');
|
|
140
|
-
if (!server_config)
|
|
141
|
-
return undefined;
|
|
142
|
-
}
|
|
143
|
-
const root_uri = file_path_to_uri(workspace_root);
|
|
144
|
-
const startup = {
|
|
145
|
-
cancelled: false,
|
|
146
|
-
promise: Promise.resolve(undefined),
|
|
147
|
-
};
|
|
148
|
-
const start_promise = (async () => {
|
|
149
|
-
const client = create_client({
|
|
150
|
-
command: server_config.command,
|
|
151
|
-
args: server_config.args,
|
|
152
|
-
root_uri,
|
|
153
|
-
language_id_for_uri: (uri) => language_id_for_file(uri),
|
|
154
|
-
});
|
|
155
|
-
try {
|
|
156
|
-
await client.start();
|
|
157
|
-
}
|
|
158
|
-
catch (error) {
|
|
159
|
-
if (startup.cancelled) {
|
|
160
|
-
throw new LspStartupCancelledError(language, workspace_root);
|
|
161
|
-
}
|
|
162
|
-
const failure = to_lsp_tool_error(file_path, language, workspace_root, server_config.command, server_config.install_hint, error);
|
|
163
|
-
failed_servers.set(key, failure);
|
|
164
|
-
throw new LspToolError(failure);
|
|
165
|
-
}
|
|
166
|
-
if (startup.cancelled) {
|
|
167
|
-
await Promise.allSettled([client.stop()]);
|
|
168
|
-
throw new LspStartupCancelledError(language, workspace_root);
|
|
169
|
-
}
|
|
170
|
-
const state = {
|
|
171
|
-
client,
|
|
172
|
-
language,
|
|
173
|
-
workspace_root,
|
|
174
|
-
root_uri,
|
|
175
|
-
command: server_config.command,
|
|
176
|
-
install_hint: server_config.install_hint,
|
|
177
|
-
};
|
|
178
|
-
clients_by_server.set(key, state);
|
|
179
|
-
failed_servers.delete(key);
|
|
180
|
-
return state;
|
|
181
|
-
})();
|
|
182
|
-
startup.promise = start_promise;
|
|
183
|
-
starting_servers.set(key, startup);
|
|
184
|
-
try {
|
|
185
|
-
return await start_promise;
|
|
186
|
-
}
|
|
187
|
-
finally {
|
|
188
|
-
if (starting_servers.get(key) === startup) {
|
|
189
|
-
starting_servers.delete(key);
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
};
|
|
193
|
-
const open_file = async (state, abs_path) => {
|
|
194
|
-
const text = await read_file(abs_path);
|
|
195
|
-
const uri = file_path_to_uri(abs_path);
|
|
196
|
-
await state.client.ensure_document_open(uri, text);
|
|
197
|
-
return uri;
|
|
198
|
-
};
|
|
199
|
-
const get_file_state = async (file, ctx) => {
|
|
200
|
-
const abs = resolve_abs(file);
|
|
201
|
-
const state = await get_or_start_client(abs, ctx);
|
|
202
|
-
if (!state)
|
|
203
|
-
return undefined;
|
|
204
|
-
const uri = await open_file(state, abs);
|
|
205
|
-
return { abs, uri, state };
|
|
206
|
-
};
|
|
207
|
-
const resolve_file_state = async (file, ctx) => {
|
|
208
|
-
const abs = resolve_abs(file);
|
|
209
|
-
try {
|
|
210
|
-
const result = await get_file_state(abs, ctx);
|
|
211
|
-
if (!result) {
|
|
212
|
-
return {
|
|
213
|
-
ok: false,
|
|
214
|
-
error: {
|
|
215
|
-
kind: 'unsupported_language',
|
|
216
|
-
file: abs,
|
|
217
|
-
message: `No language server configured for ${abs}`,
|
|
218
|
-
},
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
return {
|
|
222
|
-
ok: true,
|
|
223
|
-
result,
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
catch (error) {
|
|
227
|
-
if (error instanceof LspToolError) {
|
|
228
|
-
return {
|
|
229
|
-
ok: false,
|
|
230
|
-
error: error.details,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
return {
|
|
234
|
-
ok: false,
|
|
235
|
-
error: {
|
|
236
|
-
kind: 'tool_execution_failed',
|
|
237
|
-
file: abs,
|
|
238
|
-
message: error instanceof Error ? error.message : String(error),
|
|
239
|
-
},
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
};
|
|
243
|
-
const with_file_state = async (file, ctx, run) => {
|
|
244
|
-
const resolved = await resolve_file_state(file, ctx);
|
|
245
|
-
if (!resolved.ok) {
|
|
246
|
-
return make_tool_error(resolved.error);
|
|
247
|
-
}
|
|
248
|
-
const { result } = resolved;
|
|
249
|
-
try {
|
|
250
|
-
const text = await run(result);
|
|
251
|
-
return make_tool_result(text, {
|
|
252
|
-
ok: true,
|
|
253
|
-
language: result.state.language,
|
|
254
|
-
command: result.state.command,
|
|
255
|
-
workspace_root: result.state.workspace_root,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
catch (error) {
|
|
259
|
-
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));
|
|
260
|
-
}
|
|
261
|
-
};
|
|
262
|
-
pi.registerTool(defineTool({
|
|
263
|
-
name: 'lsp_diagnostics',
|
|
264
|
-
label: 'LSP: diagnostics',
|
|
265
|
-
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.',
|
|
266
|
-
parameters: Type.Object({
|
|
267
|
-
file: Type.String({
|
|
268
|
-
description: 'Path to the file to check (relative to cwd or absolute).',
|
|
269
|
-
}),
|
|
270
|
-
wait_ms: Type.Optional(Type.Number({
|
|
271
|
-
description: 'Max ms to wait for diagnostics after opening the file. Default 1500.',
|
|
272
|
-
})),
|
|
273
|
-
}),
|
|
274
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
275
|
-
const diagnostics = await result.state.client.wait_for_diagnostics(result.uri, params.wait_ms ?? 1500);
|
|
276
|
-
return format_diagnostics(result.abs, diagnostics);
|
|
277
|
-
}),
|
|
278
|
-
}));
|
|
279
|
-
pi.registerTool(defineTool({
|
|
280
|
-
name: 'lsp_diagnostics_many',
|
|
281
|
-
label: 'LSP: diagnostics many',
|
|
282
|
-
description: 'Get language server diagnostics for multiple files in one call. Useful for package-level sweeps and summarization.',
|
|
283
|
-
parameters: Type.Object({
|
|
284
|
-
files: Type.Array(Type.String(), {
|
|
285
|
-
minItems: 1,
|
|
286
|
-
maxItems: 100,
|
|
287
|
-
description: 'Files to check (relative to cwd or absolute).',
|
|
288
|
-
}),
|
|
289
|
-
wait_ms: Type.Optional(Type.Number({
|
|
290
|
-
description: 'Max ms to wait for diagnostics after opening each file. Default 1500.',
|
|
291
|
-
})),
|
|
292
|
-
}),
|
|
293
|
-
execute: async (_id, params, _signal, _on_update, ctx) => {
|
|
294
|
-
const wait_ms = params.wait_ms ?? 1500;
|
|
295
|
-
const lines_with_stats = await map_with_concurrency(params.files, DIAGNOSTICS_MANY_CONCURRENCY, async (file) => {
|
|
296
|
-
const resolved = await resolve_file_state(file, ctx);
|
|
297
|
-
if (!resolved.ok) {
|
|
298
|
-
return {
|
|
299
|
-
line: format_tool_error(resolved.error),
|
|
300
|
-
diagnostics: 0,
|
|
301
|
-
error: true,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
try {
|
|
305
|
-
const diagnostics = await resolved.result.state.client.wait_for_diagnostics(resolved.result.uri, wait_ms);
|
|
306
|
-
return {
|
|
307
|
-
line: format_diagnostics(resolved.result.abs, diagnostics),
|
|
308
|
-
diagnostics: diagnostics.length,
|
|
309
|
-
error: false,
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
|
-
catch (error) {
|
|
313
|
-
return {
|
|
314
|
-
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)),
|
|
315
|
-
diagnostics: 0,
|
|
316
|
-
error: true,
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
let diagnostic_count = 0;
|
|
321
|
-
let clean_count = 0;
|
|
322
|
-
let error_count = 0;
|
|
323
|
-
const lines = [];
|
|
324
|
-
for (const entry of lines_with_stats) {
|
|
325
|
-
lines.push(entry.line);
|
|
326
|
-
if (entry.error) {
|
|
327
|
-
error_count += 1;
|
|
328
|
-
}
|
|
329
|
-
else {
|
|
330
|
-
diagnostic_count += entry.diagnostics;
|
|
331
|
-
if (entry.diagnostics === 0)
|
|
332
|
-
clean_count += 1;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
return make_tool_result([
|
|
336
|
-
`Checked ${params.files.length} file(s): ${diagnostic_count} diagnostic(s), ${clean_count} clean, ${error_count} error(s)`,
|
|
337
|
-
...lines,
|
|
338
|
-
].join('\n\n'), {
|
|
339
|
-
ok: error_count === 0,
|
|
340
|
-
checked: params.files.length,
|
|
341
|
-
diagnostic_count,
|
|
342
|
-
clean_count,
|
|
343
|
-
error_count,
|
|
344
|
-
});
|
|
345
|
-
},
|
|
346
|
-
}));
|
|
347
|
-
pi.registerTool(defineTool({
|
|
348
|
-
name: 'lsp_find_symbol',
|
|
349
|
-
label: 'LSP: find symbol',
|
|
350
|
-
description: 'Find symbols in a file by name or detail text using document symbols. Supports exact matching, kind filters, and top-level-only mode.',
|
|
351
|
-
parameters: Type.Object({
|
|
352
|
-
file: Type.String(),
|
|
353
|
-
query: Type.String({
|
|
354
|
-
description: 'Substring to match against symbol names/details.',
|
|
355
|
-
}),
|
|
356
|
-
max_results: Type.Optional(Type.Number({
|
|
357
|
-
description: 'Max number of matches to return. Default 20.',
|
|
358
|
-
})),
|
|
359
|
-
top_level_only: Type.Optional(Type.Boolean({
|
|
360
|
-
description: 'Only match top-level symbols. Default false.',
|
|
361
|
-
})),
|
|
362
|
-
exact_match: Type.Optional(Type.Boolean({
|
|
363
|
-
description: 'Match whole symbol names/details exactly instead of substring matching. Default false.',
|
|
364
|
-
})),
|
|
365
|
-
kinds: Type.Optional(Type.Array(SYMBOL_KIND_SCHEMA, {
|
|
366
|
-
minItems: 1,
|
|
367
|
-
maxItems: SYMBOL_KIND_NAMES.length,
|
|
368
|
-
description: 'Restrict matches to these symbol kinds.',
|
|
369
|
-
})),
|
|
370
|
-
}),
|
|
371
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
372
|
-
const symbols = await result.state.client.document_symbols(result.uri);
|
|
373
|
-
const matches = find_symbol_matches(symbols, params.query, {
|
|
374
|
-
max_results: params.max_results ?? 20,
|
|
375
|
-
top_level_only: params.top_level_only ?? false,
|
|
376
|
-
exact_match: params.exact_match ?? false,
|
|
377
|
-
kinds: new Set(params.kinds ?? []),
|
|
378
|
-
});
|
|
379
|
-
return format_symbol_matches(result.abs, params.query, matches);
|
|
380
|
-
}),
|
|
381
|
-
}));
|
|
382
|
-
pi.registerTool(defineTool({
|
|
383
|
-
name: 'lsp_hover',
|
|
384
|
-
label: 'LSP: hover',
|
|
385
|
-
description: 'Get hover info (types, docs) at a position in a file. Positions are zero-based.',
|
|
386
|
-
parameters: Type.Object({
|
|
387
|
-
file: Type.String(),
|
|
388
|
-
line: Type.Number({
|
|
389
|
-
description: 'Zero-based line number.',
|
|
390
|
-
}),
|
|
391
|
-
character: Type.Number({
|
|
392
|
-
description: 'Zero-based character offset.',
|
|
393
|
-
}),
|
|
394
|
-
}),
|
|
395
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
396
|
-
const hover = await result.state.client.hover(result.uri, {
|
|
397
|
-
line: params.line,
|
|
398
|
-
character: params.character,
|
|
399
|
-
});
|
|
400
|
-
return format_hover(hover);
|
|
401
|
-
}),
|
|
402
|
-
}));
|
|
403
|
-
pi.registerTool(defineTool({
|
|
404
|
-
name: 'lsp_definition',
|
|
405
|
-
label: 'LSP: go to definition',
|
|
406
|
-
description: 'Find definition locations for the symbol at a position. Positions are zero-based.',
|
|
407
|
-
parameters: Type.Object({
|
|
408
|
-
file: Type.String(),
|
|
409
|
-
line: Type.Number(),
|
|
410
|
-
character: Type.Number(),
|
|
411
|
-
}),
|
|
412
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
413
|
-
const locations = await result.state.client.definition(result.uri, {
|
|
414
|
-
line: params.line,
|
|
415
|
-
character: params.character,
|
|
416
|
-
});
|
|
417
|
-
return format_locations(locations, 'No definition found.');
|
|
418
|
-
}),
|
|
419
|
-
}));
|
|
420
|
-
pi.registerTool(defineTool({
|
|
421
|
-
name: 'lsp_references',
|
|
422
|
-
label: 'LSP: find references',
|
|
423
|
-
description: 'Find references to the symbol at a position. Positions are zero-based.',
|
|
424
|
-
parameters: Type.Object({
|
|
425
|
-
file: Type.String(),
|
|
426
|
-
line: Type.Number(),
|
|
427
|
-
character: Type.Number(),
|
|
428
|
-
include_declaration: Type.Optional(Type.Boolean()),
|
|
429
|
-
}),
|
|
430
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
431
|
-
const locations = await result.state.client.references(result.uri, {
|
|
432
|
-
line: params.line,
|
|
433
|
-
character: params.character,
|
|
434
|
-
}, params.include_declaration ?? true);
|
|
435
|
-
return format_locations(locations, 'No references found.');
|
|
436
|
-
}),
|
|
437
|
-
}));
|
|
438
|
-
pi.registerTool(defineTool({
|
|
439
|
-
name: 'lsp_document_symbols',
|
|
440
|
-
label: 'LSP: document symbols',
|
|
441
|
-
description: 'List symbols in a file (functions, classes, variables) using the language server.',
|
|
442
|
-
parameters: Type.Object({
|
|
443
|
-
file: Type.String(),
|
|
444
|
-
}),
|
|
445
|
-
execute: async (_id, params, _signal, _on_update, ctx) => with_file_state(params.file, ctx, async (result) => {
|
|
446
|
-
const symbols = await result.state.client.document_symbols(result.uri);
|
|
447
|
-
return format_document_symbols(result.abs, symbols);
|
|
448
|
-
}),
|
|
449
|
-
}));
|
|
8
|
+
const manager = new LspServerManager(options);
|
|
9
|
+
register_lsp_tools(pi, manager);
|
|
450
10
|
pi.on('before_agent_start', async (event) => {
|
|
451
11
|
if (!should_inject_lsp_prompt(event))
|
|
452
12
|
return {};
|
|
453
13
|
return {
|
|
454
|
-
systemPrompt: event.systemPrompt
|
|
455
|
-
`
|
|
456
|
-
|
|
457
|
-
## Language server support via LSP tools
|
|
458
|
-
|
|
459
|
-
You have access to Language Server Protocol tools for diagnostics, hover/type information, definitions, references, and document symbols. Use them when:
|
|
460
|
-
- Debugging TypeScript, JavaScript, Svelte, or other language-server-supported errors
|
|
461
|
-
- Checking types, symbol definitions, or API documentation from code
|
|
462
|
-
- Finding references more precisely than text search
|
|
463
|
-
- Validating focused code changes before reporting completion
|
|
464
|
-
|
|
465
|
-
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),
|
|
466
15
|
};
|
|
467
16
|
});
|
|
468
|
-
pi
|
|
469
|
-
description: 'Show or manage language server state',
|
|
470
|
-
getArgumentCompletions: (prefix) => {
|
|
471
|
-
const parts = prefix.trim().split(/\s+/);
|
|
472
|
-
const subcommands = ['status', 'list', 'restart'];
|
|
473
|
-
if (!prefix.trim()) {
|
|
474
|
-
return subcommands.map((value) => ({
|
|
475
|
-
value,
|
|
476
|
-
label: value,
|
|
477
|
-
}));
|
|
478
|
-
}
|
|
479
|
-
if (parts.length <= 1) {
|
|
480
|
-
return subcommands
|
|
481
|
-
.filter((value) => value.startsWith(parts[0]))
|
|
482
|
-
.map((value) => ({ value, label: value }));
|
|
483
|
-
}
|
|
484
|
-
if (parts[0] === 'restart') {
|
|
485
|
-
const candidate = parts[1] ?? '';
|
|
486
|
-
return ['all', ...list_supported_languages()]
|
|
487
|
-
.filter((value) => value.startsWith(candidate))
|
|
488
|
-
.map((value) => ({
|
|
489
|
-
value: `restart ${value}`,
|
|
490
|
-
label: value,
|
|
491
|
-
}));
|
|
492
|
-
}
|
|
493
|
-
return null;
|
|
494
|
-
},
|
|
495
|
-
handler: async (args, ctx) => {
|
|
496
|
-
await handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state);
|
|
497
|
-
},
|
|
498
|
-
});
|
|
17
|
+
register_lsp_command(pi, manager);
|
|
499
18
|
pi.on('session_shutdown', async () => {
|
|
500
|
-
await clear_language_state();
|
|
19
|
+
await manager.clear_language_state();
|
|
501
20
|
});
|
|
502
21
|
};
|
|
503
22
|
}
|
|
504
23
|
export default create_lsp_extension();
|
|
505
|
-
async function handle_lsp_command(args, ctx, cwd, clients_by_server, failed_servers, clear_language_state) {
|
|
506
|
-
const parts = args.trim() ? args.trim().split(/\s+/, 2) : [];
|
|
507
|
-
if (parts.length === 0 && has_modal_ui(ctx)) {
|
|
508
|
-
while (true) {
|
|
509
|
-
const selected = await show_lsp_home_modal(ctx, cwd, clients_by_server, failed_servers);
|
|
510
|
-
if (!selected)
|
|
511
|
-
return;
|
|
512
|
-
if (selected === 'restart') {
|
|
513
|
-
await handle_lsp_restart_modal(ctx, clear_language_state);
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
if (selected === 'restart-all') {
|
|
517
|
-
await restart_all_lsp_servers(ctx, clear_language_state);
|
|
518
|
-
continue;
|
|
519
|
-
}
|
|
520
|
-
await show_lsp_text_modal(ctx, selected === 'running'
|
|
521
|
-
? 'Running LSP servers'
|
|
522
|
-
: selected === 'failed'
|
|
523
|
-
? 'Failed LSP servers'
|
|
524
|
-
: 'LSP status', format_lsp_view(selected, cwd, clients_by_server, failed_servers));
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
const [subcommand = 'status', target] = parts;
|
|
528
|
-
switch (subcommand) {
|
|
529
|
-
case 'status':
|
|
530
|
-
case 'list':
|
|
531
|
-
await present_lsp_text(ctx, 'LSP status', format_status_lines(cwd, clients_by_server, failed_servers).join('\n'));
|
|
532
|
-
return;
|
|
533
|
-
case 'restart': {
|
|
534
|
-
if (!target && has_modal_ui(ctx)) {
|
|
535
|
-
await handle_lsp_restart_modal(ctx, clear_language_state);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
if (!target || target === 'all') {
|
|
539
|
-
await clear_language_state();
|
|
540
|
-
ctx.ui.notify('Restarted all language server state.');
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
if (!list_supported_languages().includes(target)) {
|
|
544
|
-
ctx.ui.notify(`Unknown language: ${target}. Use one of: ${list_supported_languages().join(', ')}`, 'warning');
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
await clear_language_state(target);
|
|
548
|
-
ctx.ui.notify(`Restarted ${target} language server state.`);
|
|
549
|
-
return;
|
|
550
|
-
}
|
|
551
|
-
default:
|
|
552
|
-
ctx.ui.notify(`Unknown subcommand: ${subcommand}. Use: status, list, restart`, 'warning');
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
function has_modal_ui(ctx) {
|
|
556
|
-
return ctx.hasUI && typeof ctx.ui.custom === 'function';
|
|
557
|
-
}
|
|
558
|
-
async function present_lsp_text(ctx, title, text) {
|
|
559
|
-
if (has_modal_ui(ctx)) {
|
|
560
|
-
await show_lsp_text_modal(ctx, title, text);
|
|
561
|
-
return;
|
|
562
|
-
}
|
|
563
|
-
ctx.ui.notify(text);
|
|
564
|
-
}
|
|
565
|
-
async function show_lsp_home_modal(ctx, cwd, clients_by_server, failed_servers) {
|
|
566
|
-
const running_count = clients_by_server.size;
|
|
567
|
-
const failed_count = failed_servers.size;
|
|
568
|
-
return await show_picker_modal(ctx, {
|
|
569
|
-
title: 'Language servers',
|
|
570
|
-
subtitle: `${running_count} running • ${failed_count} failed • ${list_supported_languages().length} supported`,
|
|
571
|
-
items: [
|
|
572
|
-
{
|
|
573
|
-
value: 'status',
|
|
574
|
-
label: 'Status',
|
|
575
|
-
description: `All configured language servers for ${cwd}`,
|
|
576
|
-
},
|
|
577
|
-
{
|
|
578
|
-
value: 'running',
|
|
579
|
-
label: 'Running servers',
|
|
580
|
-
description: `${running_count} active workspace server(s)`,
|
|
581
|
-
},
|
|
582
|
-
{
|
|
583
|
-
value: 'failed',
|
|
584
|
-
label: 'Failed servers',
|
|
585
|
-
description: `${failed_count} failed server(s)`,
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
value: 'restart',
|
|
589
|
-
label: 'Restart server',
|
|
590
|
-
description: 'Pick a supported language to restart',
|
|
591
|
-
},
|
|
592
|
-
{
|
|
593
|
-
value: 'restart-all',
|
|
594
|
-
label: 'Restart all',
|
|
595
|
-
description: 'Stop every running language server',
|
|
596
|
-
},
|
|
597
|
-
],
|
|
598
|
-
footer: 'enter opens • esc close/back',
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
async function show_lsp_text_modal(ctx, title, text) {
|
|
602
|
-
await show_text_modal(ctx, {
|
|
603
|
-
title,
|
|
604
|
-
text,
|
|
605
|
-
max_visible_lines: 20,
|
|
606
|
-
overlay_options: { width: '90%', minWidth: 72 },
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
async function handle_lsp_restart_modal(ctx, clear_language_state) {
|
|
610
|
-
const selected = await show_picker_modal(ctx, {
|
|
611
|
-
title: 'Restart LSP server',
|
|
612
|
-
subtitle: 'Clear cached language server state',
|
|
613
|
-
items: [
|
|
614
|
-
{
|
|
615
|
-
value: 'all',
|
|
616
|
-
label: 'All servers',
|
|
617
|
-
description: 'Stop every running language server',
|
|
618
|
-
},
|
|
619
|
-
...list_supported_languages().map((language) => ({
|
|
620
|
-
value: language,
|
|
621
|
-
label: language,
|
|
622
|
-
description: `Restart ${language} language server state`,
|
|
623
|
-
})),
|
|
624
|
-
],
|
|
625
|
-
footer: 'enter restarts • esc back',
|
|
626
|
-
});
|
|
627
|
-
if (!selected)
|
|
628
|
-
return;
|
|
629
|
-
if (selected === 'all') {
|
|
630
|
-
await restart_all_lsp_servers(ctx, clear_language_state);
|
|
631
|
-
return;
|
|
632
|
-
}
|
|
633
|
-
await clear_language_state(selected);
|
|
634
|
-
ctx.ui.notify(`Restarted ${selected} language server state.`);
|
|
635
|
-
}
|
|
636
|
-
async function restart_all_lsp_servers(ctx, clear_language_state) {
|
|
637
|
-
await clear_language_state();
|
|
638
|
-
ctx.ui.notify('Restarted all language server state.');
|
|
639
|
-
}
|
|
640
24
|
//# sourceMappingURL=index.js.map
|