@@ -369,7 +369,10 @@ Only use a single line of '=======' between search and replacement content, beca
369369
370370 for ( const replacement of replacements ) {
371371 let { searchContent, replaceContent } = replacement
372- let startLine = replacement . startLine + ( replacement . startLine === 0 ? 0 : delta )
372+ // Store original start line for search window calculation
373+ const originalStartLine = replacement . startLine
374+ // Calculate the adjusted start line for reporting/context, applying delta *after* finding the match
375+ let adjustedStartLine = originalStartLine + ( originalStartLine === 0 ? 0 : delta )
373376
374377 // First unescape any escaped markers in the content
375378 searchContent = this . unescapeMarkers ( searchContent )
@@ -411,7 +414,8 @@ Only use a single line of '=======' between search and replacement content, beca
411414 continue
412415 }
413416
414- let endLine = replacement . startLine + searchLines . length - 1
417+ // Use original start line for end line calculation relative to the original file state
418+ let originalEndLine = originalStartLine + searchLines . length - 1
415419
416420 // Initialize search variables
417421 let matchIndex = - 1
@@ -424,40 +428,59 @@ Only use a single line of '=======' between search and replacement content, beca
424428 let searchEndIndex = resultLines . length
425429
426430 // Validate and handle line range if provided
427- if ( startLine ) {
428- // Convert to 0-based index
429- const exactStartIndex = startLine - 1
431+ if ( originalStartLine ) {
432+ // Convert original start line to 0-based index for the *current* resultLines
433+ const exactStartIndex = originalStartLine - 1
430434 const searchLen = searchLines . length
431435 const exactEndIndex = exactStartIndex + searchLen - 1
432436
433- // Try exact match first
434- const originalChunk = resultLines . slice ( exactStartIndex , exactEndIndex + 1 ) . join ( "\n" )
435- const similarity = getSimilarity ( originalChunk , searchChunk )
436- if ( similarity >= this . fuzzyThreshold ) {
437- matchIndex = exactStartIndex
438- bestMatchScore = similarity
439- bestMatchContent = originalChunk
440- } else {
441- // Set bounds for buffered search
442- searchStartIndex = Math . max ( 0 , startLine - ( this . bufferLines + 1 ) )
443- searchEndIndex = Math . min ( resultLines . length , startLine + searchLines . length + this . bufferLines )
437+ // Check if the exact range is valid within the current resultLines
438+ if ( exactStartIndex >= 0 && exactEndIndex < resultLines . length ) {
439+ // Try exact match first using a slightly relaxed threshold (0.99)
440+ const originalChunk = resultLines . slice ( exactStartIndex , exactEndIndex + 1 ) . join ( "\n" )
441+ const similarity = getSimilarity ( originalChunk , searchChunk )
442+ // Use 1.0 threshold if fuzzyThreshold is 1.0, otherwise use 0.99 for initial check
443+ const initialCheckThreshold = this . fuzzyThreshold === 1.0 ? 1.0 : 0.99
444+ if ( similarity >= initialCheckThreshold ) {
445+ matchIndex = exactStartIndex
446+ bestMatchScore = similarity
447+ bestMatchContent = originalChunk
448+ }
449+ }
450+
451+ // If exact match failed or range was invalid, set bounds for buffered search
452+ // Use the *original* start line to calculate the search window in the *current* resultLines
453+ if ( matchIndex === - 1 ) {
454+ searchStartIndex = Math . max ( 0 , originalStartLine - ( this . bufferLines + 1 ) )
455+ searchEndIndex = Math . min (
456+ resultLines . length ,
457+ originalStartLine + searchLines . length + this . bufferLines ,
458+ )
444459 }
445460 }
446461
447- // If no match found yet, try middle-out search within bounds
462+ // Determine the effective fuzzy threshold for this block
463+ // Use strict 1.0 if fuzzyThreshold is 1.0, otherwise use the specified threshold
464+ const effectiveThreshold = this . fuzzyThreshold === 1.0 ? 1.0 : this . fuzzyThreshold
465+
466+ // If no exact match found yet, try middle-out fuzzy search within bounds
448467 if ( matchIndex === - 1 ) {
449468 const {
450469 bestScore,
451470 bestMatchIndex,
452471 bestMatchContent : midContent ,
453472 } = fuzzySearch ( resultLines , searchChunk , searchStartIndex , searchEndIndex )
454- matchIndex = bestMatchIndex
455- bestMatchScore = bestScore
456- bestMatchContent = midContent
473+
474+ // Check against the effective threshold
475+ if ( bestMatchIndex !== - 1 && bestScore >= effectiveThreshold ) {
476+ matchIndex = bestMatchIndex
477+ bestMatchScore = bestScore
478+ bestMatchContent = midContent
479+ }
457480 }
458481
459482 // Try aggressive line number stripping as a fallback if regular matching fails
460- if ( matchIndex === - 1 || bestMatchScore < this . fuzzyThreshold ) {
483+ if ( matchIndex === - 1 ) {
461484 // Strip both search and replace content once (simultaneously)
462485 const aggressiveSearchContent = stripLineNumbers ( searchContent , true )
463486 const aggressiveReplaceContent = stripLineNumbers ( replaceContent , true )
@@ -471,7 +494,9 @@ Only use a single line of '=======' between search and replacement content, beca
471494 bestMatchIndex,
472495 bestMatchContent : aggContent ,
473496 } = fuzzySearch ( resultLines , aggressiveSearchChunk , searchStartIndex , searchEndIndex )
474- if ( bestMatchIndex !== - 1 && bestScore >= this . fuzzyThreshold ) {
497+
498+ // Check against the effective threshold
499+ if ( bestMatchIndex !== - 1 && bestScore >= effectiveThreshold ) {
475500 matchIndex = bestMatchIndex
476501 bestMatchScore = bestScore
477502 bestMatchContent = aggContent
@@ -483,77 +508,89 @@ Only use a single line of '=======' between search and replacement content, beca
483508 } else {
484509 // No match found with either method
485510 const originalContentSection =
486- startLine !== undefined && endLine !== undefined
487- ? `\n\nOriginal Content:\n${ addLineNumbers (
511+ originalStartLine !== undefined && originalEndLine !== undefined
512+ ? `\n\nOriginal Content (around line ${ originalStartLine } ) :\n${ addLineNumbers (
488513 resultLines
489514 . slice (
490- Math . max ( 0 , startLine - 1 - this . bufferLines ) ,
491- Math . min ( resultLines . length , endLine + this . bufferLines ) ,
515+ // Show context based on original line numbers, clamped to current bounds
516+ Math . max ( 0 , originalStartLine - 1 - this . bufferLines ) ,
517+ Math . min ( resultLines . length , originalEndLine + this . bufferLines ) ,
492518 )
493519 . join ( "\n" ) ,
494- Math . max ( 1 , startLine - this . bufferLines ) ,
520+ // Start numbering from the calculated start line
521+ Math . max ( 1 , originalStartLine - this . bufferLines ) ,
495522 ) } `
496523 : `\n\nOriginal Content:\n${ addLineNumbers ( resultLines . join ( "\n" ) ) } `
497524
498525 const bestMatchSection = bestMatchContent
499- ? `\n\nBest Match Found:\n${ addLineNumbers ( bestMatchContent , matchIndex + 1 ) } `
500- : `\n\nBest Match Found:\n(no match)`
526+ ? `\n\nBest Fuzzy Match Found (Score: ${ Math . floor ( bestMatchScore * 100 ) } %) :\n${ addLineNumbers ( bestMatchContent , matchIndex + 1 ) } `
527+ : `\n\nBest Fuzzy Match Found:\n(no match below threshold )`
501528
502- const lineRange = startLine ? ` at line: ${ startLine } ` : ""
529+ const lineRange = originalStartLine ? ` near original line: ${ originalStartLine } ` : ""
530+ const thresholdInfo = `(${ Math . floor ( bestMatchScore * 100 ) } % similar, needs ${ Math . floor ( effectiveThreshold * 100 ) } %)`
503531
504532 diffResults . push ( {
505533 success : false ,
506- 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 ? `starting at line ${ startLine } ` : "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 } ` ,
534+ error : `No sufficiently similar match found${ lineRange } ${ thresholdInfo } \n\nDebug Info:\n- Similarity Score: ${ Math . floor ( bestMatchScore * 100 ) } %\n- Required Threshold: ${ Math . floor ( effectiveThreshold * 100 ) } %\n- Search Range: Lines ${ searchStartIndex + 1 } to ${ searchEndIndex } \n- Tried standard and aggressive line number stripping\n- Tip: Use read_file to verify the current file content , as it might have changed. \n\nSearch Content:\n${ searchChunk } ${ bestMatchSection } ${ originalContentSection } ` ,
507535 } )
508- continue
536+ continue // Skip to the next replacement block
509537 }
510538 }
511539
512- // Get the matched lines from the original content
540+ // --- Start: Robust Indentation Logic ---
513541 const matchedLines = resultLines . slice ( matchIndex , matchIndex + searchLines . length )
514542
515- // Get the exact indentation (preserving tabs/spaces) of each line
516- const originalIndents = matchedLines . map ( ( line ) => {
517- const match = line . match ( / ^ [ \t ] * / )
518- return match ? match [ 0 ] : ""
519- } )
543+ // Calculate the indentation of the *first line being replaced* in the target file
544+ const targetBaseIndentMatch = matchedLines [ 0 ] ?. match ( / ^ [ \t ] * / )
545+ const targetBaseIndent = targetBaseIndentMatch ? targetBaseIndentMatch [ 0 ] : ""
520546
521- // Get the exact indentation of each line in the search block
522- const searchIndents = searchLines . map ( ( line ) => {
523- const match = line . match ( / ^ [ \t ] * / )
524- return match ? match [ 0 ] : ""
525- } )
547+ // Calculate the indentation of the *first line* of the search block
548+ const searchBaseIndentMatch = searchLines [ 0 ] ?. match ( / ^ [ \t ] * / )
549+ const searchBaseIndent = searchBaseIndentMatch ? searchBaseIndentMatch [ 0 ] : ""
526550
527- // Apply the replacement while preserving exact indentation
528- const indentedReplaceLines = replaceLines . map ( ( line , i ) => {
529- // Get the matched line's exact indentation
530- const matchedIndent = originalIndents [ 0 ] || ""
551+ // Determine the primary indentation character (tab or space) from targetBaseIndent
552+ const targetIndentChar = targetBaseIndent . startsWith ( "\t" ) ? "\t" : " "
531553
532- // Get the current line's indentation relative to the search content
554+ // Calculate the indentation of the *first line* of the replacement block
555+ const replaceBaseIndentMatch = replaceLines [ 0 ] ?. match ( / ^ [ \t ] * / )
556+ const replaceBaseIndent = replaceBaseIndentMatch ? replaceBaseIndentMatch [ 0 ] : ""
557+
558+ // Apply indentation to replacement lines based on difference from searchBaseIndent
559+ const indentedReplaceLines = replaceLines . map ( ( line ) => {
560+ // Get current line's indent
533561 const currentIndentMatch = line . match ( / ^ [ \t ] * / )
534562 const currentIndent = currentIndentMatch ? currentIndentMatch [ 0 ] : ""
535- const searchBaseIndent = searchIndents [ 0 ] || ""
536-
537- // Calculate the relative indentation level
538- const searchBaseLevel = searchBaseIndent . length
539- const currentLevel = currentIndent . length
540- const relativeLevel = currentLevel - searchBaseLevel
541-
542- // If relative level is negative, remove indentation from matched indent
543- // If positive, add to matched indent
544- const finalIndent =
545- relativeLevel < 0
546- ? matchedIndent . slice ( 0 , Math . max ( 0 , matchedIndent . length + relativeLevel ) )
547- : matchedIndent + currentIndent . slice ( searchBaseLevel )
563+ let finalIndent = ""
564+
565+ // Calculate relative indentation based on the SEARCH block's base indent
566+ if ( currentIndent . startsWith ( searchBaseIndent ) ) {
567+ // Indented or same level relative to search base: Append the relative part to target base
568+ const relativePart = currentIndent . substring ( searchBaseIndent . length )
569+ finalIndent = targetBaseIndent + relativePart
570+ } else if ( searchBaseIndent . startsWith ( currentIndent ) ) {
571+ // De-dented relative to search base: Remove the difference length from target base
572+ const diffLength = searchBaseIndent . length - currentIndent . length
573+ const finalLength = Math . max ( 0 , targetBaseIndent . length - diffLength )
574+ finalIndent = targetBaseIndent . substring ( 0 , finalLength )
575+ } else {
576+ // Unrelated indentation structure (e.g., mixed tabs/spaces):
577+ // Fallback: Use targetBaseIndent. This preserves the original file's
578+ // base level for the block but doesn't apply complex relative changes.
579+ finalIndent = targetBaseIndent
580+ }
548581
549- return finalIndent + line . trim ( )
582+ // Combine the calculated final indent with the non-indented part of the line
583+ return finalIndent + line . trimStart ( )
550584 } )
585+ // --- End: Robust Indentation Logic ---
551586
552587 // Construct the final content
553588 const beforeMatch = resultLines . slice ( 0 , matchIndex )
554589 const afterMatch = resultLines . slice ( matchIndex + searchLines . length )
555590 resultLines = [ ...beforeMatch , ...indentedReplaceLines , ...afterMatch ]
556- delta = delta - matchedLines . length + replaceLines . length
591+
592+ // Update delta based on the change in line count for *this specific block*
593+ delta = delta - searchLines . length + replaceLines . length
557594 appliedCount ++
558595 }
559596 const finalContent = resultLines . join ( lineEnding )
0 commit comments