From e83c80741d7896af3e092d851325bb4a4c8a298f Mon Sep 17 00:00:00 2001 From: Roo Code Date: Mon, 22 Sep 2025 20:50:36 +0000 Subject: [PATCH 1/2] fix: preserve trailing newlines in stripLineNumbers for apply_diff - Modified stripLineNumbers function to preserve trailing newlines - This fixes issue #8020 where line numbers at the last line were not being stripped correctly - Added comprehensive test cases for trailing newline preservation - All existing tests pass without regression --- ...ti-search-replace-trailing-newline.spec.ts | 163 ++++++++++++++++++ src/integrations/misc/extract-text.ts | 11 +- 2 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/core/diff/strategies/__tests__/multi-search-replace-trailing-newline.spec.ts diff --git a/src/core/diff/strategies/__tests__/multi-search-replace-trailing-newline.spec.ts b/src/core/diff/strategies/__tests__/multi-search-replace-trailing-newline.spec.ts new file mode 100644 index 00000000000..95512193941 --- /dev/null +++ b/src/core/diff/strategies/__tests__/multi-search-replace-trailing-newline.spec.ts @@ -0,0 +1,163 @@ +import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace" + +describe("MultiSearchReplaceDiffStrategy - trailing newline preservation", () => { + let strategy: MultiSearchReplaceDiffStrategy + + beforeEach(() => { + strategy = new MultiSearchReplaceDiffStrategy() + }) + + it("should preserve trailing newlines in SEARCH content with line numbers", async () => { + // This test verifies the fix for issue #8020 + // The regex should not consume trailing newlines, allowing stripLineNumbers to work correctly + const originalContent = `class Example { + constructor() { + this.value = 0; + } +}` + const diffContent = `<<<<<<< SEARCH +1 | class Example { +2 | constructor() { +3 | this.value = 0; +4 | } +5 | } +======= +class Example { + constructor() { + this.value = 1; + } +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`class Example { + constructor() { + this.value = 1; + } +}`) + } + }) + + it("should handle Windows line endings with trailing newlines and line numbers", async () => { + const originalContent = "function test() {\r\n return true;\r\n}\r\n" + const diffContent = `<<<<<<< SEARCH +1 | function test() { +2 | return true; +3 | } +======= +function test() { + return false; +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + // Should preserve Windows line endings + expect(result.content).toBe("function test() {\r\n return false;\r\n}\r\n") + } + }) + + it("should handle multiple search/replace blocks with trailing newlines", async () => { + const originalContent = `function one() { + return 1; +} + +function two() { + return 2; +}` + const diffContent = `<<<<<<< SEARCH +1 | function one() { +2 | return 1; +3 | } +======= +function one() { + return 10; +} +>>>>>>> REPLACE + +<<<<<<< SEARCH +5 | function two() { +6 | return 2; +7 | } +======= +function two() { + return 20; +} +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe(`function one() { + return 10; +} + +function two() { + return 20; +}`) + } + }) + + it("should handle content with line numbers at the last line", async () => { + // This specifically tests the scenario from the bug report + const originalContent = ` List addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10 + : CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList) + + CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList) + + CollectionUtils.size(personIdentityInfoList));` + + const diffContent = `<<<<<<< SEARCH +1476 | List addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10 +1477 | : CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList) +1478 | + CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList) +1479 | + CollectionUtils.size(personIdentityInfoList)); +======= + + // Filter addresses if optimization is enabled + if (isAddressDisplayOptimizeEnabled()) { + homeAddressInfoList = filterAddressesByThreeYearRule(homeAddressInfoList); + personIdentityInfoList = filterAddressesByThreeYearRule(personIdentityInfoList); + idNoAddressInfoList = filterAddressesByThreeYearRule(idNoAddressInfoList); + workAddressInfoList = filterAddressesByThreeYearRule(workAddressInfoList); + } + + List addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10 + : CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList) + + CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList) + + CollectionUtils.size(personIdentityInfoList)); +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toContain("// Filter addresses if optimization is enabled") + expect(result.content).toContain("if (isAddressDisplayOptimizeEnabled())") + // Verify the last line doesn't have line numbers + expect(result.content).not.toContain("1488 |") + expect(result.content).not.toContain("1479 |") + } + }) + + it("should correctly strip line numbers even when last line has no trailing newline", async () => { + const originalContent = "line 1\nline 2\nline 3" // No trailing newline + const diffContent = `<<<<<<< SEARCH +1 | line 1 +2 | line 2 +3 | line 3 +======= +line 1 +modified line 2 +line 3 +>>>>>>> REPLACE` + + const result = await strategy.applyDiff(originalContent, diffContent) + expect(result.success).toBe(true) + if (result.success) { + expect(result.content).toBe("line 1\nmodified line 2\nline 3") + // Verify no line numbers remain + expect(result.content).not.toContain(" | ") + } + }) +}) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index 8231c609be7..b0a620a3276 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -163,7 +163,16 @@ export function stripLineNumbers(content: string, aggressive: boolean = false): // Join back with original line endings (carriage return (\r) + line feed (\n) or just line feed (\n)) const lineEnding = content.includes("\r\n") ? "\r\n" : "\n" - return processedLines.join(lineEnding) + let result = processedLines.join(lineEnding) + + // Preserve trailing newline if original content had one + if (content.endsWith("\n") || content.endsWith("\r\n")) { + if (!result.endsWith("\n") && !result.endsWith("\r\n")) { + result += lineEnding + } + } + + return result } /** From cfc7da8b3404b45e8f240d8d358d08a7b25c80d0 Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Fri, 24 Oct 2025 10:06:27 -0400 Subject: [PATCH 2/2] PR feedback --- src/integrations/misc/extract-text.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/integrations/misc/extract-text.ts b/src/integrations/misc/extract-text.ts index b0a620a3276..bafa7a5bab1 100644 --- a/src/integrations/misc/extract-text.ts +++ b/src/integrations/misc/extract-text.ts @@ -165,9 +165,9 @@ export function stripLineNumbers(content: string, aggressive: boolean = false): const lineEnding = content.includes("\r\n") ? "\r\n" : "\n" let result = processedLines.join(lineEnding) - // Preserve trailing newline if original content had one - if (content.endsWith("\n") || content.endsWith("\r\n")) { - if (!result.endsWith("\n") && !result.endsWith("\r\n")) { + // Preserve trailing newline if present in original content + if (content.endsWith(lineEnding)) { + if (!result.endsWith(lineEnding)) { result += lineEnding } }