Skip to content

Commit ad56791

Browse files
roomote[bot]roomotemrubens
authored
fix: preserve trailing newlines in stripLineNumbers for apply_diff (#8227)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: Matt Rubens <[email protected]>
1 parent 1a9e7ca commit ad56791

File tree

2 files changed

+173
-1
lines changed

2 files changed

+173
-1
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
2+
3+
describe("MultiSearchReplaceDiffStrategy - trailing newline preservation", () => {
4+
let strategy: MultiSearchReplaceDiffStrategy
5+
6+
beforeEach(() => {
7+
strategy = new MultiSearchReplaceDiffStrategy()
8+
})
9+
10+
it("should preserve trailing newlines in SEARCH content with line numbers", async () => {
11+
// This test verifies the fix for issue #8020
12+
// The regex should not consume trailing newlines, allowing stripLineNumbers to work correctly
13+
const originalContent = `class Example {
14+
constructor() {
15+
this.value = 0;
16+
}
17+
}`
18+
const diffContent = `<<<<<<< SEARCH
19+
1 | class Example {
20+
2 | constructor() {
21+
3 | this.value = 0;
22+
4 | }
23+
5 | }
24+
=======
25+
class Example {
26+
constructor() {
27+
this.value = 1;
28+
}
29+
}
30+
>>>>>>> REPLACE`
31+
32+
const result = await strategy.applyDiff(originalContent, diffContent)
33+
expect(result.success).toBe(true)
34+
if (result.success) {
35+
expect(result.content).toBe(`class Example {
36+
constructor() {
37+
this.value = 1;
38+
}
39+
}`)
40+
}
41+
})
42+
43+
it("should handle Windows line endings with trailing newlines and line numbers", async () => {
44+
const originalContent = "function test() {\r\n return true;\r\n}\r\n"
45+
const diffContent = `<<<<<<< SEARCH
46+
1 | function test() {
47+
2 | return true;
48+
3 | }
49+
=======
50+
function test() {
51+
return false;
52+
}
53+
>>>>>>> REPLACE`
54+
55+
const result = await strategy.applyDiff(originalContent, diffContent)
56+
expect(result.success).toBe(true)
57+
if (result.success) {
58+
// Should preserve Windows line endings
59+
expect(result.content).toBe("function test() {\r\n return false;\r\n}\r\n")
60+
}
61+
})
62+
63+
it("should handle multiple search/replace blocks with trailing newlines", async () => {
64+
const originalContent = `function one() {
65+
return 1;
66+
}
67+
68+
function two() {
69+
return 2;
70+
}`
71+
const diffContent = `<<<<<<< SEARCH
72+
1 | function one() {
73+
2 | return 1;
74+
3 | }
75+
=======
76+
function one() {
77+
return 10;
78+
}
79+
>>>>>>> REPLACE
80+
81+
<<<<<<< SEARCH
82+
5 | function two() {
83+
6 | return 2;
84+
7 | }
85+
=======
86+
function two() {
87+
return 20;
88+
}
89+
>>>>>>> REPLACE`
90+
91+
const result = await strategy.applyDiff(originalContent, diffContent)
92+
expect(result.success).toBe(true)
93+
if (result.success) {
94+
expect(result.content).toBe(`function one() {
95+
return 10;
96+
}
97+
98+
function two() {
99+
return 20;
100+
}`)
101+
}
102+
})
103+
104+
it("should handle content with line numbers at the last line", async () => {
105+
// This specifically tests the scenario from the bug report
106+
const originalContent = ` List<ContactInfoItemResp> addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10
107+
: CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList)
108+
+ CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList)
109+
+ CollectionUtils.size(personIdentityInfoList));`
110+
111+
const diffContent = `<<<<<<< SEARCH
112+
1476 | List<ContactInfoItemResp> addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10
113+
1477 | : CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList)
114+
1478 | + CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList)
115+
1479 | + CollectionUtils.size(personIdentityInfoList));
116+
=======
117+
118+
// Filter addresses if optimization is enabled
119+
if (isAddressDisplayOptimizeEnabled()) {
120+
homeAddressInfoList = filterAddressesByThreeYearRule(homeAddressInfoList);
121+
personIdentityInfoList = filterAddressesByThreeYearRule(personIdentityInfoList);
122+
idNoAddressInfoList = filterAddressesByThreeYearRule(idNoAddressInfoList);
123+
workAddressInfoList = filterAddressesByThreeYearRule(workAddressInfoList);
124+
}
125+
126+
List<ContactInfoItemResp> addressInfoList = new ArrayList<>(CollectionUtils.size(repairInfoList) > 10 ? 10
127+
: CollectionUtils.size(repairInfoList) + CollectionUtils.size(homeAddressInfoList)
128+
+ CollectionUtils.size(idNoAddressInfoList) + CollectionUtils.size(workAddressInfoList)
129+
+ CollectionUtils.size(personIdentityInfoList));
130+
>>>>>>> REPLACE`
131+
132+
const result = await strategy.applyDiff(originalContent, diffContent)
133+
expect(result.success).toBe(true)
134+
if (result.success) {
135+
expect(result.content).toContain("// Filter addresses if optimization is enabled")
136+
expect(result.content).toContain("if (isAddressDisplayOptimizeEnabled())")
137+
// Verify the last line doesn't have line numbers
138+
expect(result.content).not.toContain("1488 |")
139+
expect(result.content).not.toContain("1479 |")
140+
}
141+
})
142+
143+
it("should correctly strip line numbers even when last line has no trailing newline", async () => {
144+
const originalContent = "line 1\nline 2\nline 3" // No trailing newline
145+
const diffContent = `<<<<<<< SEARCH
146+
1 | line 1
147+
2 | line 2
148+
3 | line 3
149+
=======
150+
line 1
151+
modified line 2
152+
line 3
153+
>>>>>>> REPLACE`
154+
155+
const result = await strategy.applyDiff(originalContent, diffContent)
156+
expect(result.success).toBe(true)
157+
if (result.success) {
158+
expect(result.content).toBe("line 1\nmodified line 2\nline 3")
159+
// Verify no line numbers remain
160+
expect(result.content).not.toContain(" | ")
161+
}
162+
})
163+
})

src/integrations/misc/extract-text.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,16 @@ export function stripLineNumbers(content: string, aggressive: boolean = false):
163163

164164
// Join back with original line endings (carriage return (\r) + line feed (\n) or just line feed (\n))
165165
const lineEnding = content.includes("\r\n") ? "\r\n" : "\n"
166-
return processedLines.join(lineEnding)
166+
let result = processedLines.join(lineEnding)
167+
168+
// Preserve trailing newline if present in original content
169+
if (content.endsWith(lineEnding)) {
170+
if (!result.endsWith(lineEnding)) {
171+
result += lineEnding
172+
}
173+
}
174+
175+
return result
167176
}
168177

169178
/**

0 commit comments

Comments
 (0)