feat(lsp): add background daemon for language servers
This commit is contained in:
60
index.ts
60
index.ts
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user