
    epj                        d Z ddlZddlmZmZmZmZ ddlmZ i dddddd	d
d	dddddddddddddddddddddddddddddddddZ	de
de
fdZd e
d!e
d"e
defd#Z	 dWd e
d%eeeef                  d&ede
fd'Z	 dXd e
d!e
d"e
d)edee
eee
         ee
         f         f
d*Zd e
d%eeeef                  d!e
d"e
dee
         f
d+Zd,e
de
fd-Zde
dee
         fd.Zd/e
d!e
d"e
de
fd0Zd"e
d e
d%eeeef                  de
fd1Zd e
d%eeeef                  d!e
d"e
de
f
d2Z	 dYd e
d%eeeef                  d"e
d!ee
         de
f
d3Zd e
d4e
deeeef                  fd5Zd e
d4e
deeeef                  fd6Zd e
d4e
deeeef                  fd7Zd e
d4e
deeeef                  fd8Zd e
d4e
deeeef                  fd9Zd e
d4e
deeeef                  fd:Zd;e
dee         fd<Zd=ee         d>eeeef                  deeeef                  fd?Zd e
d4e
deeeef                  fd@Z d e
d4e
deeeef                  fdAZ!d e
d4e
deeeef                  fdBZ"dCee
         dDedEedFedeeef         f
dGZ#d e
dCee
         dHee
         d4e
dIe
deeeef                  fdJZ$d;e
dKe
dLeeeef                  deeeef                  fdMZ%d,e
de
fdNZ&dZd!e
d e
dQedRede
f
dSZ'dTee
         dUed!e
d e
de
f
dVZ(dS )[aP  
Fuzzy Matching Module for File Operations

Implements a multi-strategy matching chain to robustly find and replace text,
accommodating variations in whitespace, indentation, and escaping common
in LLM-generated code.

The 9-strategy chain (inspired by OpenCode), tried in order:
1. Exact match - Direct string comparison
2. Line-trimmed - Strip leading/trailing whitespace per line
3. Whitespace normalized - Collapse multiple spaces/tabs to single space
4. Indentation flexible - Ignore indentation differences entirely
5. Escape normalized - Convert \n literals to actual newlines
6. Trimmed boundary - Trim first/last line whitespace only
7. Block anchor - Match first+last lines, use similarity for middle
8. Context-aware - 50% line similarity threshold

Multi-occurrence matching is handled via the replace_all flag.

Usage:
    from tools.fuzzy_match import fuzzy_find_and_replace
    
    new_content, match_count, strategy, error = fuzzy_find_and_replace(
        content="def foo():\n    pass",
        old_string="def foo():",
        new_string="def bar():",
        replace_all=False
    )
    N)TupleOptionalListCallable)SequenceMatcheru   “"u   ”u   ‘'u   ’u   —z--u   –-u   …...     u   −u    u    u    u    u    u    u    u    )u    u    u    u    u    u   　textreturnc                 p    t                                           D ]\  }}|                     ||          } | S )zBNormalizes Unicode characters to their standard ASCII equivalents.)UNICODE_MAPitemsreplace)r   charrepls      7/home/thesage/.hermes/hermes-agent/tools/fuzzy_match.py_unicode_normalizer   <   s;    !'')) ( (
d||D$''K    content
old_string
new_stringc                 x    |r%t          |                                          dk     rdS || vrdS ||k    rdS || vS )u  Return True when the requested edit is already present in the file.

    Production trajectory mining shows the most common patch failure is a
    re-send of an edit that already landed (old_string == new_string, or
    old_string gone while new_string is present) — the model's intent is
    "make the file contain this text", and it already does. Callers use
    this to convert those errors into an explicit success-shaped no-op so
    the model moves on instead of re-reading and re-patching.

    Deliberately conservative:
    - new_string must be non-trivial (>= 8 chars stripped) — a tiny target
      matching by coincidence must not mask a genuine typo'd edit;
    - new_string must appear EXACTLY in the content (no fuzzy matching —
      approximate presence is not proof the edit landed);
    - when old_string differs from new_string, old_string must be GONE
      (still-present old text means the edit is at best half-applied).
       FT)lenstrip)r   r   r   s      r   is_already_appliedr    C   sZ    $  Z--//00144u  uZtW$$r      matchescapc                    g }|d|         D ]\  }}|                      dd|          dz   }|                     dd|          dz   }|                     d|          }|dk    rt          |           }| ||                                         }	t          |	          dk    r|	dd         dz   }	|                    d	| d
