feat(lsp): add background daemon for language servers

This commit is contained in:
2026-04-29 00:04:06 -04:00
parent 60b8900a09
commit 076eee4e96
12 changed files with 707 additions and 76 deletions

View File

@@ -1,12 +1,12 @@
// LSP Extension - Registers tools that let the LLM query language servers
// for hover, definition, references, completions, document symbols, and
// diagnostics. Each tool spawns a short-lived server, runs one request,
// and tears it down (same lifecycle as the CLI).
// diagnostics. Tool calls are forwarded to the long-lived pi-lsp daemon
// (autospawned on first use) so LSP servers stay warm across calls.
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import * as path from "node:path";
import { LspClient, uriToPath } from "./src/client.ts";
import { pickServer, findRoot } from "./src/root.ts";
import { uriToPath } from "./src/client.ts";
import { daemonDiagnostics, daemonRequest } from "./src/daemonClient.ts";
// Format Hover - Turn an LSP hover response into readable text.
function formatHover(result: unknown): string {
@@ -202,47 +202,21 @@ function formatDiagnostics(result: unknown): string {
.join("\n");
}
// Run LSP Request - Spawn a server, open the file, run one request, dispose.
// Mirrors the CLI lifecycle: fresh server per request.
// Run LSP Request - Forwards to the daemon, which owns the long-lived
// LspClient cache and handles didOpen/didChange syncing. The daemon
// injects textDocument.uri from the file path, so we omit it here.
async function runLsp(
filePath: string,
method: string,
params: Record<string, unknown>,
): Promise<unknown> {
const server = pickServer(filePath);
const rootDir = findRoot(filePath, server.rootMarkers);
const client = new LspClient(server);
try {
await client.start(rootDir);
const uri = client.openDocument(filePath);
await client.waitForReady();
// Populate textDocument.uri if the params have a textDocument field
if (params.textDocument && typeof params.textDocument === "object") {
params.textDocument = { ...params.textDocument, uri };
}
return client.sendRequest(method, params);
} finally {
// Fire-and-forget shutdown; don't wait for graceful exit.
void client.dispose();
}
return daemonRequest(filePath, method, params);
}
// Run LSP Diagnostics - Diagnostics arrive as a notification, so we use
// the dedicated waitForDiagnostics helper instead of sendRequest.
// Run LSP Diagnostics - Diagnostics arrive as a notification, so the
// daemon has a dedicated op that waits for the next publish.
async function runDiagnostics(filePath: string): Promise<unknown> {
const server = pickServer(filePath);
const rootDir = findRoot(filePath, server.rootMarkers);
const client = new LspClient(server);
try {
await client.start(rootDir);
const uri = client.openDocument(filePath);
await client.waitForReady();
return client.waitForDiagnostics(uri, 1500);
} finally {
void client.dispose();
}
return daemonDiagnostics(filePath, 1500);
}
// Shared Parameters Schema - All position-based tools accept file + optional
@@ -268,7 +242,6 @@ export default function (pi: ExtensionAPI) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const filePath = path.resolve(ctx.cwd, params.file);
const lspParams = {
textDocument: {},
position: { line: params.line ?? 0, character: params.character ?? 0 },
};
const result = await runLsp(filePath, "textDocument/hover", lspParams);
@@ -289,7 +262,6 @@ export default function (pi: ExtensionAPI) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const filePath = path.resolve(ctx.cwd, params.file);
const lspParams = {
textDocument: {},
position: { line: params.line ?? 0, character: params.character ?? 0 },
};
const result = await runLsp(
@@ -314,7 +286,6 @@ export default function (pi: ExtensionAPI) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const filePath = path.resolve(ctx.cwd, params.file);
const lspParams = {
textDocument: {},
position: { line: params.line ?? 0, character: params.character ?? 0 },
context: { includeDeclaration: true },
};
@@ -340,7 +311,6 @@ export default function (pi: ExtensionAPI) {
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const filePath = path.resolve(ctx.cwd, params.file);
const lspParams = {
textDocument: {},
position: { line: params.line ?? 0, character: params.character ?? 0 },
};
const result = await runLsp(
@@ -366,9 +336,11 @@ export default function (pi: ExtensionAPI) {
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const filePath = path.resolve(ctx.cwd, params.file);
const result = await runLsp(filePath, "textDocument/documentSymbol", {
textDocument: {},
});
const result = await runLsp(
filePath,
"textDocument/documentSymbol",
{},
);
return {
content: [{ type: "text", text: formatDocumentSymbols(result) }],
details: { raw: result },