Skip to content

Commit cb03f33

Browse files
committed
Enhance MultiSearchReplaceDiffStrategy to handle line duplication heuristics in applyDiff method
1 parent 3f3825a commit cb03f33

File tree

2 files changed

+198
-2
lines changed

2 files changed

+198
-2
lines changed

src/core/diff/strategies/__tests__/multi-search-replace.test.ts

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,4 +2686,167 @@ function two() {
26862686
expect(result.error).toContain(":start_line:5 <-- Invalid location")
26872687
})
26882688
})
2689+
2690+
describe("Generalized line duplication heuristic", () => {
2691+
let strategy: MultiSearchReplaceDiffStrategy
2692+
2693+
beforeEach(() => {
2694+
strategy = new MultiSearchReplaceDiffStrategy()
2695+
})
2696+
2697+
it("should correctly append content and avoid duplicating a closing brace", async () => {
2698+
const originalContent = `function test() {
2699+
console.log("original");
2700+
}`
2701+
2702+
const diffContent = `<<<<<<< SEARCH
2703+
:start_line:2
2704+
-------
2705+
console.log("original");
2706+
=======
2707+
console.log("original");
2708+
}
2709+
function newFunction() {
2710+
console.log("appended");
2711+
}
2712+
>>>>>>> REPLACE`
2713+
2714+
const result = await strategy.applyDiff(originalContent, diffContent)
2715+
2716+
expect(result.success).toBe(true)
2717+
if (result.success)
2718+
expect(result.content).toBe(`function test() {
2719+
console.log("original");
2720+
}
2721+
function newFunction() {
2722+
console.log("appended");
2723+
}`)
2724+
})
2725+
2726+
it("should correctly append content and avoid duplicating a common line", async () => {
2727+
const originalContent = `Section A
2728+
Common Line to keep
2729+
Section B`
2730+
2731+
const diffContent = `<<<<<<< SEARCH
2732+
:start_line:1
2733+
-------
2734+
Section A
2735+
=======
2736+
Section A
2737+
Common Line to keep
2738+
New Appended Content
2739+
>>>>>>> REPLACE`
2740+
2741+
const result = await strategy.applyDiff(originalContent, diffContent)
2742+
2743+
expect(result.success).toBe(true)
2744+
if (result.success)
2745+
expect(result.content).toBe(`Section A
2746+
Common Line to keep
2747+
New Appended Content
2748+
Section B`)
2749+
})
2750+
2751+
it("should not alter behavior when the line after search prefix in REPLACE differs from original line after SEARCH", async () => {
2752+
const originalContent = `Line 1
2753+
Line 2 (original next)
2754+
Line 3`
2755+
2756+
const diffContent = `<<<<<<< SEARCH
2757+
:start_line:1
2758+
-------
2759+
Line 1
2760+
=======
2761+
Line 1
2762+
Line X (different next in replace)
2763+
Appended Content
2764+
>>>>>>> REPLACE`
2765+
2766+
const result = await strategy.applyDiff(originalContent, diffContent)
2767+
2768+
expect(result.success).toBe(true)
2769+
if (result.success)
2770+
expect(result.content).toBe(`Line 1
2771+
Line X (different next in replace)
2772+
Appended Content
2773+
Line 2 (original next)
2774+
Line 3`)
2775+
})
2776+
2777+
it("should not trigger heuristic if SEARCH content is not a prefix of REPLACE content", async () => {
2778+
const originalContent = `Alpha
2779+
Bravo
2780+
Charlie`
2781+
2782+
const diffContent = `<<<<<<< SEARCH
2783+
:start_line:1
2784+
-------
2785+
Alpha
2786+
=======
2787+
Completely New Content
2788+
Even More New Content
2789+
Bravo
2790+
>>>>>>> REPLACE`
2791+
2792+
const result = await strategy.applyDiff(originalContent, diffContent)
2793+
2794+
expect(result.success).toBe(true)
2795+
if (result.success)
2796+
expect(result.content).toBe(`Completely New Content
2797+
Even More New Content
2798+
Bravo
2799+
Bravo
2800+
Charlie`)
2801+
})
2802+
2803+
it("should handle edge case where heuristic conditions are met but no line follows search in original", async () => {
2804+
const originalContent = `Last Line`
2805+
2806+
const diffContent = `<<<<<<< SEARCH
2807+
:start_line:1
2808+
-------
2809+
Last Line
2810+
=======
2811+
Last Line
2812+
New Line
2813+
Added Content
2814+
>>>>>>> REPLACE`
2815+
2816+
const result = await strategy.applyDiff(originalContent, diffContent)
2817+
2818+
expect(result.success).toBe(true)
2819+
if (result.success)
2820+
expect(result.content).toBe(`Last Line
2821+
New Line
2822+
Added Content`)
2823+
})
2824+
2825+
it("should handle case where similarity threshold is not met", async () => {
2826+
const originalContent = `Original Content
2827+
Next Line
2828+
Final Line`
2829+
2830+
const diffContent = `<<<<<<< SEARCH
2831+
:start_line:1
2832+
-------
2833+
Original Content
2834+
=======
2835+
Completely Different Content
2836+
Next Line
2837+
Appended Content
2838+
>>>>>>> REPLACE`
2839+
2840+
const result = await strategy.applyDiff(originalContent, diffContent)
2841+
2842+
expect(result.success).toBe(true)
2843+
// Should behave as normal replacement since similarity < 0.95
2844+
if (result.success)
2845+
expect(result.content).toBe(`Completely Different Content
2846+
Next Line
2847+
Appended Content
2848+
Next Line
2849+
Final Line`)
2850+
})
2851+
})
26892852
})

