Skip to content

Commit 16a6ccd

Browse files
committed
Support editor/getDefinition and editor/getReferences server requests
Proxy the new eca navigation requests to lsp-mode with async requests, converting between eca 1-based and LSP 0-based positions and normalizing Location/LocationLink results. Start lsp in background for files of known lsp projects answering starting so the server retries, answer no-server when lsp-mode or an installed client is missing, and answer handler errors instead of leaving requests to time out. editor-code-assistant/eca#351
1 parent 50e8519 commit 16a6ccd

5 files changed

Lines changed: 456 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
## Unreleased
44

5+
- Support `editor/getDefinition` and `editor/getReferences` server requests via lsp-mode, letting the LLM navigate to a symbol definition/references using the editor's LSP. When no server is attached yet for a file of a known lsp project, lsp is started in background and `starting` is answered so the eca server retries. `starting` is also answered while an attached workspace is still initializing, since lsp-mode would reject the request with a misleading "does not support method" capability error before the initialize handshake completes. Synchronous lsp-mode errors are answered as `error` responses, and any server-request handler error now answers an error response instead of escaping into the process filter ("error in process filter" in Messages) and leaving the request pending until the server times it out (30s). Requires eca server with editor navigation support (editor-code-assistant/eca#351).
6+
57
- Bugfix: a GitHub outage no longer prevents eca from starting. The release check now has a timeout and retries (`eca-server-fetch-timeout`, `eca-server-fetch-retries`) instead of hanging Emacs, failed checks back off for 60s instead of re-blocking every start, and error payloads (e.g. rate limit) are no longer cached as a valid releases list. When an update download fails but a server is already installed, eca warns and starts the installed binary instead of not starting at all (also for errors in the async `url-retrieve` download, which were previously uncaught).
68

79
- Support adding a region of a non-file buffer (magit, vterm, compilation, the chat itself) as context with the `eca-chat-add-context-*` commands: the selection becomes a `text` context with a lines range, its content sliced from the live buffer at prompt time. The commands now signal a `user-error` instead of silently doing nothing when no context applies. #294

