definitionsAt
uses node:path
The definitions at a line of a file (with no line, every one in it), as tcm at says them: for scratch files,
definitions' documents (views), generated modules and mirrored files. A module named by its component's hash
(.tcm/cache/<hash>.ts, a packed package's modules/<hash>.js) is its component's: the member at the line, or every
member. Each with its hash (none: not in the codebase), and the name and lines it has in the file when it's read as
text. A view of another branch's, or of none, is a UsageError saying so; a file that isn't there, too.
import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { nameAt } from "#codebase/tcmts/cli/main"; import { declarations } from "#codebase/tcmts/cli/mirror"; import { UsageError } from "#codebase/tcmts/cli/ui"; import { hashesOfViewFile, srcDir, viewsBranchOf, viewText } from "#codebase/tcmts/cli/views"; import fs from "node:fs"; import path from "node:path"; export function definitionsAt( cb: Codebase, file: string, line?: number, /** the file as it was asked for, for what's said */ asked = path.relative(process.cwd(), file) || file, ): { hash?: string; name: string; from?: number; to?: number }[] { const component = /^([a-z2-7]{52})\.(?:[mc]?js|tsx?)$/.exec(path.basename(file))?.[1]; if (component && cb.store.componentMembers(component).length) { const members = cb.store.componentMembers(component); const named = (h: string) => ({ hash: h, name: cb.store.pathsOf(h)[0] ?? "(unnamed)" }); // which member: the declaration at the line, and the hash the module exports it as if (line !== undefined && members.length > 1 && fs.existsSync(file)) { const text = fs.readFileSync(file, "utf8"); const lineOf = (offset: number) => text.slice(0, offset).split("\n").length; const exported = new Map( [...text.matchAll(/export (?:type )?\{ ([\w$]+) as h_([a-z2-7]{52}) \}/g)].map((x) => [ x[1]!, x[2]!, ]), ); const d = [...declarations(file, text)].find( ([, d]) => lineOf(d.start) <= line && line <= lineOf(d.end), ); const h = d && exported.get(d[0]); if (h) return [named(h)]; } return members.map((r) => named(r.hash)); } const owner = viewsBranchOf(cb, file); const ours = owner !== undefined && !path.relative(srcDir(cb), file).startsWith(".."); const shown = asked; if (owner !== undefined && !ours) throw new UsageError( owner === null ? `${shown} is a view of a session whose branch is gone` : `${shown} is a view of ${owner}'s definitions, not ${cb.store.branch().name}'s: \`tcm at ${shown}${line === undefined ? "" : `:${line}`} --branch ${owner}\``, ); const text = ours ? viewText(cb, hashesOfViewFile(cb, file))?.text : fs.existsSync(file) ? fs.readFileSync(file, "utf8") : undefined; if (text === undefined) throw new UsageError( ours ? `${shown} isn't a view of anything on ${cb.store.branch().name}` : `no file ${shown}`, ); const lineOf = (offset: number) => text.slice(0, offset).split("\n").length; const name = nameAt(cb, file, text); return [...declarations(file, text)] .map(([leaf, d]) => ({ leaf, from: lineOf(d.doc?.start ?? d.start), to: lineOf(d.end), at: lineOf(d.start), })) .filter((d) => line === undefined || (d.from <= line && line <= d.to)) .sort((a, b) => a.from - b.from) .map((d) => { const full = name(d.leaf, d.at); let hash: string | undefined; try { hash = full ? cb.lookup(full)[0] : undefined; } catch { hash = undefined; } return { hash, name: full ?? d.leaf, from: d.from, to: d.to }; }); }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.main.nameAt
- tcmts.cli.mirror.declarations
- tcmts.cli.ui.UsageError
- tcmts.cli.views.hashesOfViewFile
- tcmts.cli.views.srcDir
- tcmts.cli.views.viewText
- tcmts.cli.views.viewsBranchOf