|	            t          |          |z
  }
|
dk    r|                    d|
 d           d                    |          S )a  Render up to ``cap`` match positions as 'L<line>: <snippet>' rows.

    Gives the model the information it needs to disambiguate an ambiguous
    old_string in ONE follow-up (add neighboring context, or choose
    replace_all) instead of re-reading the file to find the occurrences.
    N
r      P   M   r   z  Lz: z
  ... and z more)countrfindfindr   r   appendjoin)r   r"   r#   rowsstart_endline_no
line_startline_endsnippetextras              r   _format_match_locationsr7   ^   s*    Dtt} 	0 	0t--a//!3]]4E22Q6
<<j11r>>7||H*X-.4466w<<"crclU*G.'..W..////LL3Eqyy----...99T??r   Freplace_allc           
         |s| dddfS |                                 s| dddfS ||k    r| dddfS dt          fdt          fdt          fd	t          fd
t
          fdt          fdt          fdt          fdt          fg	}ddh}|D ]\  }} || |          }|rt          |          dk    r-|s+t          | |          }	| dddt          |           d|	 fc S |r3t          |          dk    r ||v r| dddt          |           d| dfc S |dk    rt          | |||          }
|
r| dd|
fc S t          || |          }|dk    rt          | |||          }t          | |||dk    r|nd          }|t          |          |dfc S | dddfS )a)  
    Find and replace text using a chain of increasingly fuzzy matching strategies.

    Args:
        content: The file content to search in
        old_string: The text to find
        new_string: The replacement text
        replace_all: If True, replace all occurrences; if False, require uniqueness

    Returns:
        Tuple of (new_content, match_count, strategy_name, error_message)
        - If successful: (modified_content, number_of_replacements, strategy_used, None)
        - If failed: (original_content, 0, None, error_description)
    r   Nzold_string cannot be emptyuA   old_string is only whitespace — provide non-blank text to matchz'old_string and new_string are identicalexactline_trimmedwhitespace_normalizedindentation_flexibleescape_normalizedtrimmed_boundaryunicode_normalizedblock_anchorcontext_awarer&   zFound zc matches for old_string. Provide more context to make it unique, or use replace_all=True. Matches:
z approximate matches via the 'z' strategy; replace_all only applies to exact matches. Provide the precise text (whitespace included) so an exact/line-trimmed match can be made.)r   z1Could not find a match for old_string in the file)r   _strategy_exact_strategy_line_trimmed_strategy_whitespace_normalized_strategy_indentation_flexible_strategy_escape_normalized_strategy_trimmed_boundary_strategy_unicode_normalized_strategy_block_anchor_strategy_context_awarer   r7   _detect_escape_drift_maybe_unescape_new_string _preserve_unicode_in_replacement_apply_replacements)r   r   r   r8   
strategies_SIMILARITY_STRATEGIESstrategy_namestrategy_fnr"   	locations	drift_erreffective_newnew_contents                r   fuzzy_find_and_replacerX   w   s      >4!=== e
 4!dddZ4!JJJ 
/"	/0	 "AB	!?@	9:	78	;<	/0	12
.J" -o>&0 RB RB"{+gz22 O	B7||a3GWEE	4-S\\ - -!*- -     s7||a//ME[4[4[4=S\\ = =%= = =   " ''0':zZZ	 7"AtY66662 7GW M  444 @Wj-! ! .-)6')A)A::t  K GmTAAAA_O	Bd AtPPPr   c                      d|vrd|vrdS d                      fd|D                       }dD ]!}||v r||v r||vr|d         }d|d	|d
c S "dS )u'  Detect tool-call escape-drift artifacts in new_string.

    Looks for ``\'`` or ``\"`` sequences that are present in both
    old_string and new_string (i.e. the model copy-pasted them as "context"
    it intended to preserve) but don't exist in the matched region of the
    file. That pattern indicates the transport layer inserted spurious
    shell-style escapes around apostrophes or quotes — writing new_string
    verbatim would literally insert ``\'`` into source code.

    Returns an error string if drift is detected, None otherwise.
    \'\"N c              3   2   K   | ]\  }}||         V  d S N .0r0   endr   s      r   	<genexpr>z'_detect_escape_drift.<locals>.<genexpr>  0      KKZUCgeCi0KKKKKKr   )rZ   r[   r&   zNEscape-drift detected: old_string and new_string contain the literal sequence a   but the matched region of the file does not. This is almost always a tool-call serialization artifact where an apostrophe or quote got prefixed with a spurious backslash. Re-read the file with read_file and pass old_string/new_string without backslash-escaping z characters.)r.   )r   r"   r   r   matched_regionssuspectplains   `      r   rL   rL      s      J5
