branch: externals/elpa commit 42177d02d765c039a1343f5f6966dc50ec278c21 Author: João Távora <joaotav...@gmail.com> Commit: João Távora <joaotav...@gmail.com>
New "deferred requests" that wait until server is ready Calling textDocument/hover or textDocument/documentHighlight before the server has had a chance to process a textDocument/didChange is normally useless. The matter is worse for servers like RLS which only become ready much later and send a special notif for it (see https://github.com/rust-lang-nursery/rls/issues/725). So, keeping the same coding style add a DEFERRED arg to eglot--request that makes it maybe not run the function immediately. Add a bunch of logic for probing readiness of servers. * README.md: Update * eglot.el (eglot--deferred-actions): New process-local var. (eglot--process-filter): Call deferred actions. (eglot--request): Rewrite. (eglot--sync-request): Rewrite. (eglot--call-deferred, eglot--ready-predicates) (eglot--server-ready-p): New helpers. (eglot--signal-textDocument/didChange): Set spinner and call deferred actions. (eglot-completion-at-point): Pass DEFERRED to eglot-sync-request. (eglot-eldoc-function): Pass DEFERRED to eglot-request (eglot--rls-probably-ready-for-p): New helper. (rust-mode-hook): Add eglot--setup-rls-idiosyncrasies (eglot--setup-rls-idiosyncrasies): New helper. --- README.md | 10 +-- eglot.el | 237 +++++++++++++++++++++++++++++++++++--------------------------- 2 files changed, 140 insertions(+), 107 deletions(-) diff --git a/README.md b/README.md index faf2dea..66f2131 100644 --- a/README.md +++ b/README.md @@ -111,19 +111,21 @@ User-visible differences: - Automatically restarts frequently crashing servers (like RLS). - Server-initiated edits are confirmed with the user. - Diagnostics work out-of-the-box (no `flycheck.el` needed). +- Smoother/more responsive (read below). Under the hood: - Message parser is much much simpler. -- Easier to read and maintain elisp. Yeah I know, *extremely - subjective*, so judge for yourself. +- Defers signature requests like `textDocument/hover` until server is + ready. Also sends `textDocument/didChange` for groups of edits, not + one per each tiny change. +- Easier to read and maintain elisp. Yeah I know, *very subjective*, + so judge for yourself. - About 1k LOC lighter. - Development doesn't require Cask, just Emacs. - Project support doesn't need `projectile.el`, uses Emacs's `project.el` - Requires the upcoming Emacs 26 - Contained in one file -- Sends `textDocument/didChange` for groups of edits, not one per each - tiny change. - Its missing tests! This is *not good* [lsp]: https://microsoft.github.io/language-server-protocol/ diff --git a/eglot.el b/eglot.el index d5eae03..9a0b824 100644 --- a/eglot.el +++ b/eglot.el @@ -135,6 +135,10 @@ A list (WHAT SERIOUS-P)." t) Either a list of strings (a shell command and arguments), or a list of a single string of the form <host>:<port>") +(eglot--define-process-var eglot--deferred-actions + (make-hash-table :test #'equal) + "Actions deferred to when server is thought to be ready.") + (defun eglot--make-process (name managed-major-mode contact) "Make a process from CONTACT. NAME is a name to give the inferior process or connection. @@ -442,7 +446,8 @@ INTERACTIVE is t if called interactively." (throw done :waiting-for-more-bytes-in-this-message)))))))) ;; Saved parsing state for next visit to this filter ;; - (setf (eglot--expected-bytes proc) expected-bytes)))))) + (setf (eglot--expected-bytes proc) expected-bytes)))) + (eglot--call-deferred proc))) (defun eglot-events-buffer (process &optional interactive) "Display events buffer for current LSP connection PROCESS. @@ -549,110 +554,118 @@ is a symbol saying if this is a client or server originated." (interactive (list (eglot--current-process-or-lose))) (setf (eglot--status process) nil)) -(cl-defun eglot--request (process - method - params - &key success-fn error-fn timeout-fn (async-p t) - (timeout eglot-request-timeout)) - "Make a request to PROCESS, expecting a reply. -Return the ID of this request, unless ASYNC-P is nil, in which -case never returns locally. Wait TIMEOUT seconds for a -response." - (let* ((id (eglot--next-request-id)) - (timeout-fn (or timeout-fn - (lambda () - (eglot--warn - "(request) Tired of waiting for reply to %s" id)))) - (error-fn (or error-fn - (cl-function - (lambda (&key code message &allow-other-keys) - (setf (eglot--status process) `(,message t)) - (eglot--warn - "(request) Request id=%s errored with code=%s: %s" - id code message))))) - (success-fn (or success-fn - (cl-function - (lambda (&rest result-body) - (eglot--debug - "(request) Request id=%s replied to with result=%s" - id result-body))))) - (catch-tag (cl-gensym (format "eglot--tag-%d-" id)))) - (eglot--process-send process - (eglot--obj :jsonrpc "2.0" - :id id - :method method - :params params)) - (catch catch-tag - (let ((timeout-timer - (run-with-timer - timeout nil - (if async-p - (lambda () - (remhash id (eglot--pending-continuations process)) - (funcall timeout-fn)) - (lambda () - (remhash id (eglot--pending-continuations process)) - (throw catch-tag (funcall timeout-fn))))))) - (puthash id - (list (if async-p - success-fn - (lambda (&rest args) - (throw catch-tag (apply success-fn args)))) - (if async-p - error-fn - (lambda (&rest args) - (throw catch-tag (apply error-fn args)))) - timeout-timer) - (eglot--pending-continuations process)) - (unless async-p - (unwind-protect - (while t - (unless (process-live-p process) - (cond ((eglot--moribund process) - (throw catch-tag (delete-process process))) - (t - (eglot--error - "(request) Proc %s died unexpectedly during request with code %s" - process - (process-exit-status process))))) - (accept-process-output nil 0.01)) - (when (memq timeout-timer timer-list) - (eglot--message - "(request) Last-change cancelling timer for continuation %s" id) - (cancel-timer timeout-timer)))))) - ;; Finally, return the id. - id)) +(defun eglot--call-deferred (proc) + "Call PROC's deferred actions, who may again defer themselves." + (let ((actions (hash-table-values (eglot--deferred-actions proc)))) + (eglot--log-event proc `(:running-deferred ,(length actions))) + (mapc #'funcall (mapcar #'car actions)))) + +(defvar eglot--ready-predicates '(eglot--server-ready-p) + "Special hook of predicates controlling deferred actions. +When one of these functions returns nil, a deferrable +`eglot--request' will be deferred. Each predicate is passed the +an symbol for the request request and a process object.") + +(defun eglot--server-ready-p (_what _proc) + "Tell if server of PROC ready for processing deferred WHAT." + (not (eglot--outstanding-edits-p))) (cl-defmacro eglot--lambda (cl-lambda-list &body body) (declare (indent 1) (debug (sexp &rest form))) `(cl-function (lambda ,cl-lambda-list ,@body))) -(defun eglot--sync-request (proc method params) +(cl-defun eglot--request (proc + method + params + &rest args + &key success-fn error-fn timeout-fn + (timeout eglot-request-timeout) + (deferred nil)) + "Make a request to PROCESS, expecting a reply. +Return the ID of this request. Wait TIMEOUT seconds for response. +If DEFERRED, maybe defer request to the future, or never at all, +in case a new request with identical DEFERRED and for the same +buffer overrides it. However, if that happens, the original +timeout keeps counting." + (let* ((id (eglot--next-request-id)) + (existing-timer nil) + (make-timeout + (lambda ( ) + (or existing-timer + (run-with-timer + timeout nil + (lambda () + (remhash id (eglot--pending-continuations proc)) + (funcall (or timeout-fn + (lambda () + (eglot--error + "Tired of waiting for reply to %s, id=%s" + method id)))))))))) + (when deferred + (let* ((buf (current-buffer)) + (existing (gethash (list deferred buf) (eglot--deferred-actions proc)))) + (when existing (setq existing-timer (cadr existing))) + (if (run-hook-with-args-until-failure 'eglot--ready-predicates + deferred proc) + (remhash (list deferred buf) (eglot--deferred-actions proc)) + (eglot--log-event proc `(:deferring ,method :id ,id :params ,params)) + (let* ((buf (current-buffer)) (point (point)) + (later (lambda () + (when (buffer-live-p buf) + (with-current-buffer buf + (save-excursion (goto-char point) + (apply #'eglot--request proc + method params args))))))) + (puthash (list deferred buf) (list later (funcall make-timeout)) + (eglot--deferred-actions proc)) + (cl-return-from eglot--request nil))))) + ;; Really run it + ;; + (puthash id + (list (or success-fn (eglot--lambda (&rest result-body) + (eglot--debug + "Request %s, id=%s replied to with result=%s" + method id result-body))) + (or error-fn (eglot--lambda + (&key code message &allow-other-keys) + (setf (eglot--status proc) `(,message t)) + (eglot--warn + "Request %s, id=%s errored with code=%s: %s" + method id code message))) + (funcall make-timeout)) + (eglot--pending-continuations proc)) + (eglot--process-send proc (eglot--obj :jsonrpc "2.0" + :id id + :method method + :params params)))) + +(defun eglot--sync-request (proc method params &optional deferred) "Like `eglot--request' for PROC, METHOD and PARAMS, but synchronous. -Meaning only return locally if successful, otherwise exit non-locally." - (let* ((timeout-error-sym (cl-gensym)) - (catch-tag (make-symbol "eglot--sync-request-catch-tag")) - (retval - (catch catch-tag - (eglot--request proc method params - :success-fn (lambda (&rest args) - (throw catch-tag (if (vectorp (car args)) - (car args) - args))) - :error-fn (eglot--lambda - (&key code message &allow-other-keys) - (eglot--error "Oops: %s: %s" code message)) - :timeout-fn (lambda () - (throw catch-tag timeout-error-sym)) - :async-p nil)))) - ;; FIXME: There's maybe an emacs bug here. Because timeout-fn runs - ;; in a timer, the better and obvious choice of throwing the erro - ;; in the lambda is not quitting the `accept-process-output' - ;; infinite loop up there. So use this contorted strategy with - ;; `cl-gensym'. - (if (eq retval timeout-error-sym) - (eglot--error "Tired of waiting for reply to sync request") - retval))) +Meaning only return locally if successful, otherwise exit non-locally. +DEFERRED is passed to `eglot--request', which see." + ;; Launching a deferred sync request with outstanding changes is a + ;; bad idea, since that might lead to the request never having a + ;; chance to run, because `eglot--ready-predicates'. + (when deferred (eglot--signal-textDocument/didChange)) + (let* ((done (make-symbol "eglot--sync-request-catch-tag")) + (res + (catch done (eglot--request + proc method params + :success-fn (lambda (&rest args) + (throw done (if (vectorp (car args)) + (car args) args))) + :error-fn (eglot--lambda + (&key code message &allow-other-keys) + (throw done + `(error ,(format "Oops: %s: %s" + code message)))) + :timeout-fn (lambda () + (throw done '(error "Timed out"))) + :deferred deferred) + ;; now spin, baby! + (while t (accept-process-output nil 0.01))))) + (when (and (listp res) (eq 'error (car res))) (eglot--error (cadr res))) + res)) (cl-defun eglot--notify (process method params) "Notify PROCESS of something, don't expect a reply.e" @@ -1113,7 +1126,9 @@ Records START, END and PRE-CHANGE-LENGTH locally." :end end-pos) :rangeLength len :text after-text)]))))) - (setq eglot--recent-changes (cons [] []))))) + (setq eglot--recent-changes (cons [] [])) + (setf (eglot--spinner proc) (list nil :textDocument/didChange t)) + (eglot--call-deferred proc)))) (defun eglot--signal-textDocument/didOpen () "Send textDocument/didOpen to server." @@ -1273,7 +1288,8 @@ DUMMY is ignored" (let* ((resp (eglot--sync-request proc :textDocument/completion - (eglot--current-buffer-TextDocumentPositionParams))) + (eglot--current-buffer-TextDocumentPositionParams) + :textDocument/completion)) (items (if (vectorp resp) resp (plist-get resp :items)))) (eglot--mapply (eglot--lambda (&key insertText label kind detail @@ -1311,7 +1327,8 @@ DUMMY is ignored" (if (vectorp contents) contents (list contents)) - "\n"))))) + "\n"))) + :deferred :textDocument/hover)) (when (eglot--server-capable :documentHighlightProvider) (eglot--request proc :textDocument/documentHighlight position-params @@ -1331,7 +1348,8 @@ DUMMY is ignored" (overlay-put ov 'evaporate t) (overlay-put ov :kind kind) ov))) - highlights)))))))) + highlights))))) + :deferred :textDocument/documentHighlight))) nil) (defun eglot-imenu (oldfun) @@ -1438,6 +1456,19 @@ Proceed? " ;;; Rust-specific ;;; +(defun eglot--rls-probably-ready-for-p (what proc) + "Guess if the RLS running in PROC is ready for WHAT." + (or (eq what :textDocument/completion) ; RLS normally ready for this + ; one, even if building + (pcase-let ((`(,_id ,what ,done) (eglot--spinner proc))) + (and (equal "Indexing" what) done)))) + +(add-hook 'rust-mode-hook 'eglot--setup-rls-idiosyncrasies) + +(defun eglot--setup-rls-idiosyncrasies () + "RLS needs special treatment..." + (add-hook 'eglot--ready-predicates 'eglot--rls-probably-ready-for-p t t)) + (cl-defun eglot--server-window/progress (process &key id done title &allow-other-keys) "Handle notification window/progress"