diff options
Diffstat (limited to 'lisp/progmodes/eglot.el')
-rw-r--r-- | lisp/progmodes/eglot.el | 692 |
1 files changed, 382 insertions, 310 deletions
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el index cb9190a7523..c4f773c8426 100644 --- a/lisp/progmodes/eglot.el +++ b/lisp/progmodes/eglot.el @@ -2,11 +2,12 @@ ;; Copyright (C) 2018-2023 Free Software Foundation, Inc. -;; Version: 1.12-emacs29 +;; Version: 1.14 ;; Author: João Távora <joaotavora@gmail.com> ;; Maintainer: João Távora <joaotavora@gmail.com> ;; URL: https://github.com/joaotavora/eglot ;; Keywords: convenience, languages +;; Package-Requires: ((emacs "26.3") (jsonrpc "1.0.16") (flymake "1.2.1") (project "0.9.8") (xref "1.6.2") (eldoc "1.14.0") (seq "2.23") (external-completion "0.1")) ;; This is a GNU ELPA :core package. Avoid adding functionality ;; that is not available in the version of Emacs recorded above or any @@ -96,34 +97,30 @@ (require 'imenu) (require 'cl-lib) -(require 'project) + (require 'url-parse) (require 'url-util) (require 'pcase) (require 'compile) ; for some faces (require 'warnings) -(require 'flymake) -(require 'xref) (eval-when-compile (require 'subr-x)) -(require 'jsonrpc) (require 'filenotify) (require 'ert) -(require 'array) -(require 'external-completion) - -;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are -;; using the latest version from GNU Elpa when we load eglot.el. Use an -;; heuristic to see if we need to `load' it in Emacs < 28. -(if (and (< emacs-major-version 28) - (not (boundp 'eldoc-documentation-strategy))) - (load "eldoc") - (require 'eldoc)) - -;; Similar issue as above for Emacs 26.3 and seq.el. -(if (< emacs-major-version 27) - (load "seq") - (require 'seq)) +(require 'text-property-search nil t) + +;; These dependencies are also GNU ELPA core packages. Because of +;; bug#62576, since there is a risk that M-x package-install, despite +;; having installed them, didn't correctly re-load them over the +;; built-in versions. +(eval-and-compile + (load "project") + (load "eldoc") + (load "seq") + (load "flymake") + (load "xref") + (load "jsonrpc") + (load "external-completion")) ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) (defvar markdown-fontify-code-blocks-natively) @@ -183,7 +180,7 @@ chosen (interactively or automatically)." when probe return (cons probe args) finally (funcall err))))))) -(defvar eglot-server-programs `(((rust-ts-mode rust-mode) . ,(eglot-alternatives '("rust-analyzer" "rls"))) +(defvar eglot-server-programs `(((rust-ts-mode rust-mode) . ("rust-analyzer")) ((cmake-mode cmake-ts-mode) . ("cmake-language-server")) (vimrc-mode . ("vim-language-server" "--stdio")) ((python-mode python-ts-mode) @@ -220,7 +217,11 @@ chosen (interactively or automatically)." ((java-mode java-ts-mode) . ("jdtls")) (dart-mode . ("dart" "language-server" "--client-id" "emacs.eglot-dart")) - (elixir-mode . ("language_server.sh")) + ((elixir-mode elixir-ts-mode heex-ts-mode) + . ,(if (and (fboundp 'w32-shell-dos-semantics) + (w32-shell-dos-semantics)) + '("language_server.bat") + '("language_server.sh"))) (ada-mode . ("ada_language_server")) (scala-mode . ,(eglot-alternatives '("metals" "metals-emacs"))) @@ -241,7 +242,7 @@ chosen (interactively or automatically)." ("css-languageserver" "--stdio")))) (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) ((dockerfile-mode dockerfile-ts-mode) . ("docker-langserver" "--stdio")) - ((clojure-mode clojurescript-mode clojurec-mode) + ((clojure-mode clojurescript-mode clojurec-mode clojure-ts-mode) . ("clojure-lsp")) ((csharp-mode csharp-ts-mode) . ,(eglot-alternatives @@ -389,14 +390,20 @@ done by `eglot-reconnect'." "If non-nil, activate Eglot in cross-referenced non-project files." :type 'boolean) +(defcustom eglot-prefer-plaintext nil + "If non-nil, always request plaintext responses to hover requests." + :type 'boolean) + (defcustom eglot-menu-string "eglot" "String displayed in mode line when Eglot is active." :type 'string) (defcustom eglot-report-progress t - "If non-nil, show progress of long running LSP server work" + "If non-nil, show progress of long running LSP server work. +If set to `messages', use *Messages* buffer, else use Eglot's +mode line indicator." :type 'boolean - :version "29.1") + :version "1.10") (defvar eglot-withhold-process-id nil "If non-nil, Eglot will not send the Emacs process id to the language server. @@ -441,6 +448,10 @@ This can be useful when using docker to run a language server.") (if (>= emacs-major-version 27) (executable-find command remote) (executable-find command))) +(defun eglot--accepted-formats () + (if (and (not eglot-prefer-plaintext) (fboundp 'gfm-view-mode)) + ["markdown" "plaintext"] ["plaintext"])) + ;;; Message verification helpers ;;; @@ -476,9 +487,7 @@ This can be useful when using docker to run a language server.") (SymbolInformation (:name :kind :location) (:deprecated :containerName)) (DocumentSymbol (:name :range :selectionRange :kind) - ;; `:containerName' isn't really allowed , but - ;; it simplifies the impl of `eglot-imenu'. - (:detail :deprecated :children :containerName)) + (:detail :deprecated :children)) (TextDocumentEdit (:textDocument :edits) ()) (TextEdit (:range :newText)) (VersionedTextDocumentIdentifier (:uri :version) ()) @@ -771,14 +780,12 @@ treated as in `eglot--dbind'." :tagSupport (:valueSet [1])) :contextSupport t) :hover (list :dynamicRegistration :json-false - :contentFormat - (if (fboundp 'gfm-view-mode) - ["markdown" "plaintext"] - ["plaintext"])) + :contentFormat (eglot--accepted-formats)) :signatureHelp (list :dynamicRegistration :json-false :signatureInformation `(:parameterInformation (:labelOffsetSupport t) + :documentationFormat ,(eglot--accepted-formats) :activeParameterSupport t)) :references `(:dynamicRegistration :json-false) :definition (list :dynamicRegistration :json-false @@ -837,12 +844,9 @@ treated as in `eglot--dbind'." :documentation "Short nickname for the associated project." :accessor eglot--project-nickname :reader eglot-project-nickname) - (major-modes - :documentation "Major modes server is responsible for in a given project." - :accessor eglot--major-modes) - (language-id - :documentation "Language ID string for the mode." - :accessor eglot--language-id) + (languages + :documentation "Alist ((MODE . LANGUAGE-ID-STRING)...) of managed languages." + :accessor eglot--languages) (capabilities :documentation "JSON object containing server capabilities." :accessor eglot--capabilities) @@ -877,6 +881,12 @@ treated as in `eglot--dbind'." :documentation "Represents a server. Wraps a process for LSP communication.") +(defun eglot--major-modes (s) "Major modes server S is responsible for." + (mapcar #'car (eglot--languages s))) + +(defun eglot--language-ids (s) "LSP Language ID strings for server S's modes." + (mapcar #'cdr (eglot--languages s))) + (cl-defmethod initialize-instance :before ((_server eglot-lsp-server) &optional args) (cl-remf args :initializationOptions)) @@ -904,7 +914,7 @@ SERVER." (unwind-protect (progn (setf (eglot--shutdown-requested server) t) - (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) + (eglot--request server :shutdown nil :timeout (or timeout 1.5)) (jsonrpc-notify server :exit nil)) ;; Now ask jsonrpc.el to shut down the server. (jsonrpc-shutdown server (not preserve-buffers)) @@ -962,42 +972,44 @@ PRESERVE-BUFFERS as in `eglot-shutdown', which see." (defun eglot--lookup-mode (mode) "Lookup `eglot-server-programs' for MODE. -Return (MANAGED-MODES LANGUAGE-ID CONTACT-PROXY). +Return (LANGUAGES . CONTACT-PROXY). MANAGED-MODES is a list with MODE as its first element. Subsequent elements are other major modes also potentially managed by the server that is to manage MODE. -If not specified in `eglot-server-programs' (which see), -LANGUAGE-ID is determined from MODE's name. +LANGUAGE-IDS is a list of the same length as MANAGED-MODES. Each +elem is derived from the corresponding mode name, if not +specified in `eglot-server-programs' (which see). CONTACT-PROXY is the value of the corresponding `eglot-server-programs' entry." - (cl-loop - for (modes . contact) in eglot-server-programs - for mode-symbols = (cons mode - (delete mode - (mapcar #'car - (mapcar #'eglot--ensure-list - (eglot--ensure-list modes))))) - thereis (cl-some - (lambda (spec) - (cl-destructuring-bind (probe &key language-id &allow-other-keys) - (eglot--ensure-list spec) - (and (provided-mode-derived-p mode probe) - (list - mode-symbols - (or language-id - (or (get mode 'eglot-language-id) - (get spec 'eglot-language-id) - (string-remove-suffix "-mode" (symbol-name mode)))) - contact)))) - (if (or (symbolp modes) (keywordp (cadr modes))) - (list modes) modes)))) + (cl-flet ((languages (main-mode-sym specs) + (let* ((res + (mapcar (jsonrpc-lambda (sym &key language-id &allow-other-keys) + (cons sym + (or language-id + (or (get sym 'eglot-language-id) + (replace-regexp-in-string + "\\(?:-ts\\)?-mode$" "" + (symbol-name sym)))))) + specs)) + (head (cl-find main-mode-sym res :key #'car))) + (cons head (delq head res))))) + (cl-loop + for (modes . contact) in eglot-server-programs + for specs = (mapcar #'eglot--ensure-list + (if (or (symbolp modes) (keywordp (cadr modes))) + (list modes) modes)) + thereis (cl-some (lambda (spec) + (cl-destructuring-bind (sym &key &allow-other-keys) spec + (and (provided-mode-derived-p mode sym) + (cons (languages sym specs) contact)))) + specs)))) (defun eglot--guess-contact (&optional interactive) "Helper for `eglot'. -Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is +Return (MANAGED-MODES PROJECT CLASS CONTACT LANG-IDS). If INTERACTIVE is non-nil, maybe prompt user, else error as soon as something can't be guessed." (let* ((guessed-mode (if buffer-file-name major-mode)) @@ -1015,11 +1027,10 @@ be guessed." ((not guessed-mode) (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) (t guessed-mode))) - (triplet (eglot--lookup-mode main-mode)) - (managed-modes (car triplet)) - (language-id (or (cadr triplet) - (string-remove-suffix "-mode" (symbol-name guessed-mode)))) - (guess (caddr triplet)) + (languages-and-contact (eglot--lookup-mode main-mode)) + (managed-modes (mapcar #'car (car languages-and-contact))) + (language-ids (mapcar #'cdr (car languages-and-contact))) + (guess (cdr languages-and-contact)) (guess (if (functionp guess) (funcall guess interactive) guess)) @@ -1067,7 +1078,7 @@ be guessed." full-program-invocation 'eglot-command-history))) guess))) - (list managed-modes (eglot--current-project) class contact language-id))) + (list managed-modes (eglot--current-project) class contact language-ids))) (defvar eglot-lsp-context) (put 'eglot-lsp-context 'variable-documentation @@ -1085,24 +1096,25 @@ suitable root directory for a given LSP server's purposes." `(transient . ,(expand-file-name default-directory))))) ;;;###autoload -(defun eglot (managed-major-mode project class contact language-id +(defun eglot (managed-major-modes project class contact language-ids &optional _interactive) - "Start LSP server in support of PROJECT's buffers under MANAGED-MAJOR-MODE. + "Start LSP server for PROJECT's buffers under MANAGED-MAJOR-MODES. -This starts a Language Server Protocol (LSP) server suitable for the -buffers of PROJECT whose `major-mode' is MANAGED-MAJOR-MODE. -CLASS is the class of the LSP server to start and CONTACT specifies -how to connect to the server. +This starts a Language Server Protocol (LSP) server suitable for +the buffers of PROJECT whose `major-mode' is among +MANAGED-MAJOR-MODES. CLASS is the class of the LSP server to +start and CONTACT specifies how to connect to the server. -Interactively, the command attempts to guess MANAGED-MAJOR-MODE -from the current buffer's `major-mode', CLASS and CONTACT from -`eglot-server-programs' looked up by the major mode, and PROJECT from -`project-find-functions'. The search for active projects in this -context binds `eglot-lsp-context' (which see). +Interactively, the command attempts to guess MANAGED-MAJOR-MODES, +CLASS, CONTACT, and LANGUAGE-IDS from `eglot-server-programs', +according to the current buffer's `major-mode'. PROJECT is +guessed from `project-find-functions'. The search for active +projects in this context binds `eglot-lsp-context' (which see). -If it can't guess, it prompts the user for the mode and the server. -With a single \\[universal-argument] prefix arg, it always prompts for COMMAND. -With two \\[universal-argument], it also always prompts for MANAGED-MAJOR-MODE. +If it can't guess, it prompts the user for the mode and the +server. With a single \\[universal-argument] prefix arg, it +always prompts for COMMAND. With two \\[universal-argument], it +also always prompts for MANAGED-MAJOR-MODE. The LSP server of CLASS is started (or contacted) via CONTACT. If this operation is successful, current *and future* file @@ -1120,8 +1132,8 @@ CONTACT specifies how to contact the server. It is a keyword-value plist used to initialize CLASS or a plain list as described in `eglot-server-programs', which see. -LANGUAGE-ID is the language ID string to send to the server for -MANAGED-MAJOR-MODE, which matters to a minority of servers. +LANGUAGE-IDS is a list of language ID string to send to the +server for each element in MANAGED-MAJOR-MODES. INTERACTIVE is ignored and provided for backward compatibility." (interactive @@ -1132,8 +1144,9 @@ INTERACTIVE is ignored and provided for backward compatibility." (user-error "[eglot] Connection attempt aborted by user.")) (prog1 (append (eglot--guess-contact t) '(t)) (when current-server (ignore-errors (eglot-shutdown current-server)))))) - (eglot--connect (eglot--ensure-list managed-major-mode) - project class contact language-id)) + (eglot--connect (eglot--ensure-list managed-major-modes) + project class contact + (eglot--ensure-list language-ids))) (defun eglot-reconnect (server &optional interactive) "Reconnect to SERVER. @@ -1145,7 +1158,7 @@ INTERACTIVE is t if called interactively." (eglot--project server) (eieio-object-class-name server) (eglot--saved-initargs server) - (eglot--language-id server)) + (eglot--language-ids server)) (eglot--message "Reconnected!")) (defvar eglot--managed-mode) ; forward decl @@ -1218,8 +1231,8 @@ Each function is passed the server as an argument") (defvar-local eglot--cached-server nil "A cached reference to the current Eglot server.") -(defun eglot--connect (managed-modes project class contact language-id) - "Connect to MANAGED-MODES, LANGUAGE-ID, PROJECT, CLASS and CONTACT. +(defun eglot--connect (managed-modes project class contact language-ids) + "Connect to MANAGED-MODES, LANGUAGE-IDS, PROJECT, CLASS and CONTACT. This docstring appeases checkdoc, that's all." (let* ((default-directory (project-root project)) (nickname (project-name project)) @@ -1292,8 +1305,9 @@ This docstring appeases checkdoc, that's all." (setf (eglot--saved-initargs server) initargs) (setf (eglot--project server) project) (setf (eglot--project-nickname server) nickname) - (setf (eglot--major-modes server) (eglot--ensure-list managed-modes)) - (setf (eglot--language-id server) language-id) + (setf (eglot--languages server) + (cl-loop for m in managed-modes for l in language-ids + collect (cons m l))) (setf (eglot--inferior-process server) autostart-inferior-process) (run-hook-with-args 'eglot-server-initialized-hook server) ;; Now start the handshake. To honor `eglot-sync-connect' @@ -1312,6 +1326,7 @@ This docstring appeases checkdoc, that's all." (eq (jsonrpc-process-type server) 'network)) (emacs-pid)) + :clientInfo '(:name "Eglot") ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' ;; into `/path/to/baz.py', so LSP groks it. :rootPath (file-local-name @@ -1456,15 +1471,27 @@ CONNECT-ARGS are passed as additional arguments to (line-beginning-position n)))) "Return position of first character in current line.") +(cl-defun eglot--request (server method params &key + immediate + timeout cancel-on-input + cancel-on-input-retval) + "Like `jsonrpc-request', but for Eglot LSP requests. +Unless IMMEDIATE, send pending changes before making request." + (unless immediate (eglot--signal-textDocument/didChange)) + (jsonrpc-request server method params + :timeout timeout + :cancel-on-input cancel-on-input + :cancel-on-input-retval cancel-on-input-retval)) + ;;; Encoding fever ;;; (define-obsolete-function-alias - 'eglot-lsp-abiding-column 'eglot-utf-16-linepos "29.1") + 'eglot-lsp-abiding-column 'eglot-utf-16-linepos "1.12") (define-obsolete-function-alias - 'eglot-current-column 'eglot-utf-32-linepos "29.1") + 'eglot-current-column 'eglot-utf-32-linepos "1.12") (define-obsolete-variable-alias - 'eglot-current-column-function 'eglot-current-linepos-function "29.1") + 'eglot-current-column-function 'eglot-current-linepos-function "1.12") (defvar eglot-current-linepos-function #'eglot-utf-16-linepos "Function calculating position relative to line beginning. @@ -1505,11 +1532,11 @@ LBP defaults to `eglot--bol'." (funcall eglot-current-linepos-function))))) (define-obsolete-function-alias - 'eglot-move-to-current-column 'eglot-move-to-utf-32-linepos "29.1") + 'eglot-move-to-current-column 'eglot-move-to-utf-32-linepos "1.12") (define-obsolete-function-alias - 'eglot-move-to-lsp-abiding-column 'eglot-move-to-utf-16-linepos "29.1") + 'eglot-move-to-lsp-abiding-column 'eglot-move-to-utf-16-linepos "1.12") (define-obsolete-variable-alias -'eglot-move-to-column-function 'eglot-move-to-linepos-function "29.1") +'eglot-move-to-column-function 'eglot-move-to-linepos-function "1.12") (defvar eglot-move-to-linepos-function #'eglot-move-to-utf-16-linepos "Function to move to a position within a line reported by the LSP server. @@ -1648,10 +1675,17 @@ Doubles as an indicator of snippet support." (setq-local markdown-fontify-code-blocks-natively t) (insert string) (let ((inhibit-message t) - (message-log-max nil)) - (ignore-errors (delay-mode-hooks (funcall mode)))) - (font-lock-ensure) - (string-trim (buffer-string))))) + (message-log-max nil) + match) + (ignore-errors (delay-mode-hooks (funcall mode))) + (font-lock-ensure) + (goto-char (point-min)) + (let ((inhibit-read-only t)) + (when (fboundp 'text-property-search-forward) ;; FIXME: use compat + (while (setq match (text-property-search-forward 'invisible)) + (delete-region (prop-match-beginning match) + (prop-match-end match))))) + (string-trim (buffer-string)))))) (define-obsolete-variable-alias 'eglot-ignored-server-capabilites 'eglot-ignored-server-capabilities "1.8") @@ -1749,9 +1783,9 @@ and just return it. PROMPT shouldn't end with a question mark." (defun eglot--plist-keys (plist) "Get keys of a plist." (cl-loop for (k _v) on plist by #'cddr collect k)) -(defun eglot--ensure-list (x) (if (listp x) x (list x))) -(when (fboundp 'ensure-list) ; Emacs 28 or later - (define-obsolete-function-alias 'eglot--ensure-list #'ensure-list "29.1")) +(defalias 'eglot--ensure-list + (if (fboundp 'ensure-list) #'ensure-list + (lambda (x) (if (listp x) x (list x))))) ;;; Minor modes @@ -1832,6 +1866,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (unless (eglot--stay-out-of-p 'xref) (add-hook 'xref-backend-functions 'eglot-xref-backend nil t)) (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) + (add-hook 'completion-in-region-mode-hook #'eglot--capf-session-flush nil t) + (add-hook 'company-after-completion-hook #'eglot--capf-session-flush nil t) (add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t) (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) @@ -1863,6 +1899,8 @@ Use `eglot-managed-p' to determine if current buffer is managed.") (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) (remove-hook 'xref-backend-functions 'eglot-xref-backend t) (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) + (remove-hook 'completion-in-region-mode-hook #'eglot--capf-session-flush t) + (remove-hook 'company-after-completion-hook #'eglot--capf-session-flush t) (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t) (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) @@ -1957,8 +1995,8 @@ If it is activated, also signal textDocument/didOpen." (when update-mode-line (force-mode-line-update t))))))) -(defun eglot-manual () "Open documentation." - (declare (obsolete info "29.1")) +(defun eglot-manual () "Read Eglot's manual." + (declare (obsolete info "1.10")) (interactive) (info "(eglot)")) (easy-menu-define eglot-menu nil "Eglot" @@ -2038,7 +2076,7 @@ Uses THING, FACE, DEFS and PREPEND." mouse-face mode-line-highlight)))) (defun eglot--mode-line-format () - "Compose the Eglot's mode-line." + "Compose Eglot's mode-line." (let* ((server (eglot-current-server)) (nick (and server (eglot-project-nickname server))) (pending (and server (hash-table-count @@ -2075,7 +2113,15 @@ Uses THING, FACE, DEFS and PREPEND." '((mouse-3 eglot-forget-pending-continuations "Forget pending continuations")) "Number of outgoing, \ -still unanswered LSP requests to the server\n")))))))) +still unanswered LSP requests to the server\n"))) + ,@(cl-loop for pr hash-values of (eglot--progress-reporters server) + when (eq (car pr) 'eglot--mode-line-reporter) + append `("/" ,(eglot--mode-line-props + (format "%s%%%%" (or (nth 4 pr) "?")) + 'eglot-mode-line + nil + (format "(%s) %s %s" (nth 1 pr) + (nth 2 pr) (nth 3 pr)))))))))) (add-to-list 'mode-line-misc-info `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) @@ -2123,8 +2169,8 @@ still unanswered LSP requests to the server\n")))))))) (server command arguments) "Execute COMMAND on SERVER with `:workspace/executeCommand'. COMMAND is a symbol naming the command." - (jsonrpc-request server :workspace/executeCommand - `(:command ,(format "%s" command) :arguments ,arguments))) + (eglot--request server :workspace/executeCommand + `(:command ,(format "%s" command) :arguments ,arguments))) (cl-defmethod eglot-handle-notification (_server (_method (eql window/showMessage)) &key type message) @@ -2134,13 +2180,14 @@ COMMAND is a symbol naming the command." type message)) (cl-defmethod eglot-handle-request - (_server (_method (eql window/showMessageRequest)) &key type message actions) + (_server (_method (eql window/showMessageRequest)) + &key type message actions &allow-other-keys) "Handle server request window/showMessageRequest." (let* ((actions (append actions nil)) ;; gh#627 (label (completing-read (concat (format (propertize "[eglot] Server reports (type=%s): %s" - 'face (if (<= type 1) 'error)) + 'face (if (or (not type) (<= type 1)) 'error)) type message) "\nChoose an option: ") (or (mapcar (lambda (obj) (plist-get obj :title)) actions) @@ -2156,28 +2203,39 @@ COMMAND is a symbol naming the command." (_server (_method (eql telemetry/event)) &rest _any) "Handle notification telemetry/event.") ;; noop, use events buffer +(defalias 'eglot--reporter-update + (if (> emacs-major-version 26) #'progress-reporter-update + (lambda (a b &optional _c) (progress-reporter-update a b)))) + (cl-defmethod eglot-handle-notification (server (_method (eql $/progress)) &key token value) "Handle $/progress notification identified by TOKEN from SERVER." (when eglot-report-progress (cl-flet ((fmt (&rest args) (mapconcat #'identity args " ")) + (mkpr (title) + (if (eq eglot-report-progress 'messages) + (make-progress-reporter + (format "[eglot] %s %s: %s" + (eglot-project-nickname server) token title)) + (list 'eglot--mode-line-reporter token title))) (upd (pcnt msg &optional (pr (gethash token (eglot--progress-reporters server)))) - (when pr (progress-reporter-update pr pcnt msg)))) + (cond + ((eq (car pr) 'eglot--mode-line-reporter) + (setcdr (cddr pr) (list msg pcnt)) + (force-mode-line-update t)) + (pr (progress-reporter-update pr pcnt msg))))) (eglot--dbind ((WorkDoneProgress) kind title percentage message) value (pcase kind ("begin" - (let ((prefix (format (concat "[eglot] %s %s:" (when percentage " ")) - (eglot-project-nickname server) token))) - (upd percentage (fmt title message) - (puthash token - (if percentage - (make-progress-reporter prefix 0 100 percentage 1 0) - (make-progress-reporter prefix nil nil nil 1 0)) - (eglot--progress-reporters server))))) - ("report" (upd percentage (fmt title message))) - ("end" (upd (or percentage 100) (fmt title message)) - (remhash token (eglot--progress-reporters server)))))))) + (upd percentage (fmt title message) + (puthash token (mkpr title) + (eglot--progress-reporters server)))) + ("report" (upd percentage message)) + ("end" (upd (or percentage 100) message) + (run-at-time 2 nil + (lambda () + (remhash token (eglot--progress-reporters server)))))))))) (cl-defmethod eglot-handle-notification (_server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics @@ -2194,7 +2252,7 @@ COMMAND is a symbol naming the command." (buffer (find-buffer-visiting path))) (with-current-buffer buffer (cl-loop - initially (assoc-delete-all path flymake-list-only-diagnostics #'string=) + initially (assoc-delete-all path flymake-list-only-diagnostics) for diag-spec across diagnostics collect (eglot--dbind ((Diagnostic) range code message severity source tags) diag-spec @@ -2248,7 +2306,7 @@ COMMAND is a symbol naming the command." into diags finally (setq flymake-list-only-diagnostics - (assoc-delete-all path flymake-list-only-diagnostics #'string=)) + (assoc-delete-all path flymake-list-only-diagnostics)) (push (cons path diags) flymake-list-only-diagnostics))))) (cl-defun eglot--register-unregister (server things how) @@ -2303,7 +2361,7 @@ THINGS are either registrations or unregisterations (sic)." (append (eglot--VersionedTextDocumentIdentifier) (list :languageId - (eglot--language-id (eglot--current-server-or-lose)) + (alist-get major-mode (eglot--languages (eglot--current-server-or-lose))) :text (eglot--widening (buffer-substring-no-properties (point-min) (point-max)))))) @@ -2416,16 +2474,6 @@ Records BEG, END and PRE-CHANGE-LENGTH locally." (run-hooks 'eglot--document-changed-hook) (setq eglot--change-idle-timer nil)))))))) -;; HACK! 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 `jsonrpc-connection-ready-p'. -(advice-add #'jsonrpc-request :before - (cl-function (lambda (_proc _method _params &key - deferred &allow-other-keys) - (when (and eglot--managed-mode deferred) - (eglot--signal-textDocument/didChange)))) - '((name . eglot--signal-textDocument/didChange))) - (defvar-local eglot-workspace-configuration () "Configure LSP servers specifically for a given project. @@ -2578,8 +2626,8 @@ When called interactively, use the currently active server" (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) (ignore-errors (eglot--apply-text-edits - (jsonrpc-request server :textDocument/willSaveWaitUntil params - :timeout 0.5)))))) + (eglot--request server :textDocument/willSaveWaitUntil params + :timeout 0.5)))))) (defun eglot--signal-textDocument/didSave () "Maybe send textDocument/didSave to server." @@ -2691,8 +2739,8 @@ If BUFFER, switch to it before." (propertize (alist-get kind eglot--symbol-kind-names "Unknown") 'face 'shadow)) 'eglot--lsp-workspaceSymbol wss))) - (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol - `(:query ,pat))))) + (eglot--request (eglot--current-server-or-lose) :workspace/symbol + `(:query ,pat))))) (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) "Yet another tricky connection between LSP and Elisp completion semantics." @@ -2748,7 +2796,7 @@ If BUFFER, switch to it before." (cadr (split-string (symbol-name method) "/")))))) (let ((response - (jsonrpc-request + (eglot--request (eglot--current-server-or-lose) method (append (eglot--TextDocumentPositionParams) extra-params)))) (eglot--collecting-xrefs (collect) @@ -2811,9 +2859,9 @@ If BUFFER, switch to it before." (eglot--lambda ((SymbolInformation) name location) (eglot--dbind ((Location) uri range) location (collect (eglot--xref-make-match name uri range)))) - (jsonrpc-request (eglot--current-server-or-lose) - :workspace/symbol - `(:query ,pattern)))))) + (eglot--request (eglot--current-server-or-lose) + :workspace/symbol + `(:query ,pattern)))))) (defun eglot-format-buffer () "Format contents of current buffer." @@ -2845,7 +2893,7 @@ for which LSP on-type-formatting should be requested." '(:textDocument/formatting :documentFormattingProvider nil))))) (eglot--server-capable-or-lose cap) (eglot--apply-text-edits - (jsonrpc-request + (eglot--request (eglot--current-server-or-lose) method (cl-list* @@ -2854,8 +2902,14 @@ for which LSP on-type-formatting should be requested." :insertSpaces (if indent-tabs-mode :json-false t) :insertFinalNewline (if require-final-newline t :json-false) :trimFinalNewlines (if delete-trailing-lines t :json-false)) - args) - :deferred method)))) + args))))) + +(defvar eglot-cache-session-completions t + "If non-nil Eglot caches data during completion sessions.") + +(defvar eglot--capf-session :none "A cache used by `eglot-completion-at-point'.") + +(defun eglot--capf-session-flush (&optional _) (setq eglot--capf-session :none)) (defun eglot-completion-at-point () "Eglot's `completion-at-point' function." @@ -2872,41 +2926,50 @@ for which LSP on-type-formatting should be requested." :sortText))))) (metadata `(metadata (category . eglot) (display-sort-function . ,sort-completions))) - resp items (cached-proxies :none) + (local-cache :none) + (bounds (bounds-of-thing-at-point 'symbol)) + (orig-pos (point)) + (resolved (make-hash-table)) (proxies (lambda () - (if (listp cached-proxies) cached-proxies - (setq resp - (jsonrpc-request server - :textDocument/completion - (eglot--CompletionParams) - :deferred :textDocument/completion - :cancel-on-input t)) - (setq items (append - (if (vectorp resp) resp (plist-get resp :items)) - nil)) - (setq cached-proxies - (mapcar - (jsonrpc-lambda - (&rest item &key label insertText insertTextFormat - textEdit &allow-other-keys) - (let ((proxy - ;; Snippet or textEdit, it's safe to - ;; display/insert the label since - ;; it'll be adjusted. If no usable - ;; insertText at all, label is best, - ;; too. - (cond ((or (eql insertTextFormat 2) - textEdit - (null insertText) - (string-empty-p insertText)) - (string-trim-left label)) - (t insertText)))) - (unless (zerop (length proxy)) - (put-text-property 0 1 'eglot--lsp-item item proxy)) - proxy)) - items))))) - (resolved (make-hash-table)) + (if (listp local-cache) local-cache + (let* ((resp (eglot--request server + :textDocument/completion + (eglot--CompletionParams) + :cancel-on-input t)) + (items (append + (if (vectorp resp) resp (plist-get resp :items)) + nil)) + (cachep (and (listp resp) items + eglot-cache-session-completions + (eq (plist-get resp :isIncomplete) :json-false))) + (bounds (or bounds + (cons (point) (point)))) + (proxies + (mapcar + (jsonrpc-lambda + (&rest item &key label insertText insertTextFormat + textEdit &allow-other-keys) + (let ((proxy + ;; Snippet or textEdit, it's safe to + ;; display/insert the label since + ;; it'll be adjusted. If no usable + ;; insertText at all, label is best, + ;; too. + (cond ((or (eql insertTextFormat 2) + textEdit + (null insertText) + (string-empty-p insertText)) + (string-trim-left label)) + (t insertText)))) + (unless (zerop (length proxy)) + (put-text-property 0 1 'eglot--lsp-item item proxy)) + proxy)) + items))) + ;; (trace-values "Requested" (length proxies) cachep bounds) + (setq eglot--capf-session + (if cachep (list bounds proxies resolved orig-pos) :none)) + (setq local-cache proxies))))) (resolve-maybe ;; Maybe completion/resolve JSON object `lsp-comp' into ;; another JSON object, if at all possible. Otherwise, @@ -2917,13 +2980,21 @@ for which LSP on-type-formatting should be requested." (if (and (eglot--server-capable :completionProvider :resolveProvider) (plist-get lsp-comp :data)) - (jsonrpc-request server :completionItem/resolve - lsp-comp :cancel-on-input t) - lsp-comp))))) - (bounds (bounds-of-thing-at-point 'symbol))) + (eglot--request server :completionItem/resolve + lsp-comp :cancel-on-input t) + lsp-comp)))))) + (unless bounds (setq bounds (cons (point) (point)))) + (when (and (consp eglot--capf-session) + (= (car bounds) (car (nth 0 eglot--capf-session))) + (>= (cdr bounds) (cdr (nth 0 eglot--capf-session)))) + (setq local-cache (nth 1 eglot--capf-session) + resolved (nth 2 eglot--capf-session) + orig-pos (nth 3 eglot--capf-session)) + ;; (trace-values "Recalling cache" (length local-cache) bounds orig-pos) + ) (list - (or (car bounds) (point)) - (or (cdr bounds) (point)) + (car bounds) + (cdr bounds) (lambda (probe pred action) (cond ((eq action 'metadata) metadata) ; metadata @@ -2994,7 +3065,7 @@ for which LSP on-type-formatting should be requested." :company-require-match 'never :company-prefix-length (save-excursion - (when (car bounds) (goto-char (car bounds))) + (goto-char (car bounds)) (when (listp completion-capability) (looking-back (regexp-opt @@ -3002,6 +3073,7 @@ for which LSP on-type-formatting should be requested." (eglot--bol)))) :exit-function (lambda (proxy status) + (eglot--capf-session-flush) (when (memq status '(finished exact)) ;; To assist in using this whole `completion-at-point' ;; function inside `completion-in-region', ensure the exit @@ -3025,17 +3097,12 @@ for which LSP on-type-formatting should be requested." (let ((snippet-fn (and (eql insertTextFormat 2) (eglot--snippet-expansion-fn)))) (cond (textEdit - ;; Undo (yes, undo) the newly inserted completion. - ;; If before completion the buffer was "foo.b" and - ;; now is "foo.bar", `proxy' will be "bar". We - ;; want to delete only "ar" (`proxy' minus the - ;; symbol whose bounds we've calculated before) - ;; (github#160). - (delete-region (+ (- (point) (length proxy)) - (if bounds - (- (cdr bounds) (car bounds)) - 0)) - (point)) + ;; Revert buffer back to state when the edit + ;; was obtained from server. If a `proxy' + ;; "bar" was obtained from a buffer with + ;; "foo.b", the LSP edit applies to that' + ;; state, _not_ the current "foo.bar". + (delete-region orig-pos (point)) (eglot--dbind ((TextEdit) range newText) textEdit (pcase-let ((`(,beg . ,end) (eglot--range-region range))) @@ -3058,62 +3125,56 @@ for which LSP on-type-formatting should be requested." (mapconcat #'eglot--format-markup (if (vectorp contents) contents (list contents)) "\n")) -(defun eglot--sig-info (sigs active-sig sig-help-active-param) - (cl-loop - for (sig . moresigs) on (append sigs nil) for i from 0 - concat - (eglot--dbind ((SignatureInformation) label documentation parameters activeParameter) sig - (with-temp-buffer - (save-excursion (insert label)) - (let ((active-param (or activeParameter sig-help-active-param)) - params-start params-end) - ;; Ad-hoc attempt to parse label as <name>(<params>) - (when (looking-at "\\([^(]*\\)(\\([^)]+\\))") - (setq params-start (match-beginning 2) params-end (match-end 2)) - (add-face-text-property (match-beginning 1) (match-end 1) - 'font-lock-function-name-face)) - (when (eql i active-sig) - ;; Decide whether to add one-line-summary to signature line - (when (and (stringp documentation) - (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" - documentation)) - (setq documentation (match-string 1 documentation)) - (unless (string-prefix-p (string-trim documentation) label) - (goto-char (point-max)) - (insert ": " (eglot--format-markup documentation)))) - ;; Decide what to do with the active parameter... - (when (and (eql i active-sig) active-param - (< -1 active-param (length parameters))) - (eglot--dbind ((ParameterInformation) label documentation) - (aref parameters active-param) - ;; ...perhaps highlight it in the formals list - (when params-start - (goto-char params-start) - (pcase-let - ((`(,beg ,end) - (if (stringp label) - (let ((case-fold-search nil)) - (and (re-search-forward - (concat "\\<" (regexp-quote label) "\\>") - params-end t) - (list (match-beginning 0) (match-end 0)))) - (mapcar #'1+ (append label nil))))) - (if (and beg end) - (add-face-text-property - beg end - 'eldoc-highlight-function-argument)))) - ;; ...and/or maybe add its doc on a line by its own. - (when documentation - (goto-char (point-max)) - (insert "\n" - (propertize - (if (stringp label) - label - (apply #'buffer-substring (mapcar #'1+ label))) - 'face 'eldoc-highlight-function-argument) - ": " (eglot--format-markup documentation)))))) - (buffer-string)))) - when moresigs concat "\n")) +(defun eglot--sig-info (sig &optional sig-active briefp) + (eglot--dbind ((SignatureInformation) + ((:label siglabel)) + ((:documentation sigdoc)) parameters activeParameter) + sig + (with-temp-buffer + (save-excursion (insert siglabel)) + ;; Ad-hoc attempt to parse label as <name>(<params>) + (when (looking-at "\\([^(]*\\)(\\([^)]+\\))") + (add-face-text-property (match-beginning 1) (match-end 1) + 'font-lock-function-name-face)) + ;; Add documentation, indented so we can distinguish multiple signatures + (when-let (doc (and (not briefp) sigdoc (eglot--format-markup sigdoc))) + (goto-char (point-max)) + (insert "\n" (replace-regexp-in-string "^" " " doc))) + ;; Now to the parameters + (cl-loop + with active-param = (or sig-active activeParameter) + for i from 0 for parameter across parameters do + (eglot--dbind ((ParameterInformation) + ((:label parlabel)) + ((:documentation pardoc))) + parameter + ;; ...perhaps highlight it in the formals list + (when (and (eq i active-param)) + (save-excursion + (goto-char (point-min)) + (pcase-let + ((`(,beg ,end) + (if (stringp parlabel) + (let ((case-fold-search nil)) + (and (search-forward parlabel (line-end-position) t) + (list (match-beginning 0) (match-end 0)))) + (mapcar #'1+ (append parlabel nil))))) + (if (and beg end) + (add-face-text-property + beg end + 'eldoc-highlight-function-argument))))) + ;; ...and/or maybe add its doc on a line by its own. + (let (fpardoc) + (when (and pardoc (not briefp) + (not (string-empty-p + (setq fpardoc (eglot--format-markup pardoc))))) + (insert "\n " + (propertize + (if (stringp parlabel) parlabel + (apply #'substring siglabel (mapcar #'1+ parlabel))) + 'face (and (eq i active-param) 'eldoc-highlight-function-argument)) + ": " fpardoc))))) + (buffer-string)))) (defun eglot-signature-eldoc-function (cb) "A member of `eldoc-documentation-functions', for signatures." @@ -3124,13 +3185,18 @@ for which LSP on-type-formatting should be requested." :textDocument/signatureHelp (eglot--TextDocumentPositionParams) :success-fn (eglot--lambda ((SignatureHelp) - signatures activeSignature activeParameter) + signatures activeSignature (activeParameter 0)) (eglot--when-buffer-window buf - (funcall cb - (unless (seq-empty-p signatures) - (eglot--sig-info signatures - activeSignature - activeParameter))))) + (let ((active-sig (and (cl-plusp (length signatures)) + (aref signatures (or activeSignature 0))))) + (if (not active-sig) (funcall cb nil) + (funcall + cb (mapconcat (lambda (s) + (eglot--sig-info s (and (eq s active-sig) + activeParameter) + nil)) + signatures "\n") + :echo (eglot--sig-info active-sig activeParameter t)))))) :deferred :textDocument/signatureHelp)) t)) @@ -3145,7 +3211,8 @@ for which LSP on-type-formatting should be requested." (eglot--when-buffer-window buf (let ((info (unless (seq-empty-p contents) (eglot--hover-info contents range)))) - (funcall cb info :buffer t)))) + (funcall cb info + :echo (and info (string-match "\n" info)))))) :deferred :textDocument/hover)) (eglot--highlight-piggyback cb) t)) @@ -3179,49 +3246,55 @@ for which LSP on-type-formatting should be requested." :deferred :textDocument/documentHighlight) nil))) +(defun eglot--imenu-SymbolInformation (res) + "Compute `imenu--index-alist' for RES vector of SymbolInformation." + (mapcar + (pcase-lambda (`(,kind . ,objs)) + (cons + (alist-get kind eglot--symbol-kind-names "Unknown") + (mapcan + (pcase-lambda (`(,container . ,objs)) + (let ((elems (mapcar + (eglot--lambda ((SymbolInformation) kind name location) + (let ((reg (eglot--range-region + (plist-get location :range))) + (kind (alist-get kind eglot--symbol-kind-names))) + (cons (propertize name + 'breadcrumb-region reg + 'breadcrumb-kind kind) + (car reg)))) + objs))) + (if container (list (cons container elems)) elems))) + (seq-group-by + (eglot--lambda ((SymbolInformation) containerName) containerName) objs)))) + (seq-group-by (eglot--lambda ((SymbolInformation) kind) kind) res))) + +(defun eglot--imenu-DocumentSymbol (res) + "Compute `imenu--index-alist' for RES vector of DocumentSymbol." + (cl-labels ((dfs (&key name children range kind &allow-other-keys) + (let* ((reg (eglot--range-region range)) + (kind (alist-get kind eglot--symbol-kind-names)) + (name (propertize name + 'breadcrumb-region reg + 'breadcrumb-kind kind))) + (if (seq-empty-p children) + (cons name (car reg)) + (cons name + (mapcar (lambda (c) (apply #'dfs c)) children)))))) + (mapcar (lambda (s) (apply #'dfs s)) res))) + (defun eglot-imenu () "Eglot's `imenu-create-index-function'. Returns a list as described in docstring of `imenu--index-alist'." - (cl-labels - ((unfurl (obj) - (eglot--dcase obj - (((SymbolInformation)) (list obj)) - (((DocumentSymbol) name children) - (cons obj - (mapcar - (lambda (c) - (plist-put - c :containerName - (let ((existing (plist-get c :containerName))) - (if existing (format "%s::%s" name existing) - name)))) - (mapcan #'unfurl children))))))) - (mapcar - (pcase-lambda (`(,kind . ,objs)) - (cons - (alist-get kind eglot--symbol-kind-names "Unknown") - (mapcan (pcase-lambda (`(,container . ,objs)) - (let ((elems (mapcar - (lambda (obj) - (cons (plist-get obj :name) - (car (eglot--range-region - (eglot--dcase obj - (((SymbolInformation) location) - (plist-get location :range)) - (((DocumentSymbol) selectionRange) - selectionRange)))))) - objs))) - (if container (list (cons container elems)) elems))) - (seq-group-by - (lambda (e) (plist-get e :containerName)) objs)))) - (seq-group-by - (lambda (obj) (plist-get obj :kind)) - (mapcan #'unfurl - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/documentSymbol - `(:textDocument - ,(eglot--TextDocumentIdentifier)) - :cancel-on-input non-essential)))))) + (let* ((res (eglot--request (eglot--current-server-or-lose) + :textDocument/documentSymbol + `(:textDocument + ,(eglot--TextDocumentIdentifier)) + :cancel-on-input non-essential)) + (head (and res (elt res 0)))) + (eglot--dcase head + (((SymbolInformation)) (eglot--imenu-SymbolInformation res)) + (((DocumentSymbol)) (eglot--imenu-DocumentSymbol res))))) (cl-defun eglot--apply-text-edits (edits &optional version) "Apply EDITS for current buffer if at VERSION, or if it's nil." @@ -3292,9 +3365,9 @@ Returns a list as described in docstring of `imenu--index-alist'." (symbol-name (symbol-at-point))))) (eglot--server-capable-or-lose :renameProvider) (eglot--apply-workspace-edit - (jsonrpc-request (eglot--current-server-or-lose) - :textDocument/rename `(,@(eglot--TextDocumentPositionParams) - :newName ,newname)) + (eglot--request (eglot--current-server-or-lose) + :textDocument/rename `(,@(eglot--TextDocumentPositionParams) + :newName ,newname)) current-prefix-arg)) (defun eglot--region-bounds () @@ -3320,7 +3393,7 @@ at point. With prefix argument, prompt for ACTION-KIND." (eglot--server-capable-or-lose :codeActionProvider) (let* ((server (eglot--current-server-or-lose)) (actions - (jsonrpc-request + (eglot--request server :textDocument/codeAction (list :textDocument (eglot--TextDocumentIdentifier) @@ -3332,8 +3405,7 @@ at point. With prefix argument, prompt for ACTION-KIND." when (cdr (assoc 'eglot-lsp-diag (eglot--diag-data diag))) collect it)] - ,@(when action-kind `(:only [,action-kind])))) - :deferred t)) + ,@(when action-kind `(:only [,action-kind])))))) ;; Redo filtering, in case the `:only' didn't go through. (actions (cl-loop for a across actions when (or (not action-kind) |