Skip to content

Commit e0bef89

Browse files
committed
Add a search-and-replace diff strategy
1 parent 1d47fa6 commit e0bef89

File tree

7 files changed

+295
-7
lines changed

7 files changed

+295
-7
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Roo Cline Changelog
22

3+
## [2.1.17]
4+
5+
- Switch to search/replace diffs in experimental diff editing mode
6+
7+
## [2.1.16]
8+
9+
- Allow copying prompts from the history screen
10+
311
## [2.1.15]
412

513
- Incorporate dbasclpy's [PR](https://github.com/RooVetGit/Roo-Cline/pull/54) to add support for gemini-exp-1206

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ A fork of Cline, an autonomous coding agent, with some added experimental config
99
- Support for OpenRouter compression
1010
- Support for editing through diffs (very experimental)
1111
- Support for gemini-exp-1206
12+
- Support for copying prompts from the history screen
1213

1314
Here's an example of Roo-Cline autonomously creating a snake game with "Always approve write operations" and "Always approve browser actions" turned on:
1415

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"displayName": "Roo Cline",
44
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
55
"publisher": "RooVeterinaryInc",
6-
"version": "2.1.16",
6+
"version": "2.1.17",
77
"icon": "assets/icons/rocket.png",
88
"galleryBanner": {
99
"color": "#617A91",

src/core/diff/DiffStrategy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import type { DiffStrategy } from './types'
22
import { UnifiedDiffStrategy } from './strategies/unified'
3-
3+
import { SearchReplaceDiffStrategy } from './strategies/search-replace'
44
/**
55
* Get the appropriate diff strategy for the given model
66
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
77
* @returns The appropriate diff strategy for the model
88
*/
99
export function getDiffStrategy(model: string): DiffStrategy {
10-
// For now, return UnifiedDiffStrategy for all models
10+
// For now, return SearchReplaceDiffStrategy for all models
1111
// This architecture allows for future optimizations based on model capabilities
12-
return new UnifiedDiffStrategy()
12+
return new SearchReplaceDiffStrategy()
1313
}
1414

1515
export type { DiffStrategy }
16-
export { UnifiedDiffStrategy }
16+
export { UnifiedDiffStrategy, SearchReplaceDiffStrategy }
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { SearchReplaceDiffStrategy } from '../search-replace'
2+
3+
describe('SearchReplaceDiffStrategy', () => {
4+
let strategy: SearchReplaceDiffStrategy
5+
6+
beforeEach(() => {
7+
strategy = new SearchReplaceDiffStrategy()
8+
})
9+
10+
describe('applyDiff', () => {
11+
it('should replace matching content', () => {
12+
const originalContent = `function hello() {
13+
console.log("hello")
14+
}
15+
`
16+
const diffContent = `test.ts
17+
<<<<<<< SEARCH
18+
function hello() {
19+
console.log("hello")
20+
}
21+
=======
22+
function hello() {
23+
console.log("hello world")
24+
}
25+
>>>>>>> REPLACE`
26+
27+
const result = strategy.applyDiff(originalContent, diffContent)
28+
expect(result).toBe(`function hello() {
29+
console.log("hello world")
30+
}
31+
`)
32+
})
33+
34+
it('should handle extra whitespace in search/replace blocks', () => {
35+
const originalContent = `function test() {
36+
return true;
37+
}
38+
`
39+
const diffContent = `test.ts
40+
<<<<<<< SEARCH
41+
42+
function test() {
43+
return true;
44+
}
45+
46+
=======
47+
function test() {
48+
return false;
49+
}
50+
>>>>>>> REPLACE`
51+
52+
const result = strategy.applyDiff(originalContent, diffContent)
53+
expect(result).toBe(`function test() {
54+
return false;
55+
}
56+
`)
57+
})
58+
59+
it('should match content with different surrounding whitespace', () => {
60+
const originalContent = `
61+
function example() {
62+
return 42;
63+
}
64+
65+
`
66+
const diffContent = `test.ts
67+
<<<<<<< SEARCH
68+
function example() {
69+
return 42;
70+
}
71+
=======
72+
function example() {
73+
return 43;
74+
}
75+
>>>>>>> REPLACE`
76+
77+
const result = strategy.applyDiff(originalContent, diffContent)
78+
expect(result).toBe(`
79+
function example() {
80+
return 43;
81+
}
82+
83+
`)
84+
})
85+
86+
it('should return false if search content does not match', () => {
87+
const originalContent = `function hello() {
88+
console.log("hello")
89+
}
90+
`
91+
const diffContent = `test.ts
92+
<<<<<<< SEARCH
93+
function hello() {
94+
console.log("wrong")
95+
}
96+
=======
97+
function hello() {
98+
console.log("hello world")
99+
}
100+
>>>>>>> REPLACE`
101+
102+
const result = strategy.applyDiff(originalContent, diffContent)
103+
expect(result).toBe(false)
104+
})
105+
106+
it('should return false if diff format is invalid', () => {
107+
const originalContent = `function hello() {
108+
console.log("hello")
109+
}
110+
`
111+
const diffContent = `test.ts
112+
Invalid diff format`
113+
114+
const result = strategy.applyDiff(originalContent, diffContent)
115+
expect(result).toBe(false)
116+
})
117+
118+
it('should handle multiple lines with proper indentation', () => {
119+
const originalContent = `class Example {
120+
constructor() {
121+
this.value = 0
122+
}
123+
124+
getValue() {
125+
return this.value
126+
}
127+
}
128+
`
129+
const diffContent = `test.ts
130+
<<<<<<< SEARCH
131+
getValue() {
132+
return this.value
133+
}
134+
=======
135+
getValue() {
136+
// Add logging
137+
console.log("Getting value")
138+
return this.value
139+
}
140+
>>>>>>> REPLACE`
141+
142+
const result = strategy.applyDiff(originalContent, diffContent)
143+
expect(result).toBe(`class Example {
144+
constructor() {
145+
this.value = 0
146+
}
147+
148+
getValue() {
149+
// Add logging
150+
console.log("Getting value")
151+
return this.value
152+
}
153+
}
154+
`)
155+
})
156+
157+
it('should preserve whitespace exactly in the output', () => {
158+
const originalContent = " indented\n more indented\n back\n"
159+
const diffContent = `test.ts
160+
<<<<<<< SEARCH
161+
indented
162+
more indented
163+
back
164+
=======
165+
modified
166+
still indented
167+
end
168+
>>>>>>> REPLACE`
169+
170+
const result = strategy.applyDiff(originalContent, diffContent)
171+
expect(result).toBe(" modified\n still indented\n end\n")
172+
})
173+
})
174+
175+
describe('getToolDescription', () => {
176+
it('should include the current working directory', () => {
177+
const cwd = '/test/dir'
178+
const description = strategy.getToolDescription(cwd)
179+
expect(description).toContain(`relative to the current working directory ${cwd}`)
180+
})
181+
182+
it('should include required format elements', () => {
183+
const description = strategy.getToolDescription('/test')
184+
expect(description).toContain('<<<<<<< SEARCH')
185+
expect(description).toContain('=======')
186+
expect(description).toContain('>>>>>>> REPLACE')
187+
expect(description).toContain('<apply_diff>')
188+
expect(description).toContain('</apply_diff>')
189+
})
190+
})
191+
})
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { DiffStrategy } from "../types"
2+
3+
export class SearchReplaceDiffStrategy implements DiffStrategy {
4+
getToolDescription(cwd: string): string {
5+
return `## apply_diff
6+
Description: Request to replace existing code using search and replace blocks.
7+
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
8+
Only use this tool when you need to replace/fix existing code.
9+
The tool will maintain proper indentation and formatting while making changes.
10+
Only a single operation is allowed per tool use.
11+
The SEARCH section must exactly match existing content including whitespace and indentation.
12+
13+
Parameters:
14+
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
15+
- diff: (required) The search/replace blocks defining the changes.
16+
17+
Format:
18+
1. First line must be the file path
19+
2. Followed by search/replace blocks:
20+
\`\`\`
21+
<<<<<<< SEARCH
22+
[exact content to find including whitespace]
23+
=======
24+
[new content to replace with]
25+
>>>>>>> REPLACE
26+
\`\`\`
27+
28+
Example:
29+
30+
Original file:
31+
\`\`\`
32+
def calculate_total(items):
33+
total = 0
34+
for item in items:
35+
total += item
36+
return total
37+
\`\`\`
38+
39+
Search/Replace content:
40+
\`\`\`
41+
main.py
42+
<<<<<<< SEARCH
43+
def calculate_total(items):
44+
total = 0
45+
for item in items:
46+
total += item
47+
return total
48+
=======
49+
def calculate_total(items):
50+
"""Calculate total with 10% markup"""
51+
return sum(item * 1.1 for item in items)
52+
>>>>>>> REPLACE
53+
\`\`\`
54+
55+
Usage:
56+
<apply_diff>
57+
<path>File path here</path>
58+
<diff>
59+
Your search/replace content here
60+
</diff>
61+
</apply_diff>`
62+
}
63+
64+
applyDiff(originalContent: string, diffContent: string): string | false {
65+
// Extract the search and replace blocks
66+
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
67+
if (!match) {
68+
return false;
69+
}
70+
71+
const [_, searchContent, replaceContent] = match;
72+
73+
// Trim both search and replace content
74+
const trimmedSearch = searchContent.trim();
75+
const trimmedReplace = replaceContent.trim();
76+
77+
// Trim the original content for comparison
78+
const trimmedOriginal = originalContent.trim();
79+
80+
// Verify the search content exists in the trimmed original
81+
if (!trimmedOriginal.includes(trimmedSearch)) {
82+
return false;
83+
}
84+
85+
// Replace the content, maintaining original whitespace
86+
return originalContent.replace(trimmedSearch, trimmedReplace);
87+
}
88+
}

0 commit comments

Comments
 (0)