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 ]*/);