@narumitw/pi-lsp 0.1.26 → 0.1.27
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 +91 -97
- package/package.json +6 -7
- package/src/adapters.ts +211 -101
- package/src/command.ts +0 -5
- package/src/files.ts +2 -2
- package/src/lsp-client.ts +38 -60
- package/src/pi-lsp.ts +105 -146
- package/src/routes.ts +98 -0
- package/src/runner.ts +46 -80
- package/src/types.ts +20 -19
package/src/lsp-client.ts
CHANGED
|
@@ -9,7 +9,6 @@ import type {
|
|
|
9
9
|
JsonRpcMessage,
|
|
10
10
|
LspDiagnostic,
|
|
11
11
|
LspServerAdapter,
|
|
12
|
-
LspTextEdit,
|
|
13
12
|
ServerCommand,
|
|
14
13
|
} from "./types.js";
|
|
15
14
|
|
|
@@ -50,12 +49,13 @@ export class LspClient {
|
|
|
50
49
|
async start() {
|
|
51
50
|
if (!commandExists(this.#command.command, this.#cwd)) {
|
|
52
51
|
throw new Error(
|
|
53
|
-
`${this.#adapter.
|
|
52
|
+
`${this.#adapter.name} LSP command not found: ${this.#command.command}. ${this.#adapter.missingCommandHint}`,
|
|
54
53
|
);
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
const child = spawn(this.#command.command, this.#command.args, {
|
|
58
57
|
cwd: this.#cwd,
|
|
58
|
+
env: { ...process.env, ...this.#adapter.env },
|
|
59
59
|
stdio: "pipe",
|
|
60
60
|
});
|
|
61
61
|
this.#child = child;
|
|
@@ -64,7 +64,7 @@ export class LspClient {
|
|
|
64
64
|
this.#onData(chunk);
|
|
65
65
|
} catch (error) {
|
|
66
66
|
this.#fail(
|
|
67
|
-
`${this.#adapter.
|
|
67
|
+
`${this.#adapter.name} LSP server sent invalid JSON-RPC data: ${formatErrorMessage(error)}.${this.#formatStderr()}`,
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
});
|
|
@@ -73,7 +73,7 @@ export class LspClient {
|
|
|
73
73
|
});
|
|
74
74
|
child.stdin.on("error", (error) => {
|
|
75
75
|
this.#fail(
|
|
76
|
-
`${this.#adapter.
|
|
76
|
+
`${this.#adapter.name} LSP stdin write failed: ${formatErrorMessage(error)}.${this.#formatStderr()}`,
|
|
77
77
|
);
|
|
78
78
|
});
|
|
79
79
|
child.once("exit", (code, signal) => {
|
|
@@ -81,14 +81,14 @@ export class LspClient {
|
|
|
81
81
|
const reason = signal ? `signal ${signal}` : `code ${code ?? "unknown"}`;
|
|
82
82
|
this.#rejectPending(
|
|
83
83
|
(id) =>
|
|
84
|
-
`${this.#adapter.
|
|
84
|
+
`${this.#adapter.name} LSP server exited before response ${id} (${reason}).${this.#formatStderr()}`,
|
|
85
85
|
);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
await new Promise<void>((resolve, reject) => {
|
|
89
89
|
child.once("spawn", resolve);
|
|
90
90
|
child.once("error", (error) => {
|
|
91
|
-
const message = `${this.#adapter.
|
|
91
|
+
const message = `${this.#adapter.name} LSP process failed to start: ${error.message}.${this.#formatStderr()}`;
|
|
92
92
|
this.#rejectPending(message);
|
|
93
93
|
if (this.#child === child) this.#child = undefined;
|
|
94
94
|
reject(new Error(message));
|
|
@@ -99,49 +99,32 @@ export class LspClient {
|
|
|
99
99
|
async initialize(root: string) {
|
|
100
100
|
const rootUri = directoryUri(root);
|
|
101
101
|
const workspaceFolders = [{ uri: rootUri, name: path.basename(root) || "workspace" }];
|
|
102
|
-
const init = this.#adapter.initialize;
|
|
103
102
|
await this.request("initialize", {
|
|
104
103
|
processId: process.pid,
|
|
105
104
|
rootUri,
|
|
106
|
-
workspaceFolders
|
|
105
|
+
workspaceFolders,
|
|
106
|
+
initializationOptions: this.#adapter.initialization ?? {},
|
|
107
107
|
capabilities: {
|
|
108
108
|
textDocument: {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
dynamicRegistration: init.codeActionDynamicRegistration,
|
|
113
|
-
resolveSupport: { properties: ["edit"] },
|
|
114
|
-
},
|
|
115
|
-
}
|
|
116
|
-
: {}),
|
|
117
|
-
diagnostic: { dynamicRegistration: init.diagnosticDynamicRegistration },
|
|
118
|
-
...(init.formattingDynamicRegistration === undefined
|
|
119
|
-
? {}
|
|
120
|
-
: { formatting: { dynamicRegistration: init.formattingDynamicRegistration } }),
|
|
121
|
-
publishDiagnostics: {},
|
|
122
|
-
synchronization: {
|
|
123
|
-
didSave: true,
|
|
124
|
-
...(init.didSaveDynamicRegistration === undefined
|
|
125
|
-
? {}
|
|
126
|
-
: { dynamicRegistration: init.didSaveDynamicRegistration }),
|
|
109
|
+
codeAction: {
|
|
110
|
+
dynamicRegistration: true,
|
|
111
|
+
resolveSupport: { properties: ["edit"] },
|
|
127
112
|
},
|
|
113
|
+
diagnostic: { dynamicRegistration: true, relatedDocumentSupport: true },
|
|
114
|
+
publishDiagnostics: {},
|
|
115
|
+
synchronization: { didSave: true },
|
|
128
116
|
},
|
|
129
117
|
workspace: {
|
|
130
118
|
configuration: true,
|
|
131
|
-
...(init.didChangeConfigurationDynamicRegistration === undefined
|
|
132
|
-
? {}
|
|
133
|
-
: {
|
|
134
|
-
didChangeConfiguration: {
|
|
135
|
-
dynamicRegistration: init.didChangeConfigurationDynamicRegistration,
|
|
136
|
-
},
|
|
137
|
-
}),
|
|
138
119
|
workspaceEdit: { documentChanges: true },
|
|
139
|
-
workspaceFolders:
|
|
120
|
+
workspaceFolders: true,
|
|
140
121
|
},
|
|
141
122
|
},
|
|
142
123
|
});
|
|
143
124
|
this.notify("initialized", {});
|
|
144
|
-
if (this.#adapter.
|
|
125
|
+
if (this.#adapter.initialization) {
|
|
126
|
+
this.notify("workspace/didChangeConfiguration", { settings: this.#adapter.initialization });
|
|
127
|
+
}
|
|
145
128
|
}
|
|
146
129
|
|
|
147
130
|
didOpen(uri: string, text: string, languageId: string) {
|
|
@@ -168,19 +151,11 @@ export class LspClient {
|
|
|
168
151
|
const result = response.result as { items?: LspDiagnostic[] } | undefined;
|
|
169
152
|
return result?.items ?? [];
|
|
170
153
|
} catch (error) {
|
|
171
|
-
if (!
|
|
154
|
+
if (!isUnsupportedMethodError(error)) throw error;
|
|
172
155
|
return this.#waitForPublishedDiagnostics(uri);
|
|
173
156
|
}
|
|
174
157
|
}
|
|
175
158
|
|
|
176
|
-
async format(uri: string) {
|
|
177
|
-
const response = await this.request("textDocument/formatting", {
|
|
178
|
-
textDocument: { uri },
|
|
179
|
-
options: this.#adapter.formattingOptions,
|
|
180
|
-
});
|
|
181
|
-
return (response.result as LspTextEdit[] | null | undefined) ?? [];
|
|
182
|
-
}
|
|
183
|
-
|
|
184
159
|
async codeActions(uri: string, text: string, diagnostics: LspDiagnostic[], kind: string) {
|
|
185
160
|
const response = await this.request("textDocument/codeAction", {
|
|
186
161
|
textDocument: { uri },
|
|
@@ -202,7 +177,7 @@ export class LspClient {
|
|
|
202
177
|
const response = await this.request("codeAction/resolve", action);
|
|
203
178
|
resolvedActions.push((response.result as CodeAction | undefined) ?? action);
|
|
204
179
|
} catch (error) {
|
|
205
|
-
if (!
|
|
180
|
+
if (!isUnsupportedMethodError(error)) throw error;
|
|
206
181
|
resolvedActions.push(action);
|
|
207
182
|
}
|
|
208
183
|
}
|
|
@@ -224,7 +199,7 @@ export class LspClient {
|
|
|
224
199
|
}
|
|
225
200
|
|
|
226
201
|
close() {
|
|
227
|
-
this.#rejectPending(`${this.#adapter.
|
|
202
|
+
this.#rejectPending(`${this.#adapter.name} LSP request cancelled.`);
|
|
228
203
|
|
|
229
204
|
if (this.#child && !this.#child.killed) this.#child.kill("SIGTERM");
|
|
230
205
|
this.#child = undefined;
|
|
@@ -258,7 +233,7 @@ export class LspClient {
|
|
|
258
233
|
const timeout = setTimeout(() => {
|
|
259
234
|
this.#pending.delete(id);
|
|
260
235
|
reject(
|
|
261
|
-
new Error(`${this.#adapter.
|
|
236
|
+
new Error(`${this.#adapter.name} LSP request timed out: ${method}.${this.#formatStderr()}`),
|
|
262
237
|
);
|
|
263
238
|
}, this.#timeoutMs);
|
|
264
239
|
this.#pending.set(id, { resolve, reject, timeout });
|
|
@@ -280,14 +255,14 @@ export class LspClient {
|
|
|
280
255
|
}
|
|
281
256
|
|
|
282
257
|
#send(message: JsonRpcMessage) {
|
|
283
|
-
if (!this.#child) throw new Error(`${this.#adapter.
|
|
258
|
+
if (!this.#child) throw new Error(`${this.#adapter.name} LSP server is not running.`);
|
|
284
259
|
|
|
285
260
|
const body = JSON.stringify(message);
|
|
286
261
|
try {
|
|
287
262
|
this.#child.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
288
263
|
} catch (error) {
|
|
289
264
|
const errorMessage =
|
|
290
|
-
`${this.#adapter.
|
|
265
|
+
`${this.#adapter.name} LSP stdin write failed: ${formatErrorMessage(error)}.` +
|
|
291
266
|
this.#formatStderr();
|
|
292
267
|
this.#fail(errorMessage);
|
|
293
268
|
throw new Error(errorMessage);
|
|
@@ -323,7 +298,7 @@ export class LspClient {
|
|
|
323
298
|
clearTimeout(pending.timeout);
|
|
324
299
|
this.#pending.delete(message.id as number);
|
|
325
300
|
if (message.error) {
|
|
326
|
-
pending.reject(new Error(`${this.#adapter.
|
|
301
|
+
pending.reject(new Error(`${this.#adapter.name} LSP error: ${message.error.message}`));
|
|
327
302
|
} else {
|
|
328
303
|
pending.resolve(message);
|
|
329
304
|
}
|
|
@@ -362,7 +337,11 @@ export class LspClient {
|
|
|
362
337
|
const waiters = this.#diagnosticWaiters.get(uri)?.filter((entry) => entry !== waiter) ?? [];
|
|
363
338
|
if (waiters.length) this.#diagnosticWaiters.set(uri, waiters);
|
|
364
339
|
else this.#diagnosticWaiters.delete(uri);
|
|
365
|
-
|
|
340
|
+
reject(
|
|
341
|
+
new Error(
|
|
342
|
+
`${this.#adapter.name} LSP did not return diagnostics for ${uri} before timeout.`,
|
|
343
|
+
),
|
|
344
|
+
);
|
|
366
345
|
}, this.#timeoutMs),
|
|
367
346
|
};
|
|
368
347
|
this.#diagnosticWaiters.set(uri, [...(this.#diagnosticWaiters.get(uri) ?? []), waiter]);
|
|
@@ -371,11 +350,11 @@ export class LspClient {
|
|
|
371
350
|
|
|
372
351
|
#respondToServerRequest(message: JsonRpcMessage) {
|
|
373
352
|
if (message.method === "workspace/configuration") {
|
|
374
|
-
const params = message.params as { items?:
|
|
353
|
+
const params = message.params as { items?: Array<{ section?: string }> } | undefined;
|
|
375
354
|
this.#send({
|
|
376
355
|
jsonrpc: "2.0",
|
|
377
356
|
id: message.id,
|
|
378
|
-
result: (params?.items ?? []).map(() => (
|
|
357
|
+
result: (params?.items ?? []).map((item) => this.#configurationValue(item.section)),
|
|
379
358
|
});
|
|
380
359
|
return;
|
|
381
360
|
}
|
|
@@ -385,9 +364,7 @@ export class LspClient {
|
|
|
385
364
|
this.#send({
|
|
386
365
|
jsonrpc: "2.0",
|
|
387
366
|
id: message.id,
|
|
388
|
-
result: this.#
|
|
389
|
-
? [{ uri: rootUri, name: path.basename(this.#cwd) || "workspace" }]
|
|
390
|
-
: null,
|
|
367
|
+
result: [{ uri: rootUri, name: path.basename(this.#cwd) || "workspace" }],
|
|
391
368
|
});
|
|
392
369
|
return;
|
|
393
370
|
}
|
|
@@ -407,6 +384,11 @@ export class LspClient {
|
|
|
407
384
|
});
|
|
408
385
|
}
|
|
409
386
|
|
|
387
|
+
#configurationValue(section: string | undefined) {
|
|
388
|
+
if (!section) return this.#adapter.initialization ?? {};
|
|
389
|
+
return this.#adapter.initialization?.[section] ?? {};
|
|
390
|
+
}
|
|
391
|
+
|
|
410
392
|
#formatStderr() {
|
|
411
393
|
const stderr = this.#stderr.trim();
|
|
412
394
|
return stderr ? `\nServer stderr:\n${stderr}` : "";
|
|
@@ -420,7 +402,3 @@ function isUnsupportedMethodError(error: unknown) {
|
|
|
420
402
|
function formatErrorMessage(error: unknown) {
|
|
421
403
|
return error instanceof Error ? error.message : String(error);
|
|
422
404
|
}
|
|
423
|
-
|
|
424
|
-
function wait(ms: number) {
|
|
425
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
426
|
-
}
|
package/src/pi-lsp.ts
CHANGED
|
@@ -1,185 +1,140 @@
|
|
|
1
1
|
import { defineTool, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
3
|
+
import { loadRuntime } from "./adapters.js";
|
|
4
4
|
import { commandExists, commandFromEnv } from "./command.js";
|
|
5
|
-
import {
|
|
5
|
+
import { resolveRoot } from "./files.js";
|
|
6
|
+
import { selectDiagnosticRoutes, selectFixRoute } from "./routes.js";
|
|
7
|
+
import { DEFAULT_FILE_LIMIT, runDiagnostics, runFix, textResult } from "./runner.js";
|
|
6
8
|
|
|
7
9
|
const STATUS_KEY = "lsp";
|
|
8
10
|
|
|
9
|
-
const
|
|
11
|
+
const ServerParameter = Type.Optional(
|
|
12
|
+
Type.Union([Type.String(), Type.Array(Type.String())], {
|
|
13
|
+
description:
|
|
14
|
+
"Optional configured LSP server name, or names for diagnostics. Defaults to all servers matching the file extension.",
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const DiagnosticsParameters = Type.Object({
|
|
10
19
|
paths: Type.Optional(
|
|
11
20
|
Type.Array(Type.String(), {
|
|
12
|
-
description:
|
|
21
|
+
description:
|
|
22
|
+
"Files or directories to check. Defaults to the workspace root and routes by configured server extensions.",
|
|
13
23
|
}),
|
|
14
24
|
),
|
|
15
25
|
root: Type.Optional(
|
|
16
|
-
Type.String({ description: "Workspace root for
|
|
26
|
+
Type.String({ description: "Workspace root for language servers. Defaults to cwd." }),
|
|
17
27
|
),
|
|
18
|
-
limit: Type.Optional(
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
};
|
|
28
|
+
limit: Type.Optional(Type.Number({ description: "Maximum files to open per selected server." })),
|
|
29
|
+
server: ServerParameter,
|
|
30
|
+
});
|
|
22
31
|
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
}),
|
|
28
|
-
),
|
|
32
|
+
const SingleFileParameters = {
|
|
33
|
+
path: Type.String({
|
|
34
|
+
description: "File to process. The server is selected from configured file extensions.",
|
|
35
|
+
}),
|
|
29
36
|
root: Type.Optional(
|
|
30
|
-
Type.String({ description: "Workspace root for
|
|
37
|
+
Type.String({ description: "Workspace root for language servers. Defaults to cwd." }),
|
|
31
38
|
),
|
|
32
|
-
|
|
33
|
-
Type.
|
|
39
|
+
write: Type.Optional(
|
|
40
|
+
Type.Boolean({ description: "Write changed text back to the file. Defaults to false." }),
|
|
41
|
+
),
|
|
42
|
+
server: Type.Optional(
|
|
43
|
+
Type.String({
|
|
44
|
+
description: "Optional configured LSP server name. Defaults to extension-based inference.",
|
|
45
|
+
}),
|
|
34
46
|
),
|
|
35
47
|
};
|
|
36
48
|
|
|
37
|
-
const
|
|
38
|
-
name: "
|
|
39
|
-
label: "
|
|
40
|
-
description: "Run
|
|
41
|
-
promptSnippet: "Get
|
|
42
|
-
promptGuidelines: [
|
|
43
|
-
"Use biome_lsp_diagnostics when JavaScript, TypeScript, JSON, CSS, GraphQL, or framework files need Biome lint/format diagnostics.",
|
|
44
|
-
"If Biome is missing, report the configuration error and suggest installing @biomejs/biome or setting PI_BIOME_LSP_COMMAND.",
|
|
45
|
-
],
|
|
46
|
-
parameters: Type.Object(BiomePathsParameters),
|
|
47
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
48
|
-
return runDiagnostics(biomeAdapter, params, signal, ctx, STATUS_KEY);
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const biomeFormatTool = defineTool({
|
|
53
|
-
name: "biome_lsp_format",
|
|
54
|
-
label: "Biome LSP: Format",
|
|
55
|
-
description: "Format a Biome-supported file through Biome's language server.",
|
|
56
|
-
promptSnippet: "Format a file through Biome LSP",
|
|
57
|
-
parameters: Type.Object({
|
|
58
|
-
path: Type.String({ description: "File to format." }),
|
|
59
|
-
root: Type.Optional(
|
|
60
|
-
Type.String({ description: "Workspace root for the Biome language server. Defaults to cwd." }),
|
|
61
|
-
),
|
|
62
|
-
write: Type.Optional(
|
|
63
|
-
Type.Boolean({ description: "Write formatted text back to the file. Defaults to false." }),
|
|
64
|
-
),
|
|
65
|
-
}),
|
|
66
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
67
|
-
return runFormat(biomeAdapter, params, signal, ctx, STATUS_KEY);
|
|
68
|
-
},
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const biomeFixTool = defineTool({
|
|
72
|
-
name: "biome_lsp_fix",
|
|
73
|
-
label: "Biome LSP: Fix",
|
|
74
|
-
description: "Apply Biome LSP source fixes or import organization to a file.",
|
|
75
|
-
promptSnippet: "Apply Biome LSP fixes to a file",
|
|
76
|
-
parameters: Type.Object({
|
|
77
|
-
path: Type.String({ description: "File to fix." }),
|
|
78
|
-
root: Type.Optional(
|
|
79
|
-
Type.String({ description: "Workspace root for the Biome language server. Defaults to cwd." }),
|
|
80
|
-
),
|
|
81
|
-
kind: Type.Optional(
|
|
82
|
-
Type.String({
|
|
83
|
-
description:
|
|
84
|
-
"Biome source action kind. Defaults to source.fixAll.biome. Common value: source.organizeImports.biome.",
|
|
85
|
-
}),
|
|
86
|
-
),
|
|
87
|
-
write: Type.Optional(
|
|
88
|
-
Type.Boolean({ description: "Write fixed text back to the file. Defaults to false." }),
|
|
89
|
-
),
|
|
90
|
-
}),
|
|
91
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
92
|
-
return runFix(biomeAdapter, params, signal, ctx, STATUS_KEY);
|
|
93
|
-
},
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
const tyDiagnosticsTool = defineTool({
|
|
97
|
-
name: "ty_lsp_diagnostics",
|
|
98
|
-
label: "Python LSP: ty Diagnostics",
|
|
99
|
-
description: "Run ty's language server and return Python type diagnostics for files.",
|
|
100
|
-
promptSnippet: "Get Python type diagnostics from ty's language server",
|
|
49
|
+
const lspDiagnosticsTool = defineTool({
|
|
50
|
+
name: "lsp_diagnostics",
|
|
51
|
+
label: "LSP: Diagnostics",
|
|
52
|
+
description: "Run diagnostics using configured, language-agnostic LSP server routes.",
|
|
53
|
+
promptSnippet: "Get diagnostics from configured LSP servers selected by file extension",
|
|
101
54
|
promptGuidelines: [
|
|
102
|
-
"Use
|
|
103
|
-
"
|
|
55
|
+
"Use lsp_diagnostics when files need diagnostics from a configured LSP server.",
|
|
56
|
+
"Use the server parameter only when the user asks for a specific configured LSP server or multiple servers match the same extension.",
|
|
57
|
+
"If a configured server command is missing, report the configuration error and suggest installing the command or setting its PI_<SERVER>_LSP_COMMAND environment variable.",
|
|
104
58
|
],
|
|
105
|
-
parameters:
|
|
59
|
+
parameters: DiagnosticsParameters,
|
|
106
60
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
107
|
-
|
|
61
|
+
const requestedRoot = resolveRoot(params.root);
|
|
62
|
+
const { adapters, timeoutMs } = loadRuntime(requestedRoot);
|
|
63
|
+
const { root, routes } = selectDiagnosticRoutes(
|
|
64
|
+
adapters,
|
|
65
|
+
{ ...params, root: requestedRoot },
|
|
66
|
+
DEFAULT_FILE_LIMIT,
|
|
67
|
+
);
|
|
68
|
+
const results = [];
|
|
69
|
+
for (const route of routes) {
|
|
70
|
+
const result = await runDiagnostics(
|
|
71
|
+
route.adapter,
|
|
72
|
+
{ root, paths: params.paths, limit: params.limit, files: route.files },
|
|
73
|
+
timeoutMs,
|
|
74
|
+
signal,
|
|
75
|
+
ctx,
|
|
76
|
+
STATUS_KEY,
|
|
77
|
+
);
|
|
78
|
+
results.push({ route, result });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const text = results
|
|
82
|
+
.map(({ route, result }) => `${route.reason}\n\n${textFromResult(result)}`)
|
|
83
|
+
.join("\n\n---\n\n");
|
|
84
|
+
return textResult(text, {
|
|
85
|
+
root,
|
|
86
|
+
routes: results.map(({ route, result }) => ({
|
|
87
|
+
server: route.adapter.name,
|
|
88
|
+
backend: route.adapter.name,
|
|
89
|
+
reason: route.reason,
|
|
90
|
+
files: route.files,
|
|
91
|
+
details: result.details,
|
|
92
|
+
})),
|
|
93
|
+
});
|
|
108
94
|
},
|
|
109
95
|
});
|
|
110
96
|
|
|
111
|
-
const
|
|
112
|
-
name: "
|
|
113
|
-
label: "
|
|
114
|
-
description: "
|
|
115
|
-
promptSnippet: "
|
|
97
|
+
const lspFixTool = defineTool({
|
|
98
|
+
name: "lsp_fix",
|
|
99
|
+
label: "LSP: Fix",
|
|
100
|
+
description: "Apply source fixes or import organization using configured LSP server routes.",
|
|
101
|
+
promptSnippet: "Apply configured LSP source fixes to a file",
|
|
116
102
|
promptGuidelines: [
|
|
117
|
-
"Use
|
|
118
|
-
"
|
|
103
|
+
"Use lsp_fix for files handled by a configured LSP code-action server.",
|
|
104
|
+
"Use kind when the server needs a specific source action kind such as source.organizeImports.",
|
|
119
105
|
],
|
|
120
|
-
parameters: Type.Object(PythonPathsParameters),
|
|
121
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
122
|
-
return runDiagnostics(ruffAdapter, params, signal, ctx, STATUS_KEY);
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
const ruffFormatTool = defineTool({
|
|
127
|
-
name: "ruff_lsp_format",
|
|
128
|
-
label: "Python LSP: Ruff Format",
|
|
129
|
-
description: "Format a Python file through Ruff's language server.",
|
|
130
|
-
promptSnippet: "Format a Python file through Ruff LSP",
|
|
131
106
|
parameters: Type.Object({
|
|
132
|
-
|
|
133
|
-
root: Type.Optional(
|
|
134
|
-
Type.String({ description: "Workspace root for the language server. Defaults to cwd." }),
|
|
135
|
-
),
|
|
136
|
-
write: Type.Optional(
|
|
137
|
-
Type.Boolean({ description: "Write formatted text back to the file. Defaults to false." }),
|
|
138
|
-
),
|
|
139
|
-
}),
|
|
140
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
141
|
-
return runFormat(ruffAdapter, params, signal, ctx, STATUS_KEY);
|
|
142
|
-
},
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
const ruffFixTool = defineTool({
|
|
146
|
-
name: "ruff_lsp_fix",
|
|
147
|
-
label: "Python LSP: Ruff Fix",
|
|
148
|
-
description: "Apply Ruff LSP source fixes or import organization to a Python file.",
|
|
149
|
-
promptSnippet: "Apply Ruff LSP fixes to a Python file",
|
|
150
|
-
parameters: Type.Object({
|
|
151
|
-
path: Type.String({ description: "Python file to fix." }),
|
|
152
|
-
root: Type.Optional(
|
|
153
|
-
Type.String({ description: "Workspace root for the language server. Defaults to cwd." }),
|
|
154
|
-
),
|
|
107
|
+
...SingleFileParameters,
|
|
155
108
|
kind: Type.Optional(
|
|
156
109
|
Type.String({
|
|
157
|
-
description:
|
|
158
|
-
"Ruff source action kind. Defaults to source.fixAll.ruff. Common value: source.organizeImports.ruff.",
|
|
110
|
+
description: "Source action kind. Defaults to source.fixAll.",
|
|
159
111
|
}),
|
|
160
112
|
),
|
|
161
|
-
write: Type.Optional(
|
|
162
|
-
Type.Boolean({ description: "Write fixed text back to the file. Defaults to false." }),
|
|
163
|
-
),
|
|
164
113
|
}),
|
|
165
114
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
166
|
-
|
|
115
|
+
const requestedRoot = resolveRoot(params.root);
|
|
116
|
+
const { adapters, timeoutMs } = loadRuntime(requestedRoot);
|
|
117
|
+
const { root, route } = selectFixRoute(adapters, { ...params, root: requestedRoot });
|
|
118
|
+
return runFix(
|
|
119
|
+
route.adapter,
|
|
120
|
+
{ root, path: params.path, kind: params.kind, write: params.write },
|
|
121
|
+
timeoutMs,
|
|
122
|
+
signal,
|
|
123
|
+
ctx,
|
|
124
|
+
STATUS_KEY,
|
|
125
|
+
);
|
|
167
126
|
},
|
|
168
127
|
});
|
|
169
128
|
|
|
170
129
|
export default function lsp(pi: ExtensionAPI) {
|
|
171
|
-
pi.registerTool(
|
|
172
|
-
pi.registerTool(
|
|
173
|
-
pi.registerTool(biomeFixTool);
|
|
174
|
-
pi.registerTool(tyDiagnosticsTool);
|
|
175
|
-
pi.registerTool(ruffDiagnosticsTool);
|
|
176
|
-
pi.registerTool(ruffFormatTool);
|
|
177
|
-
pi.registerTool(ruffFixTool);
|
|
130
|
+
pi.registerTool(lspDiagnosticsTool);
|
|
131
|
+
pi.registerTool(lspFixTool);
|
|
178
132
|
|
|
179
133
|
pi.registerCommand("lsp", {
|
|
180
134
|
description: "Show shared LSP extension configuration",
|
|
181
135
|
handler: async (_args, ctx) => {
|
|
182
|
-
ctx.
|
|
136
|
+
const { adapters } = loadRuntime(ctx.cwd);
|
|
137
|
+
ctx.ui.notify(buildStatusMessage(adapters, ctx.cwd), statusLevel(adapters, ctx.cwd));
|
|
183
138
|
},
|
|
184
139
|
});
|
|
185
140
|
|
|
@@ -192,22 +147,26 @@ export default function lsp(pi: ExtensionAPI) {
|
|
|
192
147
|
});
|
|
193
148
|
}
|
|
194
149
|
|
|
195
|
-
function
|
|
150
|
+
function textFromResult(result: { content?: Array<{ type?: string; text?: string }> }) {
|
|
151
|
+
return result.content?.find((item) => item.type === "text")?.text ?? "";
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function buildStatusMessage(adapters: ReturnType<typeof loadRuntime>["adapters"], cwd: string) {
|
|
196
155
|
return adapters
|
|
197
156
|
.flatMap((adapter) => {
|
|
198
157
|
const command = commandFromEnv(adapter.commandEnvVar, adapter.defaultCommand);
|
|
199
158
|
return [
|
|
200
|
-
`${adapter.
|
|
201
|
-
`${adapter.
|
|
159
|
+
`${adapter.name} LSP command: ${command.command} ${command.args.join(" ")}`.trim(),
|
|
160
|
+
`${adapter.name} status: ${commandExists(command.command, cwd) ? "ready" : "command missing"}`,
|
|
202
161
|
];
|
|
203
162
|
})
|
|
204
163
|
.join("\n");
|
|
205
164
|
}
|
|
206
165
|
|
|
207
|
-
function statusLevel() {
|
|
166
|
+
function statusLevel(adapters: ReturnType<typeof loadRuntime>["adapters"], cwd: string) {
|
|
208
167
|
return adapters.every((adapter) => {
|
|
209
168
|
const command = commandFromEnv(adapter.commandEnvVar, adapter.defaultCommand);
|
|
210
|
-
return commandExists(command.command);
|
|
169
|
+
return commandExists(command.command, cwd);
|
|
211
170
|
})
|
|
212
171
|
? "info"
|
|
213
172
|
: "warning";
|
package/src/routes.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { collectSupportedFiles, resolveRoot } from "./files.js";
|
|
3
|
+
import type { LspServerAdapter } from "./types.js";
|
|
4
|
+
|
|
5
|
+
export type LspAction = "diagnostics" | "fix";
|
|
6
|
+
|
|
7
|
+
export interface DiagnosticRoute {
|
|
8
|
+
adapter: LspServerAdapter;
|
|
9
|
+
reason: string;
|
|
10
|
+
files: string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface SingleFileRoute {
|
|
14
|
+
adapter: LspServerAdapter;
|
|
15
|
+
reason: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface DiagnosticRouteParams {
|
|
19
|
+
root?: string;
|
|
20
|
+
paths?: string[];
|
|
21
|
+
limit?: number;
|
|
22
|
+
server?: string | string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SingleFileRouteParams {
|
|
26
|
+
root?: string;
|
|
27
|
+
path: string;
|
|
28
|
+
server?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SUPPORTED_SERVER_DESCRIPTION =
|
|
32
|
+
"Supported LSP servers are defined by pi-lsp config and selected by file extension.";
|
|
33
|
+
|
|
34
|
+
export function selectDiagnosticRoutes(
|
|
35
|
+
adapters: LspServerAdapter[],
|
|
36
|
+
params: DiagnosticRouteParams,
|
|
37
|
+
defaultLimit: number,
|
|
38
|
+
) {
|
|
39
|
+
const root = resolveRoot(params.root);
|
|
40
|
+
const candidates = filterAdapters(adapters, params.server);
|
|
41
|
+
const filesByExtensions = new Map<string, string[]>();
|
|
42
|
+
const routes = candidates
|
|
43
|
+
.map((adapter) => {
|
|
44
|
+
const key = adapter.extensions.join("\0");
|
|
45
|
+
let files = filesByExtensions.get(key);
|
|
46
|
+
if (!files) {
|
|
47
|
+
files = collectSupportedFiles(adapter, root, params.paths, params.limit ?? defaultLimit);
|
|
48
|
+
filesByExtensions.set(key, files);
|
|
49
|
+
}
|
|
50
|
+
return { adapter, reason: `${adapter.name} diagnostics`, files };
|
|
51
|
+
})
|
|
52
|
+
.filter((route) => route.files.length > 0);
|
|
53
|
+
|
|
54
|
+
if (routes.length === 0) {
|
|
55
|
+
const scope = params.paths?.length ? ` in requested paths: ${params.paths.join(", ")}` : "";
|
|
56
|
+
throw new Error(`No supported files found${scope}. ${SUPPORTED_SERVER_DESCRIPTION}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return { root, routes };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function selectFixRoute(adapters: LspServerAdapter[], params: SingleFileRouteParams) {
|
|
63
|
+
const root = resolveRoot(params.root);
|
|
64
|
+
const file = path.resolve(root, params.path);
|
|
65
|
+
const candidates = filterAdapters(adapters, params.server).filter((adapter) => adapter.isSupportedFile(file));
|
|
66
|
+
if (candidates.length === 0) throw unsupportedFileError("fix", params.path, params.server);
|
|
67
|
+
if (!params.server && candidates.length > 1) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`Multiple LSP servers support ${params.path}: ${candidates.map((adapter) => adapter.name).join(", ")}. ` +
|
|
70
|
+
"Specify the server parameter for lsp_fix.",
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
const adapter = candidates[0];
|
|
74
|
+
return {
|
|
75
|
+
root,
|
|
76
|
+
route: {
|
|
77
|
+
adapter,
|
|
78
|
+
reason: `${adapter.name} fix`,
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function filterAdapters(adapters: LspServerAdapter[], selected: string | string[] | undefined) {
|
|
84
|
+
if (!selected) return adapters;
|
|
85
|
+
const names = [...new Set((Array.isArray(selected) ? selected : [selected]).map((name) => name.trim()))].filter(
|
|
86
|
+
(name) => name.length > 0,
|
|
87
|
+
);
|
|
88
|
+
if (names.length === 0) throw new Error("LSP server parameter must not be blank.");
|
|
89
|
+
const matched = adapters.filter((adapter) => names.includes(adapter.name));
|
|
90
|
+
const missing = names.filter((name) => !adapters.some((adapter) => adapter.name === name));
|
|
91
|
+
if (missing.length) throw new Error(`Unknown LSP server(s): ${missing.join(", ")}.`);
|
|
92
|
+
return matched;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function unsupportedFileError(action: LspAction, filePath: string, server: string | undefined) {
|
|
96
|
+
const override = server ? ` for server '${server}'` : "";
|
|
97
|
+
return new Error(`No ${action} route supports ${filePath}${override}. ${SUPPORTED_SERVER_DESCRIPTION}`);
|
|
98
|
+
}
|