mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 04:11:10 -05:00
Handle pure insertions and deletions with diffs
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Roo Cline Changelog
|
# Roo Cline Changelog
|
||||||
|
|
||||||
|
## [2.2.12]
|
||||||
|
|
||||||
|
- Better support for pure deletion and insertion diffs
|
||||||
|
|
||||||
## [2.2.11]
|
## [2.2.11]
|
||||||
|
|
||||||
- Added settings checkbox for verbose diff debugging
|
- Added settings checkbox for verbose diff debugging
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.11",
|
"version": "2.2.12",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "roo-cline",
|
"name": "roo-cline",
|
||||||
"version": "2.2.11",
|
"version": "2.2.12",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
"@anthropic-ai/bedrock-sdk": "^0.10.2",
|
||||||
"@anthropic-ai/sdk": "^0.26.0",
|
"@anthropic-ai/sdk": "^0.26.0",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"displayName": "Roo Cline",
|
"displayName": "Roo Cline",
|
||||||
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
|
||||||
"publisher": "RooVeterinaryInc",
|
"publisher": "RooVeterinaryInc",
|
||||||
"version": "2.2.11",
|
"version": "2.2.12",
|
||||||
"icon": "assets/icons/rocket.png",
|
"icon": "assets/icons/rocket.png",
|
||||||
"galleryBanner": {
|
"galleryBanner": {
|
||||||
"color": "#617A91",
|
"color": "#617A91",
|
||||||
|
|||||||
@@ -711,6 +711,212 @@ this.init();
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('insertion/deletion', () => {
|
||||||
|
let strategy: SearchReplaceDiffStrategy
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
strategy = new SearchReplaceDiffStrategy()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('deletion', () => {
|
||||||
|
it('should delete code when replace block is empty', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
console.log("hello");
|
||||||
|
// Comment to remove
|
||||||
|
console.log("world");
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Comment to remove
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
console.log("hello");
|
||||||
|
console.log("world");
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should delete multiple lines when replace block is empty', () => {
|
||||||
|
const originalContent = `class Example {
|
||||||
|
constructor() {
|
||||||
|
// Initialize
|
||||||
|
this.value = 0;
|
||||||
|
// Set defaults
|
||||||
|
this.name = "";
|
||||||
|
// End init
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Initialize
|
||||||
|
this.value = 0;
|
||||||
|
// Set defaults
|
||||||
|
this.name = "";
|
||||||
|
// End init
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`class Example {
|
||||||
|
constructor() {
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve indentation when deleting nested code', () => {
|
||||||
|
const originalContent = `function outer() {
|
||||||
|
if (true) {
|
||||||
|
// Remove this
|
||||||
|
console.log("test");
|
||||||
|
// And this
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
// Remove this
|
||||||
|
console.log("test");
|
||||||
|
// And this
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function outer() {
|
||||||
|
if (true) {
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('insertion', () => {
|
||||||
|
it('should insert code at specified line when search block is empty', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
const x = 1;
|
||||||
|
return x;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("Adding log");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 2, 2)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
console.log("Adding log");
|
||||||
|
const x = 1;
|
||||||
|
return x;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should preserve indentation when inserting at nested location', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
if (true) {
|
||||||
|
const x = 1;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("Before");
|
||||||
|
console.log("After");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 3, 3)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
if (true) {
|
||||||
|
console.log("Before");
|
||||||
|
console.log("After");
|
||||||
|
const x = 1;
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle insertion at start of file', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
// Copyright 2024
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 1, 1)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`// Copyright 2024
|
||||||
|
// License: MIT
|
||||||
|
|
||||||
|
function test() {
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle insertion at end of file', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
|
||||||
|
// End of file
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent, 4, 4)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`function test() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// End of file`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should insert at the start of the file if no start_line is provided for insertion', () => {
|
||||||
|
const originalContent = `function test() {
|
||||||
|
return true;
|
||||||
|
}`
|
||||||
|
const diffContent = `test.ts
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
console.log("test");
|
||||||
|
>>>>>>> REPLACE`
|
||||||
|
|
||||||
|
const result = strategy.applyDiff(originalContent, diffContent)
|
||||||
|
expect(result.success).toBe(true)
|
||||||
|
if (result.success) {
|
||||||
|
expect(result.content).toBe(`console.log("test");
|
||||||
|
function test() {
|
||||||
|
return true;
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe('fuzzy matching', () => {
|
describe('fuzzy matching', () => {
|
||||||
let strategy: SearchReplaceDiffStrategy
|
let strategy: SearchReplaceDiffStrategy
|
||||||
|
|
||||||
@@ -1241,8 +1447,8 @@ function two() {
|
|||||||
|
|
||||||
it('should document start_line and end_line parameters', () => {
|
it('should document start_line and end_line parameters', () => {
|
||||||
const description = strategy.getToolDescription('/test')
|
const description = strategy.getToolDescription('/test')
|
||||||
expect(description).toContain('start_line: (required) The line number where the search block starts.')
|
expect(description).toContain('start_line: (required) The line number where the search block starts (inclusive).')
|
||||||
expect(description).toContain('end_line: (required) The line number where the search block ends.')
|
expect(description).toContain('end_line: (required) The line number where the search block ends (inclusive).')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ function levenshteinDistance(a: string, b: string): number {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSimilarity(original: string, search: string): number {
|
function getSimilarity(original: string, search: string): number {
|
||||||
|
if (original === '' || search === '') {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Normalize strings by removing extra whitespace but preserve case
|
// Normalize strings by removing extra whitespace but preserve case
|
||||||
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
const normalizeStr = (str: string) => str.replace(/\s+/g, ' ').trim();
|
||||||
|
|
||||||
@@ -71,8 +75,8 @@ If you're not confident in the exact content to search for, use the read_file to
|
|||||||
Parameters:
|
Parameters:
|
||||||
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
- path: (required) The path of the file to modify (relative to the current working directory ${cwd})
|
||||||
- diff: (required) The search/replace block defining the changes.
|
- diff: (required) The search/replace block defining the changes.
|
||||||
- start_line: (required) The line number where the search block starts.
|
- start_line: (required) The line number where the search block starts (inclusive).
|
||||||
- end_line: (required) The line number where the search block ends.
|
- end_line: (required) The line number where the search block ends (inclusive).
|
||||||
|
|
||||||
Diff format:
|
Diff format:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
@@ -94,35 +98,84 @@ Original file:
|
|||||||
5 | return total
|
5 | return total
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Search/Replace content:
|
1. Search/replace a specific chunk of code:
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
<apply_diff>
|
||||||
|
<path>File path here</path>
|
||||||
|
<diff>
|
||||||
<<<<<<< SEARCH
|
<<<<<<< SEARCH
|
||||||
def calculate_total(items):
|
|
||||||
total = 0
|
total = 0
|
||||||
for item in items:
|
for item in items:
|
||||||
total += item
|
total += item
|
||||||
return total
|
return total
|
||||||
=======
|
=======
|
||||||
def calculate_total(items):
|
|
||||||
"""Calculate total with 10% markup"""
|
"""Calculate total with 10% markup"""
|
||||||
return sum(item * 1.1 for item in items)
|
return sum(item * 1.1 for item in items)
|
||||||
>>>>>>> REPLACE
|
>>>>>>> REPLACE
|
||||||
|
</diff>
|
||||||
|
<start_line>2</start_line>
|
||||||
|
<end_line>5</end_line>
|
||||||
|
</apply_diff>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
Usage:
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
2 | """Calculate total with 10% markup"""
|
||||||
|
3 | return sum(item * 1.1 for item in items)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
2. Insert code at a specific line (start_line and end_line must be the same, and the content gets inserted before whatever is currently at that line):
|
||||||
|
\`\`\`
|
||||||
<apply_diff>
|
<apply_diff>
|
||||||
<path>File path here</path>
|
<path>File path here</path>
|
||||||
<diff>
|
<diff>
|
||||||
Your search/replace content here
|
<<<<<<< SEARCH
|
||||||
|
=======
|
||||||
|
"""TODO: Write a test for this"""
|
||||||
|
>>>>>>> REPLACE
|
||||||
</diff>
|
</diff>
|
||||||
<start_line>1</start_line>
|
<start_line>2</start_line>
|
||||||
|
<end_line>2</end_line>
|
||||||
|
</apply_diff>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
2 | """TODO: Write a test for this"""
|
||||||
|
3 | """Calculate total with 10% markup"""
|
||||||
|
4 | return sum(item * 1.1 for item in items)
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
3. Delete code at a specific line range:
|
||||||
|
\`\`\`
|
||||||
|
<apply_diff>
|
||||||
|
<path>File path here</path>
|
||||||
|
<diff>
|
||||||
|
<<<<<<< SEARCH
|
||||||
|
total = 0
|
||||||
|
for item in items:
|
||||||
|
total += item
|
||||||
|
return total
|
||||||
|
=======
|
||||||
|
>>>>>>> REPLACE
|
||||||
|
</diff>
|
||||||
|
<start_line>2</start_line>
|
||||||
<end_line>5</end_line>
|
<end_line>5</end_line>
|
||||||
</apply_diff>`
|
</apply_diff>
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
Result:
|
||||||
|
\`\`\`
|
||||||
|
1 | def calculate_total(items):
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
|
||||||
// Extract the search and replace blocks
|
// Extract the search and replace blocks
|
||||||
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
|
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
|
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include both SEARCH and REPLACE sections with correct markers` : '';
|
||||||
|
|
||||||
@@ -133,7 +186,7 @@ Your search/replace content here
|
|||||||
}
|
}
|
||||||
|
|
||||||
let [_, searchContent, replaceContent] = match;
|
let [_, searchContent, replaceContent] = match;
|
||||||
|
|
||||||
// Detect line ending from original content
|
// Detect line ending from original content
|
||||||
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
|
||||||
|
|
||||||
@@ -145,7 +198,7 @@ Your search/replace content here
|
|||||||
|
|
||||||
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
|
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
|
||||||
const stripLineNumbers = (content: string) => {
|
const stripLineNumbers = (content: string) => {
|
||||||
return content.replace(/^\d+\s+\|(?!\|)/gm, '')
|
return content.replace(/^\d+\s+\|(?!\|)/gm, '');
|
||||||
};
|
};
|
||||||
|
|
||||||
searchContent = stripLineNumbers(searchContent);
|
searchContent = stripLineNumbers(searchContent);
|
||||||
@@ -153,8 +206,8 @@ Your search/replace content here
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Split content into lines, handling both \n and \r\n
|
// Split content into lines, handling both \n and \r\n
|
||||||
const searchLines = searchContent.split(/\r?\n/);
|
const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
|
||||||
const replaceLines = replaceContent.split(/\r?\n/);
|
const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
|
||||||
const originalLines = originalContent.split(/\r?\n/);
|
const originalLines = originalContent.split(/\r?\n/);
|
||||||
|
|
||||||
// First try exact line range if provided
|
// First try exact line range if provided
|
||||||
@@ -167,9 +220,15 @@ Your search/replace content here
|
|||||||
const exactStartIndex = startLine - 1;
|
const exactStartIndex = startLine - 1;
|
||||||
const exactEndIndex = endLine - 1;
|
const exactEndIndex = endLine - 1;
|
||||||
|
|
||||||
if (exactStartIndex < 0 || exactEndIndex >= originalLines.length || exactStartIndex > exactEndIndex) {
|
if (exactStartIndex < 0 || exactEndIndex > originalLines.length || exactStartIndex > exactEndIndex) {
|
||||||
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
|
const debugInfo = this.debugEnabled ? `\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}` : '';
|
||||||
|
|
||||||
|
// Log detailed debug information
|
||||||
|
console.log('Invalid Line Range Debug:', {
|
||||||
|
requestedRange: { start: startLine, end: endLine },
|
||||||
|
fileBounds: { start: 1, end: originalLines.length }
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
|
error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)${debugInfo}`,
|
||||||
@@ -263,7 +322,7 @@ Your search/replace content here
|
|||||||
// Apply the replacement while preserving exact indentation
|
// Apply the replacement while preserving exact indentation
|
||||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||||
// Get the matched line's exact indentation
|
// Get the matched line's exact indentation
|
||||||
const matchedIndent = originalIndents[0];
|
const matchedIndent = originalIndents[0] || '';
|
||||||
|
|
||||||
// Get the current line's indentation relative to the search content
|
// Get the current line's indentation relative to the search content
|
||||||
const currentIndentMatch = line.match(/^[\t ]*/);
|
const currentIndentMatch = line.match(/^[\t ]*/);
|
||||||
|
|||||||
Reference in New Issue
Block a user