diff --git a/src/core/diff/strategies/__tests__/multi-search-replace.test.ts b/src/core/diff/strategies/__tests__/multi-search-replace.test.ts
index 8fc16d2303..e4a52af6d4 100644
--- a/src/core/diff/strategies/__tests__/multi-search-replace.test.ts
+++ b/src/core/diff/strategies/__tests__/multi-search-replace.test.ts
@@ -1,16 +1,64 @@
import { MultiSearchReplaceDiffStrategy } from "../multi-search-replace"
describe("MultiSearchReplaceDiffStrategy", () => {
- describe("exact matching", () => {
+ describe("validateMarkerSequencing", () => {
let strategy: MultiSearchReplaceDiffStrategy
beforeEach(() => {
- strategy = new MultiSearchReplaceDiffStrategy(1.0, 5) // Default 1.0 threshold for exact matching, 5 line buffer for tests
+ strategy = new MultiSearchReplaceDiffStrategy()
})
- it("should replace matching content", async () => {
- const originalContent = 'function hello() {\n console.log("hello")\n}\n'
- const diffContent = `test.ts
+ it("validates correct marker sequence", () => {
+ const diff = "<<<<<<< SEARCH\n" + "some content\n" + "=======\n" + "new content\n" + ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("validates multiple correct marker sequences", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "content1\n" +
+ "=======\n" +
+ "new1\n" +
+ ">>>>>>> REPLACE\n\n" +
+ "<<<<<<< SEARCH\n" +
+ "content2\n" +
+ "=======\n" +
+ "new2\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("detects separator before search", () => {
+ const diff = "=======\n" + "content\n" + ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("'=======' found in your diff content")
+ })
+
+ it("detects replace before separator", () => {
+ const diff = "<<<<<<< SEARCH\n" + "content\n" + ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("'>>>>>>> REPLACE' found in your diff content")
+ })
+
+ it("detects incomplete sequence", () => {
+ const diff = "<<<<<<< SEARCH\n" + "content\n" + "=======\n" + "new content"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("Expected '>>>>>>> REPLACE' was not found")
+ })
+
+ describe("exact matching", () => {
+ let strategy: MultiSearchReplaceDiffStrategy
+
+ beforeEach(() => {
+ strategy = new MultiSearchReplaceDiffStrategy(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")
@@ -21,16 +69,16 @@ function hello() {
}
>>>>>>> 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')
- }
- })
+ 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
+ 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;
@@ -41,16 +89,16 @@ function example() {
}
>>>>>>> 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")
- }
- })
+ 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
+ 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;
@@ -61,16 +109,16 @@ function test() {
}
>>>>>>> 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")
- }
- })
+ 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
+ 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;
@@ -81,16 +129,16 @@ function test() {
}
>>>>>>> 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")
- }
- })
+ 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
+ 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() {
@@ -105,18 +153,18 @@ function test() {
\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}",
- )
- }
- })
+ 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
+ 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;
@@ -128,16 +176,16 @@ function test() {
}
>>>>>>> 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}")
- }
- })
+ 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
+ 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;
@@ -150,18 +198,18 @@ function test() {
\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}",
- )
- }
- })
+ 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
+ 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;
@@ -172,16 +220,16 @@ function test() {
}
>>>>>>> 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")
- }
- })
+ 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
+ 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")
@@ -192,22 +240,22 @@ function hello() {
}
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(false)
- })
+ 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`
+ 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)
- })
+ 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
+ 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
@@ -220,18 +268,18 @@ function hello() {
}
>>>>>>> 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',
- )
- }
- })
+ 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
+ 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
@@ -242,16 +290,16 @@ function hello() {
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")
- }
- })
+ 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
+ it("should preserve indentation when adding new lines after existing content", async () => {
+ const originalContent = " onScroll={() => updateHighlights()}"
+ const diffContent = `test.ts
<<<<<<< SEARCH
onScroll={() => updateHighlights()}
=======
@@ -262,17 +310,17 @@ function hello() {
}}
>>>>>>> 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 }}",
- )
- }
- })
+ 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 = `
+ it("should handle varying indentation levels correctly", async () => {
+ const originalContent = `
class Example {
constructor() {
this.value = 0;
@@ -282,7 +330,7 @@ class Example {
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
class Example {
constructor() {
@@ -305,11 +353,11 @@ class Example {
}
>>>>>>> REPLACE`.trim()
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe(
- `
+ 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;
@@ -320,12 +368,12 @@ class Example {
}
}
}`.trim(),
- )
- }
- })
+ )
+ }
+ })
- it("should handle mixed indentation styles in the same file", async () => {
- const originalContent = `class Example {
+ it("should handle mixed indentation styles in the same file", async () => {
+ const originalContent = `class Example {
constructor() {
this.value = 0;
if (true) {
@@ -333,7 +381,7 @@ class Example {
}
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
constructor() {
this.value = 0;
@@ -351,10 +399,10 @@ class Example {
}
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe(`class Example {
+ 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) {
@@ -363,17 +411,17 @@ class Example {
}
}
}`)
- }
- })
+ }
+ })
- it("should handle Python-style significant whitespace", async () => {
- const originalContent = `def example():
+ 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
+ const diffContent = `test.ts
<<<<<<< SEARCH
if condition:
do_something()
@@ -387,28 +435,28 @@ class Example {
process(item)
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe(`def example():
+ 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() {
+ it("should preserve empty lines with indentation", async () => {
+ const originalContent = `function test() {
const x = 1;
if (x) {
return true;
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
const x = 1;
@@ -420,10 +468,10 @@ class Example {
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 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
@@ -431,18 +479,18 @@ class Example {
return true;
}
}`)
- }
- })
+ }
+ })
- it("should handle indentation when replacing entire blocks", async () => {
- const originalContent = `class Test {
+ it("should handle indentation when replacing entire blocks", async () => {
+ const originalContent = `class Test {
method() {
if (true) {
console.log("test");
}
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
method() {
if (true) {
@@ -461,10 +509,10 @@ class Example {
}
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe(`class Test {
+ 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) {
@@ -475,11 +523,11 @@ class Example {
}
}
}`)
- }
- })
+ }
+ })
- it("should handle negative indentation relative to search content", async () => {
- const originalContent = `class Example {
+ it("should handle negative indentation relative to search content", async () => {
+ const originalContent = `class Example {
constructor() {
if (true) {
this.init();
@@ -487,7 +535,7 @@ class Example {
}
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
this.init();
this.setup();
@@ -496,10 +544,10 @@ class Example {
this.setup();
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(true)
- if (result.success) {
- expect(result.content).toBe(`class Example {
+ 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();
@@ -507,39 +555,39 @@ class Example {
}
}
}`)
- }
- })
+ }
+ })
- it("should handle extreme negative indentation (no indent)", async () => {
- const originalContent = `class Example {
+ it("should handle extreme negative indentation (no indent)", async () => {
+ const originalContent = `class Example {
constructor() {
if (true) {
this.init();
}
}
}`.trim()
- const diffContent = `test.ts
+ 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 {
+ 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 {
+ it("should handle mixed indentation changes in replace block", async () => {
+ const originalContent = `class Example {
constructor() {
if (true) {
this.init();
@@ -548,7 +596,7 @@ this.init();
}
}
}`.trim()
- const diffContent = `test.ts
+ const diffContent = `test.ts
<<<<<<< SEARCH
this.init();
this.setup();
@@ -559,10 +607,10 @@ 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 {
+ 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();
@@ -571,11 +619,11 @@ this.init();
}
}
}`)
- }
- })
+ }
+ })
- it("should find matches from middle out", async () => {
- const originalContent = `
+ it("should find matches from middle out", async () => {
+ const originalContent = `
function one() {
return "target";
}
@@ -596,20 +644,20 @@ function five() {
return "target";
}`.trim()
- const diffContent = `test.ts
+ 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() {
+ // 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";
}
@@ -628,21 +676,21 @@ function four() {
function five() {
return "target";
}`)
- }
- })
- })
-
- describe("line number stripping", () => {
- describe("line number stripping", () => {
- let strategy: MultiSearchReplaceDiffStrategy
-
- beforeEach(() => {
- strategy = new MultiSearchReplaceDiffStrategy()
+ }
})
+ })
- 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
+ describe("line number stripping", () => {
+ describe("line number stripping", () => {
+ let strategy: MultiSearchReplaceDiffStrategy
+
+ beforeEach(() => {
+ strategy = new MultiSearchReplaceDiffStrategy()
+ })
+
+ 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;
@@ -653,16 +701,16 @@ function five() {
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")
- }
- })
+ 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
+ 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;
@@ -673,16 +721,16 @@ function five() {
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")
- }
- })
+ 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
+ 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;
@@ -693,13 +741,13 @@ function five() {
3 | }
>>>>>>> REPLACE`
- const result = await strategy.applyDiff(originalContent, diffContent)
- expect(result.success).toBe(false)
- })
+ 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
+ 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 | |---|---|
@@ -710,16 +758,16 @@ function five() {
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")
- }
- })
+ 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
+ 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;
@@ -730,16 +778,16 @@ function five() {
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")
- }
- })
+ 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
+ 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;
@@ -750,11 +798,563 @@ function five() {
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")
- }
+ 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("detects search marker when expecting replace", () => {
+ const diff = "<<<<<<< SEARCH\n" + "content\n" + "=======\n" + "new content\n" + "<<<<<<< SEARCH"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("'<<<<<<< SEARCH' found in your diff content")
+ })
+
+ it("allows escaped search marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(true)
+ })
+
+ it("allows escaped separator in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(true)
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "test.ts\n" +
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "test.ts\n" +
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("allows escaped search marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped separator in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped search marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped search marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped separator in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped replace marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped separator in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped replace marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("allows escaped replace marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ expect(strategy["validateMarkerSequencing"](diff).success).toBe(true)
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "replaced content\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("replaced content\n")
+ }
+ })
+
+ it("processes escaped replace marker in content", async () => {
+ const originalContent = "before\n>>>>>>> REPLACE\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes multiple escaped markers in content", async () => {
+ const originalContent = "<<<<<<< SEARCH\n=======\n>>>>>>> REPLACE\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "\\=======\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped search marker in content", async () => {
+ const originalContent = "before\n<<<<<<< SEARCH\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped replace marker in content", async () => {
+ const originalContent = "before\n>>>>>>> REPLACE\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped separator in content", async () => {
+ const originalContent = "before\n=======\nafter\n"
+ const diffContent =
+ "test.ts\n" +
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\=======\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped replace marker in content", async () => {
+ const originalContent = "before\n>>>>>>> REPLACE\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes multiple escaped markers in content", async () => {
+ const originalContent = "<<<<<<< SEARCH\n=======\n>>>>>>> REPLACE\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "\\=======\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes escaped replace marker in content", async () => {
+ const originalContent = "before\n>>>>>>> REPLACE\nafter\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("processes multiple escaped markers in content", async () => {
+ const originalContent = "<<<<<<< SEARCH\n=======\n>>>>>>> REPLACE\n"
+ const diffContent =
+ "<<<<<<< SEARCH\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "\\=======\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "=======\n" +
+ "unchanged\n" +
+ ">>>>>>> REPLACE"
+ const result = await strategy.applyDiff(originalContent, diffContent)
+ expect(result.success).toBe(true)
+ if (result.success) {
+ expect(result.content).toBe("unchanged\n")
+ }
+ })
+
+ it("allows escaped replace marker in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "before\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "after\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(true)
+ })
+
+ it("allows multiple escaped markers in content", () => {
+ const diff =
+ "<<<<<<< SEARCH\n" +
+ "\\<<<<<<< SEARCH\n" +
+ "\\=======\n" +
+ "\\>>>>>>> REPLACE\n" +
+ "=======\n" +
+ "new content\n" +
+ ">>>>>>> REPLACE"
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(true)
+ })
+
+ it("detects separator when expecting replace", () => {
+ const diff = "<<<<<<< SEARCH\n" + "content\n" + "=======\n" + "new content\n" + "======="
+ const result = strategy["validateMarkerSequencing"](diff)
+ expect(result.success).toBe(false)
+ expect(result.error).toContain("'=======' found in your diff content")
+ })
})
it("should not strip content that starts with pipe but no line number", async () => {
@@ -980,7 +1580,6 @@ function test() {
:end_line:4
-------
=======
-
// End of file
>>>>>>> REPLACE`
@@ -990,7 +1589,6 @@ function test() {
expect(result.content).toBe(`function test() {
return true;
}
-
// End of file`)
}
})
diff --git a/src/core/diff/strategies/multi-search-replace.ts b/src/core/diff/strategies/multi-search-replace.ts
index ad12448d04..bf80f09052 100644
--- a/src/core/diff/strategies/multi-search-replace.ts
+++ b/src/core/diff/strategies/multi-search-replace.ts
@@ -73,6 +73,7 @@ Diff format:
\`\`\`
+
Example:
Original file:
@@ -128,6 +129,7 @@ def calculate_sum(items):
>>>>>>> REPLACE
\`\`\`
+
Usage:
File path here
@@ -139,22 +141,140 @@ Only use a single line of '=======' between search and replacement content, beca
`
}
+ private unescapeMarkers(content: string): string {
+ return content
+ .replace(/^\\<<<<<<< SEARCH/gm, "<<<<<<< SEARCH")
+ .replace(/^\\=======/gm, "=======")
+ .replace(/^\\>>>>>>> REPLACE/gm, ">>>>>>> REPLACE")
+ .replace(/^\\-------/gm, "-------")
+ .replace(/^\\:end_line:/gm, ":end_line:")
+ .replace(/^\\:start_line:/gm, ":start_line:")
+ }
+
+ private validateMarkerSequencing(diffContent: string): { success: boolean; error?: string } {
+ enum State {
+ START,
+ AFTER_SEARCH,
+ AFTER_SEPARATOR,
+ }
+ const state = { current: State.START, line: 0 }
+
+ const SEARCH = "<<<<<<< SEARCH"
+ const SEP = "======="
+ const REPLACE = ">>>>>>> REPLACE"
+
+ const reportError = (found: string, expected: string) => ({
+ success: false,
+ error:
+ `ERROR: Special marker '${found}' found in your diff content at line ${state.line}:\n` +
+ "\n" +
+ `When removing merge conflict markers like '${found}' from files, you MUST escape them\n` +
+ "in your SEARCH section by prepending a backslash (\\) at the beginning of the line:\n" +
+ "\n" +
+ "CORRECT FORMAT:\n\n" +
+ "<<<<<<< SEARCH\n" +
+ "content before\n" +
+ `\\${found} <-- Note the backslash here in this example\n` +
+ "content after\n" +
+ "=======\n" +
+ "replacement content\n" +
+ ">>>>>>> REPLACE\n" +
+ "\n" +
+ "Without escaping, the system confuses your content with diff syntax markers.\n" +
+ "You may use multiple diff blocks in a single diff request, but ANY of ONLY the following separators that occur within SEARCH or REPLACE content must be escaped, as follows:\n" +
+ `\\${SEARCH}\n` +
+ `\\${SEP}\n` +
+ `\\${REPLACE}\n`,
+ })
+
+ for (const line of diffContent.split("\n")) {
+ state.line++
+ const marker = line.trim()
+
+ switch (state.current) {
+ case State.START:
+ if (marker === SEP) return reportError(SEP, SEARCH)
+ if (marker === REPLACE) return reportError(REPLACE, SEARCH)
+ if (marker === SEARCH) state.current = State.AFTER_SEARCH
+ break
+
+ case State.AFTER_SEARCH:
+ if (marker === SEARCH) return reportError(SEARCH, SEP)
+ if (marker === REPLACE) return reportError(REPLACE, SEP)
+ if (marker === SEP) state.current = State.AFTER_SEPARATOR
+ break
+
+ case State.AFTER_SEPARATOR:
+ if (marker === SEARCH) return reportError(SEARCH, REPLACE)
+ if (marker === SEP) return reportError(SEP, REPLACE)
+ if (marker === REPLACE) state.current = State.START
+ break
+ }
+ }
+
+ return state.current === State.START
+ ? { success: true }
+ : {
+ success: false,
+ error: `ERROR: Unexpected end of sequence: Expected '${state.current === State.AFTER_SEARCH ? SEP : REPLACE}' was not found.`,
+ }
+ }
+
async applyDiff(
originalContent: string,
diffContent: string,
_paramStartLine?: number,
_paramEndLine?: number,
): Promise {
+ const validseq = this.validateMarkerSequencing(diffContent)
+ if (!validseq.success) {
+ return {
+ success: false,
+ error: validseq.error!,
+ }
+ }
+
+ /*
+ Regex parts:
+
+ 1. (?:^|\n)
+ Ensures the first marker starts at the beginning of the file or right after a newline.
+
+ 2. (?>>>>>> REPLACE)(?=\n|$)
+ Matches the final “>>>>>>> REPLACE” marker on its own line (and requires a following newline or the end of file).
+ */
+
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,
+ /(?:^|\n)(?>>>>>> REPLACE)(?=\n|$)/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`,
+ 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 on new lines`,
}
}
// Detect line ending from original content
@@ -176,12 +296,29 @@ Only use a single line of '=======' between search and replacement content, beca
startLine += startLine === 0 ? 0 : delta
endLine += delta
+ // First unescape any escaped markers in the content
+ searchContent = this.unescapeMarkers(searchContent)
+ replaceContent = this.unescapeMarkers(replaceContent)
+
// 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)
}
+ // Validate that search and replace content are not identical
+ if (searchContent === replaceContent) {
+ diffResults.push({
+ success: false,
+ error:
+ `Search and replace content are identical - no changes would be made\n\n` +
+ `Debug Info:\n` +
+ `- Search and replace must be different to make changes\n` +
+ `- Use read_file to verify the content you want to change`,
+ })
+ continue
+ }
+
// Split content into lines, handling both \n and \r\n
const searchLines = searchContent === "" ? [] : searchContent.split(/\r?\n/)
const replaceLines = replaceContent === "" ? [] : replaceContent.split(/\r?\n/)
diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts
index fc7d029cf8..6777642c2e 100644
--- a/src/core/webview/ClineProvider.ts
+++ b/src/core/webview/ClineProvider.ts
@@ -2046,6 +2046,7 @@ export class ClineProvider extends EventEmitter implements
apiConfiguration.apiModelId || apiConfiguration.openRouterModelId || "",
fuzzyMatchThreshold,
Experiments.isEnabled(experiments, EXPERIMENT_IDS.DIFF_STRATEGY),
+ Experiments.isEnabled(experiments, EXPERIMENT_IDS.MULTI_SEARCH_AND_REPLACE),
)
const cwd = this.cwd