#:#:t ggKKKK7KKKKKO!  j  W
%:%:wo?]?]AJE<(/< < ',< < <   4r   linec                     d}|t          |           k     r,| |         dv r"|dz  }|t          |           k     r
| |         dv "| d|         S )z=Return the leading whitespace prefix of a line (spaces/tabs).r   r   	r&   Nr   )rh   is     r   _leading_whitespacern   (  sY    	A
c$ii--DG{22	Q c$ii--DG{228Or   c                 f    |                      d          D ]}|                                r|c S dS )zReturn the first line of ``text`` that has any non-whitespace content.

    Returns ``None`` if no such line exists (text is empty or all whitespace).
    r%   N)splitr   )r   rh   s     r   _first_meaningful_linerq   0  sB    
 

4    ::<< 	KKK	4r   file_regionc                 N   |s|S t          |          }t          |           }|||S t          |          }t          |          }||k    r|S g }|                    d          D ]}|                                s|                    |           ,t          |          }	|	                    |          r0|t          |          d         }
|                    ||
z              |                    ||                    d          z              d                    |          S )a[  Adjust ``new_string`` so its indentation matches ``file_region``.

    Used after a non-exact fuzzy match: the LLM may have sent old_string and
    new_string with a different indent than the file actually has (e.g.
    2-space indent in tool args vs 4-space indent on disk). The fuzzy
    strategy successfully matched anyway, but writing ``new_string`` verbatim
    would corrupt the file's indentation.

    Approach:

    1. For each non-blank line in ``new_string``, compute its indent
       *relative* to the shallowest non-blank line of ``old_string`` (the
       LLM's base indent).
    2. Anchor that relative indent onto the file's actual base indent (the
       leading whitespace of the file_region's first non-blank line).
    3. Re-emit each non-blank line as ``file_base + (line_indent - llm_base)``.

    Blank lines and lines less-indented than the LLM's base are anchored
    directly to the file's base indent.

    No-op cases (returns ``new_string`` unchanged):
    - file_region or old_string has no meaningful line
    - LLM base indent equals file base indent
    - new_string is empty
    Nr%    	)	rq   rn   rp   r   r-   
startswithr   lstripr.   )rr   r   r   	old_first
file_first
old_indentfile_indent	out_linesrh   line_indent	remainders              r   _reindent_replacementr~   ;  sF   4  &z22I'44JJ.$Y//J%j11K[   I  && ? ?zz|| 	T""")$//!!*-- 	? S__--.I[945555 [4;;u+=+==>>>>99Yr   c                     d| vrd| vr| S d                     fd|D                       }| }d|v rd|v r|                    dd          }d|v rd|v r|                    dd          }|S )u  Conditionally unescape ``\t``/``\r`` in new_string.

    LLMs frequently send the two-character sequences ``\t`` (backslash + t)
    and ``\r`` (backslash + r) inside JSON tool-call arguments where they
    meant a real tab or carriage-return byte. Writing the string verbatim
    corrupts tab-indented files with literal backslash-letter pairs.

    The unescape is only applied per-sequence when the *matched region of
    the file* actually contains the corresponding control character — that
    is, we only convert ``\t`` -> tab when the file region we're replacing
    contains a real tab byte. Files that legitimately contain the literal
    two-character string ``"\t"`` (e.g. a Python source line that defines
    ``sep = "\t"``) get a backslash+t in the matched region instead of a
    tab, so we leave new_string alone.

    ``\n`` is intentionally excluded: newlines serialize correctly through
    JSON and rewriting backslash-n would corrupt escape sequences in
    string literals far more often than it would help.
    \t\rr\   c              3   2   K   | ]\  }}||         V  d S r^   r_   r`   s      r   rc   z-_maybe_unescape_new_string.<locals>.<genexpr>  rd   r   rk   )r.   r   )r   r   r"   re   outs    `   r   rM   rM   |  s    0 J5
