
    `gj                    h    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y)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Optionalc                    | r| j                         ng }|r|j                         ng |k(  rd S t        j                  |d      }|j                         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    H/root/.hermes/venv/lib/python3.12/site-packages/agent/lsp/range_shift.py<lambda>z"build_line_shift.<locals>.<lambda>6   s    D     F)abautojunkc                    D ]:  \  }}}}}|| cxk  r|k  r n n|dk(  r
| |z
  |z   c S |dk(  r y |dk(  r y | |k  s: n rt        dt              dz
        S 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>)(?)#  by	$ /9s1c*o)*BdBr   )r   intreturnzOptional[int])
splitlinesdifflibSequenceMatcherget_opcodes)pre_text	post_text	pre_linessmr!   r   r    s        @@r   build_line_shiftr,   !   sh      *2##%rI+4%%'"J J   
	 	 9
U	KBnnGC0 Lr   c                   | j                  d      xs i }|j                  d      xs i }|j                  d      xs i }t        |j                  dd            }t        |j                  d|            } ||      }|y ||      }||}t        |       }	|t        |j                  dd            d|t        |j                  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   r1   )r/   r0   )getr"   dict)
diagr!   rngr/   r0   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7L>*N&L%4jG #UYY{A67

 !SWW[!45
	GG Nr   c                z    g }| D ]3  }t        |t              st        ||      }|#|j                  |       5 |S )zNApply ``shift`` to every diagnostic in ``baseline``, dropping deleted entries.)
isinstancer3   r;   append)baseliner!   outdr:   s        r   shift_baselinerB      sJ     !#C  !T"(E2JJw  Jr   )r,   r;   rB   )r(   strr)   rC   r#   Callable[[int], Optional[int]])r4   zDict[str, Any]r!   rD   r#   zOptional[Dict[str, Any]])r?   List[Dict[str, Any]]r!   rD   r#   rE   )__doc__
__future__r   r%   typingr   r   r   r   r   r,   r;   rB   __all__r   r   r   <module>rJ      sQ   4 #  6 67t*"@*E]*Z
8
=Q
 Kr   