testSearchCode
Code search finds text in definitions as they read now (current names), and in pages, by line; a regular
expression (written /…/, or all the text with regex), whole words and case work as an editor's do; under keeps
to a namespace or a definition, and it says it stopped early only when there was more to search.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { searchCode, searchTexts, searchTextsKey } from "#codebase/tcmts/cli/pages"; import type { SearchOptions } from "#codebase/tcmts/cli/pages"; import assert from "node:assert/strict"; export const testSearchCode = () => { const texts = [ { path: "lib.square", hash: "h1", kind: "term", text: "// lib.square #h1\nexport const square = (x: number): number => x * x;\n", first: 1, }, { path: "docs.guide", hash: "h2", kind: "file", text: "# Guide\n\nSquare things with `square`.\n", first: 0, }, ]; const codebase = (db: string, branch: string, head: number, sessionDir?: string) => ({ lay: { dbFile: db }, sessionDir, store: { branch: () => ({ name: branch }), logHead: () => head }, }) as unknown as Codebase; const lines = (q: string, opts: SearchOptions = {}, these = texts) => { const cb = codebase("db", "b", 0); searchTexts.clear(); searchTexts.set(searchTextsKey(cb), these); return searchCode(cb, q, opts, (h) => `file:///${h}`); }; // the texts are of one codebase's branch, as of its last history entry (a text version shown is one), and whether // documents are read (not in a session) const keys = [ codebase("db", "b", 0), codebase("other", "b", 0), codebase("db", "main", 0), codebase("db", "b", 1), codebase("db", "b", 0, "session"), ].map(searchTextsKey); assert.equal(new Set(keys).size, keys.length, keys.join(", ")); const r = lines("square"); assert.deepEqual( r.hits.map((h) => [h.path, h.lines.map((l) => l.line)]), [ ["lib.square", [1]], ["docs.guide", [2]], ], ); assert.equal(r.matches, 3); assert.equal(r.hits[0]!.uri, "file:///h1"); assert.equal(r.hits[1]!.uri, undefined); assert.deepEqual(r.hits[0]!.lines[0]!.ranges, [[13, 19]]); // the header line, with the name in it, isn't searched assert.equal(lines("lib.square").matches, 0); assert.equal(lines("Square", { caseSensitive: true }).matches, 1); assert.equal(lines("squar", { wholeWord: true }).matches, 0); assert.equal(lines("x \\* x", { regex: true }).matches, 1); assert.match(lines("(", { regex: true }).error ?? "", /Invalid regular expression/); // a regular expression written /…/, its case as its flags say; `(` is text assert.equal(lines("/x \\* x/").matches, 1); assert.equal(lines("/SQUARE/").matches, 0); assert.equal(lines("/SQUARE|guide/i").matches, 4); assert.equal(lines("(").error, undefined); // under a namespace, or a definition; not a prefix of a name assert.deepEqual( lines("square", { under: "docs" }).hits.map((h) => h.path), ["docs.guide"], ); assert.deepEqual( lines("square", { under: "lib.square" }).hits.map((h) => h.path), ["lib.square"], ); assert.deepEqual(lines("square", { under: "lib.sq" }).hits, []); // it stops at 500 definitions: truncated only when there was more to search const many = (n: number) => Array.from({ length: n }, (_, i) => ({ path: `ns.d${i}`, hash: `h${i}`, kind: "term", text: "// ns\nconst a = 1;\n", first: 1, })); assert.equal(lines("a", {}, many(500)).truncated, false); const over = lines("a", {}, many(501)); assert.deepEqual([over.truncated, over.hits.length], [true, 500]); assert.deepEqual( [ lines("a", { under: "ns.d1" }, many(501)).truncated, lines("a", { under: "ns.d1" }, many(501)).hits.length, ], [false, 1], ); }