@@ -29,6 +29,48 @@ function getSimilarity(original: string, search: string): number {
2929 return 1 - dist / maxLength
3030}
3131
32+ /**
33+ * Performs a "middle-out" search of `lines` (between [startIndex, endIndex]) to find
34+ * the slice that is most similar to `searchChunk`. Returns the best score, index, and matched text.
35+ */
36+ function fuzzySearch ( lines : string [ ] , searchChunk : string , startIndex : number , endIndex : number ) {
37+ let bestScore = 0
38+ let bestMatchIndex = - 1
39+ let bestMatchContent = ""
40+ const searchLen = searchChunk . split ( / \r ? \n / ) . length
41+
42+ // Middle-out from the midpoint
43+ const midPoint = Math . floor ( ( startIndex + endIndex ) / 2 )
44+ let leftIndex = midPoint
45+ let rightIndex = midPoint + 1
46+
47+ while ( leftIndex >= startIndex || rightIndex <= endIndex - searchLen ) {
48+ if ( leftIndex >= startIndex ) {
49+ const originalChunk = lines . slice ( leftIndex , leftIndex + searchLen ) . join ( "\n" )
50+ const similarity = getSimilarity ( originalChunk , searchChunk )
51+ if ( similarity > bestScore ) {
52+ bestScore = similarity
53+ bestMatchIndex = leftIndex
54+ bestMatchContent = originalChunk
55+ }
56+ leftIndex --
57+ }
58+
59+ if ( rightIndex <= endIndex - searchLen ) {
60+ const originalChunk = lines . slice ( rightIndex , rightIndex + searchLen ) . join ( "\n" )
61+ const similarity = getSimilarity ( originalChunk , searchChunk )
62+ if ( similarity > bestScore ) {
63+ bestScore = similarity
64+ bestMatchIndex = rightIndex
65+ bestMatchContent = originalChunk
66+ }
67+ rightIndex ++
68+ }
69+ }
70+
71+ return { bestScore, bestMatchIndex, bestMatchContent }
72+ }
73+
3274export class MultiSearchReplaceDiffStrategy implements DiffStrategy {
3375 private fuzzyThreshold : number
3476 private bufferLines : number
@@ -253,7 +295,9 @@ Only use a single line of '=======' between search and replacement content, beca
253295 ? { success : true }
254296 : {
255297 success : false ,
256- error : `ERROR: Unexpected end of sequence: Expected '${ state . current === State . AFTER_SEARCH ? SEP : REPLACE } ' was not found.` ,
298+ error : `ERROR: Unexpected end of sequence: Expected '${
299+ state . current === State . AFTER_SEARCH ? "=======" : ">>>>>>> REPLACE"
300+ } ' was not found.`,
257301 }
258302 }
259303
@@ -329,19 +373,21 @@ Only use a single line of '=======' between search and replacement content, beca
329373 } ) )
330374 . sort ( ( a , b ) => a . startLine - b . startLine )
331375
332- for ( let { searchContent, replaceContent, startLine, endLine } of replacements ) {
333- startLine += startLine === 0 ? 0 : delta
334- endLine += delta
376+ for ( const replacement of replacements ) {
377+ let { searchContent, replaceContent } = replacement
378+ let startLine = replacement . startLine + ( replacement . startLine === 0 ? 0 : delta )
379+ let endLine = replacement . endLine + delta
335380
336381 // First unescape any escaped markers in the content
337382 searchContent = this . unescapeMarkers ( searchContent )
338383 replaceContent = this . unescapeMarkers ( replaceContent )
339384
340385 // Strip line numbers from search and replace content if every line starts with a line number
341- if (
386+ const hasAllLineNumbers =
342387 ( everyLineHasLineNumbers ( searchContent ) && everyLineHasLineNumbers ( replaceContent ) ) ||
343388 ( everyLineHasLineNumbers ( searchContent ) && replaceContent . trim ( ) === "" )
344- ) {
389+
390+ if ( hasAllLineNumbers ) {
345391 searchContent = stripLineNumbers ( searchContent )
346392 replaceContent = stripLineNumbers ( replaceContent )
347393 }
@@ -360,8 +406,8 @@ Only use a single line of '=======' between search and replacement content, beca
360406 }
361407
362408 // Split content into lines, handling both \n and \r\n
363- const searchLines = searchContent === "" ? [ ] : searchContent . split ( / \r ? \n / )
364- const replaceLines = replaceContent === "" ? [ ] : replaceContent . split ( / \r ? \n / )
409+ let searchLines = searchContent === "" ? [ ] : searchContent . split ( / \r ? \n / )
410+ let replaceLines = replaceContent === "" ? [ ] : replaceContent . split ( / \r ? \n / )
365411
366412 // Validate that empty search requires start line
367413 if ( searchLines . length === 0 && ! startLine ) {
@@ -385,7 +431,7 @@ Only use a single line of '=======' between search and replacement content, beca
385431 let matchIndex = - 1
386432 let bestMatchScore = 0
387433 let bestMatchContent = ""
388- const searchChunk = searchLines . join ( "\n" )
434+ let searchChunk = searchLines . join ( "\n" )
389435
390436 // Determine search bounds
391437 let searchStartIndex = 0
@@ -421,68 +467,70 @@ Only use a single line of '=======' between search and replacement content, beca
421467
422468 // If no match found yet, try middle-out search within bounds
423469 if ( matchIndex === - 1 ) {
424- const midPoint = Math . floor ( ( searchStartIndex + searchEndIndex ) / 2 )
425- let leftIndex = midPoint
426- let rightIndex = midPoint + 1
427-
428- // Search outward from the middle within bounds
429- while ( leftIndex >= searchStartIndex || rightIndex <= searchEndIndex - searchLines . length ) {
430- // Check left side if still in range
431- if ( leftIndex >= searchStartIndex ) {
432- const originalChunk = resultLines . slice ( leftIndex , leftIndex + searchLines . length ) . join ( "\n" )
433- const similarity = getSimilarity ( originalChunk , searchChunk )
434- if ( similarity > bestMatchScore ) {
435- bestMatchScore = similarity
436- matchIndex = leftIndex
437- bestMatchContent = originalChunk
438- }
439- leftIndex --
440- }
441-
442- // Check right side if still in range
443- if ( rightIndex <= searchEndIndex - searchLines . length ) {
444- const originalChunk = resultLines . slice ( rightIndex , rightIndex + searchLines . length ) . join ( "\n" )
445- const similarity = getSimilarity ( originalChunk , searchChunk )
446- if ( similarity > bestMatchScore ) {
447- bestMatchScore = similarity
448- matchIndex = rightIndex
449- bestMatchContent = originalChunk
450- }
451- rightIndex ++
452- }
453- }
470+ const {
471+ bestScore,
472+ bestMatchIndex,
473+ bestMatchContent : midContent ,
474+ } = fuzzySearch ( resultLines , searchChunk , searchStartIndex , searchEndIndex )
475+ matchIndex = bestMatchIndex
476+ bestMatchScore = bestScore
477+ bestMatchContent = midContent
454478 }
455479
456- // Require similarity to meet threshold
480+ // Try aggressive line number stripping as a fallback if regular matching fails
457481 if ( matchIndex === - 1 || bestMatchScore < this . fuzzyThreshold ) {
458- const searchChunk = searchLines . join ( "\n" )
459- const originalContentSection =
460- startLine !== undefined && endLine !== undefined
461- ? `\n\nOriginal Content:\n${ addLineNumbers (
462- resultLines
463- . slice (
464- Math . max ( 0 , startLine - 1 - this . bufferLines ) ,
465- Math . min ( resultLines . length , endLine + this . bufferLines ) ,
466- )
467- . join ( "\n" ) ,
468- Math . max ( 1 , startLine - this . bufferLines ) ,
469- ) } `
470- : `\n\nOriginal Content:\n${ addLineNumbers ( resultLines . join ( "\n" ) ) } `
471-
472- const bestMatchSection = bestMatchContent
473- ? `\n\nBest Match Found:\n${ addLineNumbers ( bestMatchContent , matchIndex + 1 ) } `
474- : `\n\nBest Match Found:\n(no match)`
475-
476- const lineRange =
477- startLine || endLine
478- ? ` at ${ startLine ? `start: ${ startLine } ` : "start" } to ${ endLine ? `end: ${ endLine } ` : "end" } `
479- : ""
482+ // Strip both search and replace content once (simultaneously)
483+ const aggressiveSearchContent = stripLineNumbers ( searchContent , true )
484+ const aggressiveReplaceContent = stripLineNumbers ( replaceContent , true )
485+
486+ const aggressiveSearchLines = aggressiveSearchContent ? aggressiveSearchContent . split ( / \r ? \n / ) : [ ]
487+ const aggressiveSearchChunk = aggressiveSearchLines . join ( "\n" )
488+
489+ // Try middle-out search again with aggressive stripped content (respecting the same search bounds)
490+ const {
491+ bestScore,
492+ bestMatchIndex,
493+ bestMatchContent : aggContent ,
494+ } = fuzzySearch ( resultLines , aggressiveSearchChunk , searchStartIndex , searchEndIndex )
495+ if ( bestMatchIndex !== - 1 && bestScore >= this . fuzzyThreshold ) {
496+ matchIndex = bestMatchIndex
497+ bestMatchScore = bestScore
498+ bestMatchContent = aggContent
499+ // Replace the original search/replace with their stripped versions
500+ searchContent = aggressiveSearchContent
501+ replaceContent = aggressiveReplaceContent
502+ searchLines = aggressiveSearchLines
503+ replaceLines = replaceContent ? replaceContent . split ( / \r ? \n / ) : [ ]
504+ } else {
505+ // No match found with either method
506+ const originalContentSection =
507+ startLine !== undefined && endLine !== undefined
508+ ? `\n\nOriginal Content:\n${ addLineNumbers (
509+ resultLines
510+ . slice (
511+ Math . max ( 0 , startLine - 1 - this . bufferLines ) ,
512+ Math . min ( resultLines . length , endLine + this . bufferLines ) ,
513+ )
514+ . join ( "\n" ) ,
515+ Math . max ( 1 , startLine - this . bufferLines ) ,
516+ ) } `
517+ : `\n\nOriginal Content:\n${ addLineNumbers ( resultLines . join ( "\n" ) ) } `
518+
519+ const bestMatchSection = bestMatchContent
520+ ? `\n\nBest Match Found:\n${ addLineNumbers ( bestMatchContent , matchIndex + 1 ) } `
521+ : `\n\nBest Match Found:\n(no match)`
522+
523+ const lineRange =
524+ startLine || endLine
525+ ? ` at ${ startLine ? `start: ${ startLine } ` : "start" } to ${ endLine ? `end: ${ endLine } ` : "end" } `
526+ : ""
480527
481- diffResults . push ( {
482- success : false ,
483- 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 the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${ searchChunk } ${ bestMatchSection } ${ originalContentSection } ` ,
484- } )
485- continue
528+ diffResults . push ( {
529+ success : false ,
530+ 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- Tried both standard and aggressive line number stripping\n- Tip: Use the read_file tool to get the latest content of the file before attempting to use the apply_diff tool again, as the file content may have changed\n\nSearch Content:\n${ searchChunk } ${ bestMatchSection } ${ originalContentSection } ` ,
531+ } )
532+ continue
533+ }
486534 }
487535
488536 // Get the matched lines from the original content
0 commit comments