patchedTogether

tcmts.cli.main.patchedTogether · #gh2j3fvx · term · runs on the server

uses tcmts.loader.render, which uses node:path

The file tcm patch --patches saves: the definitions patched, and the rest of the cycles they're in (saved with them, as they are), written together as tcm edit writes definitions, with each patch applied to its definition as tcm view shows it. Written together, a name can read otherwise than alone (find from two namespaces, an import two of them name alike, a definition named as another's local is): the lines an edit leaves are the file's, and the lines it writes are given the file's names. Also each definition's view before and after (the change, to show), its lines in the file (from 0) and where they start in its view (to say where a problem is), and the cycles' other members (mates). They're written in an order that typechecks (declarationOrder): one whose value uses another's after it. A target's imports are written with the file's (they go if nothing uses them). Places are runs of lines, numbered as the definition's view, its edits made, numbers them (a // tcm: path line written among them is in none).

import type { Codebase } from "#codebase/tcmts/cli/codebase";
import { lineDiff } from "#codebase/tcmts/cli/diff";
import { applyPatch, declarationOrder, patchImportLines, renamed, sameNames } from "#codebase/tcmts/cli/main";
import type { PatchEdit, PatchImport } from "#codebase/tcmts/cli/main";
import { joinName } from "#codebase/tcmts/cli/mirror";
import { UsageError } from "#codebase/tcmts/cli/ui";
import type { DefRecord } from "#codebase/tcmts/core/types";
import { render } from "#codebase/tcmts/loader";

