@ian-pascoe/pi-lsp 0.1.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.
@@ -0,0 +1,1214 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import type { ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { type Static, Type } from "typebox";
6
+ import { Value } from "typebox/value";
7
+ import {
8
+ CallHierarchyIncomingCallsRequest,
9
+ CallHierarchyOutgoingCallsRequest,
10
+ CallHierarchyPrepareRequest,
11
+ CodeActionRequest,
12
+ CodeActionResolveRequest,
13
+ CodeLensRequest,
14
+ CodeLensResolveRequest,
15
+ CompletionRequest,
16
+ CompletionResolveRequest,
17
+ DeclarationRequest,
18
+ DefinitionRequest,
19
+ DocumentColorRequest,
20
+ DocumentFormattingRequest,
21
+ DocumentHighlightRequest,
22
+ DocumentLinkRequest,
23
+ DocumentLinkResolveRequest,
24
+ DocumentOnTypeFormattingRequest,
25
+ DocumentRangeFormattingRequest,
26
+ DocumentSymbolRequest,
27
+ FoldingRangeRequest,
28
+ HoverRequest,
29
+ ImplementationRequest,
30
+ InlayHintRequest,
31
+ InlayHintResolveRequest,
32
+ PositionEncodingKind,
33
+ PrepareRenameRequest,
34
+ ReferencesRequest,
35
+ RenameRequest,
36
+ SelectionRangeRequest,
37
+ SignatureHelpRequest,
38
+ TypeDefinitionRequest,
39
+ TypeHierarchyPrepareRequest,
40
+ TypeHierarchySubtypesRequest,
41
+ TypeHierarchySupertypesRequest,
42
+ WorkspaceSymbolRequest,
43
+ WorkspaceSymbolResolveRequest,
44
+ type LSPAny,
45
+ type Position,
46
+ type WorkspaceEdit,
47
+ } from "vscode-languageserver-protocol/node";
48
+ import {
49
+ convertLspCodePointPosition,
50
+ convertLspProtocolPosition,
51
+ normalizeLspPositionEncoding,
52
+ type LspCodePointPosition,
53
+ type LspPositionEncoding,
54
+ } from "./lsp-position-encoding.js";
55
+ import type {
56
+ LspDocumentDiagnosticResult,
57
+ LspSynchronizedDocument,
58
+ LspWorkspaceDiagnosticResult,
59
+ } from "./lsp-server-client.js";
60
+ import {
61
+ normalizeLspFilePath,
62
+ type LspServerFailure,
63
+ type LspServerManager,
64
+ type LspServerReadResult,
65
+ type LspServerRoute,
66
+ } from "./lsp-server-manager.js";
67
+ import type { LspSessionFiles } from "./lsp-session-files.js";
68
+ import {
69
+ LspToolParametersSchema,
70
+ LspWorkspaceEditPreviewRecordSchema,
71
+ MutationManifestSchema,
72
+ type LspToolParameters,
73
+ type LspToolResultDetails,
74
+ type MutationManifest,
75
+ type ServerOperationOutcome,
76
+ } from "./lsp-tool-contract.js";
77
+ import {
78
+ createLspToolOutput as createBaseLspToolOutput,
79
+ formatLspToolValue,
80
+ } from "./lsp-tool-output.js";
81
+ import { renderLspToolCall, renderLspToolResult } from "./lsp-tool-rendering.js";
82
+ import { LspWorkspaceEditError, type LspWorkspaceEditStore } from "./lsp-workspace-edit.js";
83
+
84
+ const ProtocolRecordSchema = Type.Record(Type.String(), Type.Any());
85
+ const ProtocolStringSchema = Type.String();
86
+ const ProtocolFoldingRangeSchema = Type.Object(
87
+ {
88
+ startLine: Type.Integer({ minimum: 0 }),
89
+ startCharacter: Type.Optional(Type.Integer({ minimum: 0 })),
90
+ endLine: Type.Integer({ minimum: 0 }),
91
+ endCharacter: Type.Optional(Type.Integer({ minimum: 0 })),
92
+ },
93
+ { additionalProperties: true },
94
+ );
95
+
96
+ const ApplyPreviewArgumentsSchema = Type.Object(
97
+ {
98
+ operation: Type.Literal("apply"),
99
+ preview_id: Type.String({ minLength: 1 }),
100
+ mutation_manifest: Type.Optional(Type.Any()),
101
+ },
102
+ { additionalProperties: true },
103
+ );
104
+
105
+ type ApplyPreviewArguments = Static<typeof ApplyPreviewArgumentsSchema>;
106
+
107
+ type FileReadOperation =
108
+ | "diagnostics"
109
+ | "document_symbols"
110
+ | "document_links"
111
+ | "folding_ranges"
112
+ | "code_lenses"
113
+ | "document_colors";
114
+ type PositionReadOperation =
115
+ | "completion"
116
+ | "hover"
117
+ | "signature_help"
118
+ | "declaration"
119
+ | "goto_definition"
120
+ | "goto_type_definition"
121
+ | "goto_implementation"
122
+ | "find_references"
123
+ | "document_highlights"
124
+ | "call_hierarchy"
125
+ | "incoming_calls"
126
+ | "outgoing_calls"
127
+ | "type_hierarchy"
128
+ | "supertypes"
129
+ | "subtypes"
130
+ | "prepare_rename";
131
+ interface FileReadParameters {
132
+ readonly operation: FileReadOperation;
133
+ readonly file_path: string;
134
+ readonly server_id?: string;
135
+ }
136
+ interface PositionReadParameters {
137
+ readonly operation: PositionReadOperation;
138
+ readonly file_path: string;
139
+ readonly line: number;
140
+ readonly character: number;
141
+ readonly server_id?: string;
142
+ readonly include_declaration?: boolean;
143
+ }
144
+ /** Public language-server client surface consumed by tool dispatch. */
145
+ export interface LspToolServerClient {
146
+ /** Negotiated static capabilities plus supported dynamic registrations. */
147
+ readonly capabilities: LSPAny;
148
+ /** Negotiated protocol character encoding. */
149
+ readonly positionEncoding: PositionEncodingKind;
150
+ /** Report whether one protocol request is currently supported. */
151
+ hasCapability(method: string): boolean;
152
+ /** Open or update one UTF-8 document before a document request. */
153
+ synchronizeDocument(filePath: string, languageId: string): Promise<LspSynchronizedDocument>;
154
+ /** Send one cancellable protocol request. */
155
+ request<TResult>(method: string, parameters: LSPAny, signal?: AbortSignal): Promise<TResult>;
156
+ /** Synchronize and return fresh document diagnostics. */
157
+ documentDiagnostics(
158
+ filePath: string,
159
+ languageId: string,
160
+ signal?: AbortSignal,
161
+ ): Promise<LspDocumentDiagnosticResult>;
162
+ /** Return pull workspace diagnostics or the cached push fallback. */
163
+ workspaceDiagnostics(signal?: AbortSignal): Promise<LspWorkspaceDiagnosticResult>;
164
+ /** Gracefully shut down the owned server process. */
165
+ shutdown(): Promise<void>;
166
+ }
167
+
168
+ /** Narrow Pi registration surface used to install exactly one LSP tool. */
169
+ export interface LspToolRegistrar {
170
+ /** Register the session-bound strict LSP ToolDefinition. */
171
+ registerTool(tool: ToolDefinition<typeof LspToolParametersSchema, LspToolResultDetails>): void;
172
+ }
173
+
174
+ /** Runtime owners used by the single registered Pi LSP tool. */
175
+ export interface LspToolDependencies {
176
+ /** Session-scoped lazy language-server registry. */
177
+ readonly manager: LspServerManager<LspToolServerClient>;
178
+ /** Session-scoped Workspace Edit Preview and Validated Workspace Edit store. */
179
+ readonly workspaceEdits: LspWorkspaceEditStore;
180
+ /** Private Result Spill storage for complete truncated output. */
181
+ readonly sessionFiles: LspSessionFiles;
182
+ }
183
+
184
+ interface LspReadValue {
185
+ readonly root_path: string;
186
+ readonly server_id: string;
187
+ readonly value: LSPAny;
188
+ }
189
+
190
+ interface PreparedDocument {
191
+ readonly client: LspToolServerClient;
192
+ readonly document: LspSynchronizedDocument;
193
+ readonly positionEncoding: LspPositionEncoding;
194
+ readonly route: LspServerRoute;
195
+ }
196
+
197
+ function piLspError(message: string): Error {
198
+ return new Error(message.startsWith("Pi LSP:") ? message : `Pi LSP: ${message}`);
199
+ }
200
+
201
+ async function createLspToolOutput(
202
+ text: string,
203
+ details: LspToolResultDetails,
204
+ dependencies: LspToolDependencies,
205
+ ) {
206
+ const previewRecords = dependencies.workspaceEdits.takeUnreportedPreviewRecords();
207
+ const normalizedPreviewRecords = previewRecords.map((record) =>
208
+ Value.Parse(LspWorkspaceEditPreviewRecordSchema, record),
209
+ );
210
+ const mergedDetails =
211
+ normalizedPreviewRecords.length === 0
212
+ ? details
213
+ : {
214
+ ...details,
215
+ preview_records: [...(details.preview_records ?? []), ...normalizedPreviewRecords],
216
+ };
217
+ const previewNotice =
218
+ previewRecords.length === 0
219
+ ? ""
220
+ : `\n\nServer Workspace Edit Preview${previewRecords.length === 1 ? "" : "s"}: ${previewRecords
221
+ .map(({ preview_id: previewId }) => previewId)
222
+ .join(", ")}`;
223
+ return createBaseLspToolOutput(
224
+ `${text}${previewNotice}`,
225
+ mergedDetails,
226
+ dependencies.sessionFiles,
227
+ );
228
+ }
229
+
230
+ function parseLspToolParameters(input: LSPAny): LspToolParameters {
231
+ try {
232
+ return Value.Parse(LspToolParametersSchema, input);
233
+ } catch (cause) {
234
+ const message = cause instanceof Error ? cause.message : String(cause);
235
+ throw piLspError(`invalid tool arguments: ${message}`);
236
+ }
237
+ }
238
+
239
+ function absoluteLspFilePath(filePath: string, context: ExtensionContext): string {
240
+ return resolve(context.cwd, normalizeLspFilePath(filePath));
241
+ }
242
+
243
+ async function prepareLspDocument(
244
+ client: LspToolServerClient,
245
+ route: LspServerRoute,
246
+ filePath: string,
247
+ ): Promise<PreparedDocument> {
248
+ return {
249
+ client,
250
+ document: await client.synchronizeDocument(filePath, route.language.languageId),
251
+ positionEncoding: normalizeLspPositionEncoding(client.positionEncoding),
252
+ route,
253
+ };
254
+ }
255
+
256
+ function protocolPosition(prepared: PreparedDocument, position: LspCodePointPosition): Position {
257
+ return convertLspCodePointPosition(prepared.document.text, position, prepared.positionEncoding);
258
+ }
259
+
260
+ function serverOutcomeForFailure(failure: LspServerFailure): ServerOperationOutcome {
261
+ let outcome: ServerOperationOutcome["outcome"];
262
+ if (failure.code === "server-unavailable") outcome = "unavailable";
263
+ else if (failure.code === "no-capable-server") outcome = "unsupported";
264
+ else if (failure.message.toLowerCase().includes("timed out")) outcome = "timeout";
265
+ else outcome = "error";
266
+ return { server_id: failure.serverId, outcome, message: failure.message };
267
+ }
268
+
269
+ function operationDetails(
270
+ operation: LspToolParameters["operation"],
271
+ outcomes: readonly ServerOperationOutcome[],
272
+ previewRecords: readonly LSPAny[] = [],
273
+ ): LspToolResultDetails {
274
+ const details: LSPAny = {
275
+ kind: "operation",
276
+ operation,
277
+ server_outcomes: [...outcomes],
278
+ };
279
+ if (previewRecords.length > 0) details.preview_records = previewRecords;
280
+ return details;
281
+ }
282
+
283
+ function requireReadSuccess<T>(result: LspServerReadResult<T>): void {
284
+ if (result.successes.length > 0) return;
285
+ throw piLspError(result.failures.map(({ message }) => message).join("; "));
286
+ }
287
+
288
+ function readOperationValue<T>(result: LspServerReadResult<T>): LspReadValue[] {
289
+ return result.successes.map((success) => ({
290
+ root_path: success.rootPath,
291
+ server_id: success.serverId,
292
+ value: success.value,
293
+ }));
294
+ }
295
+
296
+ function readOperationOutcomes<T>(result: LspServerReadResult<T>): ServerOperationOutcome[] {
297
+ return [
298
+ ...result.successes.map(({ serverId }): ServerOperationOutcome => ({
299
+ server_id: serverId,
300
+ outcome: "success",
301
+ })),
302
+ ...result.failures.map(serverOutcomeForFailure),
303
+ ];
304
+ }
305
+
306
+ function protocolRecord(value: LSPAny): Record<string, LSPAny> | undefined {
307
+ return Value.Check(ProtocolRecordSchema, value) ? value : undefined;
308
+ }
309
+
310
+ function protocolPositionValue(value: LSPAny): Position | undefined {
311
+ const record = protocolRecord(value);
312
+ if (
313
+ record === undefined ||
314
+ !Number.isSafeInteger(record.line) ||
315
+ !Number.isSafeInteger(record.character) ||
316
+ Object.keys(record).some((key) => key !== "line" && key !== "character")
317
+ ) {
318
+ return undefined;
319
+ }
320
+ return { line: record.line, character: record.character };
321
+ }
322
+
323
+ function normalizeProtocolFoldingRange(
324
+ value: LSPAny,
325
+ text: string,
326
+ encoding: LspPositionEncoding,
327
+ ): Record<string, LSPAny> | undefined {
328
+ if (!Value.Check(ProtocolFoldingRangeSchema, value)) return undefined;
329
+ const start = convertLspProtocolPosition(
330
+ text,
331
+ { line: value.startLine, character: value.startCharacter ?? 0 },
332
+ encoding,
333
+ );
334
+ const end = convertLspProtocolPosition(
335
+ text,
336
+ { line: value.endLine, character: value.endCharacter ?? 0 },
337
+ encoding,
338
+ );
339
+ const normalized = {
340
+ ...value,
341
+ startLine: start.line,
342
+ endLine: end.line,
343
+ };
344
+ if (value.startCharacter !== undefined && value.endCharacter !== undefined) {
345
+ return { ...normalized, startCharacter: start.character, endCharacter: end.character };
346
+ }
347
+ if (value.startCharacter !== undefined) {
348
+ return { ...normalized, startCharacter: start.character };
349
+ }
350
+ if (value.endCharacter !== undefined) return { ...normalized, endCharacter: end.character };
351
+ return normalized;
352
+ }
353
+
354
+ async function textForProtocolUri(uri: string): Promise<string | undefined> {
355
+ if (!uri.startsWith("file:")) return undefined;
356
+ try {
357
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
358
+ await readFile(fileURLToPath(uri)),
359
+ );
360
+ } catch {
361
+ return undefined;
362
+ }
363
+ }
364
+
365
+ async function normalizeProtocolResult(
366
+ value: LSPAny,
367
+ prepared: PreparedDocument | undefined,
368
+ inheritedText?: string,
369
+ inheritedEncoding?: LspPositionEncoding,
370
+ ): Promise<LSPAny> {
371
+ if (Array.isArray(value)) {
372
+ return Promise.all(
373
+ value.map((entry) =>
374
+ normalizeProtocolResult(entry, prepared, inheritedText, inheritedEncoding),
375
+ ),
376
+ );
377
+ }
378
+ if (value instanceof Map) {
379
+ return Promise.all(
380
+ [...value.entries()]
381
+ .sort(([left], [right]) => String(left).localeCompare(String(right)))
382
+ .map(async ([key, entryValue]) => ({
383
+ uri: key,
384
+ value: await normalizeProtocolResult(
385
+ entryValue,
386
+ prepared,
387
+ await textForProtocolUri(key),
388
+ inheritedEncoding,
389
+ ),
390
+ })),
391
+ );
392
+ }
393
+
394
+ const position = protocolPositionValue(value);
395
+ const text = inheritedText ?? prepared?.document.text;
396
+ const positionEncoding = inheritedEncoding ?? prepared?.positionEncoding;
397
+ if (position !== undefined && text !== undefined && positionEncoding !== undefined) {
398
+ return convertLspProtocolPosition(text, position, positionEncoding);
399
+ }
400
+ if (text !== undefined && positionEncoding !== undefined) {
401
+ const foldingRange = normalizeProtocolFoldingRange(value, text, positionEncoding);
402
+ if (foldingRange !== undefined) return foldingRange;
403
+ }
404
+
405
+ const record = protocolRecord(value);
406
+ if (record === undefined) return value;
407
+ const uriValue = Value.Check(ProtocolStringSchema, record.uri) ? record.uri : undefined;
408
+ const targetUriValue = Value.Check(ProtocolStringSchema, record.targetUri)
409
+ ? record.targetUri
410
+ : undefined;
411
+ const sourceText = inheritedText ?? prepared?.document.text;
412
+ const uriText = uriValue === undefined ? undefined : await textForProtocolUri(uriValue);
413
+ const targetText =
414
+ targetUriValue === undefined ? undefined : await textForProtocolUri(targetUriValue);
415
+ const localText = uriText ?? targetText ?? sourceText;
416
+ const entries = await Promise.all(
417
+ Object.entries(record).map(async ([key, entryValue]) => {
418
+ if ((key === "uri" || key === "targetUri") && Value.Check(ProtocolStringSchema, entryValue)) {
419
+ return [
420
+ key,
421
+ entryValue.startsWith("file:") ? fileURLToPath(entryValue) : entryValue,
422
+ ] as const;
423
+ }
424
+ return [
425
+ key,
426
+ await normalizeProtocolResult(
427
+ entryValue,
428
+ prepared,
429
+ targetUriValue !== undefined && key === "originSelectionRange"
430
+ ? sourceText
431
+ : targetUriValue !== undefined &&
432
+ (key === "targetRange" || key === "targetSelectionRange")
433
+ ? targetText
434
+ : localText,
435
+ positionEncoding,
436
+ ),
437
+ ] as const;
438
+ }),
439
+ );
440
+ return Object.fromEntries(entries);
441
+ }
442
+
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
+ function supportsResolveProvider(value: LSPAny): boolean {
453
+ return protocolRecord(value)?.resolveProvider === true;
454
+ }
455
+
456
+ async function resolveProtocolItems(
457
+ client: LspToolServerClient,
458
+ value: LSPAny,
459
+ method: string,
460
+ signal: AbortSignal | undefined,
461
+ ): Promise<LSPAny> {
462
+ if (Array.isArray(value)) {
463
+ return Promise.all(value.map((item) => client.request<LSPAny>(method, item, signal)));
464
+ }
465
+ const record = protocolRecord(value);
466
+ if (record === undefined || !Array.isArray(record.items)) return value;
467
+ return {
468
+ ...record,
469
+ items: await Promise.all(
470
+ record.items.map((item: LSPAny) => client.request<LSPAny>(method, item, signal)),
471
+ ),
472
+ };
473
+ }
474
+
475
+ async function resolveCodeActionItems(
476
+ client: LspToolServerClient,
477
+ actions: LSPAny,
478
+ signal: AbortSignal | undefined,
479
+ ): Promise<LSPAny> {
480
+ if (!Array.isArray(actions)) return actions;
481
+ return Promise.all(
482
+ actions.map((action) => {
483
+ const record = protocolRecord(action);
484
+ return record !== undefined && Value.Check(ProtocolStringSchema, record.command)
485
+ ? action
486
+ : client.request<LSPAny>(CodeActionResolveRequest.method, action, signal);
487
+ }),
488
+ );
489
+ }
490
+
491
+ function formattingOptions(
492
+ parameters: Extract<
493
+ LspToolParameters,
494
+ { operation: "format_document" | "format_range" | "format_on_type" }
495
+ >,
496
+ ): LSPAny {
497
+ return {
498
+ tabSize: parameters.tab_size,
499
+ insertSpaces: parameters.insert_spaces,
500
+ trimTrailingWhitespace: parameters.trim_trailing_whitespace,
501
+ insertFinalNewline: parameters.insert_final_newline,
502
+ trimFinalNewlines: parameters.trim_final_newlines,
503
+ };
504
+ }
505
+
506
+ function workspaceEditFromTextEdits(uri: string, edits: LSPAny): WorkspaceEdit {
507
+ return { changes: { [uri]: Array.isArray(edits) ? edits : [] } };
508
+ }
509
+
510
+ function storeManifestEntries(manifest: LSPAny): readonly LSPAny[] {
511
+ if (Array.isArray(manifest)) return manifest;
512
+ const record = protocolRecord(manifest);
513
+ if (record !== undefined && Array.isArray(record.entries)) return record.entries;
514
+ throw piLspError("Workspace Edit Preview returned an invalid Mutation Manifest");
515
+ }
516
+
517
+ function normalizeStoreMutationManifest(manifest: LSPAny): MutationManifest {
518
+ const entries = storeManifestEntries(manifest).map((entry) => {
519
+ const record = protocolRecord(entry);
520
+ if (record === undefined) throw piLspError("Mutation Manifest contains an invalid entry");
521
+ if (record.operation === "rename") {
522
+ return {
523
+ operation: "rename",
524
+ path: record.path,
525
+ destination_path: record.destination_path ?? record.to,
526
+ };
527
+ }
528
+ return { operation: record.operation, path: record.path };
529
+ });
530
+ try {
531
+ return Value.Parse(MutationManifestSchema, entries);
532
+ } catch (cause) {
533
+ const message = cause instanceof Error ? cause.message : String(cause);
534
+ throw piLspError(`invalid canonical Mutation Manifest: ${message}`);
535
+ }
536
+ }
537
+
538
+ function sameMutationManifest(left: MutationManifest, right: MutationManifest): boolean {
539
+ return JSON.stringify(left) === JSON.stringify(right);
540
+ }
541
+
542
+ async function workspacePreviewOutput(
543
+ dependencies: LspToolDependencies,
544
+ operation: "format_document" | "format_range" | "format_on_type" | "rename" | "code_actions",
545
+ serverId: string,
546
+ edit: WorkspaceEdit,
547
+ positionEncoding: PositionEncodingKind,
548
+ ): Promise<
549
+ ReturnType<typeof createLspToolOutput> extends Promise<infer TResult> ? TResult : never
550
+ > {
551
+ const preview = await dependencies.workspaceEdits.createPreview({
552
+ edit,
553
+ serverId,
554
+ positionEncoding,
555
+ });
556
+ dependencies.workspaceEdits.markPreviewReported(preview.preview_id);
557
+ const manifest = normalizeStoreMutationManifest(
558
+ dependencies.workspaceEdits.prepareMutationManifest(preview.preview_id),
559
+ );
560
+ const details: LSPAny = {
561
+ kind: "workspace_edit_preview",
562
+ preview_id: preview.preview_id,
563
+ operation,
564
+ summary: preview.summary,
565
+ mutation_manifest: manifest,
566
+ preview_record: preview,
567
+ state: "available",
568
+ };
569
+ return createLspToolOutput(
570
+ `Workspace Edit Preview ${preview.preview_id}\n${preview.summary}`,
571
+ details,
572
+ dependencies,
573
+ );
574
+ }
575
+
576
+ async function executePositionRead(
577
+ dependencies: LspToolDependencies,
578
+ parameters: PositionReadParameters,
579
+ context: ExtensionContext,
580
+ signal: AbortSignal | undefined,
581
+ ): Promise<LspServerReadResult<LSPAny>> {
582
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
583
+ const methodByOperation = {
584
+ completion: CompletionRequest.method,
585
+ hover: HoverRequest.method,
586
+ signature_help: SignatureHelpRequest.method,
587
+ declaration: DeclarationRequest.method,
588
+ goto_definition: DefinitionRequest.method,
589
+ goto_type_definition: TypeDefinitionRequest.method,
590
+ goto_implementation: ImplementationRequest.method,
591
+ find_references: ReferencesRequest.method,
592
+ document_highlights: DocumentHighlightRequest.method,
593
+ call_hierarchy: CallHierarchyPrepareRequest.method,
594
+ incoming_calls: CallHierarchyPrepareRequest.method,
595
+ outgoing_calls: CallHierarchyPrepareRequest.method,
596
+ type_hierarchy: TypeHierarchyPrepareRequest.method,
597
+ supertypes: TypeHierarchyPrepareRequest.method,
598
+ subtypes: TypeHierarchyPrepareRequest.method,
599
+ prepare_rename: PrepareRenameRequest.method,
600
+ } as const;
601
+ const capabilityMethod = methodByOperation[parameters.operation];
602
+ return dependencies.manager.runRead(
603
+ filePath,
604
+ parameters.server_id,
605
+ (client) => client.hasCapability(capabilityMethod),
606
+ async (client, route) => {
607
+ const prepared = await prepareLspDocument(client, route, filePath);
608
+ const position = protocolPosition(prepared, {
609
+ line: parameters.line,
610
+ character: parameters.character,
611
+ });
612
+ const textDocument = { uri: prepared.document.uri };
613
+ let requestParameters: LSPAny = { textDocument, position };
614
+ if (parameters.operation === "find_references") {
615
+ requestParameters = {
616
+ ...requestParameters,
617
+ context: { includeDeclaration: parameters.include_declaration ?? true },
618
+ };
619
+ }
620
+ let value = await requestDocumentMethod(
621
+ prepared,
622
+ capabilityMethod,
623
+ requestParameters,
624
+ signal,
625
+ );
626
+
627
+ if (
628
+ parameters.operation === "incoming_calls" ||
629
+ parameters.operation === "outgoing_calls" ||
630
+ parameters.operation === "supertypes" ||
631
+ parameters.operation === "subtypes"
632
+ ) {
633
+ const followupMethod =
634
+ parameters.operation === "incoming_calls"
635
+ ? CallHierarchyIncomingCallsRequest.method
636
+ : parameters.operation === "outgoing_calls"
637
+ ? CallHierarchyOutgoingCallsRequest.method
638
+ : parameters.operation === "supertypes"
639
+ ? TypeHierarchySupertypesRequest.method
640
+ : TypeHierarchySubtypesRequest.method;
641
+ const preparedItems = Array.isArray(value) ? value : [];
642
+ value = (
643
+ await Promise.all(
644
+ preparedItems.map((item) => client.request<LSPAny>(followupMethod, { item }, signal)),
645
+ )
646
+ ).flat();
647
+ } else if (
648
+ parameters.operation === "completion" &&
649
+ supportsResolveProvider(client.capabilities.completionProvider)
650
+ ) {
651
+ value = await resolveProtocolItems(client, value, CompletionResolveRequest.method, signal);
652
+ }
653
+
654
+ return normalizeProtocolResult(value, prepared);
655
+ },
656
+ );
657
+ }
658
+
659
+ async function executeFileRead(
660
+ dependencies: LspToolDependencies,
661
+ parameters: FileReadParameters,
662
+ context: ExtensionContext,
663
+ signal: AbortSignal | undefined,
664
+ ): Promise<LspServerReadResult<LSPAny>> {
665
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
666
+ const methodByOperation = {
667
+ diagnostics: "diagnostics",
668
+ document_symbols: DocumentSymbolRequest.method,
669
+ document_links: DocumentLinkRequest.method,
670
+ folding_ranges: FoldingRangeRequest.method,
671
+ code_lenses: CodeLensRequest.method,
672
+ document_colors: DocumentColorRequest.method,
673
+ } as const;
674
+ const method = methodByOperation[parameters.operation];
675
+ return dependencies.manager.runRead(
676
+ filePath,
677
+ parameters.server_id,
678
+ (client) => method === "diagnostics" || client.hasCapability(method),
679
+ async (client, route) => {
680
+ const prepared = await prepareLspDocument(client, route, filePath);
681
+ if (parameters.operation === "diagnostics") {
682
+ return normalizeProtocolResult(
683
+ await client.documentDiagnostics(filePath, route.language.languageId, signal),
684
+ prepared,
685
+ );
686
+ }
687
+ let value = await client.request<LSPAny>(
688
+ method,
689
+ { textDocument: { uri: prepared.document.uri } },
690
+ signal,
691
+ );
692
+ if (
693
+ parameters.operation === "document_links" &&
694
+ supportsResolveProvider(client.capabilities.documentLinkProvider)
695
+ ) {
696
+ value = await resolveProtocolItems(
697
+ client,
698
+ value,
699
+ DocumentLinkResolveRequest.method,
700
+ signal,
701
+ );
702
+ } else if (
703
+ parameters.operation === "code_lenses" &&
704
+ supportsResolveProvider(client.capabilities.codeLensProvider)
705
+ ) {
706
+ value = await resolveProtocolItems(client, value, CodeLensResolveRequest.method, signal);
707
+ }
708
+ return normalizeProtocolResult(value, prepared);
709
+ },
710
+ );
711
+ }
712
+
713
+ async function executeInlayHints(
714
+ dependencies: LspToolDependencies,
715
+ parameters: Extract<LspToolParameters, { operation: "inlay_hints" }>,
716
+ context: ExtensionContext,
717
+ signal: AbortSignal | undefined,
718
+ ): Promise<LspServerReadResult<LSPAny>> {
719
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
720
+ return dependencies.manager.runRead(
721
+ filePath,
722
+ parameters.server_id,
723
+ (client) => client.hasCapability(InlayHintRequest.method),
724
+ async (client, route) => {
725
+ const prepared = await prepareLspDocument(client, route, filePath);
726
+ let value = await client.request<LSPAny>(
727
+ InlayHintRequest.method,
728
+ {
729
+ textDocument: { uri: prepared.document.uri },
730
+ range: {
731
+ start: protocolPosition(prepared, parameters.range.start),
732
+ end: protocolPosition(prepared, parameters.range.end),
733
+ },
734
+ },
735
+ signal,
736
+ );
737
+ if (supportsResolveProvider(client.capabilities.inlayHintProvider)) {
738
+ value = await resolveProtocolItems(client, value, InlayHintResolveRequest.method, signal);
739
+ }
740
+ return normalizeProtocolResult(value, prepared);
741
+ },
742
+ );
743
+ }
744
+
745
+ async function executeSelectionRanges(
746
+ dependencies: LspToolDependencies,
747
+ parameters: Extract<LspToolParameters, { operation: "selection_ranges" }>,
748
+ context: ExtensionContext,
749
+ signal: AbortSignal | undefined,
750
+ ): Promise<LspServerReadResult<LSPAny>> {
751
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
752
+ return dependencies.manager.runRead(
753
+ filePath,
754
+ parameters.server_id,
755
+ (client) => client.hasCapability(SelectionRangeRequest.method),
756
+ async (client, route) => {
757
+ const prepared = await prepareLspDocument(client, route, filePath);
758
+ const value = await client.request<LSPAny>(
759
+ SelectionRangeRequest.method,
760
+ {
761
+ textDocument: { uri: prepared.document.uri },
762
+ positions: parameters.positions.map((position) => protocolPosition(prepared, position)),
763
+ },
764
+ signal,
765
+ );
766
+ return normalizeProtocolResult(value, prepared);
767
+ },
768
+ );
769
+ }
770
+
771
+ async function executeWorkspaceRead(
772
+ dependencies: LspToolDependencies,
773
+ parameters: Extract<
774
+ LspToolParameters,
775
+ { operation: "workspace_diagnostics" | "workspace_symbols" }
776
+ >,
777
+ context: ExtensionContext,
778
+ signal: AbortSignal | undefined,
779
+ ): Promise<LspServerReadResult<LSPAny>> {
780
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
781
+ if (parameters.operation === "workspace_diagnostics") {
782
+ return dependencies.manager.runRead(
783
+ filePath,
784
+ parameters.server_id,
785
+ () => true,
786
+ async (client) =>
787
+ normalizeProtocolResult(
788
+ await client.workspaceDiagnostics(signal),
789
+ undefined,
790
+ undefined,
791
+ normalizeLspPositionEncoding(client.positionEncoding),
792
+ ),
793
+ );
794
+ }
795
+ return dependencies.manager.runRead(
796
+ filePath,
797
+ parameters.server_id,
798
+ (client) => client.hasCapability(WorkspaceSymbolRequest.method),
799
+ async (client) => {
800
+ let value = await client.request<LSPAny>(
801
+ WorkspaceSymbolRequest.method,
802
+ { query: parameters.query },
803
+ signal,
804
+ );
805
+ if (supportsResolveProvider(client.capabilities.workspaceSymbolProvider)) {
806
+ value = await resolveProtocolItems(
807
+ client,
808
+ value,
809
+ WorkspaceSymbolResolveRequest.method,
810
+ signal,
811
+ );
812
+ }
813
+ return normalizeProtocolResult(
814
+ value,
815
+ undefined,
816
+ undefined,
817
+ normalizeLspPositionEncoding(client.positionEncoding),
818
+ );
819
+ },
820
+ );
821
+ }
822
+
823
+ async function executeFormattingPreview(
824
+ dependencies: LspToolDependencies,
825
+ parameters: Extract<
826
+ LspToolParameters,
827
+ { operation: "format_document" | "format_range" | "format_on_type" }
828
+ >,
829
+ context: ExtensionContext,
830
+ signal: AbortSignal | undefined,
831
+ ) {
832
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
833
+ const method =
834
+ parameters.operation === "format_document"
835
+ ? DocumentFormattingRequest.method
836
+ : parameters.operation === "format_range"
837
+ ? DocumentRangeFormattingRequest.method
838
+ : DocumentOnTypeFormattingRequest.method;
839
+ const resolution = await dependencies.manager.resolveMutationClient(
840
+ filePath,
841
+ parameters.server_id,
842
+ (client) => client.hasCapability(method),
843
+ );
844
+ if (resolution.kind === "failure") throw piLspError(resolution.failure.message);
845
+ const { client, route } = resolution.instance;
846
+ const prepared = await prepareLspDocument(client, route, filePath);
847
+ const requestBase = {
848
+ textDocument: { uri: prepared.document.uri },
849
+ options: formattingOptions(parameters),
850
+ };
851
+ const requestParameters =
852
+ parameters.operation === "format_range"
853
+ ? {
854
+ ...requestBase,
855
+ range: {
856
+ start: protocolPosition(prepared, parameters.range.start),
857
+ end: protocolPosition(prepared, parameters.range.end),
858
+ },
859
+ }
860
+ : parameters.operation === "format_on_type"
861
+ ? {
862
+ ...requestBase,
863
+ position: protocolPosition(prepared, parameters),
864
+ ch: parameters.trigger_character,
865
+ }
866
+ : requestBase;
867
+ const edits = await client.request<LSPAny>(method, requestParameters, signal);
868
+ return workspacePreviewOutput(
869
+ dependencies,
870
+ parameters.operation,
871
+ route.serverId,
872
+ workspaceEditFromTextEdits(prepared.document.uri, edits),
873
+ client.positionEncoding,
874
+ );
875
+ }
876
+
877
+ async function executeRenamePreview(
878
+ dependencies: LspToolDependencies,
879
+ parameters: Extract<LspToolParameters, { operation: "rename" }>,
880
+ context: ExtensionContext,
881
+ signal: AbortSignal | undefined,
882
+ ) {
883
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
884
+ const resolution = await dependencies.manager.resolveMutationClient(
885
+ filePath,
886
+ parameters.server_id,
887
+ (client) => client.hasCapability(RenameRequest.method),
888
+ );
889
+ if (resolution.kind === "failure") throw piLspError(resolution.failure.message);
890
+ const { client, route } = resolution.instance;
891
+ const prepared = await prepareLspDocument(client, route, filePath);
892
+ const edit = await client.request<WorkspaceEdit | null>(
893
+ RenameRequest.method,
894
+ {
895
+ textDocument: { uri: prepared.document.uri },
896
+ position: protocolPosition(prepared, parameters),
897
+ newName: parameters.new_name,
898
+ },
899
+ signal,
900
+ );
901
+ if (edit === null) throw piLspError("rename returned no Workspace Edit Preview");
902
+ return workspacePreviewOutput(
903
+ dependencies,
904
+ "rename",
905
+ route.serverId,
906
+ edit,
907
+ client.positionEncoding,
908
+ );
909
+ }
910
+
911
+ async function executeCodeActions(
912
+ dependencies: LspToolDependencies,
913
+ parameters: Extract<LspToolParameters, { operation: "code_actions" }>,
914
+ context: ExtensionContext,
915
+ signal: AbortSignal | undefined,
916
+ ) {
917
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
918
+ const resolution = await dependencies.manager.resolveMutationClient(
919
+ filePath,
920
+ parameters.server_id,
921
+ (client) => client.hasCapability(CodeActionRequest.method),
922
+ );
923
+ if (resolution.kind === "failure") throw piLspError(resolution.failure.message);
924
+ const { client, route } = resolution.instance;
925
+ const prepared = await prepareLspDocument(client, route, filePath);
926
+ let actions = await client.request<LSPAny>(
927
+ CodeActionRequest.method,
928
+ {
929
+ textDocument: { uri: prepared.document.uri },
930
+ range: {
931
+ start: protocolPosition(prepared, parameters.range.start),
932
+ end: protocolPosition(prepared, parameters.range.end),
933
+ },
934
+ context: {
935
+ diagnostics: [],
936
+ only: parameters.only_kinds,
937
+ },
938
+ },
939
+ signal,
940
+ );
941
+ if (supportsResolveProvider(client.capabilities.codeActionProvider)) {
942
+ actions = await resolveCodeActionItems(client, actions, signal);
943
+ }
944
+ const results: LSPAny[] = [];
945
+ const previewRecords: LSPAny[] = [];
946
+ for (const action of Array.isArray(actions) ? actions : []) {
947
+ const record = protocolRecord(action);
948
+ if (record === undefined) continue;
949
+ if (record.command !== undefined || record.edit === undefined) {
950
+ results.push({
951
+ applicable: false,
952
+ command: record.command,
953
+ kind: record.kind,
954
+ title: record.title,
955
+ });
956
+ continue;
957
+ }
958
+ const preview = await dependencies.workspaceEdits.createPreview({
959
+ edit: record.edit,
960
+ serverId: route.serverId,
961
+ positionEncoding: client.positionEncoding,
962
+ });
963
+ dependencies.workspaceEdits.markPreviewReported(preview.preview_id);
964
+ previewRecords.push(preview);
965
+ results.push({
966
+ applicable: true,
967
+ kind: record.kind,
968
+ mutation_manifest: normalizeStoreMutationManifest(
969
+ dependencies.workspaceEdits.prepareMutationManifest(preview.preview_id),
970
+ ),
971
+ preview_id: preview.preview_id,
972
+ summary: preview.summary,
973
+ title: record.title,
974
+ });
975
+ }
976
+ const details = operationDetails(
977
+ "code_actions",
978
+ [{ server_id: route.serverId, outcome: "success" }],
979
+ previewRecords,
980
+ );
981
+ return createLspToolOutput(formatLspToolValue(results), details, dependencies);
982
+ }
983
+
984
+ async function executeApplyPreview(
985
+ dependencies: LspToolDependencies,
986
+ parameters: Extract<LspToolParameters, { operation: "apply" }>,
987
+ signal: AbortSignal | undefined,
988
+ ) {
989
+ const storeManifest = dependencies.workspaceEdits.prepareMutationManifest(parameters.preview_id);
990
+ const canonicalManifest = normalizeStoreMutationManifest(storeManifest);
991
+ if (
992
+ parameters.mutation_manifest === undefined ||
993
+ !sameMutationManifest(parameters.mutation_manifest, canonicalManifest)
994
+ ) {
995
+ throw piLspError("Mutation Manifest changed after argument preparation");
996
+ }
997
+ let result;
998
+ try {
999
+ result = await dependencies.workspaceEdits.applyPreview(
1000
+ parameters.preview_id,
1001
+ storeManifest,
1002
+ signal,
1003
+ );
1004
+ } catch (cause) {
1005
+ if (
1006
+ !(cause instanceof LspWorkspaceEditError) ||
1007
+ cause.code !== "workspace_edit_recovery_failed"
1008
+ ) {
1009
+ throw cause;
1010
+ }
1011
+ return createLspToolOutput(
1012
+ cause.message,
1013
+ {
1014
+ kind: "workspace_edit_apply",
1015
+ preview_id: parameters.preview_id,
1016
+ mutation_manifest: canonicalManifest,
1017
+ changed_paths: [...cause.recoveryFailures].sort((left, right) => left.localeCompare(right)),
1018
+ recovery_failure_paths: [...cause.recoveryFailures],
1019
+ state: "partial_failure",
1020
+ },
1021
+ dependencies,
1022
+ );
1023
+ }
1024
+ const record = protocolRecord(result) ?? {};
1025
+ const movedFiles = Array.isArray(record.moved_files) ? record.moved_files : [];
1026
+ const changedPaths = [
1027
+ ...(Array.isArray(record.changed_files) ? record.changed_files : []),
1028
+ ...(Array.isArray(record.created_files) ? record.created_files : []),
1029
+ ...(Array.isArray(record.deleted_files) ? record.deleted_files : []),
1030
+ ...movedFiles.flatMap((move: LSPAny) => {
1031
+ const moveRecord = protocolRecord(move);
1032
+ return moveRecord === undefined ? [] : [moveRecord.from, moveRecord.to];
1033
+ }),
1034
+ ].filter((path): path is string => Value.Check(ProtocolStringSchema, path));
1035
+ const details: LSPAny = {
1036
+ kind: "workspace_edit_apply",
1037
+ preview_id: parameters.preview_id,
1038
+ mutation_manifest: canonicalManifest,
1039
+ changed_paths: [...new Set(changedPaths)].sort((left, right) => left.localeCompare(right)),
1040
+ state: record.state === "partial_failure" ? "partial_failure" : "applied",
1041
+ };
1042
+ return createLspToolOutput(formatLspToolValue(result), details, dependencies);
1043
+ }
1044
+
1045
+ /** Create the single strict Pi LSP ToolDefinition bound to one session's runtime owners. */
1046
+ export function createLspToolDefinition(
1047
+ dependencies: LspToolDependencies,
1048
+ ): ToolDefinition<typeof LspToolParametersSchema, LspToolResultDetails> {
1049
+ return {
1050
+ name: "lsp",
1051
+ label: "LSP",
1052
+ description:
1053
+ "Query configured language servers and create/apply guarded Workspace Edit Previews. All paths accept an optional leading @. Lines and characters are one-based Unicode code points. Output is limited to 2,000 lines or 50 KB; complete truncated output is saved as a Result Spill.",
1054
+ promptSnippet: "Query configured language servers and preview guarded LSP mutations",
1055
+ promptGuidelines: [
1056
+ "Use lsp read operations for semantic source navigation and diagnostics; use preview-producing lsp operations followed by lsp apply for language-server mutations.",
1057
+ ],
1058
+ parameters: LspToolParametersSchema,
1059
+ renderCall: (argumentsValue, theme, context) =>
1060
+ renderLspToolCall(argumentsValue, theme, context.expanded, context.cwd),
1061
+ renderResult: (result, options, theme, context) =>
1062
+ renderLspToolResult(result, options, theme, context.isError),
1063
+ prepareArguments(argumentsValue) {
1064
+ if (!Value.Check(ApplyPreviewArgumentsSchema, argumentsValue)) {
1065
+ return parseLspToolParameters(argumentsValue);
1066
+ }
1067
+ const applyArguments: ApplyPreviewArguments = argumentsValue;
1068
+ const storeManifest = dependencies.workspaceEdits.prepareMutationManifest(
1069
+ applyArguments.preview_id,
1070
+ );
1071
+ return parseLspToolParameters({
1072
+ ...applyArguments,
1073
+ mutation_manifest: normalizeStoreMutationManifest(storeManifest),
1074
+ });
1075
+ },
1076
+ async execute(_toolCallId, input, signal, _onUpdate, context) {
1077
+ const parameters = parseLspToolParameters(input);
1078
+ switch (parameters.operation) {
1079
+ case "status": {
1080
+ const status = dependencies.manager.getStatus();
1081
+ const outcomes = status.servers.map((server): ServerOperationOutcome => {
1082
+ const outcome: ServerOperationOutcome = {
1083
+ server_id: server.serverId,
1084
+ outcome: server.state === "unavailable" ? "unavailable" : "success",
1085
+ };
1086
+ if (server.error === undefined) return outcome;
1087
+ return { ...outcome, message: server.error };
1088
+ });
1089
+ return createLspToolOutput(
1090
+ formatLspToolValue(status),
1091
+ operationDetails("status", outcomes),
1092
+ dependencies,
1093
+ );
1094
+ }
1095
+ case "capabilities":
1096
+ case "restart": {
1097
+ const filePath = absoluteLspFilePath(parameters.file_path, context);
1098
+ const resolution =
1099
+ parameters.operation === "capabilities"
1100
+ ? await dependencies.manager.getCapabilities(parameters.server_id, filePath)
1101
+ : await dependencies.manager.restartServer(parameters.server_id, filePath);
1102
+ if (resolution.kind === "failure") throw piLspError(resolution.failure.message);
1103
+ return createLspToolOutput(
1104
+ formatLspToolValue({
1105
+ capabilities: resolution.instance.client.capabilities,
1106
+ root_path: resolution.instance.route.rootPath,
1107
+ server_id: resolution.instance.route.serverId,
1108
+ }),
1109
+ operationDetails(parameters.operation, [
1110
+ { server_id: resolution.instance.route.serverId, outcome: "success" },
1111
+ ]),
1112
+ dependencies,
1113
+ );
1114
+ }
1115
+ case "completion":
1116
+ case "hover":
1117
+ case "signature_help":
1118
+ case "declaration":
1119
+ case "goto_definition":
1120
+ case "goto_type_definition":
1121
+ case "goto_implementation":
1122
+ case "find_references":
1123
+ case "document_highlights":
1124
+ case "call_hierarchy":
1125
+ case "incoming_calls":
1126
+ case "outgoing_calls":
1127
+ case "type_hierarchy":
1128
+ case "supertypes":
1129
+ case "subtypes":
1130
+ case "prepare_rename": {
1131
+ const result = await executePositionRead(dependencies, parameters, context, signal);
1132
+ requireReadSuccess(result);
1133
+ return createLspToolOutput(
1134
+ formatLspToolValue({
1135
+ results: readOperationValue(result),
1136
+ warnings: result.failures.map(({ message }) => message),
1137
+ }),
1138
+ operationDetails(parameters.operation, readOperationOutcomes(result)),
1139
+ dependencies,
1140
+ );
1141
+ }
1142
+ case "diagnostics":
1143
+ case "document_symbols":
1144
+ case "document_links":
1145
+ case "folding_ranges":
1146
+ case "code_lenses":
1147
+ case "document_colors": {
1148
+ const result = await executeFileRead(dependencies, parameters, context, signal);
1149
+ requireReadSuccess(result);
1150
+ return createLspToolOutput(
1151
+ formatLspToolValue({
1152
+ results: readOperationValue(result),
1153
+ warnings: result.failures.map(({ message }) => message),
1154
+ }),
1155
+ operationDetails(parameters.operation, readOperationOutcomes(result)),
1156
+ dependencies,
1157
+ );
1158
+ }
1159
+ case "workspace_diagnostics":
1160
+ case "workspace_symbols": {
1161
+ const result = await executeWorkspaceRead(dependencies, parameters, context, signal);
1162
+ requireReadSuccess(result);
1163
+ return createLspToolOutput(
1164
+ formatLspToolValue({
1165
+ results: readOperationValue(result),
1166
+ warnings: result.failures.map(({ message }) => message),
1167
+ }),
1168
+ operationDetails(parameters.operation, readOperationOutcomes(result)),
1169
+ dependencies,
1170
+ );
1171
+ }
1172
+ case "selection_ranges": {
1173
+ const result = await executeSelectionRanges(dependencies, parameters, context, signal);
1174
+ requireReadSuccess(result);
1175
+ return createLspToolOutput(
1176
+ formatLspToolValue({
1177
+ results: readOperationValue(result),
1178
+ warnings: result.failures.map(({ message }) => message),
1179
+ }),
1180
+ operationDetails(parameters.operation, readOperationOutcomes(result)),
1181
+ dependencies,
1182
+ );
1183
+ }
1184
+ case "inlay_hints": {
1185
+ const result = await executeInlayHints(dependencies, parameters, context, signal);
1186
+ requireReadSuccess(result);
1187
+ return createLspToolOutput(
1188
+ formatLspToolValue({
1189
+ results: readOperationValue(result),
1190
+ warnings: result.failures.map(({ message }) => message),
1191
+ }),
1192
+ operationDetails(parameters.operation, readOperationOutcomes(result)),
1193
+ dependencies,
1194
+ );
1195
+ }
1196
+ case "format_document":
1197
+ case "format_range":
1198
+ case "format_on_type":
1199
+ return executeFormattingPreview(dependencies, parameters, context, signal);
1200
+ case "rename":
1201
+ return executeRenamePreview(dependencies, parameters, context, signal);
1202
+ case "code_actions":
1203
+ return executeCodeActions(dependencies, parameters, context, signal);
1204
+ case "apply":
1205
+ return executeApplyPreview(dependencies, parameters, signal);
1206
+ }
1207
+ },
1208
+ };
1209
+ }
1210
+
1211
+ /** Register exactly one strict `lsp` tool for the current Pi extension session. */
1212
+ export function registerLspTool(pi: LspToolRegistrar, dependencies: LspToolDependencies): void {
1213
+ pi.registerTool(createLspToolDefinition(dependencies));
1214
+ }