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

@@ -60,6 +60,10 @@ export class LspClient {
// can await readiness.
private progressTokens = new Set<string | number>();
private progressListeners = new Set<() => void>();
// Per-URI Version Counter - LSP requires monotonically increasing
// version numbers in didOpen/didChange. We track them so the daemon
// can resync files via notifyChange after on-disk edits.
private versions = new Map<string, number>();
constructor(private readonly server: ServerConfig) {}
@@ -182,9 +186,12 @@ export class LspClient {
// Open Document - Reads the file from disk and sends didOpen. Most
// servers require this before they'll answer hover/definition/etc.
// Idempotent-ish: callers should track whether they've already opened
// a URI and prefer notifyChange for subsequent syncs.
openDocument(filePath: string): string {
const uri = pathToUri(filePath);
const text = fs.readFileSync(filePath, "utf8");
this.versions.set(uri, 1);
this.conn.sendNotification("textDocument/didOpen", {
textDocument: {
uri,
@@ -196,11 +203,32 @@ export class LspClient {
return uri;
}
// Notify Change - Re-reads the file from disk and sends a full-text
// didChange. Used by the daemon to keep the server in sync after the
// agent's edit/write tools modify a file.
notifyChange(filePath: string): string {
const uri = pathToUri(filePath);
const text = fs.readFileSync(filePath, "utf8");
const version = (this.versions.get(uri) ?? 1) + 1;
this.versions.set(uri, version);
this.conn.sendNotification("textDocument/didChange", {
textDocument: { uri, version },
contentChanges: [{ text }],
});
return uri;
}
// Send Raw LSP Request - Passthrough used by the command dispatcher.
sendRequest<R = unknown>(method: string, params: unknown): Promise<R> {
return this.conn.sendRequest(method, params) as Promise<R>;
}
// Clear Diagnostics - Drops the cached diagnostics for a URI so callers
// can force waitForDiagnostics to await a fresh publish after didChange.
clearDiagnostics(uri: string): void {
this.diagnostics.delete(uri);
}
// Wait For Diagnostics - Resolves on the first publish for `uri` or
// after `timeoutMs`. Returns whatever we have for that URI.
async waitForDiagnostics(