import { spawn } from "child_process";
import { createInterface } from "readline";
export class LSPClient {
    process = null;
    requestId = 0;
    pending = new Map();
    handlers = new Map();
    buffer = "";
    capabilities = {};
    initialized = false;
    initPromise = null;
    options;
    constructor(options) {
        this.options = options;
    }
    sendMessage(msg) {
        if (!this.process?.stdin?.writable)
            return;
        const body = JSON.stringify(msg);
        const header = `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n`;
        this.process.stdin.write(header + body);
    }
    handleData(chunk) {
        this.buffer += chunk;
        while (true) {
            const headerMatch = this.buffer.match(/^Content-Length: (\d+)\r\n\r\n/);
            if (!headerMatch)
                break;
            const contentLength = parseInt(headerMatch[1], 10);
            const headerEnd = headerMatch[0].length;
            if (this.buffer.length < headerEnd + contentLength)
                break;
            const content = this.buffer.slice(headerEnd, headerEnd + contentLength);
            this.buffer = this.buffer.slice(headerEnd + contentLength);
            try {
                const msg = JSON.parse(content);
                if ("id" in msg && "method" in msg) {
                    // Request
                    this.handleRequest(msg);
                }
                else if ("id" in msg) {
                    // Response
                    const response = msg;
                    const pending = this.pending.get(response.id);
                    if (pending) {
                        this.pending.delete(response.id);
                        if (response.error) {
                            pending.reject(new Error(response.error.message));
                        }
                        else {
                            pending.resolve(response.result);
                        }
                    }
                }
                else if ("method" in msg) {
                    // Notification
                    const notification = msg;
                    const handler = this.handlers.get(notification.method);
                    handler?.(notification.params);
                }
            }
            catch {
                // Ignore parse errors
            }
        }
    }
    async request(method, params, timeoutMs = 15_000) {
        return new Promise((resolve, reject) => {
            const id = ++this.requestId;
            const timer = setTimeout(() => {
                this.pending.delete(id);
                reject(new Error(`LSP "${method}" timed out after ${timeoutMs}ms`));
            }, timeoutMs);
            this.pending.set(id, {
                resolve: (v) => {
                    clearTimeout(timer);
                    resolve(v);
                },
                reject: (e) => {
                    clearTimeout(timer);
                    reject(e);
                },
            });
            this.sendMessage({ jsonrpc: "2.0", id, method, params });
        });
    }
    notify(method, params) {
        this.sendMessage({ jsonrpc: "2.0", method, params });
    }
    handleRequest(request) {
        const handler = this.handlers.get(request.method);
        if (handler) {
            handler(request.params);
        }
        // Respond with null for unknown methods
        this.sendMessage({ jsonrpc: "2.0", id: request.id, result: null });
    }
    onNotification(method, handler) {
        this.handlers.set(method, handler);
    }
    async start() {
        if (this.process)
            return;
        this.process = spawn(this.options.command, this.options.args ?? [], {
            cwd: this.options.cwd,
            env: { ...process.env, ...this.options.env },
            stdio: ["pipe", "pipe", "pipe"],
        });
        const rl = createInterface({ input: this.process.stdout });
        rl.on("line", (line) => this.handleData(`${line}\n`));
        this.process.on("exit", (code) => {
            this.process = null;
            this.initialized = false;
            for (const [, pending] of this.pending) {
                pending.reject(new Error(`LSP process exited with code ${code}`));
            }
            this.pending.clear();
        });
        this.process.stderr?.on("data", () => {
            // Silently consume stderr (most LSP servers log there)
        });
        // Initialize
        const result = (await this.request("initialize", {
            processId: process.pid,
            rootUri: this.options.rootUri,
            capabilities: {
                textDocument: {
                    hover: { contentFormat: ["markdown", "plaintext"] },
                    definition: {},
                    references: {},
                    documentSymbol: {},
                    completion: {},
                },
                workspace: { symbol: {} },
            },
        }));
        this.capabilities = result?.capabilities ?? {};
        this.notify("initialized", {});
        this.initialized = true;
    }
    async initializedPromise() {
        if (this.initialized)
            return;
        if (!this.initPromise) {
            this.initPromise = this.start();
        }
        return this.initPromise;
    }
    async openDocument(uri, languageId, text) {
        await this.initializedPromise();
        this.notify("textDocument/didOpen", {
            textDocument: { uri, languageId, version: 1, text },
        });
    }
    async closeDocument(uri) {
        await this.initializedPromise();
        this.notify("textDocument/didClose", {
            textDocument: { uri },
        });
    }
    async hover(uri, line, column) {
        await this.initializedPromise();
        return this.request("textDocument/hover", {
            textDocument: { uri },
            position: { line, character: column },
        });
    }
    async definition(uri, line, column) {
        await this.initializedPromise();
        return this.request("textDocument/definition", {
            textDocument: { uri },
            position: { line, character: column },
        });
    }
    async references(uri, line, column) {
        await this.initializedPromise();
        return this.request("textDocument/references", {
            textDocument: { uri },
            position: { line, character: column },
            context: { includeDeclaration: true },
        });
    }
    async documentSymbols(uri) {
        await this.initializedPromise();
        return this.request("textDocument/documentSymbol", {
            textDocument: { uri },
        });
    }
    async workspaceSymbols(query) {
        await this.initializedPromise();
        return this.request("workspace/symbol", { query });
    }
    async shutdown() {
        if (!this.process || !this.initialized)
            return;
        try {
            await this.request("shutdown");
        }
        catch {
            // Ignore
        }
        this.notify("exit");
        if (this.process) {
            this.process.kill();
            this.process = null;
        }
        this.initialized = false;
    }
    isRunning() {
        return this.process !== null && !this.process.killed;
    }
}
//# sourceMappingURL=client.js.map