#:#:ggKKKK7KKKKKO
C||//kk%&&||//kk%&&Jr   c                 (    d                      fd|D                       }t          |          }t          |          }||k    r|S t          |          }i }t          |dd                   D ]\  }	}
|
|vr|	||
<   t	          d||          }|                                }g }|D ]\  }}}}}|dk    ry|                    |d          }|}|t          |          k     r0||         |k     r$|dz  }|t          |          k     r||         |k     $|                    |||                    |dk    r|                    |||                    |d	k    r|d
k    r|                    |||                    d                     |          S )u  Preserve Unicode characters from the file in the replacement string.

    When strategy 7 (unicode_normalized) matched, the file has Unicode
    characters (em-dashes, smart quotes, ellipsis, non-breaking spaces)
    but old_string/new_string from the LLM are ASCII equivalents.
    Writing new_string verbatim would silently corrupt the file's
    Unicode — em-dashes become two hyphens, smart quotes become
    straight quotes.

    This function aligns the replacement with the file's actual Unicode
    by diffing old_string→new_string and applying only the actual edits
    to the file's original text, preserving Unicode for unchanged portions.
    r\   c              3   2   K   | ]\  }}||         V  d S r^   r_   r`   s      r   rc   z3_preserve_unicode_in_replacement.<locals>.<genexpr>  s0      GG'%),GGGGGGr   Nr'   equalr   r&   r   deleteinsert)	r.   r   _build_orig_to_norm_map	enumerater   get_opcodesgetr   r-   )r   r"   r   r   rr   norm_old	norm_filefile_orig_to_normfile_norm_to_origorig_posnpsmopcodesresult_partstagi1i2j1j2
orig_startorig_ends   `                    r   rN   rN     s   $ ''GGGGwGGGGGK "*--H";//I 9 0<<(*!"3CRC"899 - -"&&&$,b! 
x	4	4BnnG !L& 3 3RR'>>*..r155J!H3{++++%h/"44A 3{++++%h/"44 Jx,? @AAAAI
2b5 12222H__H__
2b5 122277<   r   c                     t          |d d          }| }|D ]=\  }}|| ||         }t          |||          }	n|}	|d|         |	z   ||d         z   }>|S )a  
    Apply replacements at the given positions.

    Args:
        content: Original content
        matches: List of (start, end) positions to replace
        new_string: Replacement text
        old_string: When non-None, signals that the match came from a
            non-exact fuzzy strategy; ``new_string`` is re-indented to
            match the file's actual indentation before substitution.

    Returns:
        Content with replacements applied
    c                     | d         S Nr   r_   xs    r   <lambda>z%_apply_replacements.<locals>.<lambda>  s
    1Q4 r   T)keyreverseN)sortedr~   )
r   r"   r   r   sorted_matchesresultr0   rb   rr   adjusteds
             r   rO   rO     s    $ GFFFNF$ : :
s!!%),K,[*jQQHH!H(*VCDD\9Mr   patternc                     g }d}	 |                      ||          }|dk    rn:|                    ||t          |          z   f           |t          |          z   }W|S )zStrategy 1: Exact string match.r   Tr'   )r,   r-   r   )r   r   r"   r0   poss        r   rC   rC     sp    GE
#ll7E**"99S3w<</0111 c'll"
# Nr   c                     d |                     d          D             }d                    |          }|                      d          }d |D             }t          | ||||          S )z
    Strategy 2: Match with line-by-line whitespace trimming.
    
    Strips leading/trailing whitespace from each line before matching.
    c                 6    g | ]}|                                 S r_   r   ra   rh   s     r   
<listcomp>z*_strategy_line_trimmed.<locals>.<listcomp>!  s     BBBdTZZ\\BBBr   r%   c                 6    g | ]}|                                 S r_   r   r   s     r   r   z*_strategy_line_trimmed.<locals>.<listcomp>%  s     GGG

