
    `gjܕ                        d Z ddlZddlmZmZmZmZ ddlmZ ddddddd	d
dZ	de
de
fdZ	 d>d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dZde
de
deeeef      de
fdZde
deeeef      de
de
de
f
dZ	 d?de
deeeef      de
dee
   de
f
dZde
de
deeeef      fdZde
de
deeeef      fd Zde
de
deeeef      fd!Zde
de
deeeef      fd"Zde
de
deeeef      fd#Zde
de
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
de
deeeef      fd*Zde
de
deeeef      fd+Zde
de
deeeef      fd,Z d-ee
   d.ed/ed0edeeef   f
d1Z!de
d-ee
   d2ee
   de
d3e
deeeef      fd4Z"d%e
d5e
d6eeeef      deeeef      fd7Z#d@de
de
d8ed9ede
f
d:Z$d;ee
   d<ede
de
de
f
d=Z%y)AaP  
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)SequenceMatcher"'z---z... )u   “u   ”u   ‘u   ’u   —u   –u   …    textreturnc                 b    t         j                         D ]  \  }}| j                  ||      }  | S )zBNormalizes Unicode characters to their standard ASCII equivalents.)UNICODE_MAPitemsreplace)r   charrepls      D/root/.hermes/venv/lib/python3.12/site-packages/tools/fuzzy_match.py_unicode_normalizer   +   s3    !'') (
d||D$'(K    content
old_string
new_stringreplace_allc           
         |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 ]  \  }} || |      }|st        |      dkD  r|s| dddt        |       dfc S |dk7  rt        | |||      }|r| dd|fc S t        || |      }	|dk(  rt        | |||	      }	t        | ||	|dk7  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 emptyz'old_string and new_string are identicalexactline_trimmedwhitespace_normalizedindentation_flexibleescape_normalizedtrimmed_boundaryunicode_normalizedblock_anchorcontext_aware   zFound zY matches for old_string. Provide more context to make it unique, or use replace_all=True.)r   z1Could not find a match for old_string in the file)_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_awarelen_detect_escape_drift_maybe_unescape_new_string _preserve_unicode_in_replacement_apply_replacements)r   r   r   r   
strategiesstrategy_namestrategy_fnmatches	drift_erreffective_newnew_contents              r   fuzzy_find_and_replacer<   2   s     4!===Z4!JJJ 
/"	/0	 "AB	!?@	9:	78	;<	/0	12
.J '1 DB"{gz27|a4S\N +W X  '0':zZ	"AtY662 7GWM  44 @Wj-! .-)6')A:tK GmTAAIDBN AtPPPr   r8   c                      d|vrd|vrydj                   fd|D              }dD ]   }||v s||v s||vs|d   }d|d	|d
c S  y)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   .   K   | ]  \  }}||   y wN .0startendr   s      r   	<genexpr>z'_detect_escape_drift.<locals>.<genexpr>        KZUCgeC0K   )r>   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.)join)r   r8   r   r   matched_regionssuspectplains   `      r   r1   r1      s      J5
#: ggK7KKO! j W
%:wo?]AJE((/{ 3&
 ',Yl< r   linec                 p    d}|t        |       k  r"| |   dv r|dz  }|t        |       k  r| |   dv r| d| S )z=Return the leading whitespace prefix of a line (spaces/tabs).r   )r   	r&   Nr0   )rO   is     r   _leading_whitespacerT      sI    	A
c$i-DG{2	Q c$i-DG{28Or   c                 X    | j                  d      D ]  }|j                         s|c S  y)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).
    
N)splitstrip)r   rO   s     r   _first_meaningful_linerY      s/    
 

4  ::<K r   file_regionc                    |s|S t        |      }t        |       }|||S t        |      }t        |      }||k(  r|S g }|j                  d      D ]  }|j                         s|j	                  |       %t        |      }	|	j                  |      r#|t        |      d }
|j	                  ||
z          d|j	                  ||j                  d      z           dj                  |      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
    NrV    	)	rY   rT   rW   rX   append
