
    `gj                    ^   d Z ddlmZ ddlZddlZddlZddlZddlmZm	Z	 ddl
mZ ddlmZmZmZmZmZmZmZ ddlmZmZ ddlmZmZmZmZmZmZmZmZm Z m!Z!m"Z"  ejF                  d	      Z$d
Z%dZ&dZ'dZ(dZ)dZ*dZ+dZ,ddZ-ddZ.ddZ/e G d d             Z0 G d d      Z1ddZ2d dZ3g dZ4y)!u  Async LSP client over stdin/stdout.

One :class:`LSPClient` corresponds to one ``(language_server, workspace_root)``
pair — exactly what OpenCode keys clients on, and the same shape Claude
Code uses.  The client owns a child process, drives the JSON-RPC
exchange, and exposes:

- :meth:`open_file` / :meth:`change_file` — text document sync
- :meth:`wait_for_diagnostics` — block until the server emits fresh
  diagnostics for a specific file (or a timeout fires)
- :meth:`diagnostics_for` — read the current per-file diagnostic store
- :meth:`shutdown` — graceful close + SIGTERM/SIGKILL fallback

The class is designed for async use from a single asyncio event loop.
The :class:`agent.lsp.manager.LSPService` runs an event loop in a
background thread so the synchronous file_operations layer can call
into it via :func:`agent.lsp.manager.LSPService.touch_file`.

Implementation notes:

- All per-document state lives in one :class:`_DocState` keyed by
  absolute path.  Freshness is tracked with **document versions**,
  not timestamps: every didChange bumps ``version``, and each stored
  push/pull result is tagged with the version it describes.  A
  result is fresh iff its tag >= the version being waited on, so a
  didChange implicitly invalidates everything older — no clearing,
  no clock comparisons, no race windows.  This is what prevents
  "ghost diagnostics": a slow server's leftovers from the previous
  edit can never masquerade as a verdict on the current content.

- Whole-document sync.  Even when the server advertises incremental
  sync, we send a single ``contentChanges`` entry replacing the
  entire document.  Pretending to be incremental while sending a
  full replacement is well-tolerated by every major server and saves
  range bookkeeping.  See OpenCode's ``client.ts:584-659`` for the
  same trick.

- The "touch-file dance": every ``open_file`` call also fires a
  ``workspace/didChangeWatchedFiles`` notification (CREATED on the
  first open, CHANGED thereafter).  Some servers (clangd, eslint)
  only re-scan when this notification fires, even though the LSP spec
  doesn't strictly require it.

- ``ContentModified`` (-32801) errors get retried with exponential
  backoff up to 3 times.  This matches Claude Code's
  ``LSPServerInstance.sendRequest``.
    )annotationsN)	dataclassfield)Path)Any	AwaitableCallableDictListOptionalSet)quoteunquote)ERROR_CONTENT_MODIFIEDERROR_METHOD_NOT_FOUNDLSPProtocolErrorLSPRequestErrorclassify_messageencode_messagemake_error_responsemake_notificationmake_requestmake_responseread_messagezagent.lsp.clientg     F@g      @g      $@g      @g333333?g      ?         ?c                    t         j                  j                  |       }t         j                  dk(  r(|j	                  dd      }|j                  d      sd|z   }dt        |d      z   S )u   Return ``file://`` URI for an absolute filesystem path.

    Mirrors Node's ``pathToFileURL`` — handles spaces, unicode, and
    Windows drive letters (``C:\foo`` → ``file:///C:/foo``).
    nt\/file://z/:)safe)ospathabspathnamereplace
startswithr   )r$   abs_paths     C/root/.hermes/venv/lib/python3.12/site-packages/agent/lsp/client.pyfile_urir+   X   s^     wwt$H	ww$ ##D#.""3'X~HuXD111    c                   | j                  d      s| S | t        d      d }t        j                  dk(  r,|j                  d      rt        |      dkD  r|d   dk(  r|dd }t        j                  j                  t        |            S )zInverse of :func:`file_uri`.r!   Nr   r       :   )r(   lenr#   r&   r$   normpathr   )uriraws     r*   uri_to_pathr5   h   sq    >>)$

c)no
C	ww$3>>#.3s8a<CFcM!"g77GCL))r,   c                    | sdddS | j                  d      }t        |      dz
  }|rt        |d         nd}| j                  d      r|dz   ddS ||dS )zReturn the LSP Position at the end of ``text``.

    Used to construct a single-range "replace whole document" change
    for ``textDocument/didChange`` regardless of the server's declared
    sync mode.
    r   line	characterF)keependsr0   )
)
splitlinesr1   endswith)textlines	last_linelast_cols       r*   _end_positionrD   r   sh     **OOUO+EE
QI!&s59~AH }}\"!AA66H55r,   c                      e Zd ZU dZdZded<   dZded<    ee      Z	d	ed
<    ee      Z
d	ed<   dZded<   dZded<   dZded<   dddZdddZy)	_DocStateu1  Everything the client tracks for one open document.

    ``version`` is the LSP document version we last sent (didOpen=0,
    each didChange +1).  It doubles as the freshness token: stored
    push/pull results are tagged with the version they describe
    (``push_version`` / ``pull_version``), and a result is *fresh*
    iff its tag has caught up to ``version``.  Bumping the version on
    didChange therefore invalidates all older results implicitly —
    no store-clearing, no timestamps.

    ``push_version``/``pull_version`` start at -1 = "no data yet".
    Servers that echo a document version in publishDiagnostics get
    exact tagging; those that don't are credited with the current
    version at receipt time (a push observed after we sent the
    change describes the changed content or newer).
    r   intversion strr@   )default_factoryList[Dict[str, Any]]pushpullr;   push_versionpull_versionFbool	seed_seenNc                @    | j                   || j                  k\  S |k\  S N)rO   rH   selfrH   s     r*   
fresh_pushz_DocState.fresh_push   #      W_T\\RR'RRr,   c                @    | j                   || j                  k\  S |k\  S rT   )rP   rH   rU   s     r*   
fresh_pullz_DocState.fresh_pull   rX   r,   rT   )rH   zOptional[int]returnrQ   )__name__
__module____qualname____doc__rH   __annotations__r@   r   listrM   rN   rO   rP   rR   rW   rZ    r,   r*   rF   rF      sf    " GSD#N!&t!<D
<!&t!<D
<L#L#ItSSr,   rF   c                     e Zd ZdZddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d+dZed,d       Zed-d       Zd.dZe	d/d	       Z
d.d
Zd.dZd.dZd.dZe	d0d       Zd.dZd.dZd1dZd2dZd3dZd4dZd5dZd6dZd7dZd8dZd9dZd9dZd9dZd9dZd9dZd9dZd:dZ d d!d;d"Z!d<d#Z"d<d$Z#d%dd&	 	 	 	 	 	 	 	 	 d=d'Z$d>d(Z%dd)d?d*Z&y)@	LSPClienta  Async LSP client tied to one server process and one workspace root.

    Lifecycle:

        c = LSPClient(server_id, workspace_root, command, args, init_options)
        await c.start()       # spawn + initialize
        ver = await c.open_file("/path/to/foo.py")
        await c.wait_for_diagnostics("/path/to/foo.py", ver)
        diags = c.diagnostics_for("/path/to/foo.py")
        await c.shutdown()
    NF)envcwdinitialization_optionsseed_diagnostics_on_first_pushc               N   || _         || _        t        |      | _        || _        |xs || _        |xs i | _        || _        d | _        d | _	        d | _
        d| _        i | _        | j                  | j                  | j                  | j                   | j"                  | j$                  d| _        d| j(                  i| _        i | _        i | _        d| _        d | _        d| _        d| _        t9        j:                         | _        d| _        t9        j:                         | _         y )Nr   )zwindow/workDoneProgress/createzworkspace/configurationzclient/registerCapabilityzclient/unregisterCapabilityzworkspace/workspaceFolderszworkspace/diagnostic/refreshztextDocument/publishDiagnosticsstoppedr0   F)!	server_idworkspace_rootra   _command_env_cwd_init_options_seed_first_push_proc_stderr_task_reader_task_next_id_pending_handle_work_done_create_handle_workspace_configuration_handle_register_capability_handle_unregister_capability_handle_workspace_folders_handle_diagnostic_refresh_request_handlers_handle_publish_diagnostics_notification_handlers_docs_diagnostic_registrations_state_initialize_result
_sync_kind	_stoppingasyncioEvent_push_event_push_counter_registration_event)rV   rk   rl   commandre   rf   rg   rh   s           r*   __init__zLSPClient.__init__   s%    #,W	)>	39r > <@
4848 35
 /3.K.K'+'K'K)-)I)I+/+M+M*.*H*H,0,K,KN
 .t/O/OI
# ,.
DF& %<@ $ #==?
  $+==? r,   c                t    | j                   dk(  xr( | j                  d uxr | j                  j                  d u S )Nrunning)r   rr   
returncoderV   s    r*   
is_runningzLSPClient.is_running   s4    {{i'dDJJd,BdtzzG\G\`dGddr,   c                    | j                   S rT   )r   r   s    r*   statezLSPClient.state  s    {{r,   c                  K   | j                   dv ryd| _         	 | j                          d{    | j                          d{    d| _         y7 $7 # t        $ r" d| _         | j	                          d{  7    w xY ww)u   Spawn the server and complete the initialize handshake.

        Raises any exception encountered during spawn/init.  On failure
        the process is killed and the client is left in state
        ``"error"`` — re-call ``start()`` to retry.
        >   r   startingNr   r   error)r   _spawn_initialize	Exception_cleanup_processr   s    r*   startzLSPClient.start  s      ;;11 	++-""$$$#DK  $ 	!DK'')))	sJ   BA AA AA BA A #B9A<:BBc                Z    | d   }|j                         j                  d      rddg| S | S )z?On Windows, wrap .cmd/.bat shims so CreateProcess can run them.r   )z.cmdz.batzcmd.exez/c)lowerr?   )cmdexes     r*   _win_wrap_cmdzLSPClient._win_wrap_cmd  s7     !f99; 01t*c**
r,   c           
       K   t        t        j                        }| j                  r|j	                  | j                         | j
                  }t        j                  dk(  r| j                  |      }	 t        j                  |d   g|dd  t        j                  j                  t        j                  j                  t        j                  j                  || j                  dd d {   | _        t        j"                  | j%                               | _        t        j"                  | j)                               | _        y 7 Z# t        $ r}t!        d|d    d| d      |d }~ww xY ww)	Nwin32r   r0   T)stdinstdoutstderrre   rf   start_new_sessionzLSP server binary not found: z ())dictr#   environrn   updaterm   sysplatformr   r   create_subprocess_exec
subprocessPIPEro   rr   FileNotFoundErrorr   create_task_drain_stderrrs   _reader_looprt   )rV   re   r   es       r*   r   zLSPClient._spawn!  s:    2::99JJtyy!mm<<7"$$S)C	  '==A	 QR	  ((--))..))..II"&	  	DJ" $//0B0B0DE#//0A0A0CD'	 ! 	"/Axr!A>	s>   A1E14A9E
 -E.	E
 7AE1E
 
	E.E))E..E1c                  K   | j                   | j                   j                  y 	 	 | j                   j                  j                          d {   }|sy |j                  dd      j	                         }|r$t
        j                  d| j                  |d d        w7 O# t        j                  t        f$ r Y y w xY ww)Nutf-8r'   )errorsz[%s] stderr: %si  )rr   r   readlinedecoderstriploggerdebugrk   r   CancelledErrorOSError)rV   r8   r@   s      r*   r   zLSPClient._drain_stderrG  s     ::!2!2!:		!ZZ..7799{{79{=DDFLL!2DNND$KP 9 &&0 		s;   $C (B! BB! C A	B! !B=:C <B==C c                h  K   | j                   | j                   j                  y 	 	 t        | j                   j                         d {   }|!t        j	                  d| j
                         nt        |      \  }}|dk(  r| j                  ||       nd|dk(  r&t        j                  | j                  ||             n9|dk(  r| j                  ||       n!t        j                  d| j
                  |       	 t!        | j"                  j%                               D ]-  }|j'                         r|j)                  t        d             / | j"                  j+                          y 7 "# t        $ r+}t        j                  d| j
                  |       Y d }~d }~wt        j                  t        f$ r Y w xY w# t!        | j"                  j%                               D ]-  }|j'                         r|j)                  t        d             / | j"                  j+                          w xY ww)Nz![%s] server closed stdout cleanlyresponserequestnotificationz![%s] dropping invalid message: %rz&[%s] protocol error in reader loop: %szserver connection closed)rr   r   r   r   r   rk   r   _dispatch_responser   r   _dispatch_request_dispatch_notificationwarningr   r   r   ra   rv   valuesdoneset_exceptionclear)rV   msgkindkeyr   futs         r*   r   zLSPClient._reader_loopU  s    ::!2!2!:	"():):;;;LL!DdnnU,S1	c:%++C5Y&''(>(>sC(HI^+//S9NN#FX[\    DMM0023 Txxz%%&67Q&RST MM!- <   	XNNCT^^UVWW&&0 		 DMM0023 Txxz%%&67Q&RST MM!sk   $H2#E/ 
E,B2E/ =G  >6H257H2,E/ /	F=8!FG  F=:G  <F==G   7H/87H//H2c                  K   t        | j                        | j                  t        j                         dt        | j                        dg| j                  ddiddddiddidddddddd	ddd
ddddgidddddddgiddii ddidddgidd}t        j                  | j                  d|      t               d {   }|| _	        | j                  |j                  d      xs i       | _        | j                  di        d {    | j                  r'| j                  dd| j                  i       d {    y y 7 7 :7 
w)N	workspacer&   r3   workDoneProgressTdynamicRegistrationrefreshSupportF)configurationworkspaceFoldersdidChangeWatchedFilesdiagnostics)r   didOpen	didChangedidSavewillSavewillSaveWaitUntil)r   relatedDocumentSupportvalueSetr0   r.   )relatedInformation
tagSupportversionSupportcodeDescriptionSupportdataSupportcontentFormatmarkdown	plaintextlinkSupport!hierarchicalDocumentSymbolSupport)synchronization
diagnosticpublishDiagnosticshover
definition
referencesdocumentSymbolpositionEncodingszutf-16)windowr   textDocumentgeneral)rootUrirootPath	processIdr   initializationOptionscapabilities
initializetimeoutr   initializedz workspace/didChangeConfigurationsettings)r+   rl   r#   getpidrp   r   wait_for_send_requestINITIALIZE_TIMEOUTr   _extract_sync_kindgetr   _send_notification)rV   paramsresults      r*   r   zLSPClient._initializer  s     3 34++$Xd6I6I-JK! &*%7%7-t4%)(,.CT-J$4e#<	 05#'%)#'$)-2( 0426#
 /3'1Aq6&:*.26',+ .
K/HI#0$"7"$'JD&Q/!2 0(<C"+
Z ''|V4&
 
 #)11&**^2L2RPRS%%mR888 ))2T//0  	 
 	9
s7   B?E
EA	E
E1E
=E>E
E
E
c                    | j                  d      }t        |t              r|S t        |t              r#|j                  d      }t        |t              r|S y)NtextDocumentSyncchanger0   )r  
isinstancerG   r   )r   syncr
  s      r*   r  zLSPClient._extract_sync_kind  sL     23dC KdD!XXh'F&#&r,   c                  K   | j                   ryd| _         	 | j                  rK	 t        j                  | j	                  dd      d       d{    	 | j                  dd       d{    d| _
        | j                          d{    y7 ?# t        j
                  t        t        f$ r Y ]w xY w7 J# t        $ r Y Sw xY w7 <# d| _
        | j                          d{  7   w xY ww)zBest-effort graceful shutdown.

        Sends ``shutdown`` + ``exit``, then SIGTERMs/SIGKILLs the
        process if it doesn't exit cleanly.  Idempotent.
        NTshutdowng       @r   exitrj   )r   r   r   r   r  TimeoutErrorr   r   r  r   r   r   r   s    r*   r  zLSPClient.shutdown  s      >>	*!**4+=+=j$+OY\]]]11&$??? $DK''))) ^,,o?OP  @   * $DK'')))s   C-C *B BB B7 *B5+B7 /C-	C
C-B B2/C 1B22C 5B7 7	C C CC C-C*#C&$C**C-c                |  K   | j                   I| j                   j                         s/| j                   j                          	 | j                    d {    | j                  I| j                  j                         s/| j                  j                          	 | j                   d {    | j                  }d | _        |y |j                  D	 |j                          	 t        j                  |j                         t               d {    y y 7 # t        j                  t
        f$ r Y w xY w7 # t        j                  t
        f$ r Y w xY w7 H# t        j                  $ r? 	 |j                          |j                          d {  7   n# t        $ r Y nw xY wY y Y y w xY w# t        $ r Y y w xY ww)Nr   )rt   r   cancelr   r   r   rs   rr   r   	terminater   waitSHUTDOWN_GRACEr  killProcessLookupError)rV   procs     r*   r   zLSPClient._cleanup_process  s    (1B1B1G1G1I$$&'''' (1B1B1G1G1I$$&'''' zz
<??" !**499;OOO	 # (**I6 
 (**I6  P++ 		"iik))-  * & s	  AF<D DD A F<D7 (D5)D7 -"F<F- !,E EE F<D D2/F<1D22F<5D7 7EF<EF<E F*,#FFFF*	F"F*!F""F*%F- &F<'F- (F<)F**F- -	F96F<8F99F<c           	     T  K   | j                   :| j                   j                  $| j                   j                  j                         rt        d|d      t	        j
                         }| j                  }| xj                  dz  c_        |j                         }|| j                  |<   	 | j                   j                  j                  t        t        |||                   | j                   j                  j                          d {    	 | d {   | j                  j!                  |d        S 7 +# t        t        t        f$ r3}| j                  j!                  |d        t        d|d|       |d }~ww xY w7 l# | j                  j!                  |d        w xY ww)Nzcannot send z: stdin closedr0   zsend failed for : )rr   r   
is_closingr   r   get_running_loopru   create_futurerv   writer   r   drainBrokenPipeErrorConnectionResetErrorr   pop)rV   methodr  loopreq_idr   r   s          r*   r  zLSPClient._send_request  sW    ::!1!1!9TZZ=M=M=X=X=Z"\&>#JKK'')"002 #f	NJJ"">,vvv2V#WX**""((***	,9MMfd+ +!5w? 	NMMfd+"%5fZr!#EFAM	N MMfd+sa   B*F(-A D; D9D; F FF F(9D; ;F.E==FF(F F%%F(c               V  K   t        t        dz         D ]3  }	 t        j                  | j	                  ||      |       d{   c S  y7 # t
        $ rO}|j                  t        k(  r6|t        k  r-t        j                  t        d|z  z         d{  7   Y d}~ d}~ww xY ww)u   Send a request, retrying on ``ContentModified`` (-32801).

        Other errors propagate.  The retry policy matches Claude Code's
        ``LSPServerInstance.sendRequest`` — 3 attempts with delays
        0.5s, 1.0s, 2.0s.
        r0   r   Nr.   )
rangeMAX_CONTENT_MODIFIED_RETRIESr   r   r  r   coder   sleepRETRY_BASE_DELAY)rV   r#  r  r   attemptr   s         r*   _send_request_with_retryz"LSPClient._send_request_with_retry	  s      9A=> 	G$--d.@.@.PZabbb	b" 6633B^8^!--(8AL(IJJJ	sR   B)*AAAB)A	B&>B!BB!B) B!!B&&B)c                  K   | j                   :| j                   j                  $| j                   j                  j                         ry 	 | j                   j                  j                  t	        t        ||                   | j                   j                  j                          d {    y 7 # t        t        t        f$ r,}t        j                  d| j                  ||       Y d }~y d }~ww xY ww)Nz[%s] notify %s failed: %s)rr   r   r  r  r   r   r  r   r!  r   r   r   rk   )rV   r#  r  r   s       r*   r  zLSPClient._send_notification  s     ::!1!1!9TZZ=M=M=X=X=Z	QJJ"">2CFF2S#TU**""((***!5w? 	QLL4dnnfaPP	QsC   AC5AB2 *B0+B2 /C50B2 2C2"C-(C5-C22C5c                  K   | j                   :| j                   j                  $| j                   j                  j                         ry 	 | j                   j                  j                  t	        t        ||                   | j                   j                  j                          d {    y 7 # t        t        t        f$ r Y y w xY wwrT   )
rr   r   r  r  r   r   r  r   r!  r   )rV   r%  r  s      r*   _send_responsezLSPClient._send_response"  s     ::!1!1!9TZZ=M=M=X=X=Z	JJ"">-2O#PQ**""((***!5w? 		s=   ACAB2 *B0+B2 /C0B2 2C	CC		Cc           	       K   | j                   :| j                   j                  $| j                   j                  j                         ry 	 | j                   j                  j                  t	        t        |||                   | j                   j                  j                          d {    y 7 # t        t        t        f$ r Y y w xY wwrT   )
rr   r   r  r  r   r   r  r   r!  r   )rV   r%  r)  messages       r*   _send_error_responsezLSPClient._send_error_response+  s     ::!1!1!9TZZ=M=M=X=X=Z	JJ"">2EfdT[2\#]^**""((***!5w? 		s=   ACA B3 +B1,B3 0C1B3 3C
C	C

Cc                x   | j                   j                  |      }||j                         ry d|v rh|d   xs i }|j                  t	        t        |j                  dd            t        |j                  dd            |j                  d                   y |j                  |j                  d             y )	Nr   r)   r2  unknowndata)r)  r2  r7  r  )rv   r  r   r   r   rG   rJ   
set_result)rV   r%  r   r   errs        r*   r   zLSPClient._dispatch_response4  s    mm';#((*c>g,$"CSWWVV45	9 => NN3778,-r,   c                  K   |j                  dd      }|j                  d      }| j                  j                  |      }|#| j                  |t        d|        d {    y 	  ||       d {   }| j                  ||       d {    y 7 17 !# t        $ rK}t
        j                  d| j                  ||       | j                  |dd|        d {  7   Y d }~y d }~ww xY w7 `w)Nr#  rI   r  zmethod not found: z"[%s] request handler %s failed: %sr5  zhandler failed: )	r  r}   r3  r   r   r   r   rk   r0  )rV   r%  r   r#  r  handlerr  r   s           r*   r   zLSPClient._dispatch_requestD  s     2&"((,,V4?++F4JN`ag`hLijjj	"6?*F
 !!&&111 k + 	NN?QWYZ[++FF>Nqc<RSSS	 	2sf   AC/ B!C/'B 2B3B 7C/C-C/B 	C*;C%CC% C/%C**C/c                    | j                   j                  |      }|y 	  ||j                  d             y # t        $ r,}t        j	                  d| j
                  ||       Y d }~y d }~ww xY w)Nr  z'[%s] notification handler %s failed: %s)r   r  r   r   r   rk   )rV   r#  r   r;  r   s        r*   r   z LSPClient._dispatch_notificationS  se    --11&9?	_CGGH%& 	_LLBDNNTZ\]^^	_s   8 	A-"A((A-c                   K   y wrT   rb   rV   r  s     r*   rw   z"LSPClient._handle_work_done_create`  	        c                  K   t        |t              sd gS |j                  d      xs g }g }|D ]  }t        |t              s|j                  d        %|j                  d      }|r| j                  s |j                  | j                  xs d        d| j                  }t        |      j                  d      D ]  }t        |t              r
