hoverInScratch
uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process
What a scratch file has at offset to say on hovering it (text being what it holds): a name from the codebase, its
names, hash, signature and doc, as its page has them (and its name, to pull it into the file); a definition the file declares, its type and what updating
would do to it; a local of one, whose it is and where; anything else, its type, as TypeScript has it. Where that is in the file (offsets), and it as HTML.
import { renderDoc } from "#codebase/tcmts/cli/docs"; import { classify } from "#codebase/tcmts/cli/main"; import { esc } from "#codebase/tcmts/cli/pages"; import { shortHash } from "#codebase/tcmts/core/hash"; import { cacheRefOf } from "#codebase/tcmts/core/shape"; import { declarationOf } from "#codebase/tcmts/core/ts"; import { markdownHtml } from "#codebase/tcmts/share"; import { analyzedScratch, editableCodebase, identifierAt, scratchPath } from "#codebase/tcmts/studio"; import type { Node as ts_Node } from "typescript/ast"; import { SymbolFlags as ts_SymbolFlags } from "typescript/sync"; export async function hoverInScratch( file: string, text: string, offset: number, ): Promise<{ from: number; to: number; html: string; name?: string } | null> { const cb = editableCodebase(); const abs = scratchPath(cb, file); const a = analyzedScratch(cb, abs, text); const id = identifierAt(a, abs, offset); const sf = a.program.getSourceFile(abs); if (!id || !sf) return null; const at = { from: id.getStart(sf), to: id.end }; const checker = a.checker; let sym = checker.getSymbolAtLocation(id); if (sym && sym.flags & ts_SymbolFlags.Alias) sym = checker.getAliasedSymbol(sym); const hash = sym && cacheRefOf(checker)(sym); const rec = hash ? cb.store.getDef(hash) : undefined; if (hash && rec) { const names = cb.store.pathsOf(hash); const doc = renderDoc(cb, rec, { evaluate: false }); const sig = rec.kind === "type" ? rec.signature : cb.store.shownSignature(rec); return { ...at, ...(names[0] && rec.kind !== "file" ? { name: names[0] } : {}), html: `<div class="hovhead"><b class="mono">${esc(names[0] ?? rec.nameHint)}</b> <span class="hash">${shortHash(hash)}</span></div>` + `<pre class="code hovsig">${esc(sig)}</pre>` + (doc ? `<div class="md">${markdownHtml(cb, doc.markdown)}</div>` : ""), }; } let type: string; try { type = sym && sym.flags & (ts_SymbolFlags.TypeAlias | ts_SymbolFlags.Interface) ? checker.typeToString(checker.getDeclaredTypeOfSymbol(sym)) : checker.typeToString(checker.getTypeAtLocation(id as ts_Node)); } catch { return null; } // where its name is declared, in this file const decl = sym ? declarationOf(sym) : undefined; const declName = decl && decl.getSourceFile() === sf ? ((decl as ts_Node & { name?: ts_Node }).name ?? decl) : undefined; const declAt = declName ? declName.getStart(sf) : -1; // a definition of the file's own (its name, or a use of it): what updating would do to it const def = a.defs.find( (d) => d.range.nameStart !== undefined && ((at.from >= d.range.nameStart && at.to <= d.range.nameEnd!) || d.range.nameStart === declAt), ); let said = ""; if (def && !a.diagnostics.length && !a.errors.length) { try { const row = classify(cb, a.defs, a.namespace ?? "").find((r) => r.def === def); said = row ? (( { new: "New: not in the codebase yet.", update: `An update of ${row.path}${row.note ? ` (${row.note})` : ""}.`, identical: `${row.path}, as it is in the codebase.`, } as Record<string, string> )[row.status] ?? `${row.path}: ${row.status}.`) : ""; } catch { said = ""; } } // a name declared inside one of the file's definitions (a local): whose, and where, as there's nothing to pull in if (!def && declAt >= 0) { const owner = a.defs.find((d) => declAt >= d.range.start && declAt <= d.range.end); if (owner) { const name = owner.targetPath ?? (a.namespace ? `${a.namespace}.${owner.nameHint}` : owner.nameHint); said = `A local of ${name}, declared on line ${text.slice(0, declAt).split("\n").length}.`; } } return { ...at, html: `<pre class="code hovsig">${esc(`${id.text}: ${type}`)}</pre>` + (said ? `<div class="muted">${esc(said)}</div>` : ""), }; }
uses
- tcmts.cli.docs.renderDoc
- tcmts.cli.main.classify
- tcmts.cli.pages.esc
- tcmts.core.hash.shortHash
- tcmts.core.shape.cacheRefOf
- tcmts.core.ts.declarationOf
- tcmts.share.markdownHtml
- tcmts.studio.analyzedScratch
- tcmts.studio.editableCodebase
- tcmts.studio.identifierAt
- tcmts.studio.scratchPath