Improvements to search/replace diff

This commit is contained in:
Matt Rubens
2024-12-14 20:29:52 -05:00
parent a3d10dec8e
commit 2292372e42
6 changed files with 383 additions and 54 deletions

View File

@@ -56,7 +56,7 @@ export class SearchReplaceDiffStrategy implements DiffStrategy {
getToolDescription(cwd: string): string {
return `## apply_diff
Description: Request to replace existing code using search and replace blocks.
Description: Request to replace existing code using a search and replace block.
This tool allows for precise, surgical replaces to files by specifying exactly what content to search for and what to replace it with.
The tool will maintain proper indentation and formatting while making changes.
Only a single operation is allowed per tool use.
@@ -65,33 +65,32 @@ 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 blocks defining the changes.
- 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.
Format:
1. First line must be the file path
2. Followed by search/replace blocks:
\`\`\`
<<<<<<< SEARCH
[exact content to find including whitespace]
=======
[new content to replace with]
>>>>>>> REPLACE
\`\`\`
Diff format:
\`\`\`
<<<<<<< SEARCH
[exact content to find including whitespace]
=======
[new content to replace with]
>>>>>>> REPLACE
\`\`\`
Example:
Original file:
\`\`\`
def calculate_total(items):
total = 0
for item in items:
total += item
return total
1 | def calculate_total(items):
2 | total = 0
3 | for item in items:
4 | total += item
5 | return total
\`\`\`
Search/Replace content:
\`\`\`
main.py
<<<<<<< SEARCH
def calculate_total(items):
total = 0
@@ -111,10 +110,12 @@ Usage:
<diff>
Your search/replace content here
</diff>
<start_line>1</start_line>
<end_line>5</end_line>
</apply_diff>`
}
applyDiff(originalContent: string, diffContent: string): string | false {
applyDiff(originalContent: string, diffContent: string, startLine?: number, endLine?: number): string | false {
// Extract the search and replace blocks
const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n=======\n([\s\S]*?)\n>>>>>>> REPLACE/);
if (!match) {
@@ -131,11 +132,25 @@ Your search/replace content here
const replaceLines = replaceContent.split(/\r?\n/);
const originalLines = originalContent.split(/\r?\n/);
// Determine search range based on provided line numbers
let searchStartIndex = 0;
let searchEndIndex = originalLines.length;
if (startLine !== undefined || endLine !== undefined) {
// Convert to 0-based index and add buffer
if (startLine !== undefined) {
searchStartIndex = Math.max(0, startLine - 6);
}
if (endLine !== undefined) {
searchEndIndex = Math.min(originalLines.length, endLine + 5);
}
}
// Find the search content in the original using fuzzy matching
let matchIndex = -1;
let bestMatchScore = 0;
for (let i = 0; i <= originalLines.length - searchLines.length; i++) {
for (let i = searchStartIndex; i <= searchEndIndex - searchLines.length; i++) {
// Join the lines and calculate overall similarity
const originalChunk = originalLines.slice(i, i + searchLines.length).join('\n');
const searchChunk = searchLines.join('\n');
@@ -169,40 +184,19 @@ Your search/replace content here
// Apply the replacement while preserving exact indentation
const indentedReplaceLines = replaceLines.map((line, i) => {
// Get the corresponding original and search indentations
const originalIndent = originalIndents[Math.min(i, originalIndents.length - 1)];
const searchIndent = searchIndents[Math.min(i, searchIndents.length - 1)];
// Get the matched line's exact indentation
const matchedIndent = originalIndents[0];
// Get the current line's indentation
// Get the current line's indentation relative to the search content
const currentIndentMatch = line.match(/^[\t ]*/);
const currentIndent = currentIndentMatch ? currentIndentMatch[0] : '';
const searchBaseIndent = searchIndents[0] || '';
// Get the corresponding search line's indentation
const searchLineIndex = Math.min(i, searchLines.length - 1);
const searchLineIndent = searchIndents[searchLineIndex];
// Get the corresponding original line's indentation
const originalLineIndex = Math.min(i, originalIndents.length - 1);
const originalLineIndent = originalIndents[originalLineIndex];
// If this line has the same indentation as its corresponding search line,
// use the original indentation
if (currentIndent === searchLineIndent) {
return originalLineIndent + line.trim();
}
// Otherwise, preserve the original indentation structure
const indentChar = originalLineIndent.charAt(0) || '\t';
const indentLevel = Math.floor(originalLineIndent.length / indentChar.length);
// Calculate the relative indentation from the search line
const searchLevel = Math.floor(searchLineIndent.length / indentChar.length);
const currentLevel = Math.floor(currentIndent.length / indentChar.length);
const relativeLevel = currentLevel - searchLevel;
// Apply the relative indentation to the original level
const targetLevel = Math.max(0, indentLevel + relativeLevel);
return indentChar.repeat(targetLevel) + line.trim();
// Calculate the relative indentation from the search content
const relativeIndent = currentIndent.slice(searchBaseIndent.length);
// Apply the matched indentation plus any relative indentation
return matchedIndent + relativeIndent + line.trim();
});
// Construct the final content