||v r||   }d } n |j                  |        |S w)Nitemssection.)r  r   r  appendrp   rJ   split)rV   r  rB  outitemrC  curparts           r*   rx   z)LSPClient._handle_workspace_configurationd  s      &$'6M

7#)r 	DdD)

4 hhy)G$"4"4

4--56))CG**3/ c4(TS[d)CC JJsO	  
s   C.C0c                >  K   t        |t              sy |j                  d      xs g D ]p  }t        |t              s|j                  d      }|j                  d      }|dk(  s<|s?|| j                  t	        |      <   | j
                  j                          r y w)Nregistrationsr#  idtextDocument/diagnostic)r  r   r  r   rJ   r   set)rV   r  regr#  reg_ids        r*   ry   z%LSPClient._handle_register_capability}  s     &$'::o.4" 	/Cc4(WWX&FWWT]F22v>A..s6{;((,,.	/ s   A"B%B(5Bc                   K   t        |t              sy |j                  d      xs g D ]L  }t        |t              s|j                  d      }|s(| j                  j	                  t        |      d        N y w)NunregisterationsrM  )r  r   r  r   r"  rJ   )rV   r  unregrQ  s       r*   rz   z'LSPClient._handle_unregister_capability  so     &$'ZZ 239r 	FEeT*YYt_F..223v;E	F s   AA9(A9c                <   K   dt        | j                        dgS w)Nr   r   )r+   rl   r>  s     r*   r{   z#LSPClient._handle_workspace_folders  s     $Xd6I6I-JKLLs   c                   K   y wrT   rb   r>  s     r*   r|   z$LSPClient._handle_diagnostic_refresh  r?  r@  c                J   t        |t              sy |j                  d      }t        |t              sy t	        |      }|j                  d      xs g }t        |t
              sg }|j                  d      }| j                  j                  |t        d            }| j                  r|j                  sd|_
        ||_        y d|_
        ||_        t        |t              r|n|j                  |_        | xj                  dz  c_        | j                   j#                          y )Nr3   r   rH   r;   rH   Tr0   )r  r   r  rJ   r5   ra   r   
