
    Fj                        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	 dAd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"dBde
de
d:ed;ede
f
d<Z#d=ee
         d>ede
de
de
f
d?Z$dS )CaP  
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 8-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                 p    t                                           D ]\  }}|                     ||          } | S )zBNormalizes Unicode characters to their standard ASCII equivalents.)UNICODE_MAPitemsreplace)r   charrepls      0/usr/local/lib/hermes-agent/tools/fuzzy_match.py_unicode_normalizer   +   s;    !'')) ( (
d||D$''K    F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 ]\  }} || |          }|rt          |          dk    r|s| dddt          |           dfc S |dk    rt          | |||          }|r| dd|fc S 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 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_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 8B 8B"{+gz22 5	B7||a4XS\\ X X X    ''0':zZZ	 7"AtY66662 7GW M .-)6')A)A::t  K GmTAAAAk5	Bp AtPPPr   r7   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 .0startendr   s      r   	<genexpr>z'_detect_escape_drift.<locals>.<genexpr>   0      KKZUCgeCi0KKKKKKr   )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   r7   r   r   matched_regionssuspectplains   `      r   r1   r1      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r0   )rM   is     r   _leading_whitespacerR      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).
    
N)splitstrip)r   rM   s     r   _first_meaningful_linerW      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
    NrT    	)	rW   rR   rU   rV   append
startswithr0   lstriprI   )rX   r   r   	old_first
file_first
old_indentfile_indent	out_linesrM   line_indent	remainders              r   _reindent_replacementre      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 rA   rB   rC   s      r   rG   z-_maybe_unescape_new_string.<locals>.<genexpr>*  rH   r   rO   )rI   r   )r   r   r7   rJ   outs    `   r   r2   r2     s    0 J5
#:#:ggKKKK7KKKKKO
C||//kk%&&||//kk%&&Jr   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   rB   xs    r   <lambda>z%_apply_replacements.<locals>.<lambda>E  s
    1Q4 r   T)keyreverseN)sortedre   )
r   r7   r   r   sorted_matchesresultrE   rF   rX   adjusteds
             r   r3   r3   3  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           |dz   }J|S )zStrategy 1: Exact string match.r   Tr&   )findr[   r0   )r   rx   r7   rE   poss        r   r'   r'   W  sg    GEll7E**"99S3w<</0111a 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 rB   rV   rD   rM   s     r   
<listcomp>z*_strategy_line_trimmed.<locals>.<listcomp>k  s     BBBdTZZ\\BBBr   rT   c                 6    g | ]}|                                 S rB   r   r   s     r   r   z*_strategy_line_trimmed.<locals>.<listcomp>o  s     GGG

GGGr   )rU   rI   _find_normalized_matches)r   rx   pattern_linespattern_normalizedcontent_linescontent_normalized_liness         r   r(   r(   d  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>.normalize|  s    via(((r   )r'   _map_normalized_positions)r   rx   r   r   content_normalizedmatches_in_normalizeds         r   r)   r)   x  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.
    rT   c                 6    g | ]}|                                 S rB   r]   r   s     r   r   z2_strategy_indentation_flexible.<locals>.<listcomp>  s     FFFdkkmmFFFr   c                 6    g | ]}|                                 S rB   r   r   s     r   r   z2_strategy_indentation_flexible.<locals>.<listcomp>  s     CCCtT[[]]CCCr   )rU   r   rI   )r   rx   r   content_stripped_linesr   s        r   r*   r*     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\nrT   rg   rO   rh   rj   )r   r   s    r   unescapez-_strategy_escape_normalized.<locals>.unescape  s6    yy%%--eT::BB5$OOOr   )r'   )r   rx   r   pattern_unescapeds       r   r+   r+     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.
    rT   r   r&   rz   )rU   rV   r0   rI   rangecopy_calculate_line_positionsr[   )r   rx   r   modified_patternr   r7   pattern_line_countrQ   block_linescheck_lines	start_posend_poss               r   r,   r,     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   getr0   )r   rv   norm_posr   r   s        r   _build_orig_to_norm_mapr     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.Nrz   r&   )	enumerater0   r[   )r   r   norm_to_orig_startorig_posr   resultsorig_len
norm_startnorm_end
orig_startorig_ends              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   r'   r(   r   r   )r   rx   norm_patternnorm_contentr   r   s         r   r-   r-     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.
    rT      r   rz   r&         ?gffffff?g      ?N)
