
    j                    Z    d Z ddlmZ ddlZddlmZmZmZmZm	Z	 dd
Z
ddZddZg dZdS )u$  Diff-aware line-shift map for cross-edit LSP delta filtering.

When an edit deletes or inserts lines in the middle of a file, every
diagnostic below the edit point shifts to a new line number.  The
LSPService delta filter subtracts the pre-edit baseline from the
post-edit diagnostics keyed on ``(severity, code, source, message,
range)`` — without an adjustment, the shifted-but-otherwise-identical
diagnostics look brand-new and the agent gets flooded with noise.

The fix used here is the same trick git's blame and unified diff use:
build a piecewise-linear map from pre-edit line numbers to post-edit
line numbers, then apply that map to baseline diagnostics before the
set-difference.  Diagnostics whose pre-edit line is in a region the
edit deleted return ``None`` and are dropped from the baseline (they
genuinely no longer apply).

Trade-off vs. dropping range from the key entirely (the previous
fix): preserves the "new instance of an identical error at a
different line" signal — if the model introduces a second instance
of the same error class at a different location, that one will be
surfaced as new instead of swallowed by content-only dedup.

The map is derived from ``difflib.SequenceMatcher.get_opcodes()`` and
exposed as a single callable so callers don't have to reason about
diff regions.
    )annotationsN)AnyCallableDictListOptionalpre_textstr	post_textreturnCallable[[int], Optional[int]]c                    | r|                                  ng }|r|                                 ng |k    rd S t          j        |d          }|                                d	fd}|S )
a|  Build a function mapping pre-edit line numbers to post-edit line numbers.

    Lines are 0-indexed to match the LSP wire format
    (``range.start.line`` is 0-indexed).

    The returned callable takes a pre-edit 0-indexed line number and
    returns the corresponding post-edit 0-indexed line number, or
    ``None`` if that line was deleted by the edit (no post-edit
    counterpart exists).

    Cost: one ``SequenceMatcher.get_opcodes()`` call up front; the
    returned closure is O(log n) per call (binary search over opcode
    regions).  Cheap enough to call once per write/patch and apply to
    every baseline diagnostic.
    c                    | S )N )lines    4/usr/local/lib/hermes-agent/agent/lsp/range_shift.py<lambda>z"build_line_shift.<locals>.<lambda>6   s    D     F)abautojunkr   intr   Optional[int]c                    D ]B\  }}}}}|| cxk    r|k     r%n n"|dk    r
| |z
  |z   c S |dk    r d S |dk    r d S | |k     r nCr t          dt                    dz
            nd S )Nequaldeletereplacer      )maxlen)r   tagi1i2j1j2opcodes
post_liness         r   shiftzbuild_line_shift.<locals>.shift@   s     $+ 	 	CRRTB'>>"9r>)))(??44)##  44byy 
 /9Bs1c*oo)***dBr   )r   r   r   r   )
splitlinesdifflibSequenceMatcherget_opcodes)r	   r   	pre_linessmr(   r&   r'   s        @@r   build_line_shiftr/   !   s      *29##%%%rI+4<%%'''"J J    
	 9
U	K	K	KBnnGC C C C C C C0 Lr   diagDict[str, Any]r(   Optional[Dict[str, Any]]c                   |                      d          pi }|                     d          pi }|                     d          pi }t          |                     dd                    }t          |                     d|                    } ||          }|dS  ||          }||}t          |           }	|t          |                     dd                    d|t          |                     dd                    dd	|	d<   |	S )
uH  Return a copy of ``diag`` with its line range remapped through ``shift``.

    Returns ``None`` if the diagnostic's start line maps to ``None``
    (the line was deleted by the edit) — caller drops it from the
    baseline since the diagnostic no longer applies.

    Both ``start.line`` and ``end.line`` are remapped independently;
    when only the end maps to ``None`` (rare, multi-line diagnostic
    straddling the edit boundary) we collapse to a single-line range
    at the shifted start to keep the diagnostic in the baseline.

    The original ``diag`` is not mutated.
    rangestartendr   r   N	character)r   r7   )r5   r6   )getr   dict)
r0   r(   rngr5   r6   pre_start_linepre_end_linenew_start_linenew_end_lineshifteds
             r   shift_diagnostic_ranger@   [   s!    ((7


!rCGGG"E
''%..
BC61--..Nswwv~6677LU>**Nt5&&L%4jjG #UYY{A6677
 

 !SWW[!4455
 
	 	GG Nr   baselineList[Dict[str, Any]]c                    g }| D ]?}t          |t                    st          ||          }||                    |           @|S )zNApply ``shift`` to every diagnostic in ``baseline``, dropping deleted entries.)
isinstancer9   r@   append)rA   r(   outdr?   s        r   shift_baselinerH      sZ     !#C    !T"" 	(E22JJwJr   )r/   r@   rH   )r	   r
   r   r
   r   r   )r0   r1   r(   r   r   r2   )rA   rB   r(   r   r   rB   )__doc__
__future__r   r*   typingr   r   r   r   r   r/   r@   rH   __all__r   r   r   <module>rM      s    4 # " " " " "  6 6 6 6 6 6 6 6 6 6 6 6 6 67 7 7 7t* * * *Z
 
 
 
 K
J
Jr   