applyPatch
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
- tcmts.cli.main.PatchEdit
- tcmts.cli.main.brokenLiteral
- tcmts.cli.main.escapedMatch
- tcmts.cli.main.nearestLine
used by
- tcmts.cli.main.patch
- tcmts.cli.main.patchMany
- tcmts.cli.main.patchedTogether
- tcmts.cli.main.testApplyPatch
- tcmts.cli.main.testApplyPatchKeepsTemplateLiteralsWhole
- tcmts.cli.main.testApplyPatchTakesEscapesEitherWay
- tcmts.cli.main.testPatchADocWrittenOnOneLine
- tcmts.cli.main.testPatchSaysWhatsNearest
- tcmts.cli.main.testPatchSaysWhereAsTheViewNumbers