mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 20:31:37 -05:00
351 lines
10 KiB
TypeScript
351 lines
10 KiB
TypeScript
import { Diff, Hunk, Change } from "./types"
|
|
import { findBestMatch, prepareSearchString } from "./search-strategies"
|
|
import { applyEdit } from "./edit-strategies"
|
|
import { DiffResult, DiffStrategy } from "../../types"
|
|
|
|
export class NewUnifiedDiffStrategy implements DiffStrategy {
|
|
private readonly confidenceThreshold: number
|
|
|
|
constructor(confidenceThreshold: number = 1) {
|
|
this.confidenceThreshold = Math.max(confidenceThreshold, 0.8)
|
|
}
|
|
|
|
private parseUnifiedDiff(diff: string): Diff {
|
|
const MAX_CONTEXT_LINES = 6 // Number of context lines to keep before/after changes
|
|
const lines = diff.split("\n")
|
|
const hunks: Hunk[] = []
|
|
let currentHunk: Hunk | null = null
|
|
|
|
let i = 0
|
|
while (i < lines.length && !lines[i].startsWith("@@")) {
|
|
i++
|
|
}
|
|
|
|
for (; i < lines.length; i++) {
|
|
const line = lines[i]
|
|
|
|
if (line.startsWith("@@")) {
|
|
if (
|
|
currentHunk &&
|
|
currentHunk.changes.length > 0 &&
|
|
currentHunk.changes.some((change) => change.type === "add" || change.type === "remove")
|
|
) {
|
|
const changes = currentHunk.changes
|
|
let startIdx = 0
|
|
let endIdx = changes.length - 1
|
|
|
|
for (let j = 0; j < changes.length; j++) {
|
|
if (changes[j].type !== "context") {
|
|
startIdx = Math.max(0, j - MAX_CONTEXT_LINES)
|
|
break
|
|
}
|
|
}
|
|
|
|
for (let j = changes.length - 1; j >= 0; j--) {
|
|
if (changes[j].type !== "context") {
|
|
endIdx = Math.min(changes.length - 1, j + MAX_CONTEXT_LINES)
|
|
break
|
|
}
|
|
}
|
|
|
|
currentHunk.changes = changes.slice(startIdx, endIdx + 1)
|
|
hunks.push(currentHunk)
|
|
}
|
|
currentHunk = { changes: [] }
|
|
continue
|
|
}
|
|
|
|
if (!currentHunk) {
|
|
continue
|
|
}
|
|
|
|
const content = line.slice(1)
|
|
const indentMatch = content.match(/^(\s*)/)
|
|
const indent = indentMatch ? indentMatch[0] : ""
|
|
const trimmedContent = content.slice(indent.length)
|
|
|
|
if (line.startsWith(" ")) {
|
|
currentHunk.changes.push({
|
|
type: "context",
|
|
content: trimmedContent,
|
|
indent,
|
|
originalLine: content,
|
|
})
|
|
} else if (line.startsWith("+")) {
|
|
currentHunk.changes.push({
|
|
type: "add",
|
|
content: trimmedContent,
|
|
indent,
|
|
originalLine: content,
|
|
})
|
|
} else if (line.startsWith("-")) {
|
|
currentHunk.changes.push({
|
|
type: "remove",
|
|
content: trimmedContent,
|
|
indent,
|
|
originalLine: content,
|
|
})
|
|
} else {
|
|
const finalContent = trimmedContent ? " " + trimmedContent : " "
|
|
currentHunk.changes.push({
|
|
type: "context",
|
|
content: finalContent,
|
|
indent,
|
|
originalLine: content,
|
|
})
|
|
}
|
|
}
|
|
|
|
if (
|
|
currentHunk &&
|
|
currentHunk.changes.length > 0 &&
|
|
currentHunk.changes.some((change) => change.type === "add" || change.type === "remove")
|
|
) {
|
|
hunks.push(currentHunk)
|
|
}
|
|
|
|
return { hunks }
|
|
}
|
|
|
|
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
|
|
return `# apply_diff Tool - Generate Precise Code Changes
|
|
|
|
Generate a unified diff that can be cleanly applied to modify code files.
|
|
|
|
## Step-by-Step Instructions:
|
|
|
|
1. Start with file headers:
|
|
- First line: "--- {original_file_path}"
|
|
- Second line: "+++ {new_file_path}"
|
|
|
|
2. For each change section:
|
|
- Begin with "@@ ... @@" separator line without line numbers
|
|
- Include 2-3 lines of context before and after changes
|
|
- Mark removed lines with "-"
|
|
- Mark added lines with "+"
|
|
- Preserve exact indentation
|
|
|
|
3. Group related changes:
|
|
- Keep related modifications in the same hunk
|
|
- Start new hunks for logically separate changes
|
|
- When modifying functions/methods, include the entire block
|
|
|
|
## Requirements:
|
|
|
|
1. MUST include exact indentation
|
|
2. MUST include sufficient context for unique matching
|
|
3. MUST group related changes together
|
|
4. MUST use proper unified diff format
|
|
5. MUST NOT include timestamps in file headers
|
|
6. MUST NOT include line numbers in the @@ header
|
|
|
|
## Examples:
|
|
|
|
✅ Good diff (follows all requirements):
|
|
\`\`\`diff
|
|
--- src/utils.ts
|
|
+++ src/utils.ts
|
|
@@ ... @@
|
|
def calculate_total(items):
|
|
- total = 0
|
|
- for item in items:
|
|
- total += item.price
|
|
+ return sum(item.price for item in items)
|
|
\`\`\`
|
|
|
|
❌ Bad diff (violates requirements #1 and #2):
|
|
\`\`\`diff
|
|
--- src/utils.ts
|
|
+++ src/utils.ts
|
|
@@ ... @@
|
|
-total = 0
|
|
-for item in items:
|
|
+return sum(item.price for item in items)
|
|
\`\`\`
|
|
|
|
Parameters:
|
|
- path: (required) File path relative to ${args.cwd}
|
|
- diff: (required) Unified diff content in unified format to apply to the file.
|
|
|
|
Usage:
|
|
<apply_diff>
|
|
<path>path/to/file.ext</path>
|
|
<diff>
|
|
Your diff here
|
|
</diff>
|
|
</apply_diff>`
|
|
}
|
|
|
|
// Helper function to split a hunk into smaller hunks based on contiguous changes
|
|
private splitHunk(hunk: Hunk): Hunk[] {
|
|
const result: Hunk[] = []
|
|
let currentHunk: Hunk | null = null
|
|
let contextBefore: Change[] = []
|
|
let contextAfter: Change[] = []
|
|
const MAX_CONTEXT_LINES = 3 // Keep 3 lines of context before/after changes
|
|
|
|
for (let i = 0; i < hunk.changes.length; i++) {
|
|
const change = hunk.changes[i]
|
|
|
|
if (change.type === "context") {
|
|
if (!currentHunk) {
|
|
contextBefore.push(change)
|
|
if (contextBefore.length > MAX_CONTEXT_LINES) {
|
|
contextBefore.shift()
|
|
}
|
|
} else {
|
|
contextAfter.push(change)
|
|
if (contextAfter.length > MAX_CONTEXT_LINES) {
|
|
// We've collected enough context after changes, create a new hunk
|
|
currentHunk.changes.push(...contextAfter)
|
|
result.push(currentHunk)
|
|
currentHunk = null
|
|
// Keep the last few context lines for the next hunk
|
|
contextBefore = contextAfter
|
|
contextAfter = []
|
|
}
|
|
}
|
|
} else {
|
|
if (!currentHunk) {
|
|
currentHunk = { changes: [...contextBefore] }
|
|
contextAfter = []
|
|
} else if (contextAfter.length > 0) {
|
|
// Add accumulated context to current hunk
|
|
currentHunk.changes.push(...contextAfter)
|
|
contextAfter = []
|
|
}
|
|
currentHunk.changes.push(change)
|
|
}
|
|
}
|
|
|
|
// Add any remaining changes
|
|
if (currentHunk) {
|
|
if (contextAfter.length > 0) {
|
|
currentHunk.changes.push(...contextAfter)
|
|
}
|
|
result.push(currentHunk)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
async applyDiff(
|
|
originalContent: string,
|
|
diffContent: string,
|
|
startLine?: number,
|
|
endLine?: number
|
|
): Promise<DiffResult> {
|
|
const parsedDiff = this.parseUnifiedDiff(diffContent)
|
|
const originalLines = originalContent.split("\n")
|
|
let result = [...originalLines]
|
|
|
|
if (!parsedDiff.hunks.length) {
|
|
return {
|
|
success: false,
|
|
error: "No hunks found in diff. Please ensure your diff includes actual changes and follows the unified diff format.",
|
|
}
|
|
}
|
|
|
|
for (const hunk of parsedDiff.hunks) {
|
|
const contextStr = prepareSearchString(hunk.changes)
|
|
const {
|
|
index: matchPosition,
|
|
confidence,
|
|
strategy,
|
|
} = findBestMatch(contextStr, result, 0, this.confidenceThreshold)
|
|
|
|
if (confidence < this.confidenceThreshold) {
|
|
console.log("Full hunk application failed, trying sub-hunks strategy")
|
|
// Try splitting the hunk into smaller hunks
|
|
const subHunks = this.splitHunk(hunk)
|
|
let subHunkSuccess = true
|
|
let subHunkResult = [...result]
|
|
|
|
for (const subHunk of subHunks) {
|
|
const subContextStr = prepareSearchString(subHunk.changes)
|
|
const subSearchResult = findBestMatch(subContextStr, subHunkResult, 0, this.confidenceThreshold)
|
|
|
|
if (subSearchResult.confidence >= this.confidenceThreshold) {
|
|
const subEditResult = await applyEdit(
|
|
subHunk,
|
|
subHunkResult,
|
|
subSearchResult.index,
|
|
subSearchResult.confidence,
|
|
this.confidenceThreshold
|
|
)
|
|
if (subEditResult.confidence >= this.confidenceThreshold) {
|
|
subHunkResult = subEditResult.result
|
|
continue
|
|
}
|
|
}
|
|
subHunkSuccess = false
|
|
break
|
|
}
|
|
|
|
if (subHunkSuccess) {
|
|
result = subHunkResult
|
|
continue
|
|
}
|
|
|
|
// If sub-hunks also failed, return the original error
|
|
const contextLines = hunk.changes.filter((c) => c.type === "context").length
|
|
const totalLines = hunk.changes.length
|
|
const contextRatio = contextLines / totalLines
|
|
|
|
let errorMsg = `Failed to find a matching location in the file (${Math.floor(
|
|
confidence * 100
|
|
)}% confidence, needs ${Math.floor(this.confidenceThreshold * 100)}%)\n\n`
|
|
errorMsg += "Debug Info:\n"
|
|
errorMsg += `- Search Strategy Used: ${strategy}\n`
|
|
errorMsg += `- Context Lines: ${contextLines} out of ${totalLines} total lines (${Math.floor(
|
|
contextRatio * 100
|
|
)}%)\n`
|
|
errorMsg += `- Attempted to split into ${subHunks.length} sub-hunks but still failed\n`
|
|
|
|
if (contextRatio < 0.2) {
|
|
errorMsg += "\nPossible Issues:\n"
|
|
errorMsg += "- Not enough context lines to uniquely identify the location\n"
|
|
errorMsg += "- Add a few more lines of unchanged code around your changes\n"
|
|
} else if (contextRatio > 0.5) {
|
|
errorMsg += "\nPossible Issues:\n"
|
|
errorMsg += "- Too many context lines may reduce search accuracy\n"
|
|
errorMsg += "- Try to keep only 2-3 lines of context before and after changes\n"
|
|
} else {
|
|
errorMsg += "\nPossible Issues:\n"
|
|
errorMsg += "- The diff may be targeting a different version of the file\n"
|
|
errorMsg +=
|
|
"- There may be too many changes in a single hunk, try splitting the changes into multiple hunks\n"
|
|
}
|
|
|
|
if (startLine && endLine) {
|
|
errorMsg += `\nSearch Range: lines ${startLine}-${endLine}\n`
|
|
}
|
|
|
|
return { success: false, error: errorMsg }
|
|
}
|
|
|
|
const editResult = await applyEdit(hunk, result, matchPosition, confidence, this.confidenceThreshold)
|
|
if (editResult.confidence >= this.confidenceThreshold) {
|
|
result = editResult.result
|
|
} else {
|
|
// Edit failure - likely due to content mismatch
|
|
let errorMsg = `Failed to apply the edit using ${editResult.strategy} strategy (${Math.floor(
|
|
editResult.confidence * 100
|
|
)}% confidence)\n\n`
|
|
errorMsg += "Debug Info:\n"
|
|
errorMsg += "- The location was found but the content didn't match exactly\n"
|
|
errorMsg += "- This usually means the file has been modified since the diff was created\n"
|
|
errorMsg += "- Or the diff may be targeting a different version of the file\n"
|
|
errorMsg += "\nPossible Solutions:\n"
|
|
errorMsg += "1. Refresh your view of the file and create a new diff\n"
|
|
errorMsg += "2. Double-check that the removed lines (-) match the current file content\n"
|
|
errorMsg += "3. Ensure your diff targets the correct version of the file"
|
|
|
|
return { success: false, error: errorMsg }
|
|
}
|
|
}
|
|
|
|
return { success: true, content: result.join("\n") }
|
|
}
|
|
}
|