src/core/diff/strategies/multi-search-replace.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -588,11 +588,44 @@ Only use a single line of '=======' between search and replacement content, beca
588588
return finalIndent + line.trim()
589589
})
590590

591+
// Initialize effectiveSearchLinesCount (determines how many lines from original are considered "replaced")
592+
let effectiveSearchLinesCount = searchLines.length // Default
593+
594+
// Heuristic to adjust effectiveSearchLinesCount for superfluous duplicated line pattern
595+
if (searchLines.length > 0 && replaceLines.length > searchLines.length) {
596+
const searchBlockContent = searchLines.join("\n")
597+
// Ensure replaceLines has enough elements before slicing
598+
const firstPartOfReplaceBlock = replaceLines.slice(0, searchLines.length).join("\n")
599+
600+
// Check if the search content is highly similar to the beginning of the replace content
601+
if (getSimilarity(searchBlockContent, firstPartOfReplaceBlock) > 0.95) {
602+
// Ensure there's a line in replaceLines immediately after the part that matches searchLines
603+
if (searchLines.length < replaceLines.length) {
604+
const lineInReplaceAfterPrefix = replaceLines[searchLines.length]
605+
606+
// Ensure there's a line in the original content (resultLines) immediately after the matched search block
607+
if (matchIndex + searchLines.length < resultLines.length) {
608+
const lineInOriginalAfterMatchedSearch = resultLines[matchIndex + searchLines.length]
609+
610+
// If the line in the replace block (after the prefix) is identical (ignoring leading/trailing whitespace)
611+
// to the line in the original content (after the search match),
612+
// it's likely a duplicated line scenario.
613+
if (lineInReplaceAfterPrefix.trim() === lineInOriginalAfterMatchedSearch.trim()) {
614+
effectiveSearchLinesCount = searchLines.length + 1 // Consume the duplicated line from the original
615+
}
616+
}
617+
}
618+
}
619+
}
620+
591621
// Construct the final content
592622
const beforeMatch = resultLines.slice(0, matchIndex)
593-
const afterMatch = resultLines.slice(matchIndex + searchLines.length)
623+
// Use effectiveSearchLinesCount here to determine the slice point
624+
const afterMatch = resultLines.slice(matchIndex + effectiveSearchLinesCount)
625+
594626
resultLines = [...beforeMatch, ...indentedReplaceLines, ...afterMatch]
595-
delta = delta - matchedLines.length + replaceLines.length
627+
// Use effectiveSearchLinesCount for delta calculation
628+
delta = delta - effectiveSearchLinesCount + replaceLines.length
596629
appliedCount++
597630
}
598631
const finalContent = resultLines.join(lineEnding)

0 commit comments

Comments
 (0)