From cc579d9bfc5c4a19cc0b10a2fe842324edca42eb Mon Sep 17 00:00:00 2001 From: Rod Boev Date: Wed, 10 Jun 2026 19:35:19 -0400 Subject: [PATCH] Respect Python indentation significance in Apply diff pipeline --- core/diff/streamDiff.ts | 2 + core/diff/util.ts | 8 ++- core/edit/lazy/deterministic-python.vitest.ts | 50 +++++++++++++++++++ core/edit/lazy/streamLazyApply.ts | 2 +- core/edit/streamDiffLines.ts | 2 +- 5 files changed, 61 insertions(+), 3 deletions(-) create mode 100644 core/edit/lazy/deterministic-python.vitest.ts diff --git a/core/diff/streamDiff.ts b/core/diff/streamDiff.ts index 6f89116794c..d8095d351d3 100644 --- a/core/diff/streamDiff.ts +++ b/core/diff/streamDiff.ts @@ -14,6 +14,7 @@ import { LineStream, matchLine } from "./util.js"; export async function* streamDiff( oldLines: string[], newLines: LineStream, + filename?: string, ): AsyncGenerator { const oldLinesCopy = [...oldLines]; @@ -27,6 +28,7 @@ export async function* streamDiff( newLineResult.value, oldLinesCopy, seenIndentationMistake, + filename, ); if (!seenIndentationMistake && newLineResult.value !== newLine) { diff --git a/core/diff/util.ts b/core/diff/util.ts index 571ede7b977..a8b688e8b40 100644 --- a/core/diff/util.ts +++ b/core/diff/util.ts @@ -49,6 +49,7 @@ export function matchLine( newLine: string, oldLines: string[], permissiveAboutIndentation = false, + filename?: string, ): MatchLineResult { // Only match empty lines if it's the next one: if (newLine.trim() === "" && oldLines[0]?.trim() === "") { @@ -77,9 +78,14 @@ export function matchLine( } if (linesMatch(newLineTrimmed, oldLineTrimmed, i)) { // This is a way to fix indentation, but only for sufficiently long lines to avoid matching whitespace or short lines + // For Python files, indentation is syntax and should never be permissive + const isPythonFile = filename?.endsWith(".py"); + const shouldBePermissive = + !isPythonFile && + (permissiveAboutIndentation || newLine.trim().length > 8); if ( newLineTrimmed.trimStart() === oldLineTrimmed.trimStart() && - (permissiveAboutIndentation || newLine.trim().length > 8) + shouldBePermissive ) { return { matchIndex: i, diff --git a/core/edit/lazy/deterministic-python.vitest.ts b/core/edit/lazy/deterministic-python.vitest.ts new file mode 100644 index 00000000000..217e4202970 --- /dev/null +++ b/core/edit/lazy/deterministic-python.vitest.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from "vitest"; + +import { DiffLine } from "../.."; +import { streamDiff } from "../../diff/streamDiff"; + +async function* toLineStream( + lines: string[], +): AsyncGenerator { + for (const line of lines) { + yield line; + } +} + +async function collectStreamDiff( + oldText: string, + newText: string, + filename: string, +): Promise { + const oldLines = oldText.split("\n"); + const newLines = toLineStream(newText.split("\n")); + const diffs: DiffLine[] = []; + for await (const line of streamDiff(oldLines, newLines, filename)) { + diffs.push(line); + } + return diffs; +} + +describe("streamDiff python indentation awareness", () => { + test("treats indentation changes as real diffs for .py files", async () => { + const oldText = 'def hello():\n print("world")'; + const newText = 'def hello():\n print("world")'; + + const diffs = await collectStreamDiff(oldText, newText, "example.py"); + + const newLines = diffs.filter((d) => d.type === "new").map((d) => d.line); + const oldLines = diffs.filter((d) => d.type === "old").map((d) => d.line); + expect(newLines).toContain(' print("world")'); + expect(oldLines).toContain(' print("world")'); + }); + + test("still treats indentation as cosmetic for non-python files", async () => { + const oldText = 'function hello() {\n console.log("world");\n}'; + const newText = 'function hello() {\n console.log("world");\n}'; + + const diffs = await collectStreamDiff(oldText, newText, "example.js"); + + const allSame = diffs.every((d) => d.type === "same"); + expect(allSame).toBe(true); + }); +}); diff --git a/core/edit/lazy/streamLazyApply.ts b/core/edit/lazy/streamLazyApply.ts index f806e956f4c..2641ccc025c 100644 --- a/core/edit/lazy/streamLazyApply.ts +++ b/core/edit/lazy/streamLazyApply.ts @@ -62,7 +62,7 @@ export async function* streamLazyApply( // Convert output to diff const oldLines = oldCode.split(/\r?\n/); - let diffLines = streamDiff(oldLines, lines); + let diffLines = streamDiff(oldLines, lines, filename); diffLines = filterLeadingAndTrailingNewLineInsertion(diffLines); for await (const diffLine of diffLines) { yield diffLine; diff --git a/core/edit/streamDiffLines.ts b/core/edit/streamDiffLines.ts index 50f26128d50..01ab5761ace 100644 --- a/core/edit/streamDiffLines.ts +++ b/core/edit/streamDiffLines.ts @@ -176,7 +176,7 @@ export async function* streamDiffLines( lines = filterEnglishLinesAtEnd(lines); } - let diffLines = streamDiff(oldLines, lines); + let diffLines = streamDiff(oldLines, lines, options.fileUri); diffLines = filterLeadingAndTrailingNewLineInsertion(diffLines); if (highlighted.length === 0) { const line = prefix.split("\n").slice(-1)[0];