GGGr   )rp   r.   _find_normalized_matches)r   r   pattern_linespattern_normalizedcontent_linescontent_normalized_liness         r   rD   rD     s}     CBgmmD.A.ABBBM=11MM$''MGGGGG $ 8#  r   c                 ~    d } ||          } ||           }t          ||          }|sg S t          | ||          S )zC
    Strategy 3: Collapse multiple whitespace to single space.
    c                 .    t          j        dd|           S )Nz[ \t]+r   )resubss    r   	normalizez2_strategy_whitespace_normalized.<locals>.normalize2  s    via(((r   )rC   _map_normalized_positions)r   r   r   r   content_normalizedmatches_in_normalizeds         r   rE   rE   .  sj    ) ) ) #7++"7++ ,,>@RSS  	 %W.@BWXXXr   c           	          |                      d          }d |D             }d |                     d          D             }t          | |||d                    |                    S )z
    Strategy 4: Ignore indentation differences entirely.
    
    Strips all leading whitespace from lines before matching.
    r%   c                 6    g | ]}|                                 S r_   rv   r   s     r   r   z2_strategy_indentation_flexible.<locals>.<listcomp>J  s     FFFdkkmmFFFr   c                 6    g | ]}|                                 S r_   r   r   s     r   r   z2_strategy_indentation_flexible.<locals>.<listcomp>K  s     CCCtT[[]]CCCr   )rp   r   r.   )r   r   r   content_stripped_linesr   s        r   rF   rF   C  ss     MM$''MFFFFFCCw}}T/B/BCCCM# 6=))  r   c                 N    d } ||          }||k    rg S t          | |          S )zt
    Strategy 5: Convert escape sequences to actual characters.
    
    Handles \n -> newline, \t -> tab, etc.
    c                 ~    |                      dd                               dd                               dd          S )Nz\nr%   r   rk   r   r   )r   r   s    r   unescapez-_strategy_escape_normalized.<locals>.unescapeY  s6    yy%%--eT::BB5$OOOr   )rC   )r   r   r   pattern_unescapeds       r   rG   rG   S  sG    P P P !))G##	7$5666r   c           	         |                     d          }|sg S |d                                         |d<   t          |          dk    r|d                                         |d<   d                    |          }|                      d          }g }t          |          }t	          t          |          |z
  dz             D ]}||||z            }|                                }	|	d                                         |	d<   t          |	          dk    r|	d                                         |	d<   d                    |	          |k    r<t          ||||z   t          |                     \  }
}|                    |
|f           |S )z
    Strategy 6: Trim whitespace from first and last lines only.
    
    Useful when the pattern boundaries have whitespace differences.
    r%   r   r&   r'   )rp   r   r   r.   rangecopy_calculate_line_positionsr-   )r   r   r   modified_patternr   r"   pattern_line_countrm   block_linescheck_lines	start_posend_poss               r   rH   rH   f  s    MM$''M 	 %Q'--//M!
=A)"-3355byy//MM$''M G]++3}%%(::Q>?? 1 1#Aa*<&<$<= "&&(($Q--//A{a)"o3355KO99[!!%555!:q!&8"8#g,," "Iw NNIw/000Nr   originalc                     g }d}| D ]G}|                     |           t                              |          }||t          |          ndz  }H|                     |           |S )u  Build a list mapping each original character index to its normalized index.

    Because UNICODE_MAP replacements may expand characters (e.g. em-dash → '--',
    ellipsis → '...'), the normalised string can be longer than the original.
    This map lets us convert positions in the normalised string back to the
    corresponding positions in the original string.

    Returns a list of length ``len(original) + 1``; entry ``i`` is the
    normalised index that character ``i`` maps to.
    r   Nr&   )r-   r   r   r   )r   r   norm_posr   r   s        r   r   r     sr     FH 9 9ht$$!1CIIIq8
MM(Mr   orig_to_normnorm_matchesc                 ,   i }t          | dd                   D ]\  }}||vr|||<   g }t          |           dz
  }|D ]T\  }}||vr
||         }	|	}
|
|k     r#| |
         |k     r|
dz  }
|
|k     r| |
         |k     |                    |	|
f           U|S )zNConvert (start, end) positions in the normalised string to original positions.Nr'   r&   )r   r   r-   )r   r   norm_to_orig_startr   r   resultsorig_len
norm_startnorm_endr   r   s              r   _map_positions_norm_to_origr     s     *,'SbS(9:: 4 4(---+3x(%'G<  1$H , 
/ 
/
H///'
3
 !!l8&<x&G&GMH !!l8&<x&G&G 	