setdefaultrF   rq   rR   rM   rG   rH   rO   r   r   rO  )rV   r  r3   r$   r   rH   docs          r*   r~   z%LSPClient._handle_publish_diagnostics  s    &$'jj#s#3jj/52+t,K**Y'jj##D)B*?@  
 !CM"CH '1#&>7CKK 	ar,   r   )language_idc          	     V  K   | j                   st        d      t        j                  j	                  |      }	 t        |      j                  dd      }t        |      }| j                  j                  |      }||j                  dk\  r| j                  d	d
|ddgi       d{    |j                  dz   }|j                  }	| j                  dk(  rdddt        |	      d|dg}
nd|ig}
| j                  d||d|
d       d{    ||_        ||_        |S | j                  d	d
|ddgi       d{    t!        d|      | j                  |<   | j                  dd||d|di       d{    y# t        $ r}t        d| d|       |d}~ww xY w7 7 7 g7 .w)zSend didOpen (first time) or didChange (subsequent) for ``path``.

        Returns the new document version number that the agent's
        ``wait_for_diagnostics`` should match against.
        zclient not runningr   r'   )encodingr   zcannot read r  Nr   zworkspace/didChangeWatchedFileschangesr.   )r3   typer0   r7   )r   end)r'  r@   r@   ztextDocument/didChange)r3   rH   )r   contentChanges)rH   r@   ztextDocument/didOpenr   )r3   
