Skip to content

Commit 42879fb

Browse files
committed
Generalize duplicated line detection (>=1), use match index, add tests
1 parent f3bbf95 commit 42879fb

File tree

3 files changed

+88
-35
lines changed

3 files changed

+88
-35
lines changed

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

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2748,6 +2748,42 @@ New Appended Content
27482748
Section B`)
27492749
})
27502750

2751+
it("should correctly append content and avoid duplicating multiple lines with nested braces", async () => {
2752+
const originalContent = `function devConfig() {
2753+
return {
2754+
name: "test"
2755+
};
2756+
}`
2757+
2758+
const diffContent = `<<<<<<< SEARCH
2759+
name: "test"
2760+
=======
2761+
name: "test"
2762+
};
2763+
}
2764+
function prodConfig() {
2765+
return {
2766+
name: "prod"
2767+
};
2768+
}
2769+
>>>>>>> REPLACE`
2770+
2771+
const result = await strategy.applyDiff(originalContent, diffContent)
2772+
2773+
expect(result.success).toBe(true)
2774+
if (result.success)
2775+
expect(result.content).toBe(`function devConfig() {
2776+
return {
2777+
name: "test"
2778+
};
2779+
}
2780+
function prodConfig() {
2781+
return {
2782+
name: "prod"
2783+
};
2784+
}`)
2785+
})
2786+
27512787
it("should not alter behavior when the line after search prefix in REPLACE differs from original line after SEARCH", async () => {
27522788
const originalContent = `Line 1
27532789
Line 2 (original next)
Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { SearchReplaceContext } from "../multi-search-replace"
22

33
export class SuperfluousDuplicatedLineEngine {
4-
public static process(originalContent: string, context: SearchReplaceContext): SearchReplaceContext {
4+
/**
5+
* Processes a search/replace context to detect superfluous duplicated lines.
6+
*
7+
* @param originalContent The complete original content being modified
8+
* @param context The search/replace context containing startLine, searchContent, and replaceContent
9+
* @returns The number of additional lines from the original content that should be consumed
10+
* to prevent duplication. Returns 0 if no additional lines should be consumed.
11+
*/
12+
public static process(originalContent: string, context: SearchReplaceContext): number {
513
const { startLine, searchContent, replaceContent } = context
614

715
// Early detection of superfluous duplicated line pattern
@@ -18,29 +26,38 @@ export class SuperfluousDuplicatedLineEngine {
1826
const isFirstPartSimilar = firstPartOfReplace === searchContent
1927

2028
if (isFirstPartSimilar && replaceLines.length > searchLines.length) {
21-
// Get the line that comes after the search block in replace content
22-
const lineAfterSearchInReplace = replaceLines[searchLines.length]
23-
24-
// Get the line that comes after the search block in original content
25-
const lineIndexInOriginal = startLine - 1 + searchLines.length
26-
if (lineIndexInOriginal < originalLines.length) {
27-
const lineAfterSearchInOriginal = originalLines[lineIndexInOriginal]
28-
29-
// If they match, it's likely a superfluous duplicated line scenario
30-
// We can modify the search content to include the extra line
31-
if (lineAfterSearchInReplace.trim() === lineAfterSearchInOriginal.trim()) {
32-
const modifiedSearchContent = searchContent + "\n" + lineAfterSearchInOriginal
33-
return {
34-
startLine,
35-
searchContent: modifiedSearchContent,
36-
replaceContent,
37-
}
29+
// Check for any number of consecutive matching lines after the search block
30+
let matchingLinesCount = 0
31+
const searchEndIndex = startLine - 1 + searchLines.length
32+
const maxPossibleMatches = Math.min(
33+
replaceLines.length - searchLines.length, // Available lines in replace content
34+
originalLines.length - searchEndIndex, // Available lines in original content after search
35+
)
36+
37+
for (let i = 0; i < maxPossibleMatches; i++) {
38+
const replaceLineIndex = searchLines.length + i
39+
const originalLineIndex = searchEndIndex + i
40+
41+
const lineInReplace = replaceLines[replaceLineIndex]
42+
const lineInOriginal = originalLines[originalLineIndex]
43+
44+
// Check if lines match (trimmed comparison to handle whitespace differences)
45+
if (lineInReplace && lineInOriginal && lineInReplace.trim() === lineInOriginal.trim()) {
46+
matchingLinesCount++
47+
} else {
48+
// Stop at the first non-matching line
49+
break
3850
}
3951
}
52+
53+
// If we found any matching lines, return the count
54+
if (matchingLinesCount > 0) {
55+
return matchingLinesCount
56+
}
4057
}
4158
}
4259

43-
// No modification needed, return as-is
44-
return context
60+
// No additional lines to consume
61+
return 0
4562
}
4663
}

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

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ Only use a single line of '=======' between search and replacement content, beca
401401
let delta = 0
402402
let diffResults: DiffResult[] = []
403403
let appliedCount = 0
404-
const replacements = matches
404+
const replacements: SearchReplaceContext[] = matches
405405
.map((match) => ({
406406
startLine: Number(match[2] ?? 0),
407407
searchContent: match[6],
@@ -413,19 +413,6 @@ Only use a single line of '=======' between search and replacement content, beca
413413
let { searchContent, replaceContent } = replacement
414414
let startLine = replacement.startLine + (replacement.startLine === 0 ? 0 : delta)
415415

416-
// --- Start: Replacement Engine Processing ---
417-
let context = SuperfluousDuplicatedLineEngine.process(originalContent, {
418-
startLine,
419-
searchContent,
420-
replaceContent,
421-
})
422-
423-
// Update variables from engine processing
424-
startLine = context.startLine
425-
searchContent = context.searchContent
426-
replaceContent = context.replaceContent
427-
// --- End: Replacement Engine Processing ---
428-
429416
// First unescape any escaped markers in the content
430417
searchContent = this.unescapeMarkers(searchContent)
431418
replaceContent = this.unescapeMarkers(replaceContent)
@@ -568,6 +555,19 @@ Only use a single line of '=======' between search and replacement content, beca
568555
}
569556
}
570557

558+
// --- Start: Replacement Engine Processing ---
559+
// Now that we found the match, call the engine with the actual match location
560+
const engineContext = {
561+
startLine: matchIndex + 1, // Convert back to 1-based index for the engine
562+
searchContent: replacement.searchContent,
563+
replaceContent: replacement.replaceContent,
564+
}
565+
const additionalLinesToConsume = SuperfluousDuplicatedLineEngine.process(
566+
resultLines.join("\n"),
567+
engineContext,
568+
)
569+
// --- End: Replacement Engine Processing ---
570+
571571
// Get the matched lines from the original content
572572
const matchedLines = resultLines.slice(matchIndex, matchIndex + searchLines.length)
573573

@@ -609,7 +609,7 @@ Only use a single line of '=======' between search and replacement content, beca
609609
})
610610

611611
// Initialize effectiveSearchLinesCount (determines how many lines from original are considered "replaced")
612-
let effectiveSearchLinesCount = searchLines.length // Default
612+
let effectiveSearchLinesCount = searchLines.length + additionalLinesToConsume
613613

614614
// Construct the final content
615615
const beforeMatch = resultLines.slice(0, matchIndex)

0 commit comments

Comments
 (0)