Handle pure insertions and deletions with diffs

This commit is contained in:
Matt Rubens
2024-12-16 11:43:08 -05:00
parent 47568336ad
commit 905c68dd9e
5 changed files with 290 additions and 21 deletions

View File

@@ -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
View File

@@ -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",

View File

@@ -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",

View File

@@ -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).')
}) })
}) })
}) })

View File

@@ -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` : '';
@@ -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 ]*/);