languageIdrH   r@   )r   r   r#   r$   r%   r   	read_textr   r+   r   r  rH   r  r@   r   rD   rF   )rV   r$   r[  r)   r@   r   r3   rZ  new_versionold_textcontent_changess              r*   	open_filezLSPClient.open_file  s     "#78877??4(	J>++WY+OD x jjnnX&?s{{a/))1S!456   ++/KxxH!# /0a%@#0#:" !%# %+D>"2))(,/K$H&5   &CKCH %%-a012
 	
 	
  )>

8%%""-  	!

 
	
 
	
 y  	J"\(2aS#ABI	J&	

	
sl   7F)E> AF)(F!)A$F)F#/F)=F%>:F)8F'9F)>	FFFF)#F)%F)'F)c                   K   | j                   syt        j                  j                  |      }| j	                  dddt        |      ii       d{    y7 w)z>Send didSave for ``path``.  Some linters re-scan only on save.NztextDocument/didSaver   r3   )r   r#   r$   r%   r  r+   )rV   r$   r)   s      r*   	save_filezLSPClient.save_file  sO     77??4(%%"eXh%789
 	
 	
s   AAAAc                  K   t         j                  j                  |      }| j                  j	                  |      }|r|j
                  nd}	 ddt        |      ii}| j                  d|t               d{   }t!        |t"              sy|j	                  d      }t!        |t$              r4| j                  j'                  |t)        d	            }||_        ||_        |j	                  d
      }	t!        |	t"              r|	j/                         D ]  \  }
}t!        |t"              s|j	                  d      }t!        |t$              s9| j                  j'                  t1        |
      t)        d	            }||_        |j
                  |_         yy7  # t        t        t        j                  f$ r+}t        j                  d| j                  |       Y d}~yd}~ww xY ww)u  Send ``textDocument/diagnostic`` for one file.

        Stores results into the doc's pull store, tagged with the
        document version captured at request send time.  If a didChange
        races past the in-flight request, the version bump makes the
        stored result stale automatically — no explicit invalidation.
        Silently no-ops on errors (server may not support the pull
        endpoint).
        r;   r   r3   rN  r   Nz([%s] document diagnostic pull failed: %srB  rX  relatedDocuments)r#   r$   r%   r   r  rH   r+   r-  DIAGNOSTICS_REQUEST_TIMEOUTr   r   r   r  r   r   rk   r  r   ra   rY  rF   rN   rP   rB  r5   )rV   r$   r)   rZ  sent_versionr  r  r   rB  relatedr3   sub	sub_itemsrels                 r*   _pull_document_diagnosticsz$LSPClient._pull_document_diagnostics"  s     77??4(jjnnX&&)s{{r	(: ;&F  88)3 9  F &$'