startswithr0   lstriprK   )rZ   r   r   	old_first
file_first
old_indentfile_indent	out_linesrO   line_indent	remainders              r   _reindent_replacementrg      s    4 &z2I'4JJ.$Y/J%j1K[  I  & ?zz|T")$/!!*- S_-.I[945 [4;;u+==>? 99Yr   c                     d| vrd| vr| S dj                  fd|D              }| }d|v rd|v r|j                  dd      }d|v rd|v r|j                  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   .   K   | ]  \  }}||   y wrB   rC   rD   s      r   rH   z-_maybe_unescape_new_string.<locals>.<genexpr>6  rI   rJ   rQ   )rK   r   )r   r   r8   rL   outs    `   r   r2   r2     st    0 J5
#:ggK7KKO
C|/kk%&|/kk%&Jr   c                 z    dj                   fd|D              }t        |      }t        |      }||k7  r|S t        |      }i }t        |dd       D ]  \  }	}
|
|vs|	||
<    t	        d||      }|j                         }g }|D ]  \  }}}}}|dk(  r[|j                  |d      }|}|t        |      k  r$||   |k  r|dz  }|t        |      k  r	||   |k  r|j                  |||        i|dk(  r|j                  |||        |d	k(  r|d
k(  s|j                  |||         dj                  |      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   .   K   | ]  \  }}||   y wrB   rC   rD   s      r   rH   z3_preserve_unicode_in_replacement.<locals>.<genexpr>Q  s     G'%,GrJ   Nequalr   r&   r   deleteinsert)	rK   r   _build_orig_to_norm_map	enumerater   get_opcodesgetr0   r]   )r   r8   r   r   rZ   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   r3   r3   ?  s   $ ''GwGGK "*-H";/I 9 0<(*!"3CR"89 -"&&$,b!-
 
x	4BnnG !L& 3RR'>*..r15J!H3{++%h/"4A 3{++%h/"4 Jx @AI
2b 12H_H_
2b 12!3$ 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   rC   xs    r   <lambda>z%_apply_replacements.<locals>.<lambda>  s
    1Q4 r   T)keyreverseN)sortedrg   )
r   r8   r   r   sorted_matchesresultrF   rG   rZ   adjusteds
             r   r4   r4     sq    $ GFNF$ :
s!!%,K,[*jQH!H(*VCD\9: Mr   patternc                     g }d}	 | j                  ||      }|dk(  r	 |S |j                  ||t        |      z   f       |t        |      z   }H)zStrategy 1: Exact string match.r   rp   )findr]   r0   )r   r   r8   rF   poss        r   r'   r'     sa    GE
ll7E*"9 N 	S3w</01 c'l" r   c                 
   |j                  d      D cg c]  }|j                          }}dj                  |      }| j                  d      }|D cg c]  }|j                          }}t        | ||||      S c c}w c c}w )z
    Strategy 2: Match with line-by-line whitespace trimming.
    
    Strips leading/trailing whitespace from each line before matching.
    rV   )rW   rX   rK   _find_normalized_matches)r   r   rO   pattern_linespattern_normalizedcontent_linescontent_normalized_liness          r   r(   r(     s     /6mmD.ABdTZZ\BMB=1MM$'M9FG

GG $ 8#  C  Hs   A;B c                 b    d } ||      } ||       }t        ||      }|sg S t        | ||      S )zC
    Strategy 3: Collapse multiple whitespace to single space.
    c                 0    t        j                  dd|       S )Nz[ \t]+r   )resubss    r   	normalizez2_strategy_whitespace_normalized.<locals>.normalize  s    vvia((r   )r'   _map_normalized_positions)r   r   r   r   content_normalizedmatches_in_normalizeds         r   r)   r)     sK    ) #7+"7+ ,,>@RS 	 %W.@BWXXr   c           	         | j                  d      }|D cg c]  }|j                          }}|j                  d      D cg c]  }|j                          }}t        | |||dj                  |            S c c}w c c}w )z
    Strategy 4: Ignore indentation differences entirely.
    
    Strips all leading whitespace from lines before matching.
    rV   )rW   r_   r   rK   )r   r   r   rO   content_stripped_linesr   s         r   r*   r*     sz     MM$'M8EFdkkmFF/6}}T/BCtT[[]CMC# 6=)  GCs   A9A>c                 >    d } ||      }||k(  rg S t        | |      S )zt
    Strategy 5: Convert escape sequences to actual characters.
    
    Handles \n -> newline, \t -> tab, etc.
    c                 f    | j                  dd      j                  dd      j                  dd      S )Nz\nrV   ri   rQ   rj   rl   )r   r   s    r   unescapez-_strategy_escape_normalized.<locals>.unescape  s-    yy%--eT:BB5$OOr   )r'   )r   r   r   pattern_unescapeds       r   r+   r+     s0    P !)G#	7$566r   c           	      n   |j                  d      }|sg S |d   j                         |d<   t        |      dkD  r|d   j                         |d<   dj                  |      }| j                  d      }g }t        |      }t	        t        |      |z
  dz         D ]  }||||z    }|j                         }	|	d   j                         |	d<   t        |	      dkD  r|	d   j                         |	d<   dj                  |	      |k(  sjt        ||||z   t        |             \  }
}|j                  |
|f        |S )z
    Strategy 6: Trim whitespace from first and last lines only.
    
    Useful when the pattern boundaries have whitespace differences.
    rV   r   r&   rp   )rW   rX   r0   rK   rangecopy_calculate_line_positionsr]   )r   r   r   modified_patternr   r8   pattern_line_countrS   block_linescheck_lines	start_posend_poss               r   r,   r,     sU    MM$'M	 %Q'--/M!
