testCodebaseInEditor

tcmts.cli.test.e2e.lsp.testCodebaseInEditor · #ag3mmgld · term

the codebase in the editor: tree, rendered views, definition, references, symbols, type search, edit

import { plainEnv, tcmBin, tempDir, withChildren } from "#codebase/tcmts/cli/test/e2e/harness";
import { at, client, titles } from "#codebase/tcmts/cli/test/e2e/lsp";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";

export const testCodebaseInEditor = () => withChildren(async (keep) => {
  const dir2 = tempDir("lsp2");
  const tcm = (...args: string[]) => spawnSync(process.execPath, [tcmBin(), ...args], { cwd: dir2, env: plainEnv(), encoding: "utf8" });
  tcm("init");
  fs.writeFileSync(
    path.join(dir2, "scratch.ts"),
    `// tcm: namespace geo
export type Point = { x: number; y: number };
/** Distance between two points. */
export const dist = (a: Point, b: Point): number => Math.hypot(a.x - b.x, a.y - b.y);
export const perimeter = (ps: Point[]): number => ps.reduce((n, p, i) => n + dist(p, ps[(i + 1) % ps.length]!), 0);
`,
  );
  tcm("add");
  fs.writeFileSync(path.join(dir2, "scratch.ts"), `// tcm: namespace app\nimport { dist } from "#codebase/geo";\n\nexport const far = (n: number) => dist({ x: 0, y: 0 }, { x: n, y: n }) > 10;\n`);
  tcm("add");
  const scratchText = `import { perimeter } from "#codebase/geo";\n\nperimeter([]);\n`;
  fs.writeFileSync(path.join(dir2, "scratch.ts"), scratchText);

  const c = client(keep);
  await c.request("initialize", { processId: null, rootUri: pathToFileURL(dir2).href, capabilities: {}, clientInfo: { name: "Visual Studio Code" } });
  c.notify("initialized", {});

  // the namespace tree
  const root = await c.request("tcm/tree", { namespace: "" });
  assert.deepEqual(root.namespaces.map((n: any) => [n.name, n.count]), [["app", 1], ["geo", 3]]);
  const geo = await c.request("tcm/tree", { namespace: "geo" });
  assert.deepEqual(geo.definitions.map((d: any) => [d.name, d.kind]), [["Point", "type"], ["dist", "term"], ["perimeter", "term"]]);
  assert.equal(geo.definitions[1].doc, "Distance between two points.");
  // a definition's document is its file in .tcm/src (debuggers can stop in it)
  const viewAt = (p: string) => pathToFileURL(path.join(dir2, ".tcm", "src", p)).href;
  assert.equal(geo.definitions[1].uri, viewAt("geo/dist.ts"));
  assert.ok(fs.existsSync(fileURLToPath(viewAt("geo/dist.ts"))));

  // a definition's view: rendered with current names, references link to their definitions
  const perim = await c.request("tcm/source", { uri: "tcm:/geo/perimeter.ts" });
  assert.match(perim.text, /^\/\/ geo\.perimeter #[a-z2-7]{8}\n/);
  assert.match(perim.text, /import \{ dist \} from "#codebase\/geo";/);
  const def = await c.request("textDocument/definition", { textDocument: { uri: "tcm:/geo/perimeter.ts" }, position: at(perim.text, "dist(p", 1) });
  assert.equal(def.uri, viewAt("geo/dist.ts"));
  const distText = (await c.request("tcm/source", { uri: "tcm:/geo/dist.ts" })).text;
  assert.equal(distText.split("\n")[def.range.start.line]!.slice(def.range.start.character, def.range.end.character), "dist");

  // references: from a view, and from a codebase name in a scratch file, across the codebase
  const fromView = await c.request("textDocument/references", { textDocument: { uri: "tcm:/geo/dist.ts" }, position: def.range.start, context: { includeDeclaration: false } });
  assert.deepEqual(fromView.map((l: any) => l.uri).sort(), [viewAt("app/far.ts"), viewAt("geo/perimeter.ts")]);
  const uri = pathToFileURL(path.join(dir2, "scratch.ts")).href;
  c.notify("textDocument/didOpen", { textDocument: { uri, languageId: "typescript", version: 1, text: scratchText } });
  // Cmd+click on a codebase name in a scratch file goes to its source; local names are TypeScript's
  const toSource = await c.request("textDocument/definition", { textDocument: { uri }, position: at(scratchText, "perimeter([", 2) });
  assert.equal(toSource.uri, viewAt("geo/perimeter.ts"));
  assert.equal(await c.request("textDocument/definition", { textDocument: { uri }, position: { line: 2, character: 12 } }), null, "not a name");
  const fromScratch = await c.request("textDocument/references", { textDocument: { uri }, position: at(scratchText, "perimeter([", 2), context: { includeDeclaration: true } });
  assert.deepEqual(fromScratch.map((l: any) => l.uri), [viewAt("geo/perimeter.ts")], "the declaration; nothing uses it yet");

  // lenses on a view: who uses it, edit, rename
  // the file works as the document, as tcm: does
  const lenses = await c.request("textDocument/codeLens", { textDocument: { uri: viewAt("geo/dist.ts") } });
  assert.deepEqual(titles(lenses), ["2 dependents", "Edit", "Rename"]);

  // outline, workspace symbols, hover in a view
  const symbols = await c.request("textDocument/documentSymbol", { textDocument: { uri: "tcm:/geo/dist.ts" } });
  assert.equal(symbols[0].name, "dist");
  const found = await c.request("workspace/symbol", { query: "per" });
  assert.deepEqual(found.map((s: any) => [s.name, s.containerName]), [["perimeter", "geo"]]);
  const hover = await c.request("textDocument/hover", { textDocument: { uri: "tcm:/geo/perimeter.ts" }, position: at(perim.text, "dist(p", 1) });
  assert.match(hover.contents.value, /Distance between two points\./);

  // search by type, for a picker
  const byType = await c.request("tcm/findByType", { query: "(a: Point, b: Point) => number" });
  assert.deepEqual(byType.hits.map((h: any) => [h.path, h.uri]), [["geo.dist", viewAt("geo/dist.ts")]]);

  // edit: added to the scratch file (it holds work: a watch expression), which the client then opens
  const edited = await c.request("workspace/executeCommand", { command: "tcm.edit", arguments: ["geo.dist"] });
  assert.equal(edited, pathToFileURL(path.join(dir2, "scratch.ts")).href);
  assert.match(fs.readFileSync(path.join(dir2, "scratch.ts"), "utf8"), /^import \{ perimeter \} from "#codebase\/geo";\n[\s\S]*perimeter\(\[\]\);\n\n[\s\S]*\/\/ tcm: path geo\.dist\n[\s\S]*const dist = \(a: Point, b: Point\)/);

  // a change made elsewhere (a terminal) reaches the client
  c.notes.length = 0;
  tcm("mv", "geo.dist", "geo.distance");
  await c.waitFor(() => c.notes.find((n) => n.method === "tcm/changed"));
  const moved = await c.request("tcm/tree", { namespace: "geo" });
  assert.ok(moved.definitions.some((d: any) => d.name === "distance"));
})

uses