import { createInterface } from 'node:readline'; import type { Readable, Writable } from 'node:stream'; interface RpcRequest { jsonrpc: '2.0'; id: number; method: string; params?: unknown; } interface RpcResponse { jsonrpc: '2.0'; id: number; result?: unknown; error?: { code: number; message: string }; } export class LspClient { private nextId = 1; private pending = new Map void; reject: (e: Error) => void }>(); private buffer = ''; constructor( private stdin: Writable, private stdout: Readable, ) { const rl = createInterface({ input: stdout, crlfDelay: Infinity }); rl.on('line', (line) => this.handleLine(line)); } private handleLine(line: string): void { this.buffer += line + '\n'; const match = this.buffer.match(/Content-Length: (\d+)\r?\n\r?\n/); if (!match || !match[1]) return; const len = parseInt(match[1], 10); const headerEnd = match.index! + match[0].length; const body = this.buffer.slice(headerEnd, headerEnd + len); if (body.length < len) return; this.buffer = this.buffer.slice(headerEnd + len); try { const msg: RpcResponse = JSON.parse(body); const cb = this.pending.get(msg.id); if (cb) { this.pending.delete(msg.id); cb.resolve(msg); } } catch { // Malformed JSON, ignore } } async request(method: string, params?: unknown): Promise { const id = this.nextId++; const req: RpcRequest = { jsonrpc: '2.0', id, method, params }; const body = JSON.stringify(req); const header = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n`; return new Promise((resolve, reject) => { this.pending.set(id, { resolve: (resp) => { if (resp.error) reject(new Error(resp.error.message)); else resolve(resp.result); }, reject, }); this.stdin.write(header + body); }); } async notify(method: string, params?: unknown): Promise { const body = JSON.stringify({ jsonrpc: '2.0', method, params }); const header = `Content-Length: ${Buffer.byteLength(body, 'utf8')}\r\n\r\n`; this.stdin.write(header + body); } }