7#eT"**'')B2GHCCH+C**/0gt$#MMO 	3S!#t,GGG,	i.**//C0@)TVBWXC(CH (+{{C$	3 %!
  !173G3GH 	LLCT^^UVW	sJ   AG'*F 8F9F =CG'AG'F G$9!GG'G$$G'document)moder   c               <  K   ||dkD  r|}n|dk(  rt         nt        }t        j                         j	                         |z   }t
        j                  j                  |      }	 |t        j                         j	                         z
  }|dk  ryt        j                  | j                  |            }	t        j                  | j                  |||            }
t        j                  |	|
h|t        j                         d{   \  }}|D ]  }|j                           |D ]  }	 | d{     | j                   j#                  |      }|r|j%                  |      ry|r|j'                  |      ry7 u7 M# t        j                  t        f$ r Y tw xY ww)u  Wait for the server to publish diagnostics for ``path`` at ``version``.

        ``mode`` is ``"document"`` (5s budget, document pulls) or
        ``"full"`` (10s budget, also workspace pulls).  ``timeout``
        overrides the mode's default budget when provided — this is
        how the user's ``lsp.wait_timeout`` config reaches the wait
        loop (slow servers like tsserver on big projects need more
        than the 5s default).

        Returns ``True`` when *fresh* diagnostics arrived (a push at
        or after our didChange, or a pull answered after it) and
        ``False`` on timeout.  Callers must treat ``False`` as "no
        data", NOT as "no errors" — the diagnostic stores may still
        hold stale entries from the previous edit at that point.
        Best-effort — never throws if the server doesn't support pull
        diagnostics; we still get the push side.
        Nr   fullTF)r   return_when)DIAGNOSTICS_FULL_WAITDIAGNOSTICS_DOCUMENT_WAITr   get_event_looptimer#   r$   r%   r   rr  _wait_for_fresh_pushr  FIRST_COMPLETEDr  r   r   r   r  rW   rZ   )rV   r$   rH   rt  r   budgetdeadliner)   	remaining	pull_task	push_taskr   pendingtrZ  s                  r*   wait_for_diagnosticszLSPClient.wait_for_diagnosticsO  s    2 7Q;F.2fn*B[F))+002V;77??4( 7#9#9#;#@#@#BBIA~  ++D,K,KH,UVI++D,E,EhPWYb,cdI"),,I&!#33# MD'
  
 GG **..*Cs~~g. s~~g.;  ..	: sI   DFE9#F(E=-E;.E=2AF;E==FFFFc                *  K   t        j                         j                         |z   }| j                  }	 | j                  j                  |      }|r|j                  |      r| j                  }t        j                         j                         t        z   }| j                  |k(  r|t        j                         j                         z
  }	|	dk  r	 y| j                  j                          	 t        j                  | j                  j                         |	       d{    | j                  |k(  ry|t        j                         j                         z
  }	|	dk  ry| j                  |kD  r| j                  }J| j                  j                          	 t        j                  | j                  j                         t        |	d             d{    7 # t         j                  $ r Y yw xY w7 !# t         j                  $ r Y w xY ww)zKWait until a fresh publishDiagnostics arrives for ``path`` at ``version``+.r   r   Nr   )r   rz  r{  r   r   r  rW   PUSH_DEBOUNCEr   r   r   r  r  min)
rV   r$   rH   r   r  baselinerZ  debounce_baselinedebounce_deadliner  s
             r*   r|  zLSPClient._wait_for_fresh_push  s    ))+002W<%%**..&Cs~~g.
 %)$6$6!$+$:$:$<$A$A$Cm$S!((,== 1G4J4J4L4Q4Q4S SI A~  $$**,%..t/?/?/D/D/FPYZZZ ((,==  7#9#9#;#@#@#BBIA~!!H,  --""$&&t'7'7'<'<'>IWZH[\\\;  ["//  ]'' sy   C+H.2G  G!G %H5A#H<G9 G7G9 HG G41H3G44H7G9 9HHHH)
fresh_onlyc               N   | j                   j                  t        j                  j	                  |            }|g S |rHt        |j                         r|j                  ng |j                         r|j                        S g       S t        |j                  |j                        S )uX  Return current merged + deduped diagnostics for one file.

        Diagnostics from push and pull stores are concatenated and
        deduplicated by ``(severity, code, message, range)`` content
        key.  Empty list if the server hasn't published anything.

        With ``fresh_only=True``, a store only contributes when its
        version tag has caught up to the document's current version —
        stale leftovers from the previous edit cycle are excluded.
        This is what report paths should use: after an edit, "stale
        errors" and "no errors" must not be conflated.
        )
r   r  r#   r$   r%   _deduperW   rM   rZ   rN   )rV   r$   r  rZ  s       r*   diagnostics_forzLSPClient.diagnostics_for  s     jjnnRWW__T23;INN,"NN, 24  sxx**r,   )rk   rJ   rl   rJ   r   	List[str]re   zOptional[Dict[str, str]]rf   zOptional[str]rg   zOptional[Dict[str, Any]]rh   rQ   r[   None)r[   rQ   )r[   rJ   )r[   r  )r   r  r[   r  )r   r   r[   rG   )r#  rJ   r  r   r[   r   )r#  rJ   r  r   r   floatr[   r   )r#  rJ   r  r   r[   r  )r%  r   r  r   r[   r  )r%  r   r)  rG   r2  rJ   r[   r  )r%  rG   r   r   r[   r  )r%  r   r   r   r[   r  )r#  rJ   r   r   r[   r  )r  r   r[   r   )r  r   r[   r  )r$   rJ   r[  rJ   r[   rG   )r$   rJ   r[   r  )
r$   rJ   rH   rG   rt  rJ   r   zOptional[float]r[   rQ   )r$   rJ   rH   rG   r   r  r[   r  )r$   rJ   r  rQ   r[   rL   )'r\   r]   r^   r_   r   propertyr   r   r   staticmethodr   r   r   r   r   r  r  r   r  r-  r  r0  r3  r   r   r   rw   rx   ry   rz   r{   r|   r~   rg  ri  rr  r  r|  r  rb   r,   r*   rd   rd      s   
, )-!;?/4C3 C3 	C3
 C3 &C3 C3 !9C3 )-C3 