=A)"-335byy/MM$'M G]+3}%(::Q>? 1#Aa*<&<= "&&($Q--/A{a)"o335KO99[!%55!:q!&8"8#g,"Iw NNIw/01  Nr   originalc                     g }d}| D ]:  }|j                  |       t        j                  |      }||t        |      ndz  }< |j                  |       |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   r&   )r]   r   rw   r0   )r   r   norm_posr   r   s        r   rt   rt   /  s_     FH 9ht$!1CIq89 MM(Mr   orig_to_normnorm_matchesc                     i }t        | dd       D ]  \  }}||vs|||<    g }t        |       dz
  }|D ]D  \  }}||vr||   }	|	}
|
|k  r| |
   |k  r|
dz  }
|
|k  r	| |
   |k  r|j                  |	|
f       F |S )zNConvert (start, end) positions in the normalised string to original positions.Nrp   r&   )ru   r0   r]   )r   r   norm_to_orig_startr|   r   resultsorig_len
norm_startnorm_endr   r   s              r   _map_positions_norm_to_origr   D  s     *,'Sb(9: 4(--+3x(4 &(G< 1$H , 
/
H//'
3
 !l8&<x&GMH !l8&<x&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   r'   r(   rt   r   )r   r   norm_patternnorm_contentr   r   s         r   r-   r-   a  sf     &g.L%g.Lw<7#:	"<>L-lLI	*73L&|\BBr   c           	      4   t        |      }t        |       }|j                  d      }t        |      dk  rg S |d   j                         }|d   j                         }|j                  d      }| j                  d      }t        |      }	g }
t	        t        |      |	z
  dz         D ]G  }||   j                         |k(  s|||	z   dz
     j                         |k(  s7|
j                  |       I g }t        |
      }|dk(  rdnd}|
D ]  }|	dk  rd}nLdj                  ||dz   ||	z   dz
         }dj                  |dd       }t        d	||      j                         }||k\  s]t        ||||	z   t        |             \  }}|j                  ||f        |S )
z
    Strategy 8: Match by anchoring on first and last lines.
    Adjusted with permissive thresholds and unicode normalization.
    rV      r   rp   r&         ?gffffff?g      ?N)
r   rW   r0   rX   r   r]   rK   r   ratior   )r   r   r   r   r   
first_line	last_linenorm_content_linesorig_content_linesr   potential_matchesrS   r8   candidate_count	threshold
similaritycontent_middlepattern_middler   r   s                       r   r.   r.     s    &g.L%g.L &&t,M
=A	q!'')Jb!'')I &++D1 t,]+3)*-??!CD (q!'')Z7q#559:@@BiO$$Q'(
 G+,O
 (1,$I 1"J "YY'9!A#a@R>RST>T'UVN!YY}Qr':;N(~~NTTVJ"!:"Aq+='=s7|"Iw NNIw/01  Nr   c           	         |j                  d      }| j                  d      }|sg S g }t        |      }t        t        |      |z
  dz         D ]  }||||z    }d}t        ||      D ]G  \  }	}
t	        d|	j                         |
j                               j                         }|dk\  sC|dz  }I |t        |      dz  k\  sut        ||||z   t        |             \  }}|j                  ||f        |S )z
    Strategy 9: Line-by-line similarity with 50% threshold.
    
    Finds blocks where at least 50% of lines have high similarity.
    rV   r&   r   Ng?r   )	rW   r0   r   zipr   rX   r   r   r]   )r   r   r   r   r8   r   rS   r   high_similarity_countp_linec_linesimr   r   s                 r   r/   r/     s    MM$'MMM$'M	G]+3}%(::Q>? 1#Aa*<&<= !"!-= 	+NFF!$GMMOCd{%*%	+ !C$6$<<!:q!&8"8#g,"Iw NNIw/01" 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   8   K   | ]  }t        |      d z     ywr&   NrR   rE   rO   s     r   rH   z,_calculate_line_positions.<locals>.<genexpr>  s     IdCIMI   Nc              3   8   K   | ]  }t        |      d z     ywr   rR   r   s     r   rH   z,_calculate_line_positions.<locals>.<genexpr>  s     ED#d)a-Er   r&   )summin)r   r   r   r   r   r   s         r   r   r     sN     ImKZ.HIIIEM)8,DEEIG.'*Ggr   r   r   c           	         |j                  d      }t        |      }g }t        t        |      |z
  dz         D ]O  }dj                  ||||z          }	|	|k(  s t	        ||||z   t        |             \  }
}|j                  |
|f       Q |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
    rV   r&   )rW   r0   r   rK   r   r]   )r   r   r   r   r   pattern_norm_linesnum_pattern_linesr8   rS   blockr   r   s               r   r   r     s      ,11$7./G3/03DDqHI 	1		21Q9J5JKL&&!:q!&7"7W"Iw NNIw/0	1 Nr   