H-....Nr   c                     t          |          }t          |           }|| k    r||k    rg S t          ||          }|st          ||          }|sg S t          |           }t	          ||          S )u  Strategy 7: Unicode normalisation.

    Normalises smart quotes, em/en-dashes, ellipsis, and non-breaking spaces
    to their ASCII equivalents in both *content* and *pattern*, then runs
    exact and line_trimmed matching on the normalised copies.

    Positions are mapped back to the *original* string via
    ``_build_orig_to_norm_map`` — necessary because some UNICODE_MAP
    replacements expand a single character into multiple ASCII characters,
    making a naïve position copy incorrect.
    )r   rC   rD   r   r   )r   r   norm_patternnorm_contentr   r   s         r   rI   rI     s     &g..L%g..Lw<7#:#:	"<>>L J-lLII 	*733L&|\BBBr   c           	         t          |          }t          |           }|                    d          }t          |          dk     rg S |d                                         }|d                                         }|                    d          }|                     d          }t          |          }	g }
t	          t          |          |	z
  dz             D ]Y}||                                         |k    r9|||	z   dz
                                           |k    r|
                    |           Zg }t          |
          }|dk    rdnd}|
D ]}|	dk    rd}nfd                    ||dz   ||	z   dz
                     }d                    |dd                   }t          d	||                                          }||k    r<t          ||||	z   t          |                     \  }}|                    ||f           |S )
z
    Strategy 8: Match by anchoring on first and last lines.
    Adjusted with permissive thresholds and unicode normalization.
    r%      r   r'   r&   g      ?gffffff?      ?N)
r   rp   r   r   r   r-   r.   r   ratior   )r   r   r   r   r   
first_line	last_linenorm_content_linesorig_content_linesr   potential_matchesrm   r"   candidate_count	threshold
similaritycontent_middlepattern_middler   r   s                       r   rJ   rJ     s5    &g..L%g..L &&t,,M
=A	q!''))Jb!''))I &++D11 t,,]++3)**-??!CDD ( (q!''))Z77q#559:@@BBiOO$$Q'''G+,,O
 (1,,$I 1 1""JJ "YY'9!A#a@R>RST>T:T'UVVN!YY}QrT':;;N(~~NNTTVVJ""!:"Aq+='=s7||" "Iw NNIw/000Nr   c           	      z   |                     d          }|                      d          }|sg S t          |          }|t          |          k    rg S |d                                         }|d                                         }d}dt          dt          dt          fd}g }	t          t          |          |z
  d	z             D ]}
||
|
|z            } |||d                                                   |k     r8 |||d                                                   |k     rad
}t          ||          D ]B\  }}|                                }|s |||                                          dk     rd} nC|r<t          ||
|
|z   t          |                     \  }}|	                    ||f           |	S )ai  
    Strategy 9 (last resort): anchored line-by-line similarity.

    Only considers blocks whose first AND last lines closely match the
    pattern's first/last lines (an anchor pre-filter), then requires EVERY
    non-blank pattern line to be highly similar (>=0.80) to the aligned
    content line. The anchor filter keeps this from being an O(file x pattern)
    scan on every miss, and the all-lines requirement stops a single
    coincidental line-match from silently replacing an unrelated block
    (the old 50%-of-lines threshold accepted half-garbage patterns and
    destroyed the non-matching lines).
    r%   r   r'   g?abr   c                 X    | |k    rdS t          d | |                                          S )Nr   )r   r   )r   r   s     r   _simz%_strategy_context_aware.<locals>._sim7  s.    663tQ**00222r   r&   TF)	rp   r   r   strfloatr   zipr   r-   )r   r   r   r   r   	first_patlast_patANCHOR_THRESHOLDr   r"   rm   r   	all_matchp_linec_line
