applyPatch

tcmts.cli.main.applyPatch · #ace73z6x · term · runs on the server

uses tcmts.cli.main.brokenLiteral, which uses tcmts.cli.main.templateLiterals, which uses tcmts.core.ts.parse, which uses tcmts.core.ts.compiler, which uses process

The edits made to text: each old must be in it exactly once (every one, with all), else why not. An old that's there only with other spacing or line breaks (the formatter rewraps lines) is found that way, if it's there once; one that isn't there at all is reported with the line most like its first (numbered from from + 1: the view's line of the text's first, less one). In code (code: the file name it's parsed as, x.ts or x.tsx), an old that's there only with its escapes taken the other way (\n or a line break, \` or `, \${ or ${: what JSON makes easy to get wrong) is found that way, if it's there once, with new written the same way, and said in notes; and an edit that would end or split a template literal it's inside (a bare backtick or ${ in new) isn't made: why is the problem.

import { brokenLiteral, escapedMatch, nearestLine } from "#codebase/tcmts/cli/main";
import type { PatchEdit } from "#codebase/tcmts/cli/main";

export function applyPatch(
  text: string,
  edits: PatchEdit[],
  all = false,
  from = 0,
  code?: string,
): { text: string; notes?: string[] } | { problem: string } {
  let out = text;
  const notes: string[] = [];
  for (const e of edits) {
    if (!e.old) return { problem: "an edit has no old text to find" };
    const old = JSON.stringify(e.old.length > 80 ? `${e.old.slice(0, 79)}…` : e.old);
    // where it is, and what's written there
    let places: { index: number; length: number }[] = [];
    let inserted = e.new;
    for (let i = out.indexOf(e.old); i >= 0; i = out.indexOf(e.old, i + e.old.length))
      places.push({ index: i, length: e.old.length });
    if (places.length > 1 && !all)
      return {
        problem: `found ${places.length} times: ${old} (give more of the text around it, or --all)`,
      };
    if (!places.length) {
      // the same text, spaced and wrapped otherwise: whitespace between its characters (not inside a word), and at
      // least some where it separates two words; and a comma before a closing bracket, which the formatter adds when
      // it wraps a list
      const patternWith = (ws: string) =>
        e.old
          .trim()
          .split(/(\s+)/)
          .map((part, i, parts) =>
            i % 2
              ? /\w$/.test(parts[i - 1]!) && /^\w/.test(parts[i + 1]!)
                ? `${ws}+`
                : `${ws}*`
              : [...part]
                  .map(
                    (c, j) =>
                      (j && !(/\w/.test(part[j - 1]!) && /\w/.test(c)) ? `${ws}*` : "") +
                      c.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&"),
                  )
                  .join(""),
          )
          .join("")
          .replace(/\\([)\]}])/g, "(?:,\\s*)?\\$1");
      let found = [...out.matchAll(new RegExp(patternWith("\\s"), "g"))];
      // in a doc comment, a line's leading `*` is spacing too (a doc written on one line is stored on several): there,
      // and only there, where a line starting with `*` isn't code
      if (!found.length) {
        const docs = [...out.matchAll(/\/\*\*[\s\S]*?\*\//g)].map(
          (d) => [d.index, d.index + d[0].length] as const,
        );
        found = [
          ...out.matchAll(new RegExp(patternWith("(?:\\s|\\n[ \\t]*\\*(?!/))"), "g")),
        ].filter((m) => docs.some(([from, to]) => m.index >= from && m.index + m[0].length <= to));
      }
      // else, in code, its escapes taken the other way (in JSON, `\n` is a line break; in the source, a backslash and
      // an n)
      const escaped = code && !found.length ? escapedMatch(out, e.old, e.new) : undefined;
      if (escaped && "problem" in escaped) return escaped;
      if (found.length === 1 || (found.length > 1 && all)) {
        places = found.map((m) => ({ index: m.index, length: m[0].length }));
        inserted = e.new.trim();
      } else if (escaped && (escaped.matches.length === 1 || all)) {
        places = escaped.matches;
        inserted = escaped.new;
        notes.push(
          `${old} was found only with escapes as the source writes them, and new was written alike: ${escaped.changed.join("; ")}`,
        );
      } else
        return {
          problem: escaped
            ? `not found as written: ${old}; found ${escaped.matches.length} times with ${escaped.changed.join("; ")} (give more of the text around it, or --all)`
            : found.length
              ? `found ${found.length} times, with other spacing: ${old} (give more of the text around it, or --all)`
              : /^\s*import\s/.test(e.old)
                ? `not found: ${old}: imports follow the code (a name it uses is imported, one it stops using goes); for one that can't be found that way (an alias, say), give imports, as [{"name": "…", "from": "…", "as": "…"}]`
                : `not found: ${old}${nearestLine(out, e.old, from)}`,
        };
    }
    const was = out;
    for (const p of [...places].reverse())
      out = out.slice(0, p.index) + inserted + out.slice(p.index + p.length);
    // (the template literals it's inside, as they were)
    const broken = code ? brokenLiteral(was, out, places, inserted, code, from) : undefined;
    if (broken) return { problem: broken };
  }
  return notes.length ? { text: out, notes } : { text: out };
}

uses

used by