testCodebaseInEditor
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")); })