r   rU   r0   rV   r   r[   rI   r   ratior   )r   rx   r   r   r   
first_line	last_linenorm_content_linesorig_content_linesr   potential_matchesrQ   r7   candidate_count	threshold
similaritycontent_middlepattern_middler   r   s                       r   r.   r.   +  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           	      `   |                     d          }|                      d          }|sg S g }t          |          }t          t          |          |z
  dz             D ]}||||z            }d}t          ||          D ]W\  }	}
t	          d|	                                |
                                                                          }|dk    r|dz  }X|t          |          dz  k    r<t          ||||z   t          |                     \  }}|                    ||f           |S )z
    Strategy 9: Line-by-line similarity with 50% threshold.
    
    Finds blocks where at least 50% of lines have high similarity.
    rT   r&   r   Ng?r   )	rU   r0   r   zipr   rV   r   r   r[   )r   rx   r   r   r7   r   rQ   r   high_similarity_countp_linec_linesimr   r   s                 r   r/   r/   c  sN    MM$''MMM$''M 	G]++3}%%(::Q>?? 1 1#Aa*<&<$<= !"!-== 	+ 	+NFF!$GGMMOOCd{{%*% !C$6$6$<<<!: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&   NrP   r   s     r   rG   z,_calculate_line_positions.<locals>.<genexpr>  s,      IIdCIIMIIIIIIr   Nc              3   :   K   | ]}t          |          d z   V  dS r   rP   r   s     r   rG   z,_calculate_line_positions.<locals>.<genexpr>  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
    rT   r&   )rU   r0   r   rI   r   r[   )r   r   r   rx   r   pattern_norm_linesnum_pattern_linesr7   rQ   blockr   r   s               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     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&   rZ   r   c              3   .   K   | ]\  }}|k    |V  d S rA   rB   )rD   rQ   nr   s      r   rG   z,_map_normalized_positions.<locals>.<genexpr>  s+      VV41aa:ooQooooVVr   )r0   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 X&&8H+=+F+FMH X&&8H+=+F+F 	S3x==-I-I JKKKKr   r      context_linesmax_resultsc                    | 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
                    |          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 rB   r   )rD   ls     r   r   z&find_closest_lines.<locals>.<listcomp>  s-    @@@Aaggii@aggii@@@r   Ng333333?c                     | d          S rn   rB   ro   s    r   rq   z$find_closest_lines.<locals>.<lambda>2  s    qte r   )rr   rT   c              3   F   K   | ]}|z   d z   dd|z             V  dS )r&   4dz| NrB   )rD   jr   rE   s     r   rG   z%find_closest_lines.<locals>.<genexpr>>  sY       
 
 qy1}====#;==
 
 
 
 
 
r   z
---
)
splitlinesrV   r   r   r   r[   sortsetmaxr   r0   addrI   r   )r   r   r   r   	old_linesanchor
candidatesscoredrQ   rM   strippedr   toppartsseen_ranges_line_idxrF   rr   snippetr   rE   s                       @@r   find_closest_linesr     sG     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>>%   r   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?
)r\   r   )r   r   r   r   hints        r   format_no_match_hintr  J  s^     ar (()9:: rj'22D r6==r   )FrA   )r   r   )%__doc__r   typingr   r   r   r   difflibr   r   strr   boolintr;   r1   rR   rW   re   r2   r3   r'   r(   r)   r*   r+   r,   r   r   r-   r.   r/   r   r   r   r   r  rB   r   r   <module>r
     sT   < 
			 2 2 2 2 2 2 2 2 2 2 2 2 # # # # # # SScs	 S S     16^Q ^QC ^QS ^Qc ^Q)-^Q:?S(SV-YabeYf@f:g^Q ^Q ^Q ^QB%# %U38_0E %%(%69%>Fsm% % % %Pc c     #    > s >  >  > QT >  >  >  > B!3 !(+!(,U38_(=!BE! ! ! !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        NT#Y C (+=@EJ3PS8_   & c  $s)  8<S	 '* @C HLUSVX[S[_H]       FI I I37c3h3HIMQRWX[]`X`RaMbI I I IX;! ;!3 ;! ;!S ;![^ ;!gj ;! ;! ;! ;!|> >C >%(>36>;>> > > > > >r   