p_strippedr   r   s                     r   rK   rK     s    MM$''MMM$''M 	]++C....	
 a &&((IR &&((H3 3 3 3 3 3 3
 G3}%%(::Q>?? 1 1#Aa*<&<$<= 4	;q>//11225EEE4+b///11225EEE 	!-== 	 	NFFJ tJ//$66!	 7  	1!:q!&8"8#g,," "Iw NNIw/000Nr   r   
start_lineend_linecontent_lengthc                     t          d | d|         D                       }t          d | d|         D                       dz
  }t          ||          }||fS )a  Calculate start and end character positions from line indices.

    Args:
        content_lines: List of lines (without newlines)
        start_line: Starting line index (0-based)
        end_line: Ending line index (exclusive, 0-based)
        content_length: Total length of the original content string

    Returns:
        Tuple of (start_pos, end_pos) in the original content
    c              3   :   K   | ]}t          |          d z   V  dS r&   Nrl   r   s     r   rc   z,_calculate_line_positions.<locals>.<genexpr>l  s,      IIdCIIMIIIIIIr   Nc              3   :   K   | ]}t          |          d z   V  dS r  rl   r   s     r   rc   z,_calculate_line_positions.<locals>.<genexpr>m  s,      EED#d))a-EEEEEEr   r&   )summin)r   r  r  r  r   r   s         r   r   r   _  sm     IImKZK.HIIIIIIEEM)8),DEEEEEIG.'**Ggr   r   r   c           	      `   |                     d          }t          |          }g }t          t          |          |z
  dz             D ]d}d                    ||||z                      }	|	|k    r<t	          ||||z   t          |                     \  }
}|                    |
|f           e|S )a  
    Find matches in normalized content and map back to original positions.
    
    Args:
        content: Original content string
        content_lines: Original content split by lines
        content_normalized_lines: Normalized content lines
        pattern: Original pattern
        pattern_normalized: Normalized pattern
    
    Returns:
        List of (start, end) positions in the original content
    r%   r&   )rp   r   r   r.   r   r-   )r   r   r   r   r   pattern_norm_linesnum_pattern_linesr"   rm   blockr   r   s               r   r   r   r  s      ,11$77.//G3/003DDqHII 	1 	1		21Q9J5J3JKLL&&&!:q!&7"7W" "Iw NNIw/000Nr   
normalizednormalized_matchesc           
         |sg S g }d}d}|t          |           k     r|t          |          k     r| |         ||         k    r |                    |           |dz  }|dz  }n| |         dv rI||         dk    r=|                    |           |dz  }|t          |           k     r| |         dvr|dz  }n?| |         dv r|                    |           |dz  }n|                    |           |dz  }|t          |           k     r|t          |          k     |t          |           k     r:|                    t          |                     |dz  }|t          |           k     :i }i }t          |          D ]\  }}	|	|vr|||	<   |||	<   g }
|D ]\  }|v r	|         }n(t          fdt          |          D                       }|dz
  |v r||dz
           dz   }n||z
  z   }|t          |          k     rN||dz
           dk    r?|t          |           k     r,| |         dv r"|dz  }|t          |           k     r
| |         dv "|
                    |t          |t          |                     f           |
S )z
    Map positions from normalized string back to original.
    
    This is a best-effort mapping that works for whitespace normalization.
    r   r&   rt   r   c              3   .   K   | ]\  }}|k    |V  d S r^   r_   )ra   rm   nr   s      r   rc   z,_map_normalized_positions.<locals>.<genexpr>  s+      VV41aa:ooQooooVVr   )r   r-   r   r	  )r   r  r  r   orig_idxnorm_idxr   norm_to_orig_endr   r   original_matchesr   r   r   r   s                 @r   r   r     s-     	 LHH
S]]
"
"x#j//'A'AHH!555)))MHMHHh5((Z-AS-H-H)))MH#h--''HX,>e,K,KAh5(()))MHH )))MH' S]]
"
"x#j//'A'A, S]]
"
"C
OO,,,A S]]
"
"
 '55 . .(---+3x(%-""  2 L L
H++++J7JJ VVVV9\+B+BVVVVVJ a<+++'159HH!X
%:;H c*oo%%*X\*Bc*I*IS]]**x/AU/J/JA S]]**x/AU/J/J 	S3x==-I-I JKKKKr   c                    d}g }|t          |           k     rO| |         dv rE|                    | |         dk    rdnd           |dz  }|t          |           k     r
| |         dv Ed                    |          | |d         z   S )	u   Render leading whitespace visibly (→ = tab, · = space).

    Only the leading run is visualized — interior spacing is rarely the
    culprit and full visualization makes lines unreadable.
    r   rj   rk   u   →   ·r&   r\   N)r   r-   r.   )rh   rm   prefixs      r   _visualize_whitespacer    s     	
