Skip to content

Commit 620c39c

Browse files
authored
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
2 parents 81b4507 + 8a51a6c commit 620c39c

File tree

9 files changed

+2007
-19
lines changed

9 files changed

+2007
-19
lines changed

src/core/Cline.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,10 @@ export class Cline {
184184
this.checkpointStorage = checkpointStorage
185185

186186
// Initialize diffStrategy based on current state
187-
this.updateDiffStrategy(Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY))
187+
this.updateDiffStrategy(
188+
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY),
189+
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
190+
)
188191

189192
if (startTask) {
190193
if (task || images) {
@@ -254,13 +257,23 @@ export class Cline {
254257
}
255258

256259
// Add method to update diffStrategy
257-
async updateDiffStrategy(experimentalDiffStrategy?: boolean) {
260+
async updateDiffStrategy(experimentalDiffStrategy?: boolean, multiSearchReplaceDiffStrategy?: boolean) {
258261
// If not provided, get from current state
259-
if (experimentalDiffStrategy === undefined) {
262+
if (experimentalDiffStrategy === undefined || multiSearchReplaceDiffStrategy === undefined) {
260263
const { experiments: stateExperimental } = (await this.providerRef.deref()?.getState()) ?? {}
261-
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
264+
if (experimentalDiffStrategy === undefined) {
265+
experimentalDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false
266+
}
267+
if (multiSearchReplaceDiffStrategy === undefined) {
268+
multiSearchReplaceDiffStrategy = stateExperimental?.[EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE] ?? false
269+
}
262270
}
263-
this.diffStrategy = getDiffStrategy(this.api.getModel().id, this.fuzzyMatchThreshold, experimentalDiffStrategy)
271+
this.diffStrategy = getDiffStrategy(
272+
this.api.getModel().id,
273+
this.fuzzyMatchThreshold,
274+
experimentalDiffStrategy,
275+
multiSearchReplaceDiffStrategy,
276+
)
264277
}
265278

266279
// Storing task to disk for history
@@ -1667,17 +1680,36 @@ export class Cline {
16671680
success: false,
16681681
error: "No diff strategy available",
16691682
}
1683+
let partResults = ""
1684+
16701685
if (!diffResult.success) {
16711686
this.consecutiveMistakeCount++
16721687
const currentCount =
16731688
(this.consecutiveMistakeCountForApplyDiff.get(relPath) || 0) + 1
16741689
this.consecutiveMistakeCountForApplyDiff.set(relPath, currentCount)
1675-
const errorDetails = diffResult.details
1676-
? JSON.stringify(diffResult.details, null, 2)
1677-
: ""
1678-
const formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
1679-
diffResult.error
1680-
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1690+
let formattedError = ""
1691+
if (diffResult.failParts && diffResult.failParts.length > 0) {
1692+
for (const failPart of diffResult.failParts) {
1693+
if (failPart.success) {
1694+
continue
1695+
}
1696+
const errorDetails = failPart.details
1697+
? JSON.stringify(failPart.details, null, 2)
1698+
: ""
1699+
formattedError = `<error_details>\n${
1700+
failPart.error
1701+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1702+
partResults += formattedError
1703+
}
1704+
} else {
1705+
const errorDetails = diffResult.details
1706+
? JSON.stringify(diffResult.details, null, 2)
1707+
: ""
1708+
formattedError = `Unable to apply diff to file: ${absolutePath}\n\n<error_details>\n${
1709+
diffResult.error
1710+
}${errorDetails ? `\n\nDetails:\n${errorDetails}` : ""}\n</error_details>`
1711+
}
1712+
16811713
if (currentCount >= 2) {
16821714
await this.say("error", formattedError)
16831715
}
@@ -1707,6 +1739,10 @@ export class Cline {
17071739
const { newProblemsMessage, userEdits, finalContent } =
17081740
await this.diffViewProvider.saveChanges()
17091741
this.didEditFile = true // used to determine if we should wait for busy terminal to update before sending api request
1742+
let partFailHint = ""
1743+
if (diffResult.failParts && diffResult.failParts.length > 0) {
1744+
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`
1745+
}
17101746
if (userEdits) {
17111747
await this.say(
17121748
"user_feedback_diff",
@@ -1718,6 +1754,7 @@ export class Cline {
17181754
)
17191755
pushToolResult(
17201756
`The user made the following updates to your content:\n\n${userEdits}\n\n` +
1757+
partFailHint +
17211758
`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` +
17221759
`<final_file_content path="${relPath.toPosix()}">\n${addLineNumbers(
17231760
finalContent || "",
@@ -1730,7 +1767,8 @@ export class Cline {
17301767
)
17311768
} else {
17321769
pushToolResult(
1733-
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}`,
1770+
`Changes successfully applied to ${relPath.toPosix()}:\n\n${newProblemsMessage}\n` +
1771+
partFailHint,
17341772
)
17351773
}
17361774
await this.diffViewProvider.reset()

src/core/__tests__/Cline.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -375,7 +375,7 @@ describe("Cline", () => {
375375

376376
expect(cline.diffEnabled).toBe(true)
377377
expect(cline.diffStrategy).toBeDefined()
378-
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false)
378+
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 0.9, false, false)
379379

380380
getDiffStrategySpy.mockRestore()
381381

@@ -396,7 +396,7 @@ describe("Cline", () => {
396396

397397
expect(cline.diffEnabled).toBe(true)
398398
expect(cline.diffStrategy).toBeDefined()
399-
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false)
399+
expect(getDiffStrategySpy).toHaveBeenCalledWith("claude-3-5-sonnet-20241022", 1.0, false, false)
400400

401401
getDiffStrategySpy.mockRestore()
402402

src/core/diff/DiffStrategy.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { DiffStrategy } from "./types"
22
import { UnifiedDiffStrategy } from "./strategies/unified"
33
import { SearchReplaceDiffStrategy } from "./strategies/search-replace"
44
import { NewUnifiedDiffStrategy } from "./strategies/new-unified"
5+
import { MultiSearchReplaceDiffStrategy } from "./strategies/multi-search-replace"
56
/**
67
* Get the appropriate diff strategy for the given model
78
* @param model The name of the model being used (e.g., 'gpt-4', 'claude-3-opus')
@@ -11,11 +12,17 @@ export function getDiffStrategy(
1112
model: string,
1213
fuzzyMatchThreshold?: number,
1314
experimentalDiffStrategy: boolean = false,
15+
multiSearchReplaceDiffStrategy: boolean = false,
1416
): DiffStrategy {
1517
if (experimentalDiffStrategy) {
1618
return new NewUnifiedDiffStrategy(fuzzyMatchThreshold)
1719
}
18-
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
20+
21+
if (multiSearchReplaceDiffStrategy) {
22+
return new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold)
23+
} else {
24+
return new SearchReplaceDiffStrategy(fuzzyMatchThreshold)
25+
}
1926
}
2027

2128
export type { DiffStrategy }

0 commit comments

Comments
 (0)