renameInScratch

tcmts.studio.renameInScratch · #z7rxlzle · term · runs on the server

uses tcmts.studio.editableCodebase, which uses tcmts.share.servedCodebase, which uses process

Rename the name at offset in a scratch file to to (text being what it holds): the changes to the file, and what was done in the codebase. A name the codebase has is renamed there too, as tcm mv does (its hash stays, so what uses it follows; to a leaf keeps its namespace, a dotted name moves it), and the file's import follows it; anything else is renamed in the file alone.

import { isTypeOnlyImport } from "#codebase/tcmts/core/ts";
import { HttpError } from "#codebase/tcmts/server";
import {
  analyzedScratch, editableCodebase, importEdit, nameInScratch, runTcm, scratchPath,
} from "#codebase/tcmts/studio";
import { isImportDeclaration as ts_isImportDeclaration } from "typescript/ast";
import type { Node as ts_Node } from "typescript/ast";

export async function renameInScratch(
  file: string,
  text: string,
  offset: number,
  to: string,
): Promise<{ changes: { from: number; to: number; insert: string }[]; said?: string }> {
  const cb = editableCodebase();
  const abs = scratchPath(cb, file, text);
  const a = analyzedScratch(cb, abs, text);
  const n = nameInScratch(cb, a, abs, offset);
  if (!n) throw new HttpError(400, "There's no name there to rename.");
  const wanted = to.trim();
  const leaf = wanted.slice(wanted.lastIndexOf(".") + 1);
  if (!/^[A-Za-z_$][\w$]*$/.test(leaf) || (!n.name && wanted.includes(".")))
    throw new HttpError(
      400,
      `${wanted} isn't a name${n.name ? " (a.b, or b to keep its namespace)" : ""}.`,
    );
  if (!n.name) return { changes: n.uses.map((u) => ({ ...u, insert: leaf })) };
  const ns = (p: string) => p.slice(0, Math.max(0, p.lastIndexOf(".")));
  const next = wanted.includes(".") ? wanted : ns(n.name) ? `${ns(n.name)}.${leaf}` : leaf;
  if (next === n.name) return { changes: [] };
  const out = await runTcm(cb.lay.root, ["mv", n.name, next], { branch: true });
  if (out.code !== 0)
    throw new HttpError(
      400,
      out.text.split("\n").slice(0, 8).join("\n") || `tcm mv ${n.name} ${next} failed`,
    );
  const said = out.text.split("\n")[0];
  // a definition the file holds: its name moves with it (in the file, and in the codebase)
  if (!n.imported)
    return { changes: n.uses.map((u) => ({ ...u, insert: leaf })), ...(said ? { said } : {}) };
  const spec = n.spec;
  // imported as another name (`a as b`): only what it imports changes
  const local = !spec?.propertyName;
  const changes: { from: number; to: number; insert: string }[] = local
    ? n.uses.map((u) => ({ ...u, insert: leaf }))
    : [{ from: spec!.propertyName!.getStart(n.sf), to: spec!.propertyName!.end, insert: leaf }];
  if (spec && ns(next) !== ns(n.name)) {
    // another namespace: another module to import it from
    const module = ns(next) ? `#codebase/${ns(next).replaceAll(".", "/")}` : "#codebase";
    let d: ts_Node | undefined = spec;
    while (d && !ts_isImportDeclaration(d)) d = d.parent;
    const imp = d && ts_isImportDeclaration(d) ? d : undefined;
    const elements = (spec.parent as ts_Node & { elements: readonly ts_Node[] }).elements;
    if (imp && elements.length === 1) {
      const m = imp.moduleSpecifier;
      changes.push({ from: m.getStart(n.sf), to: m.end, insert: JSON.stringify(module) });
    } else if (imp) {
      const named = spec.parent;
      const rest = elements.filter((e) => e !== spec).map((e) => e.getText(n.sf));
      // (the uses' changes inside the braces go: the import is written again)
      for (let i = changes.length - 1; i >= 0; i--)
        if (changes[i]!.from >= named.getStart(n.sf) && changes[i]!.to <= named.end)
          changes.splice(i, 1);
      changes.push({ from: named.getStart(n.sf), to: named.end, insert: `{ ${rest.join(", ")} }` });
      const added = importEdit(
        text,
        module,
        local ? leaf : `${leaf} as ${spec.name.text}`,
        isTypeOnlyImport(imp.importClause as never),
      );
      if (added) changes.push(added);
    }
  }
  return { changes: changes.sort((x, y) => x.from - y.from), ...(said ? { said } : {}) };
}

uses

used by