@ian-pascoe/pi-lsp 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -5
- package/package.json +10 -6
- package/skills/pi-lsp/SKILL.md +16 -0
- package/src/lsp-position-encoding.ts +2 -1
- package/src/lsp-post-edit-diagnostics-rendering.ts +2 -1
- package/src/lsp-post-edit-diagnostics.ts +7 -33
- package/src/lsp-server-client.ts +67 -85
- package/src/lsp-server-manager.ts +53 -19
- package/src/lsp-session-files.ts +1 -23
- package/src/lsp-tool-contract.ts +7 -11
- package/src/lsp-tool-output.ts +22 -10
- package/src/lsp-tool-rendering.ts +1 -4
- package/src/lsp-tool.ts +44 -66
- package/src/lsp-workspace-edit.ts +22 -79
- package/src/pi-lsp-extension.ts +28 -25
- package/src/pi-lsp-settings.ts +151 -71
|
@@ -18,6 +18,8 @@ export interface LspServerRoutingDefinition {
|
|
|
18
18
|
readonly serverId: string;
|
|
19
19
|
/** Languages and file patterns accepted by this server. */
|
|
20
20
|
readonly languages: readonly LspServerLanguage[];
|
|
21
|
+
/** Exclude this server when none of its root markers exists above the requested file. */
|
|
22
|
+
readonly requireRootMarker?: boolean;
|
|
21
23
|
/** Basename glob patterns that select this server instance's nearest workspace root. */
|
|
22
24
|
readonly rootMarkers?: readonly string[];
|
|
23
25
|
}
|
|
@@ -71,6 +73,7 @@ export type LspServerFailureCode =
|
|
|
71
73
|
| "no-capable-server"
|
|
72
74
|
| "no-matching-server"
|
|
73
75
|
| "request-failed"
|
|
76
|
+
| "root-marker-not-found"
|
|
74
77
|
| "server-unavailable";
|
|
75
78
|
|
|
76
79
|
/** Preserves one matching server's failure without discarding sibling successes. */
|
|
@@ -79,8 +82,6 @@ export interface LspServerFailure {
|
|
|
79
82
|
readonly code: LspServerFailureCode;
|
|
80
83
|
/** Searchable caller-facing error prefixed with `Pi LSP:`. */
|
|
81
84
|
readonly message: string;
|
|
82
|
-
/** Selected workspace root when routing reached a concrete Server Instance. */
|
|
83
|
-
readonly rootPath?: string;
|
|
84
85
|
/** Configured server ID, or the requested missing ID. */
|
|
85
86
|
readonly serverId: string;
|
|
86
87
|
}
|
|
@@ -97,7 +98,7 @@ export interface LspServerSuccess<T> {
|
|
|
97
98
|
|
|
98
99
|
/** Keeps successful multi-server reads useful when independent servers fail. */
|
|
99
100
|
export interface LspServerReadResult<T> {
|
|
100
|
-
/** Labeled failures in deterministic route order. */
|
|
101
|
+
/** Labeled operational failures in deterministic route order. */
|
|
101
102
|
readonly failures: readonly LspServerFailure[];
|
|
102
103
|
/** Labeled successful values in deterministic route order. */
|
|
103
104
|
readonly successes: readonly LspServerSuccess<T>[];
|
|
@@ -132,8 +133,6 @@ export interface LspServerStatusEntry {
|
|
|
132
133
|
|
|
133
134
|
/** Reports configuration failures and session-scoped Server Instance states. */
|
|
134
135
|
export interface LspServerManagerStatus {
|
|
135
|
-
/** False when either authored `lsp` settings layer was malformed. */
|
|
136
|
-
readonly enabled: boolean;
|
|
137
136
|
/** Server entries ordered by ID and then root. */
|
|
138
137
|
readonly servers: readonly LspServerStatusEntry[];
|
|
139
138
|
/** Strict settings failures kept visible until Pi `/reload`. */
|
|
@@ -167,8 +166,11 @@ function findNearestLspRoot(
|
|
|
167
166
|
rootMarkers: readonly string[] | undefined,
|
|
168
167
|
ancestorDirectories: readonly LspAncestorDirectory[],
|
|
169
168
|
cwd: string,
|
|
170
|
-
|
|
171
|
-
|
|
169
|
+
requireRootMarker: boolean,
|
|
170
|
+
): string | undefined {
|
|
171
|
+
if (rootMarkers === undefined || rootMarkers.length === 0) {
|
|
172
|
+
return requireRootMarker ? undefined : resolve(cwd);
|
|
173
|
+
}
|
|
172
174
|
for (const directory of ancestorDirectories) {
|
|
173
175
|
if (
|
|
174
176
|
directory.entryNames.some((entryName) =>
|
|
@@ -178,7 +180,7 @@ function findNearestLspRoot(
|
|
|
178
180
|
return directory.path;
|
|
179
181
|
}
|
|
180
182
|
}
|
|
181
|
-
return resolve(cwd);
|
|
183
|
+
return requireRootMarker ? undefined : resolve(cwd);
|
|
182
184
|
}
|
|
183
185
|
|
|
184
186
|
/** Route one file to every matching configured server in stable settings-map order. */
|
|
@@ -196,10 +198,17 @@ export function routeLspServersForFile(
|
|
|
196
198
|
languageMatchesFile(candidate, normalizedFilePath),
|
|
197
199
|
);
|
|
198
200
|
if (language === undefined) continue;
|
|
201
|
+
const rootPath = findNearestLspRoot(
|
|
202
|
+
serverDefinition.rootMarkers,
|
|
203
|
+
ancestorDirectories,
|
|
204
|
+
cwd,
|
|
205
|
+
serverDefinition.requireRootMarker ?? false,
|
|
206
|
+
);
|
|
207
|
+
if (rootPath === undefined) continue;
|
|
199
208
|
routes.push({
|
|
200
209
|
serverId: serverDefinition.serverId,
|
|
201
210
|
language,
|
|
202
|
-
rootPath
|
|
211
|
+
rootPath,
|
|
203
212
|
});
|
|
204
213
|
}
|
|
205
214
|
|
|
@@ -235,7 +244,6 @@ function unavailableFailure(route: LspServerRoute, error: string): LspServerFail
|
|
|
235
244
|
return {
|
|
236
245
|
code: "server-unavailable",
|
|
237
246
|
message: `Pi LSP: server ${route.serverId} is unavailable for ${route.rootPath}: ${error}`,
|
|
238
|
-
rootPath: route.rootPath,
|
|
239
247
|
serverId: route.serverId,
|
|
240
248
|
};
|
|
241
249
|
}
|
|
@@ -275,26 +283,32 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
275
283
|
}
|
|
276
284
|
}
|
|
277
285
|
return {
|
|
278
|
-
enabled: this.input.settings.enabled,
|
|
279
286
|
servers,
|
|
280
287
|
warnings: this.input.settings.warnings,
|
|
281
288
|
};
|
|
282
289
|
}
|
|
283
290
|
|
|
284
|
-
|
|
285
|
-
async routeFile(filePath: string): Promise<readonly LspServerRoute[]> {
|
|
286
|
-
if (!this.input.settings.enabled) return [];
|
|
291
|
+
private async routeFile(filePath: string): Promise<readonly LspServerRoute[]> {
|
|
287
292
|
const absolutePath = resolve(this.input.cwd, normalizeLspFilePath(filePath));
|
|
288
293
|
const ancestors = await readLspAncestorDirectories(absolutePath);
|
|
289
294
|
const definitions = [...this.input.settings.servers.values()].map((definition) => ({
|
|
290
295
|
languages: definition.languages,
|
|
296
|
+
requireRootMarker: definition.requireRootMarker,
|
|
291
297
|
rootMarkers: definition.rootMarkers,
|
|
292
298
|
serverId: definition.id,
|
|
293
299
|
}));
|
|
294
300
|
return routeLspServersForFile(definitions, absolutePath, this.input.cwd, ancestors);
|
|
295
301
|
}
|
|
296
302
|
|
|
297
|
-
/**
|
|
303
|
+
/** Report whether any Server Definition accepts the file language before activation gating. */
|
|
304
|
+
hasConfiguredLanguageServerForFile(filePath: string): boolean {
|
|
305
|
+
const absolutePath = resolve(this.input.cwd, normalizeLspFilePath(filePath));
|
|
306
|
+
return [...this.input.settings.servers.values()].some((definition) =>
|
|
307
|
+
definition.languages.some((language) => languageMatchesFile(language, absolutePath)),
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** Query matching capable instances while retaining independent operational failures. */
|
|
298
312
|
async runRead<T>(
|
|
299
313
|
filePath: string,
|
|
300
314
|
serverId: string | undefined,
|
|
@@ -310,14 +324,14 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
310
324
|
}
|
|
311
325
|
|
|
312
326
|
const outcomes = await Promise.all(
|
|
313
|
-
routes.map(async (route): Promise<LspServerSuccess<T> | LspServerFailure> => {
|
|
327
|
+
routes.map(async (route): Promise<LspServerSuccess<T> | LspServerFailure | undefined> => {
|
|
314
328
|
const resolution = await this.ensureClient(route);
|
|
315
329
|
if (resolution.kind === "failure") return resolution.failure;
|
|
316
330
|
if (!isCapable(resolution.instance.client)) {
|
|
331
|
+
if (serverId === undefined) return undefined;
|
|
317
332
|
return {
|
|
318
333
|
code: "no-capable-server",
|
|
319
334
|
message: `Pi LSP: server ${route.serverId} does not support the requested operation`,
|
|
320
|
-
rootPath: route.rootPath,
|
|
321
335
|
serverId: route.serverId,
|
|
322
336
|
};
|
|
323
337
|
}
|
|
@@ -331,7 +345,6 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
331
345
|
return {
|
|
332
346
|
code: "request-failed",
|
|
333
347
|
message: `Pi LSP: server ${route.serverId} request failed: ${describeLspError(error)}`,
|
|
334
|
-
rootPath: route.rootPath,
|
|
335
348
|
serverId: route.serverId,
|
|
336
349
|
};
|
|
337
350
|
}
|
|
@@ -341,9 +354,17 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
341
354
|
const failures: LspServerFailure[] = [];
|
|
342
355
|
const successes: LspServerSuccess<T>[] = [];
|
|
343
356
|
for (const outcome of outcomes) {
|
|
357
|
+
if (outcome === undefined) continue;
|
|
344
358
|
if ("code" in outcome) failures.push(outcome);
|
|
345
359
|
else successes.push(outcome);
|
|
346
360
|
}
|
|
361
|
+
if (failures.length === 0 && successes.length === 0) {
|
|
362
|
+
failures.push({
|
|
363
|
+
code: "no-capable-server",
|
|
364
|
+
message: "Pi LSP: no matching server supports the requested read operation",
|
|
365
|
+
serverId: "*",
|
|
366
|
+
});
|
|
367
|
+
}
|
|
347
368
|
return { failures, successes };
|
|
348
369
|
}
|
|
349
370
|
|
|
@@ -446,10 +467,23 @@ export class LspServerManager<TClient extends LspManagedServerClient = LspManage
|
|
|
446
467
|
}
|
|
447
468
|
|
|
448
469
|
private noMatchingFailure(serverId: string | undefined, filePath: string): LspServerFailure {
|
|
470
|
+
const normalizedFilePath = normalizeLspFilePath(filePath);
|
|
471
|
+
const definition =
|
|
472
|
+
serverId === undefined ? undefined : this.input.settings.servers.get(serverId);
|
|
473
|
+
if (
|
|
474
|
+
definition?.requireRootMarker === true &&
|
|
475
|
+
definition.languages.some((language) => languageMatchesFile(language, normalizedFilePath))
|
|
476
|
+
) {
|
|
477
|
+
return {
|
|
478
|
+
code: "root-marker-not-found",
|
|
479
|
+
message: `Pi LSP: required root marker not found for server ${definition.id} and ${normalizedFilePath}; expected one of: ${definition.rootMarkers.join(", ")}`,
|
|
480
|
+
serverId: definition.id,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
449
483
|
const requestedServer = serverId === undefined ? "any configured server" : `server ${serverId}`;
|
|
450
484
|
return {
|
|
451
485
|
code: "no-matching-server",
|
|
452
|
-
message: `Pi LSP: ${requestedServer} does not match ${
|
|
486
|
+
message: `Pi LSP: ${requestedServer} does not match ${normalizedFilePath}`,
|
|
453
487
|
serverId: serverId ?? "*",
|
|
454
488
|
};
|
|
455
489
|
}
|
package/src/lsp-session-files.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
-
/** The maximum retained byte length for one language server's stderr log. */
|
|
5
|
-
export const MAX_SERVER_STDERR_BYTES = 1024 * 1024;
|
|
6
|
-
|
|
7
4
|
/** Owns private Result Spill and bounded stderr files for one Pi session. */
|
|
8
5
|
export interface LspSessionFiles {
|
|
9
6
|
/** Private directory removed when the Pi session shuts down. */
|
|
@@ -12,15 +9,12 @@ export interface LspSessionFiles {
|
|
|
12
9
|
writeResultSpill(output: string): Promise<string>;
|
|
13
10
|
/** Create or return the bounded stderr file path for one language server. */
|
|
14
11
|
getServerStderrPath(serverId: string): Promise<string>;
|
|
15
|
-
/** Retain the latest one megabyte of one language server's stderr stream. */
|
|
16
|
-
appendServerStderr(serverId: string, chunk: Uint8Array): Promise<string>;
|
|
17
12
|
/** Remove all session files after queued writes finish. */
|
|
18
13
|
close(): Promise<void>;
|
|
19
14
|
}
|
|
20
15
|
|
|
21
16
|
interface ServerStderrFile {
|
|
22
17
|
readonly path: string;
|
|
23
|
-
content: Buffer;
|
|
24
18
|
}
|
|
25
19
|
|
|
26
20
|
class LspSessionFileStore implements LspSessionFiles {
|
|
@@ -45,22 +39,7 @@ class LspSessionFileStore implements LspSessionFiles {
|
|
|
45
39
|
getServerStderrPath(serverId: string): Promise<string> {
|
|
46
40
|
const stderrFile = this.serverStderrFile(serverId);
|
|
47
41
|
return this.enqueueSessionFileWrite(async () => {
|
|
48
|
-
await writeFile(stderrFile.path,
|
|
49
|
-
await chmod(stderrFile.path, 0o600);
|
|
50
|
-
return stderrFile.path;
|
|
51
|
-
});
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
appendServerStderr(serverId: string, chunk: Uint8Array): Promise<string> {
|
|
55
|
-
const stderrFile = this.serverStderrFile(serverId);
|
|
56
|
-
const copiedChunk = Buffer.from(chunk);
|
|
57
|
-
return this.enqueueSessionFileWrite(async () => {
|
|
58
|
-
const combined = Buffer.concat([stderrFile.content, copiedChunk]);
|
|
59
|
-
stderrFile.content =
|
|
60
|
-
combined.length <= MAX_SERVER_STDERR_BYTES
|
|
61
|
-
? combined
|
|
62
|
-
: Buffer.from(combined.subarray(combined.length - MAX_SERVER_STDERR_BYTES));
|
|
63
|
-
await writeFile(stderrFile.path, stderrFile.content, { mode: 0o600 });
|
|
42
|
+
await writeFile(stderrFile.path, "", { mode: 0o600 });
|
|
64
43
|
await chmod(stderrFile.path, 0o600);
|
|
65
44
|
return stderrFile.path;
|
|
66
45
|
});
|
|
@@ -81,7 +60,6 @@ class LspSessionFileStore implements LspSessionFiles {
|
|
|
81
60
|
if (existing !== undefined) return existing;
|
|
82
61
|
const stderrFile = {
|
|
83
62
|
path: join(this.directoryPath, `server-stderr-${this.nextFileIndex++}.log`),
|
|
84
|
-
content: Buffer.alloc(0),
|
|
85
63
|
};
|
|
86
64
|
this.stderrFiles.set(serverId, stderrFile);
|
|
87
65
|
return stderrFile;
|
package/src/lsp-tool-contract.ts
CHANGED
|
@@ -332,7 +332,6 @@ const RegularFileSnapshotSchema = Type.Object(
|
|
|
332
332
|
{
|
|
333
333
|
kind: Type.Literal("file"),
|
|
334
334
|
content_base64: Type.String(),
|
|
335
|
-
hash: Type.String({ minLength: 64, maxLength: 64 }),
|
|
336
335
|
mode: Type.Integer({ minimum: 0 }),
|
|
337
336
|
},
|
|
338
337
|
{ additionalProperties: false },
|
|
@@ -345,13 +344,17 @@ const SymlinkSnapshotSchema = Type.Object(
|
|
|
345
344
|
},
|
|
346
345
|
{ additionalProperties: false },
|
|
347
346
|
);
|
|
348
|
-
|
|
347
|
+
/** Canonical pre-mutation snapshot of one filesystem path used by guarded rollback. */
|
|
348
|
+
export const FileSnapshotSchema = Type.Union([
|
|
349
349
|
MissingFileSnapshotSchema,
|
|
350
350
|
RegularFileSnapshotSchema,
|
|
351
351
|
SymlinkSnapshotSchema,
|
|
352
352
|
]);
|
|
353
|
-
const ExistingFileSnapshotSchema = Type.Union([
|
|
354
|
-
|
|
353
|
+
export const ExistingFileSnapshotSchema = Type.Union([
|
|
354
|
+
RegularFileSnapshotSchema,
|
|
355
|
+
SymlinkSnapshotSchema,
|
|
356
|
+
]);
|
|
357
|
+
export const WorkspaceEditOperationSchema = Type.Union([
|
|
355
358
|
Type.Object(
|
|
356
359
|
{
|
|
357
360
|
kind: Type.Literal("modify"),
|
|
@@ -440,7 +443,6 @@ const WorkspaceEditApplyDetailsSchema = Type.Object(
|
|
|
440
443
|
changed_paths: Type.Array(AbsolutePathSchema),
|
|
441
444
|
preview_records: Type.Optional(Type.Array(LspWorkspaceEditPreviewRecordSchema)),
|
|
442
445
|
state: Type.Union([Type.Literal("applied"), Type.Literal("partial_failure")]),
|
|
443
|
-
recovery_failure_paths: Type.Optional(Type.Array(AbsolutePathSchema)),
|
|
444
446
|
},
|
|
445
447
|
{ additionalProperties: false },
|
|
446
448
|
);
|
|
@@ -460,9 +462,3 @@ export type LspWorkspaceEditPreviewRecord = Static<typeof LspWorkspaceEditPrevie
|
|
|
460
462
|
|
|
461
463
|
/** A normalized per-server outcome used when rendering an LSP read operation. */
|
|
462
464
|
export type ServerOperationOutcome = Static<typeof ServerOperationOutcomeSchema>;
|
|
463
|
-
|
|
464
|
-
/** A persisted Workspace Edit Preview that can be rebuilt from the active session branch. */
|
|
465
|
-
export type WorkspaceEditPreviewDetails = Static<typeof WorkspaceEditPreviewDetailsSchema>;
|
|
466
|
-
|
|
467
|
-
/** The result of applying one guarded Workspace Edit Preview. */
|
|
468
|
-
export type WorkspaceEditApplyDetails = Static<typeof WorkspaceEditApplyDetailsSchema>;
|
package/src/lsp-tool-output.ts
CHANGED
|
@@ -32,6 +32,24 @@ export function formatLspToolValue(value: LSPAny): string {
|
|
|
32
32
|
return text === undefined ? "null" : text;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/** Truncate model-visible text against Pi limits, spilling the complete text when truncated. */
|
|
36
|
+
export async function truncateLspOutputText(
|
|
37
|
+
text: string,
|
|
38
|
+
sessionFiles: LspSessionFiles,
|
|
39
|
+
subject: string,
|
|
40
|
+
): Promise<{ readonly text: string; readonly spillPath?: string }> {
|
|
41
|
+
const truncation = truncateHead(text, {
|
|
42
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
43
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
44
|
+
});
|
|
45
|
+
if (!truncation.truncated) return { text };
|
|
46
|
+
const spillPath = await sessionFiles.writeResultSpill(text);
|
|
47
|
+
return {
|
|
48
|
+
text: `${truncation.content}\n\n[Pi LSP: ${subject} truncated; complete Result Spill: ${spillPath}]`,
|
|
49
|
+
spillPath,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
35
53
|
/** Validate normalized details, truncate model-visible text, and spill every complete oversized result. */
|
|
36
54
|
export async function createLspToolOutput(
|
|
37
55
|
text: string,
|
|
@@ -39,26 +57,20 @@ export async function createLspToolOutput(
|
|
|
39
57
|
sessionFiles: LspSessionFiles,
|
|
40
58
|
): Promise<AgentToolResult<LspToolResultDetails>> {
|
|
41
59
|
const normalizedDetails = Value.Parse(LspToolResultDetailsSchema, details);
|
|
42
|
-
const truncated =
|
|
43
|
-
|
|
44
|
-
maxLines: DEFAULT_MAX_LINES,
|
|
45
|
-
});
|
|
46
|
-
if (!truncated.truncated) {
|
|
60
|
+
const truncated = await truncateLspOutputText(text, sessionFiles, "output");
|
|
61
|
+
if (truncated.spillPath === undefined) {
|
|
47
62
|
return { content: [{ type: "text", text }], details: normalizedDetails };
|
|
48
63
|
}
|
|
49
64
|
|
|
50
|
-
const spillPath = await sessionFiles.writeResultSpill(text);
|
|
51
|
-
const notice = `\n\n[Pi LSP: output truncated; complete Result Spill: ${spillPath}]`;
|
|
52
|
-
const visibleText = `${truncated.content}${notice}`;
|
|
53
65
|
const detailsWithSpill =
|
|
54
66
|
normalizedDetails.kind === "operation"
|
|
55
67
|
? Value.Parse(LspToolResultDetailsSchema, {
|
|
56
68
|
...normalizedDetails,
|
|
57
|
-
spill_path: spillPath,
|
|
69
|
+
spill_path: truncated.spillPath,
|
|
58
70
|
})
|
|
59
71
|
: normalizedDetails;
|
|
60
72
|
return {
|
|
61
|
-
content: [{ type: "text", text:
|
|
73
|
+
content: [{ type: "text", text: truncated.text }],
|
|
62
74
|
details: detailsWithSpill,
|
|
63
75
|
};
|
|
64
76
|
}
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type LspToolResultDetails,
|
|
17
17
|
type ServerOperationOutcome,
|
|
18
18
|
} from "./lsp-tool-contract.js";
|
|
19
|
+
import { pluralizedCount } from "./lsp-post-edit-diagnostics-rendering.js";
|
|
19
20
|
|
|
20
21
|
/** Theme operations used by Pi LSP tool transcript rendering. */
|
|
21
22
|
export type LspRenderTheme = Pick<Theme, "bold" | "fg">;
|
|
@@ -139,10 +140,6 @@ function semanticLspOperationMetric(
|
|
|
139
140
|
return pluralizedCount(count, noun);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
|
-
function pluralizedCount(count: number, noun: string): string {
|
|
143
|
-
return `${count} ${noun}${count === 1 ? "" : "s"}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
143
|
function outcomeColor(outcome: ServerOperationOutcome["outcome"]): ThemeColor {
|
|
147
144
|
switch (outcome) {
|
|
148
145
|
case "success":
|
package/src/lsp-tool.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
|
-
import type {
|
|
4
|
+
import type {
|
|
5
|
+
AgentToolResult,
|
|
6
|
+
ExtensionContext,
|
|
7
|
+
ToolDefinition,
|
|
8
|
+
} from "@earendil-works/pi-coding-agent";
|
|
5
9
|
import { type Static, Type } from "typebox";
|
|
6
10
|
import { Value } from "typebox/value";
|
|
7
11
|
import {
|
|
@@ -227,6 +231,23 @@ async function createLspToolOutput(
|
|
|
227
231
|
);
|
|
228
232
|
}
|
|
229
233
|
|
|
234
|
+
async function readOutput(
|
|
235
|
+
operation: LspToolParameters["operation"],
|
|
236
|
+
result: Promise<LspServerReadResult<LSPAny>>,
|
|
237
|
+
dependencies: LspToolDependencies,
|
|
238
|
+
) {
|
|
239
|
+
const resolved = await result;
|
|
240
|
+
requireReadSuccess(resolved);
|
|
241
|
+
return createLspToolOutput(
|
|
242
|
+
formatLspToolValue({
|
|
243
|
+
results: readOperationValue(resolved),
|
|
244
|
+
warnings: resolved.failures.map(({ message }) => message),
|
|
245
|
+
}),
|
|
246
|
+
operationDetails(operation, readOperationOutcomes(resolved)),
|
|
247
|
+
dependencies,
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
230
251
|
function parseLspToolParameters(input: LSPAny): LspToolParameters {
|
|
231
252
|
try {
|
|
232
253
|
return Value.Parse(LspToolParametersSchema, input);
|
|
@@ -440,15 +461,6 @@ async function normalizeProtocolResult(
|
|
|
440
461
|
return Object.fromEntries(entries);
|
|
441
462
|
}
|
|
442
463
|
|
|
443
|
-
async function requestDocumentMethod(
|
|
444
|
-
prepared: PreparedDocument,
|
|
445
|
-
method: string,
|
|
446
|
-
parameters: LSPAny,
|
|
447
|
-
signal: AbortSignal | undefined,
|
|
448
|
-
): Promise<LSPAny> {
|
|
449
|
-
return prepared.client.request<LSPAny>(method, parameters, signal);
|
|
450
|
-
}
|
|
451
|
-
|
|
452
464
|
function supportsResolveProvider(value: LSPAny): boolean {
|
|
453
465
|
return protocolRecord(value)?.resolveProvider === true;
|
|
454
466
|
}
|
|
@@ -545,9 +557,7 @@ async function workspacePreviewOutput(
|
|
|
545
557
|
serverId: string,
|
|
546
558
|
edit: WorkspaceEdit,
|
|
547
559
|
positionEncoding: PositionEncodingKind,
|
|
548
|
-
): Promise<
|
|
549
|
-
ReturnType<typeof createLspToolOutput> extends Promise<infer TResult> ? TResult : never
|
|
550
|
-
> {
|
|
560
|
+
): Promise<AgentToolResult<LspToolResultDetails>> {
|
|
551
561
|
const preview = await dependencies.workspaceEdits.createPreview({
|
|
552
562
|
edit,
|
|
553
563
|
serverId,
|
|
@@ -617,8 +627,7 @@ async function executePositionRead(
|
|
|
617
627
|
context: { includeDeclaration: parameters.include_declaration ?? true },
|
|
618
628
|
};
|
|
619
629
|
}
|
|
620
|
-
let value = await
|
|
621
|
-
prepared,
|
|
630
|
+
let value = await prepared.client.request<LSPAny>(
|
|
622
631
|
capabilityMethod,
|
|
623
632
|
requestParameters,
|
|
624
633
|
signal,
|
|
@@ -1015,7 +1024,6 @@ async function executeApplyPreview(
|
|
|
1015
1024
|
preview_id: parameters.preview_id,
|
|
1016
1025
|
mutation_manifest: canonicalManifest,
|
|
1017
1026
|
changed_paths: [...cause.recoveryFailures].sort((left, right) => left.localeCompare(right)),
|
|
1018
|
-
recovery_failure_paths: [...cause.recoveryFailures],
|
|
1019
1027
|
state: "partial_failure",
|
|
1020
1028
|
},
|
|
1021
1029
|
dependencies,
|
|
@@ -1127,72 +1135,42 @@ export function createLspToolDefinition(
|
|
|
1127
1135
|
case "type_hierarchy":
|
|
1128
1136
|
case "supertypes":
|
|
1129
1137
|
case "subtypes":
|
|
1130
|
-
case "prepare_rename":
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
formatLspToolValue({
|
|
1135
|
-
results: readOperationValue(result),
|
|
1136
|
-
warnings: result.failures.map(({ message }) => message),
|
|
1137
|
-
}),
|
|
1138
|
-
operationDetails(parameters.operation, readOperationOutcomes(result)),
|
|
1138
|
+
case "prepare_rename":
|
|
1139
|
+
return readOutput(
|
|
1140
|
+
parameters.operation,
|
|
1141
|
+
executePositionRead(dependencies, parameters, context, signal),
|
|
1139
1142
|
dependencies,
|
|
1140
1143
|
);
|
|
1141
|
-
}
|
|
1142
1144
|
case "diagnostics":
|
|
1143
1145
|
case "document_symbols":
|
|
1144
1146
|
case "document_links":
|
|
1145
1147
|
case "folding_ranges":
|
|
1146
1148
|
case "code_lenses":
|
|
1147
|
-
case "document_colors":
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
formatLspToolValue({
|
|
1152
|
-
results: readOperationValue(result),
|
|
1153
|
-
warnings: result.failures.map(({ message }) => message),
|
|
1154
|
-
}),
|
|
1155
|
-
operationDetails(parameters.operation, readOperationOutcomes(result)),
|
|
1149
|
+
case "document_colors":
|
|
1150
|
+
return readOutput(
|
|
1151
|
+
parameters.operation,
|
|
1152
|
+
executeFileRead(dependencies, parameters, context, signal),
|
|
1156
1153
|
dependencies,
|
|
1157
1154
|
);
|
|
1158
|
-
}
|
|
1159
1155
|
case "workspace_diagnostics":
|
|
1160
|
-
case "workspace_symbols":
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
formatLspToolValue({
|
|
1165
|
-
results: readOperationValue(result),
|
|
1166
|
-
warnings: result.failures.map(({ message }) => message),
|
|
1167
|
-
}),
|
|
1168
|
-
operationDetails(parameters.operation, readOperationOutcomes(result)),
|
|
1156
|
+
case "workspace_symbols":
|
|
1157
|
+
return readOutput(
|
|
1158
|
+
parameters.operation,
|
|
1159
|
+
executeWorkspaceRead(dependencies, parameters, context, signal),
|
|
1169
1160
|
dependencies,
|
|
1170
1161
|
);
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
return createLspToolOutput(
|
|
1176
|
-
formatLspToolValue({
|
|
1177
|
-
results: readOperationValue(result),
|
|
1178
|
-
warnings: result.failures.map(({ message }) => message),
|
|
1179
|
-
}),
|
|
1180
|
-
operationDetails(parameters.operation, readOperationOutcomes(result)),
|
|
1162
|
+
case "selection_ranges":
|
|
1163
|
+
return readOutput(
|
|
1164
|
+
parameters.operation,
|
|
1165
|
+
executeSelectionRanges(dependencies, parameters, context, signal),
|
|
1181
1166
|
dependencies,
|
|
1182
1167
|
);
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
return createLspToolOutput(
|
|
1188
|
-
formatLspToolValue({
|
|
1189
|
-
results: readOperationValue(result),
|
|
1190
|
-
warnings: result.failures.map(({ message }) => message),
|
|
1191
|
-
}),
|
|
1192
|
-
operationDetails(parameters.operation, readOperationOutcomes(result)),
|
|
1168
|
+
case "inlay_hints":
|
|
1169
|
+
return readOutput(
|
|
1170
|
+
parameters.operation,
|
|
1171
|
+
executeInlayHints(dependencies, parameters, context, signal),
|
|
1193
1172
|
dependencies,
|
|
1194
1173
|
);
|
|
1195
|
-
}
|
|
1196
1174
|
case "format_document":
|
|
1197
1175
|
case "format_range":
|
|
1198
1176
|
case "format_on_type":
|