AF
c$ii--DG{22tAw$eeD999	Q c$ii--DG{22 776??T!""X%%r   r      context_linesmax_resultsc                 z   | r|sdS |                                  }|                                 |rsdS |d                                         }|sd |D             }|sdS |d         }g }t                    D ]\\  }}	|	                                }
|
st          d||
                                          }|dk    r|                    ||f           ]|sdS |                    d            |d|         }g }t                      }|D ]\  }}t          d||z
            t          t                    |t          |          z   |z             }|f}||v rQ|                    |           d                    fd	t          |z
            D                       }|                    |           |sdS d
                    |          }|d         d                  }|                                |k    r8||d         k    r,|dt          |           dt          |d                    dz  }|S )zFind lines in content most similar to old_string for "did you mean?" feedback.

    Returns a formatted string showing the closest matching lines with context,
    or empty string if no useful match is found.
    r\   r   c                 ^    g | ]*}|                                 |                                 +S r_   r   )ra   ls     r   r   z&find_closest_lines.<locals>.<listcomp>  s-    @@@Aaggii@aggii@@@r   Ng333333?c                     | d          S r   r_   r   s    r   r   z$find_closest_lines.<locals>.<lambda>  s    qte r   )r   r%   c              3   F   K   | ]}|z   d z   dd|z             V  dS )r&   4dz| Nr_   )ra   jr   r0   s     r   rc   z%find_closest_lines.<locals>.<genexpr>&  sY       
 
 qy1}====#;==
 
 
 
 
 
r   z
---
r&   uF   

Whitespace difference detected (→ = tab, · = space):
  file has: z
  you sent: z.
Use the exact whitespace shown in 'file has'.)
splitlinesr   r   r   r   r-   sortsetmaxr	  r   addr.   r   r  )r   r   r  r  	old_linesanchor
candidatesscoredrm   rh   strippedr   toppartsseen_ranges_line_idxrb   r   r5   r   	best_liner   r0   s                         @@r   find_closest_linesr5    s     W r%%''I&&((M M r q\!!F @@@@@
 	2A F]++ & &4::<< 	fh77==??3;;MM5!*%%% r KKOOK$$$
+
CE%%K  8Ax-/00#m$$hY&?-&OPPcl+)) 
 
 
 
 
3;''
 
 
 
 
 	W r^^E""F c!fQi(IF""yIaL'@'@<0;;< <01>>< < <	
 Mr   errormatch_countc                 v    |dk    rdS | r|                      d          sdS t          ||          }|sdS d|z   S )u  Return a '\n\nDid you mean...' snippet for plain no-match errors.

    Gated so the hint only fires for actual "old_string not found" failures.
    Ambiguous-match ("Found N matches"), escape-drift, and identical-strings
    errors all have ``match_count == 0`` but a "did you mean?" snippet would
    be misleading — those failed for unrelated reasons.

    Returns an empty string when there's nothing useful to append.
    r   r\   zCould not findz&

Did you mean one of these sections?
)ru   r5  )r6  r7  r   r   hints        r   format_no_match_hintr:  B  s^     ar (()9:: rj'22D r6==r   )r!   )Fr^   )r   r  ))__doc__r   typingr   r   r   r   difflibr   r   r   r   boolr    intr7   rX   rL   rn   rq   r~   rM   rN   rO   rC   rD   rE   rF   rG   rH   r   r   rI   rJ   rK   r   r   r   r  r5  r:  r_   r   r   <module>r@     s   < 
			 2 2 2 2 2 2 2 2 2 2 2 2 # # # # # #cScS d c e	 s	 c c S c S  c!  S!  #+C!" c#" !S+  0S S    % % %# %$ % % % %8 () S 4c3h3H !$-0   4 16FQ FQC FQS FQc FQ)-FQ:?S(SV-YabeYf@f:gFQ FQ FQ FQR%# %U38_0E %%(%69%>Fsm% % % %Pc c     #    > s >  >  > QT >  >  >  > B!3 !(+!(,U38_(=!BE! ! ! !HA!A!c3h0A!A!!$A! 	A! A! A! A!J FJ  tE#s(O/D $'5=c]NQ   HS 3 4c3h3H    $C # $uS#X:O    (YS Y3 Y4cSVhCX Y Y Y Y*C # $uSRUXBW     7 7s 7tE#s(O?T 7 7 7 7&' 'c 'd5c?>S ' ' ' 'Tc d3i    *s)uS#X' 
%S/   :C# C CU3PS8_@U C C C C>5C 5# 5$uS#X:O 5 5 5 5p?S ?3 ?4c3h;P ? ? ? ?LT#Y C (+=@EJ3PS8_   & c  $s)  8<S	 '* @C HLUSVX[S[_H]       FN N N37c3h3HNMQRWX[]`X`RaMbN N N Nb& & & & & &K K3 K KS K[^ Kgj K K K K\> >C >%(>36>;>> > > > > >r   