Files
boocode/apps/coder/src/services/lsp/server-manager.ts
indifferentketchup ee749d8698 feat(coder): add LSP code intelligence tools
- lsp/ module: types, config, JSON-RPC client, server-manager, operations
- lsp_diagnostics: TypeScript/JavaScript diagnostics for a file
- lsp_goto_definition: find symbol definition at position
- lsp_find_references: find all references to a symbol
- Registered as READ_TOOLS in tool index
2026-06-07 17:57:35 +00:00

120 lines
3.4 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process';
import { join } from 'node:path';
import { existsSync } from 'node:fs';
import { LspClient } from './client.js';
import { getServerConfig } from './config.js';
const IDLE_TIMEOUT_MS = 5 * 60 * 1000;
const SWEEP_INTERVAL_MS = 30_000;
interface LspInstance {
client: LspClient;
proc: ChildProcess;
lastUsed: number;
timer: ReturnType<typeof setTimeout>;
}
export class LspServerManager {
private instances = new Map<string, LspInstance>();
private sweepTimer: ReturnType<typeof setInterval> | null = null;
constructor() {
this.startSweeper();
}
private startSweeper(): void {
this.sweepTimer = setInterval(() => this.sweep(), SWEEP_INTERVAL_MS);
this.sweepTimer.unref?.();
}
private findProjectRoot(filePath: string): string | null {
let dir = filePath;
const config = getServerConfig(filePath);
if (!config) return null;
while (true) {
for (const pattern of config.rootPatterns) {
if (existsSync(join(dir, pattern))) return dir;
}
const parent = join(dir, '..');
if (parent === dir) return dir;
dir = parent;
}
}
async getClient(filePath: string): Promise<LspClient | null> {
const config = getServerConfig(filePath);
if (!config) return null;
const projectRoot = this.findProjectRoot(filePath);
if (!projectRoot) return null;
const existing = this.instances.get(projectRoot);
if (existing) {
existing.lastUsed = Date.now();
clearTimeout(existing.timer);
existing.timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS);
existing.timer.unref?.();
return existing.client;
}
return this.spawn(projectRoot, config.command, config.args);
}
private async spawn(projectRoot: string, command: string, args: string[]): Promise<LspClient> {
const proc = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'], cwd: projectRoot });
const client = new LspClient(proc.stdin!, proc.stdout!);
await client.request('initialize', {
processId: process.pid,
rootUri: `file://${projectRoot}`,
capabilities: {
textDocument: {
diagnostic: { dynamicRegistration: false },
definition: { dynamicRegistration: false },
references: { dynamicRegistration: false },
},
},
});
await client.notify('initialized', {});
const timer = setTimeout(() => this.kill(projectRoot), IDLE_TIMEOUT_MS);
timer.unref?.();
this.instances.set(projectRoot, { client, proc, lastUsed: Date.now(), timer });
proc.on('exit', () => this.instances.delete(projectRoot));
return client;
}
private kill(projectRoot: string): void {
const inst = this.instances.get(projectRoot);
if (!inst) return;
this.instances.delete(projectRoot);
inst.proc.kill('SIGTERM');
setTimeout(() => {
if (inst.proc.exitCode === null) inst.proc.kill('SIGKILL');
}, 5000);
}
private sweep(): void {
const now = Date.now();
for (const [root, inst] of this.instances) {
if (now - inst.lastUsed > IDLE_TIMEOUT_MS) {
this.kill(root);
}
}
}
shutdown(): void {
if (this.sweepTimer) clearInterval(this.sweepTimer);
for (const root of [...this.instances.keys()]) {
this.kill(root);
}
}
getActiveCount(): number {
return this.instances.size;
}
}
export const lspManager = new LspServerManager();