client

tcmts.cli.test.e2e.lsp.client · #cjpolrak · term · runs on the server

uses node:child_process

A minimal LSP client: framed JSON-RPC over the server's stdio.

import type { Keep } from "#codebase/tcmts/cli/test/e2e/contributions";
import { plainEnv, tcmBin } from "#codebase/tcmts/cli/test/e2e/harness";
import type { Msg } from "#codebase/tcmts/cli/test/e2e/lsp";
import { spawn } from "node:child_process";
import os from "node:os";

export function client(keep: Keep) {
  const child = keep(spawn(process.execPath, [tcmBin(), "lsp"], { cwd: os.tmpdir(), env: plainEnv() }));
  const notes: Msg[] = [];
  const pending = new Map<number, (m: Msg) => void>();
  let stderr = "";
  child.stderr.setEncoding("utf8").on("data", (d: string) => (stderr += d));
  let buf = Buffer.alloc(0);
  child.stdout.on("data", (chunk: Buffer) => {
    buf = Buffer.concat([buf, chunk]);
    for (;;) {
      const sep = buf.indexOf("\r\n\r\n");
      if (sep < 0) return;
      const len = Number(/Content-Length: (\d+)/.exec(buf.subarray(0, sep).toString())![1]);
      if (buf.length < sep + 4 + len) return;
      const m = JSON.parse(buf.subarray(sep + 4, sep + 4 + len).toString()) as Msg;
      buf = buf.subarray(sep + 4 + len);
      if (m.id !== undefined && !m.method) pending.get(m.id as number)?.(m);
      else if (m.method && m.id !== undefined) write({ id: m.id, result: null }); // e.g. workspace/codeLens/refresh
      else notes.push(m);
    }
  });
  const write = (m: Msg) => {
    const body = Buffer.from(JSON.stringify({ jsonrpc: "2.0", ...m }));
    child.stdin.write(`Content-Length: ${body.length}\r\n\r\n`);
    child.stdin.write(body);
  };
  let id = 0;
  const request = (method: string, params: unknown) =>
    new Promise<any>((resolve, reject) => {
      const n = ++id;
      pending.set(n, (m) => (m.error ? reject(new Error(JSON.stringify(m.error))) : resolve(m.result)));
      write({ id: n, method, params });
    });
  const notify = (method: string, params: unknown) => write({ method, params });
  const waitFor = async <T>(find: () => T | undefined, ms = 20000): Promise<T> => {
    for (const end = Date.now() + ms; Date.now() < end; await new Promise((r) => setTimeout(r, 50))) {
      const v = find();
      if (v) return v;
    }
    throw new Error(`timed out; stderr:\n${stderr}\nnotifications:\n${JSON.stringify(notes, null, 1)}`);
  };
  return { request, notify, notes, waitFor, child };
}

uses

used by