C3J e e  &  $EL":=~  *.F,& Q. 2_2	M$T @K HT
+3d #'== =
 = != 
=B#J @E +r,   rd   c                     t               }g }| D ]L  }|D ]E  }t        |t              st        |      }||v r$|j	                  |       |j                  |       G N |S rT   )rO  r  r   _diagnostic_keyaddrE  )listsseenrG  lstdr   s         r*   r  r    sj    UD "C  	Aa&!!$Cd{HHSMJJqM	 Jr,   c                |   | j                  d      xs i }|j                  d      xs i }|j                  d      xs i }| j                  d      }|t        |t              st        |      }dj                  t        | j                  d      xs d      t        |xs d      t        | j                  d	      xs d      t        | j                  d
      xs d      j	                         |j                  dd       d|j                  dd       d|j                  dd       d|j                  dd       g      S )u  Content-equality key for a diagnostic.

    Matches the structural-equality used in claude-code's
    ``areDiagnosticsEqual`` — message + severity + source + code +
    range coords.  The range is reduced to a tuple to keep the key
    stable across dict orderings.
    r'  r   r`  r)   severityr0   rI   sourcer2  r8   r   r/   r9   -)r  r  rJ   joinstrip)r  rngr   r`  r)  s        r*   r  r    s    %%.
BCGGG"E
''%.
BC55=D
4 54y;;j!&Q'
Oh%2&i &B'--/yy#$AeiiQ&?%@#''&RSBTAUUVWZW^W^_jlmWnVop	
 r,   )rd   r+   r5   r  ry  rx  )r$   rJ   r[   rJ   )r3   rJ   r[   rJ   )r@   rJ   r[   zDict[str, int])r  rL   r[   rL   )r  zDict[str, Any]r[   rJ   )5r_   
__future__r   r   loggingr#   r   dataclassesr   r   pathlibr   typingr   r   r	   r
   r   r   r   urllib.parser   r   agent.lsp.protocolr   r   r   r   r   r   r   r   r   r   r   	getLoggerr   r  ry  rx  rl  r  r  r(  r+  r+   r5   rD   rF   rd   r  r  __all__rb   r,   r*   <module>r     s   .^ #   	 
 (  F F F '    
		-	.    !   !  2 *6( S S SBb+ b+J2r,   