export function patchedTogether(
  cb: Codebase,
  targets: { name: string; edits: PatchEdit[]; all?: boolean; imports?: PatchImport[] }[],
): {
  text: string;
  jsx: boolean;
  views: { name: string; before: string; after: string; notes?: string[] }[];
  places: { name: string; line: number; end: number; viewAt: number }[];
  mates: Set<string>;
  respell: Map<string, Map<string, string>>;
} {
  const pathOf = new Map<string, string>();
  const groups: {
    name: string;
    edits: PatchEdit[];
    all?: boolean;
    imports?: PatchImport[];
    recs: DefRecord[];
  }[] = [];
  for (const t of targets) {
    const recs: DefRecord[] = [];
    // (a name holding a type and a value is both, as `tcm view` shows it)
    for (const h of t.name.startsWith("#") ? [cb.lookupOne(t.name)] : cb.lookup(t.name)) {
      if (pathOf.has(h))
        throw new UsageError(`${t.name} is patched twice: give all its edits in one patch`);
      pathOf.set(h, t.name.startsWith("#") ? (cb.store.pathsOf(h)[0] ?? t.name) : t.name);
      recs.push(cb.store.getDef(h)!);
    }
    if (!recs.length) throw new UsageError(cb.notFound(t.name));
    groups.push({ ...t, recs });
  }
  // the rest of their cycles, hashed with them and so saved with them: written here, every name in the file is
  // chosen once
  const mates: DefRecord[] = [];
  for (const g of groups)
    for (const r of g.recs)
      for (const m of cb.store.componentMembers(r.componentHash)) {
        const p = cb.store.pathsOf(m.hash)[0];
        if (pathOf.has(m.hash) || !p) continue;
        pathOf.set(m.hash, p);
        mates.push(cb.store.getDef(m.hash)!);
      }
  // (each after what its value uses, whatever order they were given in)
  const units = declarationOrder(
    [
      ...groups.map((g) => ({ g, recs: g.recs })),
      ...mates.map((m) => ({ g: undefined, recs: [m] })),
    ],
    (u) => u.recs,
  );
  const members = units.flatMap((u) => u.recs);
  const nsOf = (p: string) => (p.includes(".") ? p.slice(0, p.lastIndexOf(".")) : "");
  const nss = new Set([...pathOf.values()].map(nsOf));
  const ns = nss.size === 1 ? [...nss][0]! : "";
  const opts = {
    style: "scratch" as const,
    exportDeclarations: true,
    remap: (h: string) => cb.store.patched(h),
    // (the file `tcm patch --patches` writes is where a scratch file is)
    dir: cb.sessionDir ?? cb.lay.root,
  };
  const full = render(members, cb.source(), cb.lay, opts);
  const body = render(members, cb.source(), cb.lay, { ...opts, omitImports: true }).text;
  const lines = body.split("\n");
  // each member's lines, as rendering writes them: its doc, its code, a blank line (a destructuring's bindings are
  // one statement, written once)
  const blocks = new Map<string, { from: number; doc: number; to: number }>();
  const written = new Set<string>();
  let at = 0;
  for (const m of members) {
    if (written.has(m.sourceText)) {
      blocks.set(m.hash, { from: at, doc: 0, to: at });
      continue;
    }
    written.add(m.sourceText);
    const doc = m.doc ? m.doc.split("\n").length + 2 : 0;
    const to = at + doc + m.sourceText.split("\n").length + 1;
    blocks.set(m.hash, { from: at, doc, to });
    at = to;
  }
  if (!full.text.endsWith(body) || at !== lines.length)
    throw new UsageError(
      `${targets.map((t) => t.name).join(", ")} can't be written together: patch them one at a time`,
    );
  // (the imports asked for, with the file's)
  const asked = [...new Set(targets.flatMap((t) => patchImportLines(t.imports ?? [])))];
  const head =
    (asked.length ? `${asked.join("\n")}\n` : "") +
    full.text.slice(0, full.text.length - body.length);
  const offset = (ns ? 1 : 0) + head.split("\n").length - 1;
  // where a definition doesn't go back by its name (another namespace, or a name taken here), a line says where
  const note = (h: string) => {
    const p = pathOf.get(h)!;
    return p !== joinName(ns, full.memberNames.get(h)!) ? `// tcm: path ${p}` : undefined;
  };
  // each saved as it reads alone: by path, the names that are others in the file (see `respelled`)
  const respell = new Map<string, Map<string, string>>();
  const readsAlone = (recs: DefRecord[], lines: string[]) => {
    const r = render(recs, cb.source(), cb.lay, { ...opts, omitImports: true });
    // a function's or class's name is part of its hash: one taken here would be another definition
    for (const m of recs)
      if (m.nominal && r.memberNames.get(m.hash) !== full.memberNames.get(m.hash))
        throw new UsageError(
          `${pathOf.get(m.hash)} can't be written with the others, as another is declared ${r.memberNames.get(m.hash)} too (a function's or a class's name is part of its hash): patch them apart`,
        );
    const back = new Map<string, string>();
    const view = r.text.split("\n");
    if (view.length === lines.length)
      for (const [i, l] of view.entries()) if (l !== lines[i]) sameNames(lines[i]!, l, back);
    for (const m of recs) respell.set(pathOf.get(m.hash)!, back);
    return r.text;
  };
  const out: string[] = [];
  const views: { name: string; before: string; after: string; notes?: string[] }[] = [];
  const places: { name: string; line: number; end: number; viewAt: number }[] = [];
  for (const { g, recs } of units) {
    if (!g) {
      const m = recs[0]!;
      const b = blocks.get(m.hash)!;
      const block = lines.slice(b.from, b.to);
      readsAlone([m], block);
      const n = note(m.hash);
      if (n && block.length) block.splice(b.doc, 0, n);
      const start = out.length;
      out.push(...block);
      if (block.length)
        places.push({
          name: pathOf.get(m.hash)!,
          line: offset + start,
          end: offset + out.length - 1,
          viewAt: -1,
        });
      continue;
    }
    const from = blocks.get(g.recs[0]!.hash)!.from;
    const theirs = lines.slice(from, blocks.get(g.recs.at(-1)!.hash)!.to);
    const before = readsAlone(g.recs, theirs);
    const view = before.split("\n");
    if (view.length !== theirs.length)
      throw new UsageError(`${g.name} can't be written with the others: patch it on its own`);
    // (the view's lines above these: its imports)
    const alone = render(g.recs, cb.source(), cb.lay, opts).text;
    const viewAt = alone.split("\n").length - view.length;
    const r = applyPatch(
      before,
      g.edits,
      !!g.all,
      viewAt,
      g.recs.some((m) => !!m.jsx) ? "patch.tsx" : "patch.ts",
    );
    if ("problem" in r)
      throw new UsageError(
        `${g.name}: ${r.problem}; nothing was changed (\`tcm view ${g.name}\` shows its code)`,
      );
    views.push({ name: g.name, before, after: r.text, notes: r.notes });
    // what each name of the view's is here
    const names = new Map<string, string>();
    for (const [i, l] of view.entries()) if (l !== theirs[i]) sameNames(l, theirs[i]!, names);
    // the lines it writes, given those names where they're the names (not a comment's or a string's words)
    const writes = renamed(
      r.text,
      names,
      g.recs.some((m) => !!m.jsx),
    ).split("\n");
    const notes = new Map<number, string>();
    for (const rec of g.recs) {
      const b = blocks.get(rec.hash)!;
      const n = note(rec.hash);
      if (n && b.to > b.from) notes.set(b.from + b.doc - from, n);
    }
    // each line written: its line in the view, edits made (from 0), or none (a note)
    const start = out.length;
    const lineOf: (number | undefined)[] = [];
    let i = 0;
    let j = 0;
    for (const [op] of lineDiff(view, r.text.split("\n"))) {
      if (op === "+") {
        out.push(writes[j]!);
        lineOf.push(j++);
        continue;
      }
      const n = notes.get(i);
      if (n !== undefined) {
        out.push(n);
        lineOf.push(undefined);
        notes.delete(i);
      }
      if (op === " ") {
        out.push(theirs[i]!);
        lineOf.push(j++);
      }
      i++;
    }
    for (const n of notes.values()) {
      out.push(n);
      lineOf.push(undefined);
    }
    for (let k = 0; k < lineOf.length; k++) {
      if (lineOf[k] === undefined) continue;
      let e = k;
      while (lineOf[e + 1] !== undefined && lineOf[e + 1] === lineOf[e]! + 1) e++;
      places.push({
        name: g.name,
        line: offset + start + k,
        end: offset + start + e,
        viewAt: viewAt + lineOf[k]!,
      });
      k = e;
    }
  }
  return {
    text: (ns ? `// tcm: namespace ${ns}\n` : "") + head + out.join("\n"),
    jsx: members.some((m) => !!m.jsx),
    views,
    places,
    mates: new Set(mates.map((m) => pathOf.get(m.hash)!)),
    respell,
  };
}

uses

used by