From 9ff078bdfde2970da9f7179154185cc6fd09bb8e Mon Sep 17 00:00:00 2001 From: can1357 Date: Wed, 11 Feb 2026 07:58:37 +0100 Subject: [PATCH] fix(coding-agent/patch): removed whitespace preservation override logic in patch application - Removed whitespace preservation logic that was overriding model-generated formatting choices in patch application. - Deleted `preserveWhitespaceOnlyLines` and `preserveWhitespaceOnlyLinesLoose` functions that were incorrectly reverting intentional formatting changes. - Updated test to reflect that model whitespace choices are now respected without override. --- packages/coding-agent/src/patch/hashline.ts | 67 +------------------ .../coding-agent/test/core/hashline.test.ts | 7 +- 2 files changed, 5 insertions(+), 69 deletions(-) diff --git a/packages/coding-agent/src/patch/hashline.ts b/packages/coding-agent/src/patch/hashline.ts index 8562794a4..b9c29e131 100644 --- a/packages/coding-agent/src/patch/hashline.ts +++ b/packages/coding-agent/src/patch/hashline.ts @@ -161,60 +161,6 @@ function restoreOldWrappedLines(oldLines: string[], newLines: string[]): string[ return out; } -/** - * For replace edits (N old → N new), preserve original content on lines where - * the only difference is whitespace. - * - * Models frequently reformat code (e.g., removing spaces inside import braces) - * when making targeted edits. This detects lines that changed only in - * whitespace and keeps the original, preventing spurious formatting diffs. - */ -function preserveWhitespaceOnlyLines(oldLines: string[], newLines: string[]): string[] { - if (oldLines.length !== newLines.length) return newLines; - let anyPreserved = false; - const result = new Array(newLines.length); - for (let i = 0; i < newLines.length; i++) { - if (oldLines[i] !== newLines[i] && equalsIgnoringWhitespace(oldLines[i], newLines[i])) { - result[i] = oldLines[i]; - anyPreserved = true; - } else { - result[i] = newLines[i]; - } - } - return anyPreserved ? result : newLines; -} - -/** - * A weaker variant of {@link preserveWhitespaceOnlyLines} that can preserve - * whitespace even when the replacement line counts don't match. - */ -function preserveWhitespaceOnlyLinesLoose(oldLines: string[], newLines: string[]): string[] { - const canonToOld = new Map(); - for (const oldLine of oldLines) { - const canon = stripAllWhitespace(oldLine); - const bucket = canonToOld.get(canon); - if (bucket) bucket.push(oldLine); - else canonToOld.set(canon, [oldLine]); - } - - let anyPreserved = false; - const result = new Array(newLines.length); - for (let i = 0; i < newLines.length; i++) { - const newLine = newLines[i]; - const bucket = canonToOld.get(stripAllWhitespace(newLine)); - if (bucket) { - const oldLine = bucket.find(l => l !== newLine && equalsIgnoringWhitespace(l, newLine)); - if (oldLine) { - result[i] = oldLine; - anyPreserved = true; - continue; - } - } - result[i] = newLine; - } - return anyPreserved ? result : newLines; -} - function stripInsertAnchorEchoAfter(anchorLine: string, dstLines: string[]): string[] { if (dstLines.length <= 1) return dstLines; if (equalsIgnoringWhitespace(dstLines[0], anchorLine)) { @@ -858,7 +804,6 @@ export function applyHashlineEdits( ); let nextLines = merged.newLines; nextLines = restoreIndentForPairedReplacement([origLines[0] ?? ""], nextLines); - nextLines = preserveWhitespaceOnlyLinesLoose(origLines, nextLines); if ( origLines.join("\n") === nextLines.join("\n") && origLines.some(l => CONFUSABLE_HYPHENS_RE.test(l)) @@ -874,11 +819,7 @@ export function applyHashlineEdits( const origLines = originalFileLines.slice(spec.ref.line - 1, spec.ref.line); let stripped = stripRangeBoundaryEcho(originalFileLines, spec.ref.line, spec.ref.line, dstLines); stripped = restoreOldWrappedLines(origLines, stripped); - const preserved = - stripped.length === count - ? preserveWhitespaceOnlyLines(origLines, stripped) - : preserveWhitespaceOnlyLinesLoose(origLines, stripped); - let newLines = restoreIndentForPairedReplacement(origLines, preserved); + let newLines = restoreIndentForPairedReplacement(origLines, stripped); if (origLines.join("\n") === newLines.join("\n") && origLines.some(l => CONFUSABLE_HYPHENS_RE.test(l))) { newLines = normalizeConfusableHyphensInLines(newLines); } @@ -891,11 +832,7 @@ export function applyHashlineEdits( const origLines = originalFileLines.slice(spec.start.line - 1, spec.start.line - 1 + count); let stripped = stripRangeBoundaryEcho(originalFileLines, spec.start.line, spec.end.line, dstLines); stripped = restoreOldWrappedLines(origLines, stripped); - const preserved = - stripped.length === count - ? preserveWhitespaceOnlyLines(origLines, stripped) - : preserveWhitespaceOnlyLinesLoose(origLines, stripped); - let newLines = restoreIndentForPairedReplacement(origLines, preserved); + let newLines = restoreIndentForPairedReplacement(origLines, stripped); if (origLines.join("\n") === newLines.join("\n") && origLines.some(l => CONFUSABLE_HYPHENS_RE.test(l))) { newLines = normalizeConfusableHyphensInLines(newLines); } diff --git a/packages/coding-agent/test/core/hashline.test.ts b/packages/coding-agent/test/core/hashline.test.ts index 5fd294e90..667b51e83 100644 --- a/packages/coding-agent/test/core/hashline.test.ts +++ b/packages/coding-agent/test/core/hashline.test.ts @@ -411,9 +411,8 @@ describe("applyHashlineEdits — heuristics", () => { expect(result.content).toBe(content); }); - test("preserves whitespace for unchanged lines even when range counts mismatch", () => { + test("does not override model whitespace choices in replacement content", () => { const content = ["import { foo } from 'x';", "import { bar } from 'y';", "const x = 1;"].join("\n"); - const edits: HashlineEdit[] = [ { range: { @@ -423,10 +422,10 @@ describe("applyHashlineEdits — heuristics", () => { }, }, ]; - const result = applyHashlineEdits(content, edits); const outLines = result.content.split("\n"); - expect(outLines[0]).toBe("import { foo } from 'x';"); + // Model's whitespace choice is respected -- no longer overridden + expect(outLines[0]).toBe("import {foo} from 'x';"); expect(outLines[1]).toBe("import { bar } from 'y';"); expect(outLines[2]).toBe("// added"); expect(outLines[3]).toBe("const x = 1;");