eca-editor.el

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,18 @@
1919
(require 'flycheck nil t)
2020
(require 'ht nil t)
2121

22+
(require 'eca-api)
2223
(require 'eca-util)
2324

2425
(declare-function lsp-diagnostics "lsp-mode" (current-workspace?))
2526
(declare-function lsp-get "lsp-mode" (from key))
27+
(declare-function lsp "lsp-mode" (&optional arg))
28+
(declare-function lsp-request-async "lsp-mode" (method params callback &rest plist))
29+
(declare-function lsp-workspaces "lsp-mode" ())
30+
(declare-function lsp-session "lsp-mode" ())
31+
(declare-function lsp-session-folders "lsp-mode" (session))
32+
(declare-function lsp--workspace-server-capabilities "lsp-mode" (workspace))
33+
(declare-function lsp--find-clients "lsp-mode" ())
2634
(declare-function ht-select "ht" (function table))
2735
(declare-function flymake-diagnostic-code "flymake" (diagnostic))
2836
(declare-function flycheck-running-p "flycheck" ())
@@ -261,5 +269,192 @@ If URI is non-nil, filter to that uri."
261269
(vconcat (eca-editor--collect-all-diagnostics
262270
uri workspaces)))))
263271

272+
(defun eca-editor--lsp-nav-available-p ()
273+
"Return non-nil when lsp-mode is available for navigation requests."
274+
(featurep 'lsp-mode))
275+
276+
(defun eca-editor--lsp-buffer-workspaces ()
277+
"Return the lsp-mode workspaces associated with the current buffer."
278+
(and (fboundp 'lsp-workspaces) (lsp-workspaces)))
279+
280+
(defun eca-editor--lsp-workspaces-ready-p (workspaces)
281+
"Return non-nil when some of WORKSPACES finished initializing.
282+
A lsp-mode workspace only gets its server capabilities after the
283+
initialize handshake, so none having them means the server is
284+
still starting. Fails open: workspaces that cannot be inspected
285+
count as ready so the request is still attempted."
286+
(seq-some (lambda (workspace)
287+
(condition-case nil
288+
(and (lsp--workspace-server-capabilities workspace) t)
289+
(error t)))
290+
workspaces))
291+
292+
(defun eca-editor--lsp-known-root-p (path)
293+
"Return non-nil if PATH is inside a folder known to the lsp-mode session."
294+
(and (fboundp 'lsp-session)
295+
(fboundp 'lsp-session-folders)
296+
(seq-some (lambda (folder)
297+
(f-ancestor-of? folder path))
298+
(lsp-session-folders (lsp-session)))))
299+
300+
(defun eca-editor--lsp-start ()
301+
"Start lsp-mode in the current buffer when an installed client matches.
302+
Return non-nil when a start was attempted, nil when no installed
303+
lsp-mode client supports this buffer. Checking the clients first
304+
avoids lsp-mode's interactive server-install prompt, which must
305+
never fire from the process filter."
306+
(when (or (not (fboundp 'lsp--find-clients))
307+
(lsp--find-clients))
308+
(lsp)
309+
t))
310+
311+
(defun eca-editor--lsp-request-async (method params callback error-callback)
312+
"Send async lsp-mode request METHOD with PARAMS.
313+
Call CALLBACK with the result or ERROR-CALLBACK with the error."
314+
(lsp-request-async method params callback
315+
:error-handler error-callback
316+
:mode 'detached))
317+
318+
(defun eca-editor--file-buffer (path)
319+
"Return a buffer visiting PATH, opening it in background if needed."
320+
(or (find-buffer-visiting path)
321+
(let ((enable-local-variables :safe)
322+
(large-file-warning-threshold nil))
323+
(find-file-noselect path))))
324+
325+
(defun eca-editor--lsp-obj-get (obj key)
326+
"Get keyword KEY from lsp result OBJ.
327+
Supports both lsp-mode result representations: hash-tables with
328+
string keys and plists with keyword keys."
329+
(cond
330+
((hash-table-p obj) (gethash (substring (symbol-name key) 1) obj))
331+
((listp obj) (plist-get obj key))))
332+
333+
(defun eca-editor--lsp-position->eca (position)
334+
"Convert 0-based lsp POSITION to a 1-based eca position."
335+
(list :line (1+ (or (eca-editor--lsp-obj-get position :line) 0))
336+
:character (1+ (or (eca-editor--lsp-obj-get position :character) 0))))
337+
338+
(defun eca-editor--lsp-location->eca (location)
339+
"Convert a lsp Location or LocationLink LOCATION to an eca location."
340+
(when location
341+
(let ((uri (or (eca-editor--lsp-obj-get location :uri)
342+
(eca-editor--lsp-obj-get location :targetUri)))
343+
(range (or (eca-editor--lsp-obj-get location :range)
344+
(eca-editor--lsp-obj-get location :targetSelectionRange)
345+
(eca-editor--lsp-obj-get location :targetRange))))
346+
(when (and uri range)
347+
(list :uri uri
348+
:range (list :start (eca-editor--lsp-position->eca
349+
(eca-editor--lsp-obj-get range :start))
350+
:end (eca-editor--lsp-position->eca
351+
(eca-editor--lsp-obj-get range :end))))))))
352+
353+
(defun eca-editor--lsp-locations->eca (result)
354+
"Normalize a lsp navigation RESULT into a vector of eca locations.
355+
RESULT may be nil, a single Location, or a sequence of Location
356+
or LocationLink."
357+
(let ((locations (cond
358+
((null result) nil)
359+
((vectorp result) (append result nil))
360+
((hash-table-p result) (list result))
361+
((keywordp (car-safe result)) (list result))
362+
(t result))))
363+
(vconcat (delq nil (mapcar #'eca-editor--lsp-location->eca locations)))))
364+
365+
(defun eca-editor--lsp-error-message (err)
366+
"Extract a human readable message from the lsp-mode error ERR."
367+
(cond
368+
((null err) "lsp-mode request failed")
369+
((stringp err) err)
370+
((or (hash-table-p err) (keywordp (car-safe err)))
371+
(or (eca-editor--lsp-obj-get err :message) (format "%s" err)))
372+
(t (format "%s" err))))
373+
374+
(defun eca-editor--nav-request (session request params method extra-params)
375+
"Ask lsp-mode for the METHOD locations of the symbol in PARAMS.
376+
Respond to the server REQUEST of SESSION following the status
377+
contract of the `editor/getDefinition' and `editor/getReferences'
378+
requests: success, starting, no-server or error. EXTRA-PARAMS
379+
are appended to the lsp request params. Return the response
380+
plist for sync answers or `:async' when the lsp request was fired
381+
and the response will be sent later."
382+
(let* ((uri (plist-get params :uri))
383+
(position (plist-get params :position))
384+
(path (and uri (eca--uri-to-path uri))))
385+
(cond
386+
((not (eca-editor--lsp-nav-available-p))
387+
(list :status "no-server"
388+
:message "lsp-mode is not available in this Emacs"))
389+
390+
((or (null path) (not (file-exists-p path)))
391+
(list :status "error"
392+
:message (format "File not found: %s" (or path uri))))
393+
394+
(t
395+
(with-current-buffer (eca-editor--file-buffer path)
396+
(let ((workspaces (eca-editor--lsp-buffer-workspaces)))
397+
(cond
398+
;; Workspace attached but the server has not finished the
399+
;; initialize handshake yet: requests would fail with a
400+
;; misleading capability error, so ask the server to retry.
401+
((and workspaces
402+
(not (eca-editor--lsp-workspaces-ready-p workspaces)))
403+
(list :status "starting"))
404+
405+
(workspaces
406+
(condition-case err
407+
(progn
408+
(eca-editor--lsp-request-async
409+
method
410+
(append (list :textDocument (list :uri (eca--path-to-uri path))
411+
:position (list :line (max 0 (1- (or (plist-get position :line) 1)))
412+
:character (max 0 (1- (or (plist-get position :character) 1)))))
413+
extra-params)
414+
(lambda (result)
415+
(eca-api-send-request-response
416+
session request
417+
(list :status "success"
418+
:locations (eca-editor--lsp-locations->eca result))))
419+
(lambda (err)
420+
(eca-api-send-request-response
421+
session request
422+
(list :status "error"
423+
:message (eca-editor--lsp-error-message err)))))
424+
:async)
425+
;; lsp-mode can signal synchronously, e.g. the capability
426+
;; check; answer error instead of dying in the filter.
427+
(error (list :status "error"
428+
:message (error-message-string err)))))
429+
430+
((eca-editor--lsp-known-root-p path)
431+
(condition-case err
432+
(if (eca-editor--lsp-start)
433+
(list :status "starting")
434+
(list :status "no-server"
435+
:message "No installed lsp-mode client for this file's major mode"))
436+
(error (list :status "no-server"
437+
:message (error-message-string err)))))
438+
439+
(t
440+
(list :status "no-server"
441+
:message "No lsp-mode workspace for this file; start lsp in its project once")))))))))
442+
443+
(defun eca-editor-get-definition (session request params)
444+
"Handle the `editor/getDefinition' server REQUEST for SESSION.
445+
PARAMS contain the file uri and the 1-based symbol position."
446+
(eca-editor--nav-request session request params "textDocument/definition" nil))
447+
448+
(defun eca-editor-get-references (session request params)
449+
"Handle the `editor/getReferences' server REQUEST for SESSION.
450+
PARAMS contain the file uri, the 1-based symbol position and an
451+
optional includeDeclaration flag that defaults to true."
452+
(let ((include-declaration (if (plist-member params :includeDeclaration)
453+
(and (plist-get params :includeDeclaration) t)
454+
t)))
455+
(eca-editor--nav-request
456+
session request params "textDocument/references"
457+
(list :context (list :includeDeclaration (or include-declaration :json-false))))))
458+
264459
(provide 'eca-editor)
265460
;;; eca-editor.el ends here

eca.el

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ frames captured via `backtrace-get-frames'."
246246
(params (plist-get request :params)))
247247
(pcase method
248248
("editor/getDiagnostics" (eca-editor-get-diagnostics session params))
249+
("editor/getDefinition" (eca-editor-get-definition session request params))
250+
("editor/getReferences" (eca-editor-get-references session request params))
249251
("chat/askQuestion" (eca-chat-handle-ask-question session request params))
250252
(_ (eca-warn "Unknown server request %s" method)))))
251253

@@ -281,7 +283,18 @@ backtrace. On older Emacs, runs BODY without capture."
281283
(cl-remf (eca--session-response-handlers session) id)
282284
(funcall error-callback (plist-get json-data :error)))))
283285
('notification (eca--handle-server-notification session json-data))
284-
('request (let ((response (eca--handle-server-request session json-data)))
286+
;; Handler errors must not escape into the process filter:
287+
;; answer the server with an error status instead of leaving
288+
;; the request pending until it times out server-side.
289+
('request (let ((response (condition-case req-err
290+
(eca--handle-server-request session json-data)
291+
(error
292+
(eca--log-error session req-err "handle-server-request" backtrace)
293+
(eca-warn "Error handling server request %s: %s"
294+
(plist-get json-data :method)
295+
(error-message-string req-err))
296+
(list :status "error"
297+
:message (error-message-string req-err))))))
285298
(unless (eq response :async)
286299
(eca-api-send-request-response session json-data response)))))
287300
(error
@@ -304,7 +317,9 @@ backtrace. On older Emacs, runs BODY without capture."
304317
:version (emacs-version))
305318
:capabilities (list :codeAssistant (list :chat t
306319
:chatCapabilities (list :askQuestion t)
307-
:editor (list :diagnostics t)))
320+
:editor (list :diagnostics t
321+
:definition t
322+
:references t)))
308323
:initializationOptions (list :chatAgent eca-chat-custom-agent)
309324
:workspaceFolders (vconcat (-map (lambda (folder)
310325
(list :uri (eca--path-to-uri folder)

0 commit comments

Comments
 (0)