diff --git a/CHANGELOG.md b/CHANGELOG.md
index eeba972..f7db7df 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# Roo Cline Changelog
+## [2.2.12]
+
+- Better support for pure deletion and insertion diffs
+
## [2.2.11]
- Added settings checkbox for verbose diff debugging
diff --git a/package-lock.json b/package-lock.json
index 4e5c9e4..6ea5438 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "roo-cline",
- "version": "2.2.11",
+ "version": "2.2.12",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "roo-cline",
- "version": "2.2.11",
+ "version": "2.2.12",
"dependencies": {
"@anthropic-ai/bedrock-sdk": "^0.10.2",
"@anthropic-ai/sdk": "^0.26.0",
diff --git a/package.json b/package.json
index c0dd669..2f76cca 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"displayName": "Roo Cline",
"description": "A fork of Cline, an autonomous coding agent, with some added experimental configuration and automation features.",
"publisher": "RooVeterinaryInc",
- "version": "2.2.11",
+ "version": "2.2.12",
"icon": "assets/icons/rocket.png",
"galleryBanner": {
"color": "#617A91",
diff --git a/src/core/diff/strategies/__tests__/search-replace.test.ts b/src/core/diff/strategies/__tests__/search-replace.test.ts
index 8e48680..99aff99 100644
--- a/src/core/diff/strategies/__tests__/search-replace.test.ts
+++ b/src/core/diff/strategies/__tests__/search-replace.test.ts
@@ -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', () => {
let strategy: SearchReplaceDiffStrategy
@@ -1241,8 +1447,8 @@ function two() {
it('should document start_line and end_line parameters', () => {
const description = strategy.getToolDescription('/test')
- expect(description).toContain('start_line: (required) The line number where the search block starts.')
- expect(description).toContain('end_line: (required) The line number where the search block ends.')
+ 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 (inclusive).')
})
})
})
diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts
index 2fbfe5b..660d364 100644
--- a/src/core/diff/strategies/search-replace.ts
+++ b/src/core/diff/strategies/search-replace.ts
@@ -33,6 +33,10 @@ function levenshteinDistance(a: string, b: string): number {
}
function getSimilarity(original: string, search: string): number {
+ if (original === '' || search === '') {
+ return 1;
+ }
+
// Normalize strings by removing extra whitespace but preserve case
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:
- 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.
-- start_line: (required) The line number where the search block starts.
-- end_line: (required) The line number where the search block ends.
+- start_line: (required) The line number where the search block starts (inclusive).
+- end_line: (required) The line number where the search block ends (inclusive).
Diff format:
\`\`\`
@@ -94,35 +98,84 @@ Original file:
5 | return total
\`\`\`
-Search/Replace content:
+1. Search/replace a specific chunk of code:
\`\`\`
+
+File path here
+
<<<<<<< SEARCH
-def calculate_total(items):
total = 0
for item in items:
total += item
return total
=======
-def calculate_total(items):
"""Calculate total with 10% markup"""
return sum(item * 1.1 for item in items)
>>>>>>> REPLACE
+
+2
+5
+
\`\`\`
-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):
+\`\`\`
File path here
-Your search/replace content here
+<<<<<<< SEARCH
+=======
+ """TODO: Write a test for this"""
+>>>>>>> REPLACE
-1
+2
+2
+
+\`\`\`
+
+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:
+\`\`\`
+
+File path here
+
+<<<<<<< SEARCH
+ total = 0
+ for item in items:
+ total += item
+ return total
+=======
+>>>>>>> REPLACE
+
+2
5
-`
+
+\`\`\`
+
+Result:
+\`\`\`
+1 | def calculate_total(items):
+\`\`\`
+`
}
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): DiffResult {
// 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) {
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;
-
+
// Detect line ending from original content
const lineEnding = originalContent.includes('\r\n') ? '\r\n' : '\n';
@@ -145,7 +198,7 @@ Your search/replace content here
if (hasLineNumbers(searchContent) && hasLineNumbers(replaceContent)) {
const stripLineNumbers = (content: string) => {
- return content.replace(/^\d+\s+\|(?!\|)/gm, '')
+ return content.replace(/^\d+\s+\|(?!\|)/gm, '');
};
searchContent = stripLineNumbers(searchContent);
@@ -153,8 +206,8 @@ Your search/replace content here
}
// Split content into lines, handling both \n and \r\n
- const searchLines = searchContent.split(/\r?\n/);
- const replaceLines = replaceContent.split(/\r?\n/);
+ const searchLines = searchContent === '' ? [] : searchContent.split(/\r?\n/);
+ const replaceLines = replaceContent === '' ? [] : replaceContent.split(/\r?\n/);
const originalLines = originalContent.split(/\r?\n/);
// First try exact line range if provided
@@ -167,9 +220,15 @@ Your search/replace content here
const exactStartIndex = startLine - 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}` : '';
+ // Log detailed debug information
+ console.log('Invalid Line Range Debug:', {
+ requestedRange: { start: startLine, end: endLine },
+ fileBounds: { start: 1, end: originalLines.length }
+ });
+
return {
success: false,
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
const indentedReplaceLines = replaceLines.map((line, i) => {
// 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
const currentIndentMatch = line.match(/^[\t ]*/);