mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Merge pull request #1234 from qdaxb/support_edit_multi_locations_in_apply_diff
Supports updating multiple locations of a file in one call of the apply_diff tool
This commit is contained in:
commit
620c39cd68
9 changed files with 2007 additions and 19 deletions
|
|
@ -184,7 +184,10 @@ export class Cline {
|
|||
this.checkpointStorage = checkpointStorage
|
||||
|
||||
// Initialize diffStrategy based on current state
|
||||
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
|
||||
this.updateDiffStrategy(
|
||||
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
|
||||
)
|
||||
|
||||
if (startTask) {
|
||||
if (task || images) {
|
||||
|
|
@ -254,13 +257,23 @@ export class Cline {
|
|||
}
|
||||
|
||||
// Add method to update diffStrategy
|
||||
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
|
||||
async updateDiffStrategy(experimentalDiffStrategy?: boolean, multiSearchReplaceDiffStrategy?: boolean) {
|
||||
// If not provided, get from current state
|
||||
if (experimentalDiffStrategy === undefined) {
|
||||
if (experimentalDiffStrategy === undefined || multiSearchReplaceDiffStrategy === undefined) {
|
||||
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
|
||||
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
|
||||
if (experimentalDiffStrategy === undefined) {
|
||||
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
|
||||
}
|
||||
if (multiSearchReplaceDiffStrategy === undefined) {
|
||||
multiSearchReplaceDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] ?? false
|
||||
}
|
||||
}
|
||||
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
|
||||
this.diffStrategy = getDiffStrategy(
|
||||
this.api.getModel().id,
|
||||
this.fuzzyMatchThreshold,
|
||||
experimentalDiffStrategy,
|
||||
multiSearchReplaceDiffStrategy,
|
||||
)
|
||||
}
|
||||
|
||||
// Storing task to disk for history
|
||||
|
|
@ -1667,17 +1680,36 @@ export class Cline {
|
|||
success: false,
|
||||
error: "No diff strategy available",
|
||||
}
|
||||
let partResults = ""
|
||||
|
||||
if (!diffResult.success) {
|
||||
this.consecutiveMistakeCount++
|
||||
const currentCount =
|
||||
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
|
||||
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
|
||||
const errorDetails = diffResult.details
|
||||
? JSON.stringify(diffResult.details, null, 2)
|
||||
: ""
|
||||
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||
diffResult.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
let formattedError = ""
|
||||
if (diffResult.failParts && diffResult.failParts.length > 0) {
|
||||
for (const failPart of diffResult.failParts) {
|
||||
if (failPart.success) {
|
||||
continue
|
||||
}
|
||||
const errorDetails = failPart.details
|
||||
? JSON.stringify(failPart.details, null, 2)
|
||||
: ""
|
||||
formattedError = `<error_details>\n${
|
||||
failPart.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
partResults += formattedError
|
||||
}
|
||||
} else {
|
||||
const errorDetails = diffResult.details
|
||||
? JSON.stringify(diffResult.details, null, 2)
|
||||
: ""
|
||||
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
|
||||
diffResult.error
|
||||
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
|
||||
}
|
||||
|
||||
if (currentCount >= 2) {
|
||||
await this.say("error", formattedError)
|
||||
}
|
||||
|
|
@ -1707,6 +1739,10 @@ export class Cline {
|
|||
const { newProblemsMessage, userEdits, finalContent } =
|
||||
await this.diffViewProvider.saveChanges()
|
||||
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
|
||||
let partFailHint = ""
|
||||
if (diffResult.failParts && diffResult.failParts.length > 0) {
|
||||
partFailHint = `But unable to apply all diff parts to file: ${absolutePath}, silently use <read_file> tool to check newest file version and re-apply diffs\n`
|
||||
}
|
||||
if (userEdits) {
|
||||
await this.say(
|
||||
"user_feedback_diff",
|
||||
|
|
@ -1718,6 +1754,7 @@ export class Cline {
|
|||
)
|
||||
pushToolResult(
|
||||
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
|
||||
partFailHint +
|
||||
`The updated content, which includes both your original modifications and the user's edits, has been successfully saved to ${relPath.toPosix()}. Here is the full, updated content of the file, including line numbers:\n\n` +
|
||||
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
|
||||
finalContent || "",
|
||||
|
|
@ -1730,7 +1767,8 @@ export class Cline {
|
|||
)
|
||||
} else {
|
||||
pushToolResult(
|
||||
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
|
||||
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` +
|
||||
partFailHint,
|
||||
)
|
||||
}
|
||||
await this.diffViewProvider.reset()
|
||||
|
|
|
|||
|
|
@ -375,7 +375,7 @@ describe("Cline", () => {
|
|||
|
||||
expect(cline.diffEnabled).toBe(true)
|
||||
expect(cline.diffStrategy).toBeDefined()
|
||||
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false)
|
||||
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false, false)
|
||||
|
||||
getDiffStrategySpy.mockRestore()
|
||||
|
||||
|
|
@ -396,7 +396,7 @@ describe("Cline", () => {
|
|||
|
||||
expect(cline.diffEnabled).toBe(true)
|
||||
expect(cline.diffStrategy).toBeDefined()
|
||||
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false)
|
||||
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false, false)
|
||||
|
||||
getDiffStrategySpy.mockRestore()
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import type { DiffStrategy } from "./types"
|
|||
import { UnifiedDiffStrategy } from "./strategies/unified"
|
||||
import { SearchReplaceDiffStrategy } from "./strategies/search-replace"
|
||||
import { NewUnifiedDiffStrategy } from "./strategies/new-unified"
|
||||
import { MultiSearchReplaceDiffStrategy } from "./strategies/multi-search-replace"
|
||||
/**
|
||||
* Get the appropriate diff strategy for the given model
|
||||
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
|
||||
|
|
@ -11,11 +12,17 @@ export function getDiffStrategy(
|
|||
model: string,
|
||||
fuzzyMatchThreshold?: number,
|
||||
experimentalDiffStrategy: boolean = false,
|
||||
multiSearchReplaceDiffStrategy: boolean = false,
|
||||
): DiffStrategy {
|
||||
if (experimentalDiffStrategy) {
|
||||
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
|
||||
if (multiSearchReplaceDiffStrategy) {
|
||||
return new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
} else {
|
||||
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
export type { DiffStrategy }
|
||||
|
|
|
|||
1566
src/core/diff/strategies/__tests__/multi-search-replace.test.ts
Normal file
1566
src/core/diff/strategies/__tests__/multi-search-replace.test.ts
Normal file
File diff suppressed because it is too large
Load diff
365
src/core/diff/strategies/multi-search-replace.ts
Normal file
365
src/core/diff/strategies/multi-search-replace.ts
Normal file
|
|
@ -0,0 +1,365 @@
|
|||
import { DiffStrategy, DiffResult } from "../types"
|
||||
import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text"
|
||||
import { distance } from "fastest-levenshtein"
|
||||
|
||||
const BUFFER_LINES = 40 // Number of extra context lines to show before and after matches
|
||||
|
||||
function getSimilarity(original: string, search: string): number {
|
||||
if (search === "") {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Normalize strings by removing extra whitespace but preserve case
|
||||
const normalizeStr = (str: string) => str.replace(/\s+/g, " ").trim()
|
||||
|
||||
const normalizedOriginal = normalizeStr(original)
|
||||
const normalizedSearch = normalizeStr(search)
|
||||
|
||||
if (normalizedOriginal === normalizedSearch) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Calculate Levenshtein distance using fastest-levenshtein's distance function
|
||||
const dist = distance(normalizedOriginal, normalizedSearch)
|
||||
|
||||
// Calculate similarity ratio (0 to 1, where 1 is an exact match)
|
||||
const maxLength = Math.max(normalizedOriginal.length, normalizedSearch.length)
|
||||
return 1 - dist / maxLength
|
||||
}
|
||||
|
||||
export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
|
||||
private fuzzyThreshold: number
|
||||
private bufferLines: number
|
||||
|
||||
constructor(fuzzyThreshold?: number, bufferLines?: number) {
|
||||
// Use provided threshold or default to exact matching (1.0)
|
||||
// Note: fuzzyThreshold is inverted in UI (0% = 1.0, 10% = 0.9)
|
||||
// so we use it directly here
|
||||
this.fuzzyThreshold = fuzzyThreshold ?? 1.0
|
||||
this.bufferLines = bufferLines ?? BUFFER_LINES
|
||||
}
|
||||
|
||||
getToolDescription(args: { cwd: string; toolOptions?: { [key: string]: string } }): string {
|
||||
return `## apply_diff
|
||||
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.
|
||||
The SEARCH section must exactly match existing content including whitespace and indentation.
|
||||
If you're not confident in the exact content to search for, use the read_file tool first to get the exact content.
|
||||
When applying the diffs, be extra careful to remember to change any closing brackets or other syntax that may be affected by the diff farther down in the file.
|
||||
ALWAYS make as many changes in a single 'apply_diff' request as possible using multiple SEARCH/REPLACE blocks
|
||||
|
||||
Parameters:
|
||||
- path: (required) The path of the file to modify (relative to the current working directory ${args.cwd})
|
||||
- diff: (required) The search/replace block defining the changes.
|
||||
|
||||
Diff format:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
:start_line: (required) The line number of original content where the search block starts.
|
||||
:end_line: (required) The line number of original content where the search block ends.
|
||||
-------
|
||||
[exact content to find including whitespace]
|
||||
=======
|
||||
[new content to replace with]
|
||||
>>>>>>> REPLACE
|
||||
|
||||
\`\`\`
|
||||
|
||||
Example:
|
||||
|
||||
Original file:
|
||||
\`\`\`
|
||||
1 | def calculate_total(items):
|
||||
2 | total = 0
|
||||
3 | for item in items:
|
||||
4 | total += item
|
||||
5 | return total
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
:end_line:5
|
||||
-------
|
||||
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
|
||||
|
||||
\`\`\`
|
||||
|
||||
Search/Replace content with multi edits:
|
||||
\`\`\`
|
||||
<<<<<<< SEARCH
|
||||
:start_line:1
|
||||
:end_line:2
|
||||
-------
|
||||
def calculate_sum(items):
|
||||
sum = 0
|
||||
=======
|
||||
def calculate_sum(items):
|
||||
sum = 0
|
||||
>>>>>>> REPLACE
|
||||
|
||||
<<<<<<< SEARCH
|
||||
:start_line:4
|
||||
:end_line:5
|
||||
-------
|
||||
total += item
|
||||
return total
|
||||
=======
|
||||
sum += item
|
||||
return sum
|
||||
>>>>>>> REPLACE
|
||||
\`\`\`
|
||||
|
||||
Usage:
|
||||
<apply_diff>
|
||||
<path>File path here</path>
|
||||
<diff>
|
||||
Your search/replace content here
|
||||
You can use multi search/replace block in one diff block, but make sure to include the line numbers for each block.
|
||||
Only use a single line of '=======' between search and replacement content, because multiple '=======' will corrupt the file.
|
||||
</diff>
|
||||
</apply_diff>`
|
||||
}
|
||||
|
||||
async applyDiff(
|
||||
originalContent: string,
|
||||
diffContent: string,
|
||||
_paramStartLine?: number,
|
||||
_paramEndLine?: number,
|
||||
): Promise<DiffResult> {
|
||||
let matches = [
|
||||
...diffContent.matchAll(
|
||||
/<<<<<<< SEARCH\n(:start_line:\s*(\d+)\n){0,1}(:end_line:\s*(\d+)\n){0,1}(-------\n){0,1}([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/g,
|
||||
),
|
||||
]
|
||||
|
||||
if (matches.length === 0) {
|
||||
return {
|
||||
success: false,
|
||||
error: `Invalid diff format - missing required sections\n\nDebug Info:\n- Expected Format: <<<<<<< SEARCH\\n:start_line: start line\\n:end_line: end line\\n-------\\n[search content]\\n=======\\n[replace content]\\n>>>>>>> REPLACE\n- Tip: Make sure to include start_line/end_line/SEARCH/REPLACE sections with correct markers`,
|
||||
}
|
||||
}
|
||||
// Detect line ending from original content
|
||||
const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n"
|
||||
let resultLines = originalContent.split(/\r?\n/)
|
||||
let delta = 0
|
||||
let diffResults: DiffResult[] = []
|
||||
let appliedCount = 0
|
||||
const replacements = matches
|
||||
.map((match) => ({
|
||||
startLine: Number(match[2] ?? 0),
|
||||
endLine: Number(match[4] ?? resultLines.length),
|
||||
searchContent: match[6],
|
||||
replaceContent: match[7],
|
||||
}))
|
||||
.sort((a, b) => a.startLine - b.startLine)
|
||||
|
||||
for (let { searchContent, replaceContent, startLine, endLine } of replacements) {
|
||||
startLine += startLine === 0 ? 0 : delta
|
||||
endLine += delta
|
||||
|
||||
// Strip line numbers from search and replace content if every line starts with a line number
|
||||
if (everyLineHasLineNumbers(searchContent) && everyLineHasLineNumbers(replaceContent)) {
|
||||
searchContent = stripLineNumbers(searchContent)
|
||||
replaceContent = stripLineNumbers(replaceContent)
|
||||
}
|
||||
|
||||
// Split content into lines, handling both \n and \r\n
|
||||
const searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
|
||||
const replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
|
||||
|
||||
// Validate that empty search requires start line
|
||||
if (searchLines.length === 0 && !startLine) {
|
||||
diffResults.push({
|
||||
success: false,
|
||||
error: `Empty search content requires start_line to be specified\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, specify the line number where content should be inserted`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate that empty search requires same start and end line
|
||||
if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) {
|
||||
diffResults.push({
|
||||
success: false,
|
||||
error: `Empty search content requires start_line and end_line to be the same (got ${startLine}-${endLine})\n\nDebug Info:\n- Empty search content is only valid for insertions at a specific line\n- For insertions, use the same line number for both start_line and end_line`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Initialize search variables
|
||||
let matchIndex = -1
|
||||
let bestMatchScore = 0
|
||||
let bestMatchContent = ""
|
||||
const searchChunk = searchLines.join("\n")
|
||||
|
||||
// Determine search bounds
|
||||
let searchStartIndex = 0
|
||||
let searchEndIndex = resultLines.length
|
||||
|
||||
// Validate and handle line range if provided
|
||||
if (startLine && endLine) {
|
||||
// Convert to 0-based index
|
||||
const exactStartIndex = startLine - 1
|
||||
const exactEndIndex = endLine - 1
|
||||
|
||||
if (exactStartIndex < 0 || exactEndIndex > resultLines.length || exactStartIndex > exactEndIndex) {
|
||||
diffResults.push({
|
||||
success: false,
|
||||
error: `Line range ${startLine}-${endLine} is invalid (file has ${resultLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${resultLines.length}`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Try exact match first
|
||||
const originalChunk = resultLines.slice(exactStartIndex, exactEndIndex + 1).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity >= this.fuzzyThreshold) {
|
||||
matchIndex = exactStartIndex
|
||||
bestMatchScore = similarity
|
||||
bestMatchContent = originalChunk
|
||||
} else {
|
||||
// Set bounds for buffered search
|
||||
searchStartIndex = Math.max(0, startLine - (this.bufferLines + 1))
|
||||
searchEndIndex = Math.min(resultLines.length, endLine + this.bufferLines)
|
||||
}
|
||||
}
|
||||
|
||||
// If no match found yet, try middle-out search within bounds
|
||||
if (matchIndex === -1) {
|
||||
const midPoint = Math.floor((searchStartIndex + searchEndIndex) / 2)
|
||||
let leftIndex = midPoint
|
||||
let rightIndex = midPoint + 1
|
||||
|
||||
// Search outward from the middle within bounds
|
||||
while (leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines.length) {
|
||||
// Check left side if still in range
|
||||
if (leftIndex >= searchStartIndex) {
|
||||
const originalChunk = resultLines.slice(leftIndex, leftIndex + searchLines.length).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity
|
||||
matchIndex = leftIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
leftIndex--
|
||||
}
|
||||
|
||||
// Check right side if still in range
|
||||
if (rightIndex <= searchEndIndex - searchLines.length) {
|
||||
const originalChunk = resultLines.slice(rightIndex, rightIndex + searchLines.length).join("\n")
|
||||
const similarity = getSimilarity(originalChunk, searchChunk)
|
||||
if (similarity > bestMatchScore) {
|
||||
bestMatchScore = similarity
|
||||
matchIndex = rightIndex
|
||||
bestMatchContent = originalChunk
|
||||
}
|
||||
rightIndex++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Require similarity to meet threshold
|
||||
if (matchIndex === -1 || bestMatchScore < this.fuzzyThreshold) {
|
||||
const searchChunk = searchLines.join("\n")
|
||||
const originalContentSection =
|
||||
startLine !== undefined && endLine !== undefined
|
||||
? `\n\nOriginal Content:\n${addLineNumbers(
|
||||
resultLines
|
||||
.slice(
|
||||
Math.max(0, startLine - 1 - this.bufferLines),
|
||||
Math.min(resultLines.length, endLine + this.bufferLines),
|
||||
)
|
||||
.join("\n"),
|
||||
Math.max(1, startLine - this.bufferLines),
|
||||
)}`
|
||||
: `\n\nOriginal Content:\n${addLineNumbers(resultLines.join("\n"))}`
|
||||
|
||||
const bestMatchSection = bestMatchContent
|
||||
? `\n\nBest Match Found:\n${addLineNumbers(bestMatchContent, matchIndex + 1)}`
|
||||
: `\n\nBest Match Found:\n(no match)`
|
||||
|
||||
const lineRange =
|
||||
startLine || endLine
|
||||
? ` at ${startLine ? `start: ${startLine}` : "start"} to ${endLine ? `end: ${endLine}` : "end"}`
|
||||
: ""
|
||||
|
||||
diffResults.push({
|
||||
success: false,
|
||||
error: `No sufficiently similar match found${lineRange} (${Math.floor(bestMatchScore * 100)}% similar, needs ${Math.floor(this.fuzzyThreshold * 100)}%)\n\nDebug Info:\n- Similarity Score: ${Math.floor(bestMatchScore * 100)}%\n- Required Threshold: ${Math.floor(this.fuzzyThreshold * 100)}%\n- Search Range: ${startLine && endLine ? `lines ${startLine}-${endLine}` : "start to end"}\n- Tip: Use read_file to get the latest content of the file before attempting the diff again, as the file content may have changed\n\nSearch Content:\n${searchChunk}${bestMatchSection}${originalContentSection}`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
// Get the matched lines from the original content
|
||||
const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
|
||||
|
||||
// Get the exact indentation (preserving tabs/spaces) of each line
|
||||
const originalIndents = matchedLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Get the exact indentation of each line in the search block
|
||||
const searchIndents = searchLines.map((line) => {
|
||||
const match = line.match(/^[\t ]*/)
|
||||
return match ? match[0] : ""
|
||||
})
|
||||
|
||||
// Apply the replacement while preserving exact indentation
|
||||
const indentedReplaceLines = replaceLines.map((line, i) => {
|
||||
// Get the matched line's exact indentation
|
||||
const matchedIndent = originalIndents[0] || ""
|
||||
|
||||
// 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] || ""
|
||||
|
||||
// Calculate the relative indentation level
|
||||
const searchBaseLevel = searchBaseIndent.length
|
||||
const currentLevel = currentIndent.length
|
||||
const relativeLevel = currentLevel - searchBaseLevel
|
||||
|
||||
// If relative level is negative, remove indentation from matched indent
|
||||
// If positive, add to matched indent
|
||||
const finalIndent =
|
||||
relativeLevel < 0
|
||||
? matchedIndent.slice(0, Math.max(0, matchedIndent.length + relativeLevel))
|
||||
: matchedIndent + currentIndent.slice(searchBaseLevel)
|
||||
|
||||
return finalIndent + line.trim()
|
||||
})
|
||||
|
||||
// Construct the final content
|
||||
const beforeMatch = resultLines.slice(0, matchIndex)
|
||||
const afterMatch = resultLines.slice(matchIndex + searchLines.length)
|
||||
resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch]
|
||||
delta = delta - matchedLines.length + replaceLines.length
|
||||
appliedCount++
|
||||
}
|
||||
const finalContent = resultLines.join(lineEnding)
|
||||
if (appliedCount === 0) {
|
||||
return {
|
||||
success: false,
|
||||
failParts: diffResults,
|
||||
}
|
||||
}
|
||||
return {
|
||||
success: true,
|
||||
content: finalContent,
|
||||
failParts: diffResults,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,10 +3,10 @@
|
|||
*/
|
||||
|
||||
export type DiffResult =
|
||||
| { success: true; content: string }
|
||||
| {
|
||||
| { success: true; content: string; failParts?: DiffResult[] }
|
||||
| ({
|
||||
success: false
|
||||
error: string
|
||||
error?: string
|
||||
details?: {
|
||||
similarity?: number
|
||||
threshold?: number
|
||||
|
|
@ -14,7 +14,8 @@ export type DiffResult =
|
|||
searchContent?: string
|
||||
bestMatch?: string
|
||||
}
|
||||
}
|
||||
failParts?: DiffResult[]
|
||||
} & ({ error: string } | { failParts: DiffResult[] }))
|
||||
|
||||
export interface DiffStrategy {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1689,6 +1689,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
|
|||
if (message.values[EXPERIMENT_IDS.DIFF_STRATEGY] !== undefined && this.getCurrentCline()) {
|
||||
await this.getCurrentCline()!.updateDiffStrategy(
|
||||
Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.DIFF_STRATEGY),
|
||||
Experiments.isEnabled(updatedExperiments, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ describe("experiments", () => {
|
|||
experimentalDiffStrategy: false,
|
||||
search_and_replace: false,
|
||||
insert_content: false,
|
||||
multi_search_and_replace: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
@ -30,6 +31,7 @@ describe("experiments", () => {
|
|||
experimentalDiffStrategy: false,
|
||||
search_and_replace: false,
|
||||
insert_content: false,
|
||||
multi_search_and_replace: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(true)
|
||||
})
|
||||
|
|
@ -40,6 +42,7 @@ describe("experiments", () => {
|
|||
search_and_replace: false,
|
||||
insert_content: false,
|
||||
powerSteering: false,
|
||||
multi_search_and_replace: false,
|
||||
}
|
||||
expect(Experiments.isEnabled(experiments, EXPERIMENT_IDS.POWER_STEERING)).toBe(false)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ export const EXPERIMENT_IDS = {
|
|||
SEARCH_AND_REPLACE: "search_and_replace",
|
||||
INSERT_BLOCK: "insert_content",
|
||||
POWER_STEERING: "powerSteering",
|
||||
MULTI_SEARCH_AND_REPLACE: "multi_search_and_replace",
|
||||
} as const
|
||||
|
||||
export type ExperimentKey = keyof typeof EXPERIMENT_IDS
|
||||
|
|
@ -42,6 +43,12 @@ export const experimentConfigsMap: Record<ExperimentKey, ExperimentConfig> = {
|
|||
"When enabled, Roo will remind the model about the details of its current mode definition more frequently. This will lead to stronger adherence to role definitions and custom instructions, but will use more tokens per message.",
|
||||
enabled: false,
|
||||
},
|
||||
MULTI_SEARCH_AND_REPLACE: {
|
||||
name: "Use experimental multi block diff tool",
|
||||
description:
|
||||
"When enabled, Roo will use multi block diff tool. This will try to update multiple code blocks in the file in one request.",
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const experimentDefault = Object.fromEntries(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue