Skip to content

Commit a49e81f

Browse files
authored
fix: improve search and replace symbol parsing (#9456)
1 parent 9051818 commit a49e81f

File tree

1 file changed

+24
-22
lines changed

1 file changed

+24
-22
lines changed

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

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -251,14 +251,16 @@ Each file requires its own path, start_line, and diff elements.
251251

252252
const state = { current: State.START, line: 0 }
253253

254-
// Pattern allows optional '>' after SEARCH to handle AI-generated diffs
255-
// (e.g., Sonnet 4 sometimes adds an extra '>')
256-
const SEARCH_PATTERN = /^<<<<<<< SEARCH>?$/
257-
const SEARCH = SEARCH_PATTERN.source.replace(/[\^$]/g, "") // Remove regex anchors for display
254+
// Pattern allows optional extra '<' or '>' for SEARCH to handle AI-generated diffs
255+
// (e.g., Sonnet 4 sometimes adds extra markers)
256+
const SEARCH_PATTERN = /^<{7,8} SEARCH>?$/
257+
const SEARCH = "<<<<<<< SEARCH" // Simplified for display
258258
const SEP = "======="
259-
const REPLACE = ">>>>>>> REPLACE"
260-
const SEARCH_PREFIX = "<<<<<<< "
261-
const REPLACE_PREFIX = ">>>>>>> "
259+
// Pattern allows optional extra '>' or '<' for REPLACE
260+
const REPLACE_PATTERN = /^>{7,8} REPLACE<?$/
261+
const REPLACE = ">>>>>>> REPLACE" // Simplified for display
262+
const SEARCH_PREFIX_PATTERN = /^<{7,8} /
263+
const REPLACE_PREFIX_PATTERN = /^>{7,8} /
262264

263265
const reportMergeConflictError = (found: string, _expected: string) => ({
264266
success: false,
@@ -326,7 +328,7 @@ Each file requires its own path, start_line, and diff elements.
326328
const lines = diffContent.split("\n")
327329
const searchCount = lines.filter((l) => SEARCH_PATTERN.test(l.trim())).length
328330
const sepCount = lines.filter((l) => l.trim() === SEP).length
329-
const replaceCount = lines.filter((l) => l.trim() === REPLACE).length
331+
const replaceCount = lines.filter((l) => REPLACE_PATTERN.test(l.trim())).length
330332

331333
const likelyBadStructure = searchCount !== replaceCount || sepCount < searchCount
332334

@@ -350,29 +352,29 @@ Each file requires its own path, start_line, and diff elements.
350352
return likelyBadStructure
351353
? reportInvalidDiffError(SEP, SEARCH)
352354
: reportMergeConflictError(SEP, SEARCH)
353-
if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEARCH)
354-
if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
355+
if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEARCH)
356+
if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
355357
if (SEARCH_PATTERN.test(marker)) state.current = State.AFTER_SEARCH
356-
else if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
358+
else if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
357359
break
358360

359361
case State.AFTER_SEARCH:
360-
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, SEP)
361-
if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, SEARCH)
362-
if (marker === REPLACE) return reportInvalidDiffError(REPLACE, SEP)
363-
if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, SEARCH)
362+
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, SEP)
363+
if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
364+
if (REPLACE_PATTERN.test(marker)) return reportInvalidDiffError(REPLACE, SEP)
365+
if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, SEARCH)
364366
if (marker === SEP) state.current = State.AFTER_SEPARATOR
365367
break
366368

367369
case State.AFTER_SEPARATOR:
368-
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH_PATTERN.source, REPLACE)
369-
if (marker.startsWith(SEARCH_PREFIX)) return reportMergeConflictError(marker, REPLACE)
370+
if (SEARCH_PATTERN.test(marker)) return reportInvalidDiffError(SEARCH, REPLACE)
371+
if (SEARCH_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE)
370372
if (marker === SEP)
371373
return likelyBadStructure
372374
? reportInvalidDiffError(SEP, REPLACE)
373375
: reportMergeConflictError(SEP, REPLACE)
374-
if (marker === REPLACE) state.current = State.START
375-
else if (marker.startsWith(REPLACE_PREFIX)) return reportMergeConflictError(marker, REPLACE)
376+
if (REPLACE_PATTERN.test(marker)) state.current = State.START
377+
else if (REPLACE_PREFIX_PATTERN.test(marker)) return reportMergeConflictError(marker, REPLACE)
376378
break
377379
}
378380
}
@@ -451,18 +453,18 @@ Each file requires its own path, start_line, and diff elements.
451453

452454
/* Regex parts:
453455
1. (?:^|\n) Ensures the first marker starts at the beginning of the file or right after a newline.
454-
2. (?<!\\)<<<<<<< SEARCH>?\s*\n Matches the line "<<<<<<< SEARCH" with optional '>' (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped.
456+
2. (?<!\\)<{7,8} SEARCH>?\s*\n Matches "<<<<<<< SEARCH" or "<<<<<<< SEARCH>" or "<<<<<<<<" with 7-8 '<' chars (ignoring any trailing spaces) – the negative lookbehind makes sure it isn't escaped.
455457
3. ((?:\:start_line:\s*(\d+)\s*\n))? Optionally matches a ":start_line:" line. The outer capturing group is group 1 and the inner (\d+) is group 2.
456458
4. ((?:\:end_line:\s*(\d+)\s*\n))? Optionally matches a ":end_line:" line. Group 3 is the whole match and group 4 is the digits.
457459
5. ((?<!\\)-------\s*\n)? Optionally matches the "-------" marker line (group 5).
458460
6. ([\s\S]*?)(?:\n)? Non‐greedy match for the "search content" (group 6) up to the next marker.
459461
7. (?:(?<=\n)(?<!\\)=======\s*\n) Matches the "=======" marker on its own line.
460462
8. ([\s\S]*?)(?:\n)? Non‐greedy match for the "replace content" (group 7).
461-
9. (?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$) Matches the final ">>>>>>> REPLACE" marker on its own line (and requires a following newline or the end of file).
463+
9. (?:(?<=\n)(?<!\\)>{7,8} REPLACE<?)(?=\n|$) Matches ">>>>>>> REPLACE" or ">>>>>>> REPLACE<" or ">>>>>>>>" with 7-8 '>' chars on its own line (and requires a following newline or the end of file).
462464
*/
463465
let matches = [
464466
...diffContent.matchAll(
465-
/(?:^|\n)(?<!\\)<<<<<<< SEARCH>?\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>>>>>>> REPLACE)(?=\n|$)/g,
467+
/(?:^|\n)(?<!\\)<{7,8} SEARCH>?\s*\n((?:\:start_line:\s*(\d+)\s*\n))?((?:\:end_line:\s*(\d+)\s*\n))?((?<!\\)-------\s*\n)?([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)=======\s*\n)([\s\S]*?)(?:\n)?(?:(?<=\n)(?<!\\)>{7,8} REPLACE<?)(?=\n|$)/g,
466468
),
467469
]
468470

0 commit comments

Comments
 (0)