hoverInScratch

tcmts.studio.hoverInScratch · #yw2zqctk · term · runs on the server

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

used by