From 9a13041186ea9c3263b9f0fc742664934c806d0b Mon Sep 17 00:00:00 2001 From: Matt Rubens Date: Sun, 30 Mar 2025 23:07:31 -0400 Subject: [PATCH 1/9] Remove the single diff strategy and make multi-diff the default (#2133) --- src/core/diff/DiffStrategy.ts | 7 +- .../__tests__/search-replace.test.ts | 1557 ----------------- src/core/diff/strategies/search-replace.ts | 306 ---- .../__snapshots__/system.test.ts.snap | 42 +- src/core/prompts/__tests__/system.test.ts | 24 +- src/exports/roo-code.d.ts | 1 - src/exports/types.ts | 1 - src/schemas/index.ts | 2 - src/shared/__tests__/experiments.test.ts | 3 - src/shared/experiments.ts | 6 +- .../components/settings/AdvancedSettings.tsx | 62 +- .../__tests__/ExtensionStateContext.test.tsx | 2 - webview-ui/src/i18n/locales/ca/settings.json | 2 +- webview-ui/src/i18n/locales/de/settings.json | 2 +- webview-ui/src/i18n/locales/en/settings.json | 2 +- webview-ui/src/i18n/locales/es/settings.json | 2 +- webview-ui/src/i18n/locales/fr/settings.json | 2 +- webview-ui/src/i18n/locales/hi/settings.json | 2 +- webview-ui/src/i18n/locales/it/settings.json | 2 +- webview-ui/src/i18n/locales/ja/settings.json | 2 +- webview-ui/src/i18n/locales/ko/settings.json | 2 +- webview-ui/src/i18n/locales/pl/settings.json | 2 +- .../src/i18n/locales/pt-BR/settings.json | 2 +- webview-ui/src/i18n/locales/tr/settings.json | 2 +- webview-ui/src/i18n/locales/vi/settings.json | 2 +- .../src/i18n/locales/zh-CN/settings.json | 2 +- .../src/i18n/locales/zh-TW/settings.json | 2 +- 27 files changed, 71 insertions(+), 1972 deletions(-) delete mode 100644 src/core/diff/strategies/__tests__/search-replace.test.ts delete mode 100644 src/core/diff/strategies/search-replace.ts diff --git a/src/core/diff/DiffStrategy.ts b/src/core/diff/DiffStrategy.ts index fe354196c6..eacf0f1f22 100644 --- a/src/core/diff/DiffStrategy.ts +++ b/src/core/diff/DiffStrategy.ts @@ -1,5 +1,4 @@ import type { DiffStrategy } from "./types" -import { SearchReplaceDiffStrategy } from "./strategies/search-replace" import { NewUnifiedDiffStrategy } from "./strategies/new-unified" import { MultiSearchReplaceDiffStrategy } from "./strategies/multi-search-replace" import { EXPERIMENT_IDS, ExperimentId } from "../../shared/experiments" @@ -12,7 +11,7 @@ export type { DiffStrategy } * @returns The appropriate diff strategy for the model */ -export type DiffStrategyName = "unified" | "multi-search-and-replace" | "search-and-replace" +export type DiffStrategyName = "unified" | "multi-search-and-replace" type GetDiffStrategyOptions = { model: string @@ -23,6 +22,4 @@ type GetDiffStrategyOptions = { export const getDiffStrategy = ({ fuzzyMatchThreshold, experiments }: GetDiffStrategyOptions): DiffStrategy => experiments[EXPERIMENT_IDS.DIFF_STRATEGY_UNIFIED] ? new NewUnifiedDiffStrategy(fuzzyMatchThreshold) - : experiments[EXPERIMENT_IDS.DIFF_STRATEGY_MULTI_SEARCH_AND_REPLACE] - ? new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold) - : new SearchReplaceDiffStrategy(fuzzyMatchThreshold) + : new MultiSearchReplaceDiffStrategy(fuzzyMatchThreshold) diff --git a/src/core/diff/strategies/__tests__/search-replace.test.ts b/src/core/diff/strategies/__tests__/search-replace.test.ts deleted file mode 100644 index cd71edac47..0000000000 --- a/src/core/diff/strategies/__tests__/search-replace.test.ts +++ /dev/null @@ -1,1557 +0,0 @@ -import { SearchReplaceDiffStrategy } from "../search-replace" - -describe("SearchReplaceDiffStrategy", () => { - describe("exact matching", () => { - let strategy: SearchReplaceDiffStrategy - - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy(1.0, 5) // Default 1.0 threshold for exact matching, 5 line buffer for tests - }) - - it("should replace matching content", async () => { - const originalContent = 'function hello() {\n console.log("hello")\n}\n' - const diffContent = `test.ts -<<<<<<< SEARCH -function hello() { - console.log("hello") -} -======= -function hello() { - console.log("hello world") -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe('function hello() {\n console.log("hello world")\n}\n') - } - }) - - it("should match content with different surrounding whitespace", async () => { - const originalContent = "\nfunction example() {\n return 42;\n}\n\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function example() { - return 42; -} -======= -function example() { - return 43; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("\nfunction example() {\n return 43;\n}\n\n") - } - }) - - it("should match content with different indentation in search block", async () => { - const originalContent = " function test() {\n return true;\n }\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function test() { - return true; -} -======= -function test() { - return false; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(" function test() {\n return false;\n }\n") - } - }) - - it("should handle tab-based indentation", async () => { - const originalContent = "function test() {\n\treturn true;\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function test() { -\treturn true; -} -======= -function test() { -\treturn false; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function test() {\n\treturn false;\n}\n") - } - }) - - it("should preserve mixed tabs and spaces", async () => { - const originalContent = "\tclass Example {\n\t constructor() {\n\t\tthis.value = 0;\n\t }\n\t}" - const diffContent = `test.ts -<<<<<<< SEARCH -\tclass Example { -\t constructor() { -\t\tthis.value = 0; -\t } -\t} -======= -\tclass Example { -\t constructor() { -\t\tthis.value = 1; -\t } -\t} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - "\tclass Example {\n\t constructor() {\n\t\tthis.value = 1;\n\t }\n\t}", - ) - } - }) - - it("should handle additional indentation with tabs", async () => { - const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}" - const diffContent = `test.ts -<<<<<<< SEARCH -function test() { -\treturn true; -} -======= -function test() { -\t// Add comment -\treturn false; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("\tfunction test() {\n\t\t// Add comment\n\t\treturn false;\n\t}") - } - }) - - it("should preserve exact indentation characters when adding lines", async () => { - const originalContent = "\tfunction test() {\n\t\treturn true;\n\t}" - const diffContent = `test.ts -<<<<<<< SEARCH -\tfunction test() { -\t\treturn true; -\t} -======= -\tfunction test() { -\t\t// First comment -\t\t// Second comment -\t\treturn true; -\t} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - "\tfunction test() {\n\t\t// First comment\n\t\t// Second comment\n\t\treturn true;\n\t}", - ) - } - }) - - it("should handle Windows-style CRLF line endings", async () => { - const originalContent = "function test() {\r\n return true;\r\n}\r\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function test() { - return true; -} -======= -function test() { - return false; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function test() {\r\n return false;\r\n}\r\n") - } - }) - - it("should return false if search content does not match", async () => { - const originalContent = 'function hello() {\n console.log("hello")\n}\n' - const diffContent = `test.ts -<<<<<<< SEARCH -function hello() { - console.log("wrong") -} -======= -function hello() { - console.log("hello world") -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(false) - }) - - it("should return false if diff format is invalid", async () => { - const originalContent = 'function hello() {\n console.log("hello")\n}\n' - const diffContent = `test.ts\nInvalid diff format` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(false) - }) - - it("should handle multiple lines with proper indentation", async () => { - const originalContent = - "class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n return this.value\n }\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH - getValue() { - return this.value - } -======= - getValue() { - // Add logging - console.log("Getting value") - return this.value - } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - 'class Example {\n constructor() {\n this.value = 0\n }\n\n getValue() {\n // Add logging\n console.log("Getting value")\n return this.value\n }\n}\n', - ) - } - }) - - it("should preserve whitespace exactly in the output", async () => { - const originalContent = " indented\n more indented\n back\n" - const diffContent = `test.ts -<<<<<<< SEARCH - indented - more indented - back -======= - modified - still indented - end ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(" modified\n still indented\n end\n") - } - }) - - it("should preserve indentation when adding new lines after existing content", async () => { - const originalContent = " onScroll={() => updateHighlights()}" - const diffContent = `test.ts -<<<<<<< SEARCH - onScroll={() => updateHighlights()} -======= - onScroll={() => updateHighlights()} - onDragOver={(e) => { - e.preventDefault() - e.stopPropagation() - }} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - " onScroll={() => updateHighlights()}\n onDragOver={(e) => {\n e.preventDefault()\n e.stopPropagation()\n }}", - ) - } - }) - - it("should handle varying indentation levels correctly", async () => { - const originalContent = ` -class Example { - constructor() { - this.value = 0; - if (true) { - this.init(); - } - } -}`.trim() - - const diffContent = `test.ts -<<<<<<< SEARCH - class Example { - constructor() { - this.value = 0; - if (true) { - this.init(); - } - } - } -======= - class Example { - constructor() { - this.value = 1; - if (true) { - this.init(); - this.setup(); - this.validate(); - } - } - } ->>>>>>> REPLACE`.trim() - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - ` -class Example { - constructor() { - this.value = 1; - if (true) { - this.init(); - this.setup(); - this.validate(); - } - } -}`.trim(), - ) - } - }) - - it("should handle mixed indentation styles in the same file", async () => { - const originalContent = `class Example { - constructor() { - this.value = 0; - if (true) { - this.init(); - } - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - constructor() { - this.value = 0; - if (true) { - this.init(); - } - } -======= - constructor() { - this.value = 1; - if (true) { - this.init(); - this.validate(); - } - } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`class Example { - constructor() { - this.value = 1; - if (true) { - this.init(); - this.validate(); - } - } -}`) - } - }) - - it("should handle Python-style significant whitespace", async () => { - const originalContent = `def example(): - if condition: - do_something() - for item in items: - process(item) - return True`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - if condition: - do_something() - for item in items: - process(item) -======= - if condition: - do_something() - while items: - item = items.pop() - process(item) ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`def example(): - if condition: - do_something() - while items: - item = items.pop() - process(item) - return True`) - } - }) - - it("should preserve empty lines with indentation", async () => { - const originalContent = `function test() { - const x = 1; - - if (x) { - return true; - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - const x = 1; - - if (x) { -======= - const x = 1; - - // Check x - if (x) { ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function test() { - const x = 1; - - // Check x - if (x) { - return true; - } -}`) - } - }) - - it("should handle indentation when replacing entire blocks", async () => { - const originalContent = `class Test { - method() { - if (true) { - console.log("test"); - } - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - method() { - if (true) { - console.log("test"); - } - } -======= - method() { - try { - if (true) { - console.log("test"); - } - } catch (e) { - console.error(e); - } - } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`class Test { - method() { - try { - if (true) { - console.log("test"); - } - } catch (e) { - console.error(e); - } - } -}`) - } - }) - - it("should handle negative indentation relative to search content", async () => { - const originalContent = `class Example { - constructor() { - if (true) { - this.init(); - this.setup(); - } - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - this.init(); - this.setup(); -======= - this.init(); - this.setup(); ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`class Example { - constructor() { - if (true) { - this.init(); - this.setup(); - } - } -}`) - } - }) - - it("should handle extreme negative indentation (no indent)", async () => { - const originalContent = `class Example { - constructor() { - if (true) { - this.init(); - } - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - this.init(); -======= -this.init(); ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`class Example { - constructor() { - if (true) { -this.init(); - } - } -}`) - } - }) - - it("should handle mixed indentation changes in replace block", async () => { - const originalContent = `class Example { - constructor() { - if (true) { - this.init(); - this.setup(); - this.validate(); - } - } -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH - this.init(); - this.setup(); - this.validate(); -======= - this.init(); - this.setup(); - this.validate(); ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`class Example { - constructor() { - if (true) { - this.init(); - this.setup(); - this.validate(); - } - } -}`) - } - }) - - it("should find matches from middle out", async () => { - const originalContent = ` -function one() { - return "target"; -} - -function two() { - return "target"; -} - -function three() { - return "target"; -} - -function four() { - return "target"; -} - -function five() { - return "target"; -}`.trim() - - const diffContent = `test.ts -<<<<<<< SEARCH - return "target"; -======= - return "updated"; ->>>>>>> REPLACE` - - // Search around the middle (function three) - // Even though all functions contain the target text, - // it should match the one closest to line 9 first - const result = await strategy.applyDiff(originalContent, diffContent, 9, 9) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return "target"; -} - -function two() { - return "target"; -} - -function three() { - return "updated"; -} - -function four() { - return "target"; -} - -function five() { - return "target"; -}`) - } - }) - }) - - describe("line number stripping", () => { - describe("line number stripping", () => { - let strategy: SearchReplaceDiffStrategy - - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy() - }) - - it("should strip line numbers from both search and replace sections", async () => { - const originalContent = "function test() {\n return true;\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -1 | function test() { -2 | return true; -3 | } -======= -1 | function test() { -2 | return false; -3 | } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function test() {\n return false;\n}\n") - } - }) - - it("should strip line numbers with leading spaces", async () => { - const originalContent = "function test() {\n return true;\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH - 1 | function test() { - 2 | return true; - 3 | } -======= - 1 | function test() { - 2 | return false; - 3 | } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function test() {\n return false;\n}\n") - } - }) - - it("should not strip when not all lines have numbers in either section", async () => { - const originalContent = "function test() {\n return true;\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -1 | function test() { -2 | return true; -3 | } -======= -1 | function test() { - return false; -3 | } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(false) - }) - - it("should preserve content that naturally starts with pipe", async () => { - const originalContent = "|header|another|\n|---|---|\n|data|more|\n" - const diffContent = `test.ts -<<<<<<< SEARCH -1 | |header|another| -2 | |---|---| -3 | |data|more| -======= -1 | |header|another| -2 | |---|---| -3 | |data|updated| ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("|header|another|\n|---|---|\n|data|updated|\n") - } - }) - - it("should preserve indentation when stripping line numbers", async () => { - const originalContent = " function test() {\n return true;\n }\n" - const diffContent = `test.ts -<<<<<<< SEARCH -1 | function test() { -2 | return true; -3 | } -======= -1 | function test() { -2 | return false; -3 | } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(" function test() {\n return false;\n }\n") - } - }) - - it("should handle different line numbers between sections", async () => { - const originalContent = "function test() {\n return true;\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -10 | function test() { -11 | return true; -12 | } -======= -20 | function test() { -21 | return false; -22 | } ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function test() {\n return false;\n}\n") - } - }) - - it("should not strip content that starts with pipe but no line number", async () => { - const originalContent = "| Pipe\n|---|\n| Data\n" - const diffContent = `test.ts -<<<<<<< SEARCH -| Pipe -|---| -| Data -======= -| Pipe -|---| -| Updated ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("| Pipe\n|---|\n| Updated\n") - } - }) - - it("should handle mix of line-numbered and pipe-only content", async () => { - const originalContent = "| Pipe\n|---|\n| Data\n" - const diffContent = `test.ts -<<<<<<< SEARCH -| Pipe -|---| -| Data -======= -1 | | Pipe -2 | |---| -3 | | NewData ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("1 | | Pipe\n2 | |---|\n3 | | NewData\n") - } - }) - }) - }) - - describe("insertion/deletion", () => { - let strategy: SearchReplaceDiffStrategy - - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy() - }) - - describe("deletion", () => { - it("should delete code when replace block is empty", async () => { - const originalContent = `function test() { - console.log("hello"); - // Comment to remove - console.log("world"); -}` - const diffContent = `test.ts -<<<<<<< SEARCH - // Comment to remove -======= ->>>>>>> REPLACE` - - const result = await 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", async () => { - 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 = await 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", async () => { - 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 = await 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", async () => { - const originalContent = `function test() { - const x = 1; - return x; -}` - const diffContent = `test.ts -<<<<<<< SEARCH -======= - console.log("Adding log"); ->>>>>>> REPLACE` - - const result = await 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", async () => { - const originalContent = `function test() { - if (true) { - const x = 1; - } -}` - const diffContent = `test.ts -<<<<<<< SEARCH -======= - console.log("Before"); - console.log("After"); ->>>>>>> REPLACE` - - const result = await 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", async () => { - const originalContent = `function test() { - return true; -}` - const diffContent = `test.ts -<<<<<<< SEARCH -======= -// Copyright 2024 -// License: MIT - ->>>>>>> REPLACE` - - const result = await 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", async () => { - const originalContent = `function test() { - return true; -}` - const diffContent = `test.ts -<<<<<<< SEARCH -======= - -// End of file ->>>>>>> REPLACE` - - const result = await 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 error if no start_line is provided for insertion", async () => { - const originalContent = `function test() { - return true; -}` - const diffContent = `test.ts -<<<<<<< SEARCH -======= -console.log("test"); ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(false) - }) - }) - }) - - describe("fuzzy matching", () => { - let strategy: SearchReplaceDiffStrategy - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy(0.9, 5) // 90% similarity threshold, 5 line buffer for tests - }) - - it("should match content with small differences (>90% similar)", async () => { - const originalContent = - "function getData() {\n const results = fetchData();\n return results.filter(Boolean);\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function getData() { - const result = fetchData(); - return results.filter(Boolean); -} -======= -function getData() { - const data = fetchData(); - return data.filter(Boolean); -} ->>>>>>> REPLACE` - - strategy = new SearchReplaceDiffStrategy(0.9, 5) // Use 5 line buffer for tests - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe( - "function getData() {\n const data = fetchData();\n return data.filter(Boolean);\n}\n", - ) - } - }) - - it("should not match when content is too different (<90% similar)", async () => { - const originalContent = "function processUsers(data) {\n return data.map(user => user.name);\n}\n" - const diffContent = `test.ts -<<<<<<< SEARCH -function handleItems(items) { - return items.map(item => item.username); -} -======= -function processData(data) { - return data.map(d => d.value); -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(false) - }) - - it("should match content with extra whitespace", async () => { - const originalContent = "function sum(a, b) {\n return a + b;\n}" - const diffContent = `test.ts -<<<<<<< SEARCH -function sum(a, b) { - return a + b; -} -======= -function sum(a, b) { - return a + b + 1; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe("function sum(a, b) {\n return a + b + 1;\n}") - } - }) - - it("should not exact match empty lines", async () => { - const originalContent = "function sum(a, b) {\n\n return a + b;\n}" - const diffContent = `test.ts -<<<<<<< SEARCH -function sum(a, b) { -======= -import { a } from "a"; -function sum(a, b) { ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe('import { a } from "a";\nfunction sum(a, b) {\n\n return a + b;\n}') - } - }) - }) - - describe("line-constrained search", () => { - let strategy: SearchReplaceDiffStrategy - - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy(0.9, 5) - }) - - it("should find and replace within specified line range", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return 3; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function two() { - return 2; -} -======= -function two() { - return "two"; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent, 5, 7) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return 1; -} - -function two() { - return "two"; -} - -function three() { - return 3; -}`) - } - }) - - it("should find and replace within buffer zone (5 lines before/after)", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return 3; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function three() { - return 3; -} -======= -function three() { - return "three"; -} ->>>>>>> REPLACE` - - // Even though we specify lines 5-7, it should still find the match at lines 9-11 - // because it's within the 5-line buffer zone - const result = await strategy.applyDiff(originalContent, diffContent, 5, 7) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return "three"; -}`) - } - }) - - it("should not find matches outside search range and buffer zone", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return 3; -} - -function four() { - return 4; -} - -function five() { - return 5; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function five() { - return 5; -} -======= -function five() { - return "five"; -} ->>>>>>> REPLACE` - - // Searching around function two() (lines 5-7) - // function five() is more than 5 lines away, so it shouldn't match - const result = await strategy.applyDiff(originalContent, diffContent, 5, 7) - expect(result.success).toBe(false) - }) - - it("should handle search range at start of file", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function one() { - return 1; -} -======= -function one() { - return "one"; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent, 1, 3) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return "one"; -} - -function two() { - return 2; -}`) - } - }) - - it("should handle search range at end of file", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function two() { - return 2; -} -======= -function two() { - return "two"; -} ->>>>>>> REPLACE` - - const result = await strategy.applyDiff(originalContent, diffContent, 5, 7) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return 1; -} - -function two() { - return "two"; -}`) - } - }) - - it("should match specific instance of duplicate code using line numbers", async () => { - const originalContent = ` -function processData(data) { - return data.map(x => x * 2); -} - -function unrelatedStuff() { - console.log("hello"); -} - -// Another data processor -function processData(data) { - return data.map(x => x * 2); -} - -function moreStuff() { - console.log("world"); -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function processData(data) { - return data.map(x => x * 2); -} -======= -function processData(data) { - // Add logging - console.log("Processing data..."); - return data.map(x => x * 2); -} ->>>>>>> REPLACE` - - // Target the second instance of processData - const result = await strategy.applyDiff(originalContent, diffContent, 10, 12) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function processData(data) { - return data.map(x => x * 2); -} - -function unrelatedStuff() { - console.log("hello"); -} - -// Another data processor -function processData(data) { - // Add logging - console.log("Processing data..."); - return data.map(x => x * 2); -} - -function moreStuff() { - console.log("world"); -}`) - } - }) - - it("should search from start line to end of file when only start_line is provided", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return 3; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function three() { - return 3; -} -======= -function three() { - return "three"; -} ->>>>>>> REPLACE` - - // Only provide start_line, should search from there to end of file - const result = await strategy.applyDiff(originalContent, diffContent, 8) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return "three"; -}`) - } - }) - - it("should search from start of file to end line when only end_line is provided", async () => { - const originalContent = ` -function one() { - return 1; -} - -function two() { - return 2; -} - -function three() { - return 3; -} -`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function one() { - return 1; -} -======= -function one() { - return "one"; -} ->>>>>>> REPLACE` - - // Only provide end_line, should search from start of file to there - const result = await strategy.applyDiff(originalContent, diffContent, undefined, 4) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return "one"; -} - -function two() { - return 2; -} - -function three() { - return 3; -}`) - } - }) - - it("should prioritize exact line match over expanded search", async () => { - const originalContent = ` -function one() { - return 1; -} - -function process() { - return "old"; -} - -function process() { - return "old"; -} - -function two() { - return 2; -}` - const diffContent = `test.ts -<<<<<<< SEARCH -function process() { - return "old"; -} -======= -function process() { - return "new"; -} ->>>>>>> REPLACE` - - // Should match the second instance exactly at lines 10-12 - // even though the first instance at 6-8 is within the expanded search range - const result = await strategy.applyDiff(originalContent, diffContent, 10, 12) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(` -function one() { - return 1; -} - -function process() { - return "old"; -} - -function process() { - return "new"; -} - -function two() { - return 2; -}`) - } - }) - - it("should fall back to expanded search only if exact match fails", async () => { - const originalContent = ` -function one() { - return 1; -} - -function process() { - return "target"; -} - -function two() { - return 2; -}`.trim() - const diffContent = `test.ts -<<<<<<< SEARCH -function process() { - return "target"; -} -======= -function process() { - return "updated"; -} ->>>>>>> REPLACE` - - // Specify wrong line numbers (3-5), but content exists at 6-8 - // Should still find and replace it since it's within the expanded range - const result = await strategy.applyDiff(originalContent, diffContent, 3, 5) - expect(result.success).toBe(true) - if (result.success) { - expect(result.content).toBe(`function one() { - return 1; -} - -function process() { - return "updated"; -} - -function two() { - return 2; -}`) - } - }) - }) - - describe("getToolDescription", () => { - let strategy: SearchReplaceDiffStrategy - - beforeEach(() => { - strategy = new SearchReplaceDiffStrategy() - }) - - it("should include the current working directory", async () => { - const cwd = "/test/dir" - const description = await strategy.getToolDescription({ cwd }) - expect(description).toContain(`relative to the current working directory ${cwd}`) - }) - - it("should include required format elements", async () => { - const description = await strategy.getToolDescription({ cwd: "/test" }) - expect(description).toContain("<<<<<<< SEARCH") - expect(description).toContain("=======") - expect(description).toContain(">>>>>>> REPLACE") - expect(description).toContain("") - expect(description).toContain("") - }) - - it("should document start_line and end_line parameters", async () => { - const description = await strategy.getToolDescription({ cwd: "/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.") - }) - }) -}) diff --git a/src/core/diff/strategies/search-replace.ts b/src/core/diff/strategies/search-replace.ts deleted file mode 100644 index 0f1ad1d1e8..0000000000 --- a/src/core/diff/strategies/search-replace.ts +++ /dev/null @@ -1,306 +0,0 @@ -import { DiffStrategy, DiffResult } from "../types" -import { addLineNumbers, everyLineHasLineNumbers, stripLineNumbers } from "../../../integrations/misc/extract-text" -import { distance } from "fastest-levenshtein" - -const BUFFER_LINES = 20 // 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 SearchReplaceDiffStrategy implements DiffStrategy { - private fuzzyThreshold: number - private bufferLines: number - - getName(): string { - return "SearchReplace" - } - - 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. - -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. -- start_line: (required) The line number where the search block starts. -- end_line: (required) The line number where the search block ends. - -Diff format: -\`\`\` -<<<<<<< SEARCH -[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 -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 -\`\`\` - -Usage: - -File path here - -Your search/replace content here - -1 -5 -` - } - - async applyDiff( - originalContent: string, - diffContent: string, - startLine?: number, - endLine?: number, - ): Promise { - // Extract the search and replace blocks - const match = diffContent.match(/<<<<<<< SEARCH\n([\s\S]*?)\n?=======\n([\s\S]*?)\n?>>>>>>> REPLACE/) - if (!match) { - return { - success: false, - error: `Invalid diff format - missing required SEARCH/REPLACE sections\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`, - } - } - - let [_, searchContent, replaceContent] = match - - // Detect line ending from original content - const lineEnding = originalContent.includes("\r\n") ? "\r\n" : "\n" - - // 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/) - const originalLines = originalContent.split(/\r?\n/) - - // Validate that empty search requires start line - if (searchLines.length === 0 && !startLine) { - return { - 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`, - } - } - - // Validate that empty search requires same start and end line - if (searchLines.length === 0 && startLine && endLine && startLine !== endLine) { - return { - 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`, - } - } - - // Initialize search variables - let matchIndex = -1 - let bestMatchScore = 0 - let bestMatchContent = "" - const searchChunk = searchLines.join("\n") - - // Determine search bounds - let searchStartIndex = 0 - let searchEndIndex = originalLines.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 > originalLines.length || exactStartIndex > exactEndIndex) { - return { - success: false, - error: `Line range ${startLine}-${endLine} is invalid (file has ${originalLines.length} lines)\n\nDebug Info:\n- Requested Range: lines ${startLine}-${endLine}\n- File Bounds: lines 1-${originalLines.length}`, - } - } - - // Try exact match first - const originalChunk = originalLines.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(originalLines.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 = originalLines.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 = originalLines.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( - originalLines - .slice( - Math.max(0, startLine - 1 - this.bufferLines), - Math.min(originalLines.length, endLine + this.bufferLines), - ) - .join("\n"), - Math.max(1, startLine - this.bufferLines), - )}` - : `\n\nOriginal Content:\n${addLineNumbers(originalLines.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"}` - : "" - return { - 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}`, - } - } - - // Get the matched lines from the original content - const matchedLines = originalLines.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 = originalLines.slice(0, matchIndex) - const afterMatch = originalLines.slice(matchIndex + searchLines.length) - - const finalContent = [...beforeMatch, ...indentedReplaceLines, ...afterMatch].join(lineEnding) - return { - success: true, - content: finalContent, - } - } -} diff --git a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap index dcf7f3ab33..e9538bc308 100644 --- a/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap +++ b/src/core/prompts/__tests__/__snapshots__/system.test.ts.snap @@ -4031,22 +4031,26 @@ 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 /test/path) - 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. 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: @@ -4061,6 +4065,9 @@ Original file: Search/Replace content: \`\`\` <<<<<<< SEARCH +:start_line:1 +:end_line:5 +------- def calculate_total(items): total = 0 for item in items: @@ -4071,16 +4078,43 @@ 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: File path here 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. -1 -5 ## write_to_file diff --git a/src/core/prompts/__tests__/system.test.ts b/src/core/prompts/__tests__/system.test.ts index 8fd0046501..4a29b2f4d8 100644 --- a/src/core/prompts/__tests__/system.test.ts +++ b/src/core/prompts/__tests__/system.test.ts @@ -3,11 +3,11 @@ import * as vscode from "vscode" import { SYSTEM_PROMPT } from "../system" import { McpHub } from "../../../services/mcp/McpHub" import { ClineProvider } from "../../../core/webview/ClineProvider" -import { SearchReplaceDiffStrategy } from "../../../core/diff/strategies/search-replace" import { defaultModeSlug, modes, Mode, ModeConfig } from "../../../shared/modes" import "../../../utils/path" // Import path utils to get access to toPosix string extension. import { addCustomInstructions } from "../sections/custom-instructions" import { EXPERIMENT_IDS } from "../../../shared/experiments" +import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-search-replace" // Mock the sections jest.mock("../sections/modes", () => ({ @@ -171,7 +171,7 @@ describe("SYSTEM_PROMPT", () => { beforeEach(() => { // Reset experiments before each test to ensure they're disabled by default experiments = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: false, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false, [EXPERIMENT_IDS.INSERT_BLOCK]: false, } }) @@ -295,7 +295,7 @@ describe("SYSTEM_PROMPT", () => { "/test/path", false, // supportsComputerUse undefined, // mcpHub - new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase + new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode undefined, // customModePrompts @@ -316,7 +316,7 @@ describe("SYSTEM_PROMPT", () => { "/test/path", false, // supportsComputerUse undefined, // mcpHub - new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase + new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode undefined, // customModePrompts @@ -337,7 +337,7 @@ describe("SYSTEM_PROMPT", () => { "/test/path", false, // supportsComputerUse undefined, // mcpHub - new SearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase + new MultiSearchReplaceDiffStrategy(), // Use actual diff strategy from the codebase undefined, // browserViewportSize defaultModeSlug, // mode undefined, // customModePrompts @@ -482,7 +482,7 @@ describe("SYSTEM_PROMPT", () => { it("should disable experimental tools by default", async () => { // Set experiments to explicitly disable experimental tools const experimentsConfig = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: false, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: false, [EXPERIMENT_IDS.INSERT_BLOCK]: false, } @@ -516,7 +516,7 @@ describe("SYSTEM_PROMPT", () => { it("should enable experimental tools when explicitly enabled", async () => { // Set experiments for testing experimental features const experimentsEnabled = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: true, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true, [EXPERIMENT_IDS.INSERT_BLOCK]: true, } @@ -552,7 +552,7 @@ describe("SYSTEM_PROMPT", () => { it("should selectively enable experimental tools", async () => { // Set experiments for testing selective enabling const experimentsSelective = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: true, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true, [EXPERIMENT_IDS.INSERT_BLOCK]: false, } @@ -587,7 +587,7 @@ describe("SYSTEM_PROMPT", () => { it("should list all available editing tools in base instruction", async () => { const experiments = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: true, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true, [EXPERIMENT_IDS.INSERT_BLOCK]: true, } @@ -596,7 +596,7 @@ describe("SYSTEM_PROMPT", () => { "/test/path", false, undefined, - new SearchReplaceDiffStrategy(), + new MultiSearchReplaceDiffStrategy(), undefined, defaultModeSlug, undefined, @@ -615,7 +615,7 @@ describe("SYSTEM_PROMPT", () => { }) it("should provide detailed instructions for each enabled tool", async () => { const experiments = { - [EXPERIMENT_IDS.DIFF_STRATEGY_SEARCH_AND_REPLACE]: true, + [EXPERIMENT_IDS.SEARCH_AND_REPLACE]: true, [EXPERIMENT_IDS.INSERT_BLOCK]: true, } @@ -624,7 +624,7 @@ describe("SYSTEM_PROMPT", () => { "/test/path", false, undefined, - new SearchReplaceDiffStrategy(), + new MultiSearchReplaceDiffStrategy(), undefined, defaultModeSlug, undefined, diff --git a/src/exports/roo-code.d.ts b/src/exports/roo-code.d.ts index f8bd27da01..255b58d94d 100644 --- a/src/exports/roo-code.d.ts +++ b/src/exports/roo-code.d.ts @@ -254,7 +254,6 @@ type GlobalSettings = { | { search_and_replace: boolean experimentalDiffStrategy: boolean - multi_search_and_replace: boolean insert_content: boolean powerSteering: boolean } diff --git a/src/exports/types.ts b/src/exports/types.ts index 725a458a49..58f9ea33e1 100644 --- a/src/exports/types.ts +++ b/src/exports/types.ts @@ -257,7 +257,6 @@ type GlobalSettings = { | { search_and_replace: boolean experimentalDiffStrategy: boolean - multi_search_and_replace: boolean insert_content: boolean powerSteering: boolean } diff --git a/src/schemas/index.ts b/src/schemas/index.ts index ff3417b8c7..10d3e245f5 100644 --- a/src/schemas/index.ts +++ b/src/schemas/index.ts @@ -277,7 +277,6 @@ export type CustomSupportPrompts = z.infer export const experimentIds = [ "search_and_replace", "experimentalDiffStrategy", - "multi_search_and_replace", "insert_content", "powerSteering", ] as const @@ -293,7 +292,6 @@ export type ExperimentId = z.infer const experimentsSchema = z.object({ search_and_replace: z.boolean(), experimentalDiffStrategy: z.boolean(), - multi_search_and_replace: z.boolean(), insert_content: z.boolean(), powerSteering: z.boolean(), }) diff --git a/src/shared/__tests__/experiments.test.ts b/src/shared/__tests__/experiments.test.ts index a192b260cf..4214f8e390 100644 --- a/src/shared/__tests__/experiments.test.ts +++ b/src/shared/__tests__/experiments.test.ts @@ -17,7 +17,6 @@ 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) }) @@ -28,7 +27,6 @@ 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) }) @@ -39,7 +37,6 @@ 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) }) diff --git a/src/shared/experiments.ts b/src/shared/experiments.ts index 9d931d5a07..c6f28551c3 100644 --- a/src/shared/experiments.ts +++ b/src/shared/experiments.ts @@ -4,10 +4,9 @@ import { AssertEqual, Equals, Keys, Values } from "../utils/type-fu" export type { ExperimentId } export const EXPERIMENT_IDS = { - DIFF_STRATEGY_SEARCH_AND_REPLACE: "search_and_replace", DIFF_STRATEGY_UNIFIED: "experimentalDiffStrategy", - DIFF_STRATEGY_MULTI_SEARCH_AND_REPLACE: "multi_search_and_replace", INSERT_BLOCK: "insert_content", + SEARCH_AND_REPLACE: "search_and_replace", POWER_STEERING: "powerSteering", } as const satisfies Record @@ -20,10 +19,9 @@ interface ExperimentConfig { } export const experimentConfigsMap: Record = { - DIFF_STRATEGY_SEARCH_AND_REPLACE: { enabled: false }, DIFF_STRATEGY_UNIFIED: { enabled: false }, - DIFF_STRATEGY_MULTI_SEARCH_AND_REPLACE: { enabled: false }, INSERT_BLOCK: { enabled: false }, + SEARCH_AND_REPLACE: { enabled: false }, POWER_STEERING: { enabled: false }, } diff --git a/webview-ui/src/components/settings/AdvancedSettings.tsx b/webview-ui/src/components/settings/AdvancedSettings.tsx index a54386ad30..36e0a8f380 100644 --- a/webview-ui/src/components/settings/AdvancedSettings.tsx +++ b/webview-ui/src/components/settings/AdvancedSettings.tsx @@ -6,7 +6,7 @@ import { Cog } from "lucide-react" import { EXPERIMENT_IDS, ExperimentId } from "../../../../src/shared/experiments" import { cn } from "@/lib/utils" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Slider } from "@/components/ui" +import { Slider } from "@/components/ui" import { SetCachedStateField, SetExperimentEnabled } from "./types" import { SectionHeader } from "./SectionHeader" @@ -67,9 +67,8 @@ export const AdvancedSettings = ({ onChange={(e: any) => { setCachedStateField("diffEnabled", e.target.checked) if (!e.target.checked) { - // Reset both experimental strategies when diffs are disabled. + // Reset experimental strategies when diffs are disabled. setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY_UNIFIED, false) - setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY_MULTI_SEARCH_AND_REPLACE, false) } }}> {t("settings:advanced.diff.label")} @@ -81,63 +80,6 @@ export const AdvancedSettings = ({ {diffEnabled && (
-
- - -
- {experiments[EXPERIMENT_IDS.DIFF_STRATEGY_UNIFIED] - ? t("settings:advanced.diff.strategy.descriptions.unified") - : experiments[EXPERIMENT_IDS.DIFF_STRATEGY_MULTI_SEARCH_AND_REPLACE] - ? t("settings:advanced.diff.strategy.descriptions.multiBlock") - : t("settings:advanced.diff.strategy.descriptions.standard")} -
-
-