normalizednormalized_matchesc           
          |sg S g }d}d}|t        |       k  r|t        |      k  r| |   ||   k(  r|j                  |       |dz  }|dz  }nt| |   dv r9||   dk(  r1|j                  |       |dz  }|t        |       k  rA| |   dvr:|dz  }n4| |   dv r|j                  |       |dz  }n|j                  |       |dz  }|t        |       k  r|t        |      k  r|t        |       k  r.|j                  t        |             |dz  }|t        |       k  r.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  r;||dz
     dk(  r0|t        |       k  r"| |   dv r|dz  }|t        |       k  r| |   dv r|
j                  |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&   r\   r   c              3   4   K   | ]  \  }}|k\  s|  y wrB   rC   )rE   rS   nr   s      r   rH   z,_map_normalized_positions.<locals>.<genexpr>P  s     V41aa:oQVs   )r0   r]   ru   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HH!55)MHMHh5(Z-AS-H)MH#h-'HX,>e,KAh5()MH )MH' S]
"x#j/'A, S]
"C
O,A S]
"
 '5 .(--+3x(%-".  2 L
H+++J7J V9\+BVVJ a<++'159H!X
%:;H c*o%*X\*Bc*IS]*x/AU/JA S]*x/AU/J 	S3x=-I JK/L2 r   context_linesmax_resultsc                 F   | r|sy| j                         }|j                         |rsy|d   j                         }|s6|D cg c]#  }|j                         s|j                         % }}|sy|d   }g }t              D ]L  \  }	}
|
j                         }|st        d||      j	                         }|dkD  s:|j                  ||	f       N |sy|j                  d        |d| }g }t               }|D ]  \  }}t        d||z
        t        t              |t        |      z   |z         }|f}||v rB|j                  |       dj                  fdt        |z
        D              }|j                  |        |syd	j                  |      S c c}w )
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   Ng333333?c                     | d    S r   rC   r   s    r   r   z$find_closest_lines.<locals>.<lambda>  s    qte r   )r   rV   c              3   F   K   | ]  }|z   d z   dd|z         yw)r&   4dz| NrC   )rE   jr   rF   s     r   rH   z%find_closest_lines.<locals>.<genexpr>  s9      
 qy1}R =#;"<=
s   !z
---
)
splitlinesrX   ru   r   r   r]   sortsetmaxr   r0   addrK   r   )r   r   r   r   	old_linesanchorl
candidatesscoredrS   rO   strippedr   toppartsseen_ranges_line_idxrG   r   snippetr   rF   s                        @@r   find_closest_linesr  f  s    W%%'I&&(MM q\!F)2@Aaggiaggi@
@A F]+ &4::<fh7==?3;MM5!*%&  KKOK$
+
CE%K 8Ax-/0#m$hY&?-&OPcl+)) 
3;'
 
 	W >>%  Q As   FFerrormatch_countc                 ^    |dk7  ry| r| j                  d      syt        ||      }|sy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?
)r^   r  )r  r  r   r   hints        r   format_no_match_hintr    s@     a(()9:j'2D6==r   )FrB   )r      )&__doc__r   typingr   r   r   r   difflibr   r   strr   boolintr<   r1   rT   rY   rg   r2   r3   r4   r'   r(   r)   r*   r+   r,   rt   r   r-   r.   r/   r   r   r   r  r  rC   r   r   <module>r     s  < 
 2 2 # SScs	S S  16jQC jQS jQc jQ)-jQ:?S(SV-YabeYf@f:gjQZ%# %U38_0E %%(%69%>Fsm%Pc c  # > s >  >  > QT > B!3 !(+!(,U38_(=!BE!HA!A!c3h0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*C # $uSRUXBW  7 7s 7tE#s(O?T 7&' 'c 'd5c?>S 'Tc d3i *s)uS#X' 
%S/:C# C CU3PS8_@U C>5C 5# 5$uS#X:O 5p S  3  4c3h;P  NT#Y C (+=@EJ3PS8_& c  $s)  8<S	 '* @C HLUSVX[S[_H] FN N N37c3h3HNMQRWX[]`X`RaMbNb;!3 ;! ;!S ;![^ ;!gj ;!|> >C >%(>36>;>>r   