diff options
| -rw-r--r-- | lisp/progmodes/eglot.el | 3355 |
1 files changed, 3355 insertions, 0 deletions
diff --git a/lisp/progmodes/eglot.el b/lisp/progmodes/eglot.el new file mode 100644 index 00000000000..718a42dbd75 --- /dev/null +++ b/lisp/progmodes/eglot.el | |||
| @@ -0,0 +1,3355 @@ | |||
| 1 | ;;; eglot.el --- Client for Language Server Protocol (LSP) servers -*- lexical-binding: t; -*- | ||
| 2 | |||
| 3 | ;; Copyright (C) 2018-2022 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | ;; Version: 1.8 | ||
| 6 | ;; Author: João Távora <joaotavora@gmail.com> | ||
| 7 | ;; Maintainer: João Távora <joaotavora@gmail.com> | ||
| 8 | ;; URL: https://github.com/joaotavora/eglot | ||
| 9 | ;; Keywords: convenience, languages | ||
| 10 | ;; Package-Requires: ((emacs "26.1") (jsonrpc "1.0.14") (flymake "1.2.1") (project "0.3.0") (xref "1.0.1") (eldoc "1.11.0") (seq "2.23")) | ||
| 11 | |||
| 12 | ;; This file is part of GNU Emacs. | ||
| 13 | |||
| 14 | ;; GNU Emacs is free software: you can redistribute it and/or modify | ||
| 15 | ;; it under the terms of the GNU General Public License as published by | ||
| 16 | ;; the Free Software Foundation, either version 3 of the License, or | ||
| 17 | ;; (at your option) any later version. | ||
| 18 | |||
| 19 | ;; GNU Emacs is distributed in the hope that it will be useful, | ||
| 20 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 21 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 22 | ;; GNU General Public License for more details. | ||
| 23 | |||
| 24 | ;; You should have received a copy of the GNU General Public License | ||
| 25 | ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. | ||
| 26 | |||
| 27 | ;;; Commentary: | ||
| 28 | |||
| 29 | ;; Eglot ("Emacs Polyglot") is an Emacs LSP client that stays out of | ||
| 30 | ;; your way. | ||
| 31 | ;; | ||
| 32 | ;; Typing M-x eglot should be enough to get you started, but here's a | ||
| 33 | ;; little info (see the accompanying README.md or the URL for more). | ||
| 34 | ;; | ||
| 35 | ;; M-x eglot starts a server via a shell command guessed from | ||
| 36 | ;; `eglot-server-programs', using the current major mode (for whatever | ||
| 37 | ;; language you're programming in) as a hint. If it can't guess, it | ||
| 38 | ;; prompts you in the minibuffer for these things. Actually, the | ||
| 39 | ;; server does not need to be running locally: you can connect to a | ||
| 40 | ;; running server via TCP by entering a <host:port> syntax. | ||
| 41 | ;; | ||
| 42 | ;; If the connection is successful, you should see an `eglot' | ||
| 43 | ;; indicator pop up in your mode-line. More importantly, this means | ||
| 44 | ;; that current *and future* file buffers of that major mode *inside | ||
| 45 | ;; your current project* automatically become \"managed\" by the LSP | ||
| 46 | ;; server. In other words, information about their content is | ||
| 47 | ;; exchanged periodically to provide enhanced code analysis using | ||
| 48 | ;; `xref-find-definitions', `flymake-mode', `eldoc-mode', | ||
| 49 | ;; `completion-at-point', among others. | ||
| 50 | ;; | ||
| 51 | ;; To "unmanage" these buffers, shutdown the server with | ||
| 52 | ;; M-x eglot-shutdown. | ||
| 53 | ;; | ||
| 54 | ;; To start an eglot session automatically when a foo-mode buffer is | ||
| 55 | ;; visited, you can put this in your init file: | ||
| 56 | ;; | ||
| 57 | ;; (add-hook 'foo-mode-hook 'eglot-ensure) | ||
| 58 | |||
| 59 | ;;; Code: | ||
| 60 | |||
| 61 | (require 'imenu) | ||
| 62 | (require 'cl-lib) | ||
| 63 | (require 'project) | ||
| 64 | (require 'url-parse) | ||
| 65 | (require 'url-util) | ||
| 66 | (require 'pcase) | ||
| 67 | (require 'compile) ; for some faces | ||
| 68 | (require 'warnings) | ||
| 69 | (require 'flymake) | ||
| 70 | (require 'xref) | ||
| 71 | (eval-when-compile | ||
| 72 | (require 'subr-x)) | ||
| 73 | (require 'jsonrpc) | ||
| 74 | (require 'filenotify) | ||
| 75 | (require 'ert) | ||
| 76 | (require 'array) | ||
| 77 | |||
| 78 | ;; ElDoc is preloaded in Emacs, so `require'-ing won't guarantee we are | ||
| 79 | ;; using the latest version from GNU Elpa when we load eglot.el. Use an | ||
| 80 | ;; heuristic to see if we need to `load' it in Emacs < 28. | ||
| 81 | (if (and (< emacs-major-version 28) | ||
| 82 | (not (boundp 'eldoc-documentation-strategy))) | ||
| 83 | (load "eldoc") | ||
| 84 | (require 'eldoc)) | ||
| 85 | |||
| 86 | ;; Similar issue as above for Emacs 26.3 and seq.el. | ||
| 87 | (if (< emacs-major-version 27) | ||
| 88 | (load "seq") | ||
| 89 | (require 'seq)) | ||
| 90 | |||
| 91 | ;; forward-declare, but don't require (Emacs 28 doesn't seem to care) | ||
| 92 | (defvar markdown-fontify-code-blocks-natively) | ||
| 93 | (defvar company-backends) | ||
| 94 | (defvar company-tooltip-align-annotations) | ||
| 95 | |||
| 96 | |||
| 97 | |||
| 98 | ;;; User tweakable stuff | ||
| 99 | (defgroup eglot nil | ||
| 100 | "Interaction with Language Server Protocol servers." | ||
| 101 | :prefix "eglot-" | ||
| 102 | :group 'applications) | ||
| 103 | |||
| 104 | (defun eglot-alternatives (alternatives) | ||
| 105 | "Compute server-choosing function for `eglot-server-programs'. | ||
| 106 | Each element of ALTERNATIVES is a string PROGRAM or a list of | ||
| 107 | strings (PROGRAM ARGS...) where program names an LSP server | ||
| 108 | program to start with ARGS. Returns a function of one argument. | ||
| 109 | When invoked, that function will return a list (ABSPATH ARGS), | ||
| 110 | where ABSPATH is the absolute path of the PROGRAM that was | ||
| 111 | chosen (interactively or automatically)." | ||
| 112 | (lambda (&optional interactive) | ||
| 113 | ;; JT@2021-06-13: This function is way more complicated than it | ||
| 114 | ;; could be because it accounts for the fact that | ||
| 115 | ;; `eglot--executable-find' may take much longer to execute on | ||
| 116 | ;; remote files. | ||
| 117 | (let* ((listified (cl-loop for a in alternatives | ||
| 118 | collect (if (listp a) a (list a)))) | ||
| 119 | (err (lambda () | ||
| 120 | (error "None of '%s' are valid executables" | ||
| 121 | (mapconcat #'car listified ", "))))) | ||
| 122 | (cond (interactive | ||
| 123 | (let* ((augmented (mapcar (lambda (a) | ||
| 124 | (let ((found (eglot--executable-find | ||
| 125 | (car a) t))) | ||
| 126 | (and found | ||
| 127 | (cons (car a) (cons found (cdr a)))))) | ||
| 128 | listified)) | ||
| 129 | (available (remove nil augmented))) | ||
| 130 | (cond ((cdr available) | ||
| 131 | (cdr (assoc | ||
| 132 | (completing-read | ||
| 133 | "[eglot] More than one server executable available:" | ||
| 134 | (mapcar #'car available) | ||
| 135 | nil t nil nil (car (car available))) | ||
| 136 | available #'equal))) | ||
| 137 | ((cdr (car available))) | ||
| 138 | (t | ||
| 139 | ;; Don't error when used interactively, let the | ||
| 140 | ;; Eglot prompt the user for alternative (github#719) | ||
| 141 | nil)))) | ||
| 142 | (t | ||
| 143 | (cl-loop for (p . args) in listified | ||
| 144 | for probe = (eglot--executable-find p t) | ||
| 145 | when probe return (cons probe args) | ||
| 146 | finally (funcall err))))))) | ||
| 147 | |||
| 148 | (defvar eglot-server-programs `((rust-mode . ,(eglot-alternatives '("rust-analyzer" "rls"))) | ||
| 149 | (cmake-mode . ("cmake-language-server")) | ||
| 150 | (vimrc-mode . ("vim-language-server" "--stdio")) | ||
| 151 | (python-mode | ||
| 152 | . ,(eglot-alternatives | ||
| 153 | '("pylsp" "pyls" ("pyright-langserver" "--stdio") "jedi-language-server"))) | ||
| 154 | ((js-mode typescript-mode) | ||
| 155 | . ("typescript-language-server" "--stdio")) | ||
| 156 | (sh-mode . ("bash-language-server" "start")) | ||
| 157 | ((php-mode phps-mode) | ||
| 158 | . ("php" "vendor/felixfbecker/\ | ||
| 159 | language-server/bin/php-language-server.php")) | ||
| 160 | ((c++-mode c-mode) . ,(eglot-alternatives | ||
| 161 | '("clangd" "ccls"))) | ||
| 162 | (((caml-mode :language-id "ocaml") | ||
| 163 | (tuareg-mode :language-id "ocaml") reason-mode) | ||
| 164 | . ("ocamllsp")) | ||
| 165 | (ruby-mode | ||
| 166 | . ("solargraph" "socket" "--port" :autoport)) | ||
| 167 | (haskell-mode | ||
| 168 | . ("haskell-language-server-wrapper" "--lsp")) | ||
| 169 | (elm-mode . ("elm-language-server")) | ||
| 170 | (mint-mode . ("mint" "ls")) | ||
| 171 | (kotlin-mode . ("kotlin-language-server")) | ||
| 172 | (go-mode . ("gopls")) | ||
| 173 | ((R-mode ess-r-mode) . ("R" "--slave" "-e" | ||
| 174 | "languageserver::run()")) | ||
| 175 | (java-mode . ("jdtls")) | ||
| 176 | (dart-mode . ("dart" "language-server" | ||
| 177 | "--client-id" "emacs.eglot-dart")) | ||
| 178 | (elixir-mode . ("language_server.sh")) | ||
| 179 | (ada-mode . ("ada_language_server")) | ||
| 180 | (scala-mode . ("metals-emacs")) | ||
| 181 | (racket-mode . ("racket" "-l" "racket-langserver")) | ||
| 182 | ((tex-mode context-mode texinfo-mode bibtex-mode) | ||
| 183 | . ("digestif")) | ||
| 184 | (erlang-mode . ("erlang_ls" "--transport" "stdio")) | ||
| 185 | (yaml-mode . ("yaml-language-server" "--stdio")) | ||
| 186 | (nix-mode . ("rnix-lsp")) | ||
| 187 | (gdscript-mode . ("localhost" 6008)) | ||
| 188 | ((fortran-mode f90-mode) . ("fortls")) | ||
| 189 | (futhark-mode . ("futhark" "lsp")) | ||
| 190 | (lua-mode . ("lua-lsp")) | ||
| 191 | (zig-mode . ("zls")) | ||
| 192 | (css-mode . ,(eglot-alternatives '(("vscode-css-language-server" "--stdio") ("css-languageserver" "--stdio")))) | ||
| 193 | (html-mode . ,(eglot-alternatives '(("vscode-html-language-server" "--stdio") ("html-languageserver" "--stdio")))) | ||
| 194 | (json-mode . ,(eglot-alternatives '(("vscode-json-language-server" "--stdio") ("json-languageserver" "--stdio")))) | ||
| 195 | (dockerfile-mode . ("docker-langserver" "--stdio")) | ||
| 196 | (clojure-mode . ("clojure-lsp")) | ||
| 197 | (csharp-mode . ("omnisharp" "-lsp")) | ||
| 198 | (purescript-mode . ("purescript-language-server" "--stdio")) | ||
| 199 | (perl-mode . ("perl" "-MPerl::LanguageServer" "-e" "Perl::LanguageServer::run")) | ||
| 200 | (markdown-mode . ("marksman" "server"))) | ||
| 201 | "How the command `eglot' guesses the server to start. | ||
| 202 | An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE | ||
| 203 | identifies the buffers that are to be managed by a specific | ||
| 204 | language server. The associated CONTACT specifies how to connect | ||
| 205 | to a server for those buffers. | ||
| 206 | |||
| 207 | MAJOR-MODE can be: | ||
| 208 | |||
| 209 | * In the most common case, a symbol such as `c-mode'; | ||
| 210 | |||
| 211 | * A list (MAJOR-MODE-SYMBOL :LANGUAGE-ID ID) where | ||
| 212 | MAJOR-MODE-SYMBOL is the aforementioned symbol and ID is a | ||
| 213 | string identifying the language to the server; | ||
| 214 | |||
| 215 | * A list combining the previous two alternatives, meaning | ||
| 216 | multiple major modes will be associated with a single server | ||
| 217 | program. | ||
| 218 | |||
| 219 | CONTACT can be: | ||
| 220 | |||
| 221 | * In the most common case, a list of strings (PROGRAM [ARGS...]). | ||
| 222 | PROGRAM is called with ARGS and is expected to serve LSP requests | ||
| 223 | over the standard input/output channels. | ||
| 224 | |||
| 225 | * A list (PROGRAM [ARGS...] :initializationOptions OPTIONS), | ||
| 226 | whereupon PROGRAM is called with ARGS as in the first option, | ||
| 227 | and the LSP \"initializationOptions\" JSON object is | ||
| 228 | constructed from OPTIONS. If OPTIONS is a unary function, it | ||
| 229 | is called with the server instance and should return a JSON | ||
| 230 | object. | ||
| 231 | |||
| 232 | * A list (HOST PORT [TCP-ARGS...]) where HOST is a string and | ||
| 233 | PORT is a positive integer for connecting to a server via TCP. | ||
| 234 | Remaining ARGS are passed to `open-network-stream' for | ||
| 235 | upgrading the connection with encryption or other capabilities. | ||
| 236 | |||
| 237 | * A list (PROGRAM [ARGS...] :autoport [MOREARGS...]), whereupon a | ||
| 238 | combination of previous options is used. First, an attempt is | ||
| 239 | made to find an available server port, then PROGRAM is launched | ||
| 240 | with ARGS; the `:autoport' keyword substituted for that number; | ||
| 241 | and MOREARGS. Eglot then attempts to establish a TCP | ||
| 242 | connection to that port number on the localhost. | ||
| 243 | |||
| 244 | * A cons (CLASS-NAME . INITARGS) where CLASS-NAME is a symbol | ||
| 245 | designating a subclass of `eglot-lsp-server', for representing | ||
| 246 | experimental LSP servers. INITARGS is a keyword-value plist | ||
| 247 | used to initialize the object of CLASS-NAME, or a plain list | ||
| 248 | interpreted as the previous descriptions of CONTACT. In the | ||
| 249 | latter case that plain list is used to produce a plist with a | ||
| 250 | suitable :PROCESS initarg to CLASS-NAME. The class | ||
| 251 | `eglot-lsp-server' descends from `jsonrpc-process-connection', | ||
| 252 | which you should see for the semantics of the mandatory | ||
| 253 | :PROCESS argument. | ||
| 254 | |||
| 255 | * A function of a single argument producing any of the above | ||
| 256 | values for CONTACT. The argument's value is non-nil if the | ||
| 257 | connection was requested interactively (e.g. from the `eglot' | ||
| 258 | command), and nil if it wasn't (e.g. from `eglot-ensure'). If | ||
| 259 | the call is interactive, the function can ask the user for | ||
| 260 | hints on finding the required programs, etc. Otherwise, it | ||
| 261 | should not ask the user for any input, and return nil or signal | ||
| 262 | an error if it can't produce a valid CONTACT.") | ||
| 263 | |||
| 264 | (defface eglot-highlight-symbol-face | ||
| 265 | '((t (:inherit bold))) | ||
| 266 | "Face used to highlight the symbol at point.") | ||
| 267 | |||
| 268 | (defface eglot-mode-line | ||
| 269 | '((t (:inherit font-lock-constant-face :weight bold))) | ||
| 270 | "Face for package-name in EGLOT's mode line.") | ||
| 271 | |||
| 272 | (defface eglot-diagnostic-tag-unnecessary-face | ||
| 273 | '((t (:inherit shadow))) | ||
| 274 | "Face used to render unused or unnecessary code.") | ||
| 275 | |||
| 276 | (defface eglot-diagnostic-tag-deprecated-face | ||
| 277 | '((t . (:inherit shadow :strike-through t))) | ||
| 278 | "Face used to render deprecated or obsolete code.") | ||
| 279 | |||
| 280 | (defcustom eglot-autoreconnect 3 | ||
| 281 | "Control ability to reconnect automatically to the LSP server. | ||
| 282 | If t, always reconnect automatically (not recommended). If nil, | ||
| 283 | never reconnect automatically after unexpected server shutdowns, | ||
| 284 | crashes or network failures. A positive integer number says to | ||
| 285 | only autoreconnect if the previous successful connection attempt | ||
| 286 | lasted more than that many seconds." | ||
| 287 | :type '(choice (boolean :tag "Whether to inhibit autoreconnection") | ||
| 288 | (integer :tag "Number of seconds"))) | ||
| 289 | |||
| 290 | (defcustom eglot-connect-timeout 30 | ||
| 291 | "Number of seconds before timing out LSP connection attempts. | ||
| 292 | If nil, never time out." | ||
| 293 | :type 'number) | ||
| 294 | |||
| 295 | (defcustom eglot-sync-connect 3 | ||
| 296 | "Control blocking of LSP connection attempts. | ||
| 297 | If t, block for `eglot-connect-timeout' seconds. A positive | ||
| 298 | integer number means block for that many seconds, and then wait | ||
| 299 | for the connection in the background. nil has the same meaning | ||
| 300 | as 0, i.e. don't block at all." | ||
| 301 | :type '(choice (boolean :tag "Whether to inhibit autoreconnection") | ||
| 302 | (integer :tag "Number of seconds"))) | ||
| 303 | |||
| 304 | (defcustom eglot-autoshutdown nil | ||
| 305 | "If non-nil, shut down server after killing last managed buffer." | ||
| 306 | :type 'boolean) | ||
| 307 | |||
| 308 | (defcustom eglot-send-changes-idle-time 0.5 | ||
| 309 | "Don't tell server of changes before Emacs's been idle for this many seconds." | ||
| 310 | :type 'number) | ||
| 311 | |||
| 312 | (defcustom eglot-events-buffer-size 2000000 | ||
| 313 | "Control the size of the Eglot events buffer. | ||
| 314 | If a number, don't let the buffer grow larger than that many | ||
| 315 | characters. If 0, don't use an event's buffer at all. If nil, | ||
| 316 | let the buffer grow forever. | ||
| 317 | |||
| 318 | For changes on this variable to take effect on a connection | ||
| 319 | already started, you need to restart the connection. That can be | ||
| 320 | done by `eglot-reconnect'." | ||
| 321 | :type '(choice (const :tag "No limit" nil) | ||
| 322 | (integer :tag "Number of characters"))) | ||
| 323 | |||
| 324 | (defcustom eglot-confirm-server-initiated-edits 'confirm | ||
| 325 | "Non-nil if server-initiated edits should be confirmed with user." | ||
| 326 | :type '(choice (const :tag "Don't show confirmation prompt" nil) | ||
| 327 | (symbol :tag "Show confirmation prompt" 'confirm))) | ||
| 328 | |||
| 329 | (defcustom eglot-extend-to-xref nil | ||
| 330 | "If non-nil, activate Eglot in cross-referenced non-project files." | ||
| 331 | :type 'boolean) | ||
| 332 | |||
| 333 | (defcustom eglot-menu-string "eglot" | ||
| 334 | "String displayed in mode line when Eglot is active." | ||
| 335 | :type 'string) | ||
| 336 | |||
| 337 | (defvar eglot-withhold-process-id nil | ||
| 338 | "If non-nil, Eglot will not send the Emacs process id to the language server. | ||
| 339 | This can be useful when using docker to run a language server.") | ||
| 340 | |||
| 341 | ;; Customizable via `completion-category-overrides'. | ||
| 342 | (when (assoc 'flex completion-styles-alist) | ||
| 343 | (add-to-list 'completion-category-defaults '(eglot (styles flex basic)))) | ||
| 344 | |||
| 345 | |||
| 346 | ;;; Constants | ||
| 347 | ;;; | ||
| 348 | (defconst eglot--symbol-kind-names | ||
| 349 | `((1 . "File") (2 . "Module") | ||
| 350 | (3 . "Namespace") (4 . "Package") (5 . "Class") | ||
| 351 | (6 . "Method") (7 . "Property") (8 . "Field") | ||
| 352 | (9 . "Constructor") (10 . "Enum") (11 . "Interface") | ||
| 353 | (12 . "Function") (13 . "Variable") (14 . "Constant") | ||
| 354 | (15 . "String") (16 . "Number") (17 . "Boolean") | ||
| 355 | (18 . "Array") (19 . "Object") (20 . "Key") | ||
| 356 | (21 . "Null") (22 . "EnumMember") (23 . "Struct") | ||
| 357 | (24 . "Event") (25 . "Operator") (26 . "TypeParameter"))) | ||
| 358 | |||
| 359 | (defconst eglot--kind-names | ||
| 360 | `((1 . "Text") (2 . "Method") (3 . "Function") (4 . "Constructor") | ||
| 361 | (5 . "Field") (6 . "Variable") (7 . "Class") (8 . "Interface") | ||
| 362 | (9 . "Module") (10 . "Property") (11 . "Unit") (12 . "Value") | ||
| 363 | (13 . "Enum") (14 . "Keyword") (15 . "Snippet") (16 . "Color") | ||
| 364 | (17 . "File") (18 . "Reference") (19 . "Folder") (20 . "EnumMember") | ||
| 365 | (21 . "Constant") (22 . "Struct") (23 . "Event") (24 . "Operator") | ||
| 366 | (25 . "TypeParameter"))) | ||
| 367 | |||
| 368 | (defconst eglot--tag-faces | ||
| 369 | `((1 . eglot-diagnostic-tag-unnecessary-face) | ||
| 370 | (2 . eglot-diagnostic-tag-deprecated-face))) | ||
| 371 | |||
| 372 | (defconst eglot--{} (make-hash-table :size 1) "The empty JSON object.") | ||
| 373 | |||
| 374 | (defun eglot--executable-find (command &optional remote) | ||
| 375 | "Like Emacs 27's `executable-find', ignore REMOTE on Emacs 26." | ||
| 376 | (if (>= emacs-major-version 27) (executable-find command remote) | ||
| 377 | (executable-find command))) | ||
| 378 | |||
| 379 | |||
| 380 | ;;; Message verification helpers | ||
| 381 | ;;; | ||
| 382 | (eval-and-compile | ||
| 383 | (defvar eglot--lsp-interface-alist | ||
| 384 | `( | ||
| 385 | (CodeAction (:title) (:kind :diagnostics :edit :command :isPreferred)) | ||
| 386 | (ConfigurationItem () (:scopeUri :section)) | ||
| 387 | (Command ((:title . string) (:command . string)) (:arguments)) | ||
| 388 | (CompletionItem (:label) | ||
| 389 | (:kind :detail :documentation :deprecated :preselect | ||
| 390 | :sortText :filterText :insertText :insertTextFormat | ||
| 391 | :textEdit :additionalTextEdits :commitCharacters | ||
| 392 | :command :data :tags)) | ||
| 393 | (Diagnostic (:range :message) (:severity :code :source :relatedInformation :codeDescription :tags)) | ||
| 394 | (DocumentHighlight (:range) (:kind)) | ||
| 395 | (FileSystemWatcher (:globPattern) (:kind)) | ||
| 396 | (Hover (:contents) (:range)) | ||
| 397 | (InitializeResult (:capabilities) (:serverInfo)) | ||
| 398 | (Location (:uri :range)) | ||
| 399 | (LocationLink (:targetUri :targetRange :targetSelectionRange) (:originSelectionRange)) | ||
| 400 | (LogMessageParams (:type :message)) | ||
| 401 | (MarkupContent (:kind :value)) | ||
| 402 | (ParameterInformation (:label) (:documentation)) | ||
| 403 | (Position (:line :character)) | ||
| 404 | (Range (:start :end)) | ||
| 405 | (Registration (:id :method) (:registerOptions)) | ||
| 406 | (ResponseError (:code :message) (:data)) | ||
| 407 | (ShowMessageParams (:type :message)) | ||
| 408 | (ShowMessageRequestParams (:type :message) (:actions)) | ||
| 409 | (SignatureHelp (:signatures) (:activeSignature :activeParameter)) | ||
| 410 | (SignatureInformation (:label) (:documentation :parameters :activeParameter)) | ||
| 411 | (SymbolInformation (:name :kind :location) | ||
| 412 | (:deprecated :containerName)) | ||
| 413 | (DocumentSymbol (:name :range :selectionRange :kind) | ||
| 414 | ;; `:containerName' isn't really allowed , but | ||
| 415 | ;; it simplifies the impl of `eglot-imenu'. | ||
| 416 | (:detail :deprecated :children :containerName)) | ||
| 417 | (TextDocumentEdit (:textDocument :edits) ()) | ||
| 418 | (TextEdit (:range :newText)) | ||
| 419 | (VersionedTextDocumentIdentifier (:uri :version) ()) | ||
| 420 | (WorkspaceEdit () (:changes :documentChanges)) | ||
| 421 | (WorkspaceSymbol (:name :kind) (:containerName :location :data))) | ||
| 422 | "Alist (INTERFACE-NAME . INTERFACE) of known external LSP interfaces. | ||
| 423 | |||
| 424 | INTERFACE-NAME is a symbol designated by the spec as | ||
| 425 | \"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where | ||
| 426 | REQUIRED and OPTIONAL are lists of KEYWORD designating field | ||
| 427 | names that must be, or may be, respectively, present in a message | ||
| 428 | adhering to that interface. KEY can be a keyword or a cons (SYM | ||
| 429 | TYPE), where type is used by `cl-typep' to check types at | ||
| 430 | runtime. | ||
| 431 | |||
| 432 | Here's what an element of this alist might look like: | ||
| 433 | |||
| 434 | (Command ((:title . string) (:command . string)) (:arguments))")) | ||
| 435 | |||
| 436 | (eval-and-compile | ||
| 437 | (defvar eglot-strict-mode | ||
| 438 | '(;; Uncomment next lines for fun and debugging | ||
| 439 | ;; disallow-non-standard-keys | ||
| 440 | ;; enforce-required-keys | ||
| 441 | ;; enforce-optional-keys | ||
| 442 | ) | ||
| 443 | "How strictly to check LSP interfaces at compile- and run-time. | ||
| 444 | |||
| 445 | Value is a list of symbols (if the list is empty, no checks are | ||
| 446 | performed). | ||
| 447 | |||
| 448 | If the symbol `disallow-non-standard-keys' is present, an error | ||
| 449 | is raised if any extraneous fields are sent by the server. At | ||
| 450 | compile-time, a warning is raised if a destructuring spec | ||
| 451 | includes such a field. | ||
| 452 | |||
| 453 | If the symbol `enforce-required-keys' is present, an error is | ||
| 454 | raised if any required fields are missing from the message sent | ||
| 455 | from the server. At compile-time, a warning is raised if a | ||
| 456 | destructuring spec doesn't use such a field. | ||
| 457 | |||
| 458 | If the symbol `enforce-optional-keys' is present, nothing special | ||
| 459 | happens at run-time. At compile-time, a warning is raised if a | ||
| 460 | destructuring spec doesn't use all optional fields. | ||
| 461 | |||
| 462 | If the symbol `disallow-unknown-methods' is present, Eglot warns | ||
| 463 | on unknown notifications and errors on unknown requests.")) | ||
| 464 | |||
| 465 | (cl-defun eglot--check-object (interface-name | ||
| 466 | object | ||
| 467 | &optional | ||
| 468 | (enforce-required t) | ||
| 469 | (disallow-non-standard t) | ||
| 470 | (check-types t)) | ||
| 471 | "Check that OBJECT conforms to INTERFACE. Error otherwise." | ||
| 472 | (cl-destructuring-bind | ||
| 473 | (&key types required-keys optional-keys &allow-other-keys) | ||
| 474 | (eglot--interface interface-name) | ||
| 475 | (when-let ((missing (and enforce-required | ||
| 476 | (cl-set-difference required-keys | ||
| 477 | (eglot--plist-keys object))))) | ||
| 478 | (eglot--error "A `%s' must have %s" interface-name missing)) | ||
| 479 | (when-let ((excess (and disallow-non-standard | ||
| 480 | (cl-set-difference | ||
| 481 | (eglot--plist-keys object) | ||
| 482 | (append required-keys optional-keys))))) | ||
| 483 | (eglot--error "A `%s' mustn't have %s" interface-name excess)) | ||
| 484 | (when check-types | ||
| 485 | (cl-loop | ||
| 486 | for (k v) on object by #'cddr | ||
| 487 | for type = (or (cdr (assoc k types)) t) ;; FIXME: enforce nil type? | ||
| 488 | unless (cl-typep v type) | ||
| 489 | do (eglot--error "A `%s' must have a %s as %s, but has %s" | ||
| 490 | interface-name ))) | ||
| 491 | t)) | ||
| 492 | |||
| 493 | (eval-and-compile | ||
| 494 | (defun eglot--keywordize-vars (vars) | ||
| 495 | (mapcar (lambda (var) (intern (format ":%s" var))) vars)) | ||
| 496 | |||
| 497 | (defun eglot--ensure-type (k) (if (consp k) k (cons k t))) | ||
| 498 | |||
| 499 | (defun eglot--interface (interface-name) | ||
| 500 | (let* ((interface (assoc interface-name eglot--lsp-interface-alist)) | ||
| 501 | (required (mapcar #'eglot--ensure-type (car (cdr interface)))) | ||
| 502 | (optional (mapcar #'eglot--ensure-type (cadr (cdr interface))))) | ||
| 503 | (list :types (append required optional) | ||
| 504 | :required-keys (mapcar #'car required) | ||
| 505 | :optional-keys (mapcar #'car optional)))) | ||
| 506 | |||
| 507 | (defun eglot--check-dspec (interface-name dspec) | ||
| 508 | "Check destructuring spec DSPEC against INTERFACE-NAME." | ||
| 509 | (cl-destructuring-bind (&key required-keys optional-keys &allow-other-keys) | ||
| 510 | (eglot--interface interface-name) | ||
| 511 | (cond ((or required-keys optional-keys) | ||
| 512 | (let ((too-many | ||
| 513 | (and | ||
| 514 | (memq 'disallow-non-standard-keys eglot-strict-mode) | ||
| 515 | (cl-set-difference | ||
| 516 | (eglot--keywordize-vars dspec) | ||
| 517 | (append required-keys optional-keys)))) | ||
| 518 | (ignored-required | ||
| 519 | (and | ||
| 520 | (memq 'enforce-required-keys eglot-strict-mode) | ||
| 521 | (cl-set-difference | ||
| 522 | required-keys (eglot--keywordize-vars dspec)))) | ||
| 523 | (missing-out | ||
| 524 | (and | ||
| 525 | (memq 'enforce-optional-keys eglot-strict-mode) | ||
| 526 | (cl-set-difference | ||
| 527 | optional-keys (eglot--keywordize-vars dspec))))) | ||
| 528 | (when too-many (byte-compile-warn | ||
| 529 | "Destructuring for %s has extraneous %s" | ||
| 530 | interface-name too-many)) | ||
| 531 | (when ignored-required (byte-compile-warn | ||
| 532 | "Destructuring for %s ignores required %s" | ||
| 533 | interface-name ignored-required)) | ||
| 534 | (when missing-out (byte-compile-warn | ||
| 535 | "Destructuring for %s is missing out on %s" | ||
| 536 | interface-name missing-out)))) | ||
| 537 | (t | ||
| 538 | (byte-compile-warn "Unknown LSP interface %s" interface-name)))))) | ||
| 539 | |||
| 540 | (cl-defmacro eglot--dbind (vars object &body body) | ||
| 541 | "Destructure OBJECT, binding VARS in BODY. | ||
| 542 | VARS is ([(INTERFACE)] SYMS...) | ||
| 543 | Honour `eglot-strict-mode'." | ||
| 544 | (declare (indent 2) (debug (sexp sexp &rest form))) | ||
| 545 | (let ((interface-name (if (consp (car vars)) | ||
| 546 | (car (pop vars)))) | ||
| 547 | (object-once (make-symbol "object-once")) | ||
| 548 | (fn-once (make-symbol "fn-once"))) | ||
| 549 | (cond (interface-name | ||
| 550 | (eglot--check-dspec interface-name vars) | ||
| 551 | `(let ((,object-once ,object)) | ||
| 552 | (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once | ||
| 553 | (eglot--check-object ',interface-name ,object-once | ||
| 554 | (memq 'enforce-required-keys eglot-strict-mode) | ||
| 555 | (memq 'disallow-non-standard-keys eglot-strict-mode) | ||
| 556 | (memq 'check-types eglot-strict-mode)) | ||
| 557 | ,@body))) | ||
| 558 | (t | ||
| 559 | `(let ((,object-once ,object) | ||
| 560 | (,fn-once (lambda (,@vars) ,@body))) | ||
| 561 | (if (memq 'disallow-non-standard-keys eglot-strict-mode) | ||
| 562 | (cl-destructuring-bind (&key ,@vars) ,object-once | ||
| 563 | (funcall ,fn-once ,@vars)) | ||
| 564 | (cl-destructuring-bind (&key ,@vars &allow-other-keys) ,object-once | ||
| 565 | (funcall ,fn-once ,@vars)))))))) | ||
| 566 | |||
| 567 | |||
| 568 | (cl-defmacro eglot--lambda (cl-lambda-list &body body) | ||
| 569 | "Function of args CL-LAMBDA-LIST for processing INTERFACE objects. | ||
| 570 | Honour `eglot-strict-mode'." | ||
| 571 | (declare (indent 1) (debug (sexp &rest form))) | ||
| 572 | (let ((e (cl-gensym "jsonrpc-lambda-elem"))) | ||
| 573 | `(lambda (,e) (eglot--dbind ,cl-lambda-list ,e ,@body)))) | ||
| 574 | |||
| 575 | (cl-defmacro eglot--dcase (obj &rest clauses) | ||
| 576 | "Like `pcase', but for the LSP object OBJ. | ||
| 577 | CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is | ||
| 578 | treated as in `eglot-dbind'." | ||
| 579 | (declare (indent 1) (debug (sexp &rest (sexp &rest form)))) | ||
| 580 | (let ((obj-once (make-symbol "obj-once"))) | ||
| 581 | `(let ((,obj-once ,obj)) | ||
| 582 | (cond | ||
| 583 | ,@(cl-loop | ||
| 584 | for (vars . body) in clauses | ||
| 585 | for vars-as-keywords = (eglot--keywordize-vars vars) | ||
| 586 | for interface-name = (if (consp (car vars)) | ||
| 587 | (car (pop vars))) | ||
| 588 | for condition = | ||
| 589 | (cond (interface-name | ||
| 590 | (eglot--check-dspec interface-name vars) | ||
| 591 | ;; In this mode, in runtime, we assume | ||
| 592 | ;; `eglot-strict-mode' is partially on, otherwise we | ||
| 593 | ;; can't disambiguate between certain types. | ||
| 594 | `(ignore-errors | ||
| 595 | (eglot--check-object | ||
| 596 | ',interface-name ,obj-once | ||
| 597 | t | ||
| 598 | (memq 'disallow-non-standard-keys eglot-strict-mode) | ||
| 599 | t))) | ||
| 600 | (t | ||
| 601 | ;; In this interface-less mode we don't check | ||
| 602 | ;; `eglot-strict-mode' at all: just check that the object | ||
| 603 | ;; has all the keys the user wants to destructure. | ||
| 604 | `(null (cl-set-difference | ||
| 605 | ',vars-as-keywords | ||
| 606 | (eglot--plist-keys ,obj-once))))) | ||
| 607 | collect `(,condition | ||
| 608 | (cl-destructuring-bind (&key ,@vars &allow-other-keys) | ||
| 609 | ,obj-once | ||
| 610 | ,@body))) | ||
| 611 | (t | ||
| 612 | (eglot--error "%S didn't match any of %S" | ||
| 613 | ,obj-once | ||
| 614 | ',(mapcar #'car clauses))))))) | ||
| 615 | |||
| 616 | |||
| 617 | ;;; API (WORK-IN-PROGRESS!) | ||
| 618 | ;;; | ||
| 619 | (cl-defmacro eglot--when-live-buffer (buf &rest body) | ||
| 620 | "Check BUF live, then do BODY in it." (declare (indent 1) (debug t)) | ||
| 621 | (let ((b (cl-gensym))) | ||
| 622 | `(let ((,b ,buf)) (if (buffer-live-p ,b) (with-current-buffer ,b ,@body))))) | ||
| 623 | |||
| 624 | (cl-defmacro eglot--when-buffer-window (buf &body body) | ||
| 625 | "Check BUF showing somewhere, then do BODY in it." (declare (indent 1) (debug t)) | ||
| 626 | (let ((b (cl-gensym))) | ||
| 627 | `(let ((,b ,buf)) | ||
| 628 | ;;notice the exception when testing with `ert' | ||
| 629 | (when (or (get-buffer-window ,b) (ert-running-test)) | ||
| 630 | (with-current-buffer ,b ,@body))))) | ||
| 631 | |||
| 632 | (cl-defmacro eglot--widening (&rest body) | ||
| 633 | "Save excursion and restriction. Widen. Then run BODY." (declare (debug t)) | ||
| 634 | `(save-excursion (save-restriction (widen) ,@body))) | ||
| 635 | |||
| 636 | (cl-defgeneric eglot-handle-request (server method &rest params) | ||
| 637 | "Handle SERVER's METHOD request with PARAMS.") | ||
| 638 | |||
| 639 | (cl-defgeneric eglot-handle-notification (server method &rest params) | ||
| 640 | "Handle SERVER's METHOD notification with PARAMS.") | ||
| 641 | |||
| 642 | (cl-defgeneric eglot-execute-command (server command arguments) | ||
| 643 | "Ask SERVER to execute COMMAND with ARGUMENTS.") | ||
| 644 | |||
| 645 | (cl-defgeneric eglot-initialization-options (server) | ||
| 646 | "JSON object to send under `initializationOptions'." | ||
| 647 | (:method (s) | ||
| 648 | (let ((probe (plist-get (eglot--saved-initargs s) :initializationOptions))) | ||
| 649 | (cond ((functionp probe) (funcall probe s)) | ||
| 650 | (probe) | ||
| 651 | (t eglot--{}))))) | ||
| 652 | |||
| 653 | (cl-defgeneric eglot-register-capability (server method id &rest params) | ||
| 654 | "Ask SERVER to register capability METHOD marked with ID." | ||
| 655 | (:method | ||
| 656 | (_s method _id &rest _params) | ||
| 657 | (eglot--warn "Server tried to register unsupported capability `%s'" | ||
| 658 | method))) | ||
| 659 | |||
| 660 | (cl-defgeneric eglot-unregister-capability (server method id &rest params) | ||
| 661 | "Ask SERVER to register capability METHOD marked with ID." | ||
| 662 | (:method | ||
| 663 | (_s method _id &rest _params) | ||
| 664 | (eglot--warn "Server tried to unregister unsupported capability `%s'" | ||
| 665 | method))) | ||
| 666 | |||
| 667 | (cl-defgeneric eglot-client-capabilities (server) | ||
| 668 | "What the EGLOT LSP client supports for SERVER." | ||
| 669 | (:method (s) | ||
| 670 | (list | ||
| 671 | :workspace (list | ||
| 672 | :applyEdit t | ||
| 673 | :executeCommand `(:dynamicRegistration :json-false) | ||
| 674 | :workspaceEdit `(:documentChanges t) | ||
| 675 | :didChangeWatchedFiles | ||
| 676 | `(:dynamicRegistration | ||
| 677 | ,(if (eglot--trampish-p s) :json-false t)) | ||
| 678 | :symbol `(:dynamicRegistration :json-false) | ||
| 679 | :configuration t | ||
| 680 | :workspaceFolders t) | ||
| 681 | :textDocument | ||
| 682 | (list | ||
| 683 | :synchronization (list | ||
| 684 | :dynamicRegistration :json-false | ||
| 685 | :willSave t :willSaveWaitUntil t :didSave t) | ||
| 686 | :completion (list :dynamicRegistration :json-false | ||
| 687 | :completionItem | ||
| 688 | `(:snippetSupport | ||
| 689 | ,(if (eglot--snippet-expansion-fn) | ||
| 690 | t | ||
| 691 | :json-false) | ||
| 692 | :deprecatedSupport t | ||
| 693 | :tagSupport (:valueSet [1])) | ||
| 694 | :contextSupport t) | ||
| 695 | :hover (list :dynamicRegistration :json-false | ||
| 696 | :contentFormat | ||
| 697 | (if (fboundp 'gfm-view-mode) | ||
| 698 | ["markdown" "plaintext"] | ||
| 699 | ["plaintext"])) | ||
| 700 | :signatureHelp (list :dynamicRegistration :json-false | ||
| 701 | :signatureInformation | ||
| 702 | `(:parameterInformation | ||
| 703 | (:labelOffsetSupport t) | ||
| 704 | :activeParameterSupport t)) | ||
| 705 | :references `(:dynamicRegistration :json-false) | ||
| 706 | :definition (list :dynamicRegistration :json-false | ||
| 707 | :linkSupport t) | ||
| 708 | :declaration (list :dynamicRegistration :json-false | ||
| 709 | :linkSupport t) | ||
| 710 | :implementation (list :dynamicRegistration :json-false | ||
| 711 | :linkSupport t) | ||
| 712 | :typeDefinition (list :dynamicRegistration :json-false | ||
| 713 | :linkSupport t) | ||
| 714 | :documentSymbol (list | ||
| 715 | :dynamicRegistration :json-false | ||
| 716 | :hierarchicalDocumentSymbolSupport t | ||
| 717 | :symbolKind `(:valueSet | ||
| 718 | [,@(mapcar | ||
| 719 | #'car eglot--symbol-kind-names)])) | ||
| 720 | :documentHighlight `(:dynamicRegistration :json-false) | ||
| 721 | :codeAction (list | ||
| 722 | :dynamicRegistration :json-false | ||
| 723 | :codeActionLiteralSupport | ||
| 724 | '(:codeActionKind | ||
| 725 | (:valueSet | ||
| 726 | ["quickfix" | ||
| 727 | "refactor" "refactor.extract" | ||
| 728 | "refactor.inline" "refactor.rewrite" | ||
| 729 | "source" "source.organizeImports"])) | ||
| 730 | :isPreferredSupport t) | ||
| 731 | :formatting `(:dynamicRegistration :json-false) | ||
| 732 | :rangeFormatting `(:dynamicRegistration :json-false) | ||
| 733 | :rename `(:dynamicRegistration :json-false) | ||
| 734 | :publishDiagnostics (list :relatedInformation :json-false | ||
| 735 | ;; TODO: We can support :codeDescription after | ||
| 736 | ;; adding an appropriate UI to | ||
| 737 | ;; Flymake. | ||
| 738 | :codeDescriptionSupport :json-false | ||
| 739 | :tagSupport | ||
| 740 | `(:valueSet | ||
| 741 | [,@(mapcar | ||
| 742 | #'car eglot--tag-faces)]))) | ||
| 743 | :experimental eglot--{}))) | ||
| 744 | |||
| 745 | (cl-defgeneric eglot-workspace-folders (server) | ||
| 746 | "Return workspaceFolders for SERVER." | ||
| 747 | (let ((project (eglot--project server))) | ||
| 748 | (vconcat | ||
| 749 | (mapcar (lambda (dir) | ||
| 750 | (list :uri (eglot--path-to-uri dir) | ||
| 751 | :name (abbreviate-file-name dir))) | ||
| 752 | `(,(project-root project) ,@(project-external-roots project)))))) | ||
| 753 | |||
| 754 | (defclass eglot-lsp-server (jsonrpc-process-connection) | ||
| 755 | ((project-nickname | ||
| 756 | :documentation "Short nickname for the associated project." | ||
| 757 | :accessor eglot--project-nickname | ||
| 758 | :reader eglot-project-nickname) | ||
| 759 | (major-mode | ||
| 760 | :documentation "Major mode symbol." | ||
| 761 | :accessor eglot--major-mode) | ||
| 762 | (language-id | ||
| 763 | :documentation "Language ID string for the mode." | ||
| 764 | :accessor eglot--language-id) | ||
| 765 | (capabilities | ||
| 766 | :documentation "JSON object containing server capabilities." | ||
| 767 | :accessor eglot--capabilities) | ||
| 768 | (server-info | ||
| 769 | :documentation "JSON object containing server info." | ||
| 770 | :accessor eglot--server-info) | ||
| 771 | (shutdown-requested | ||
| 772 | :documentation "Flag set when server is shutting down." | ||
| 773 | :accessor eglot--shutdown-requested) | ||
| 774 | (project | ||
| 775 | :documentation "Project associated with server." | ||
| 776 | :accessor eglot--project) | ||
| 777 | (spinner | ||
| 778 | :documentation "List (ID DOING-WHAT DONE-P) representing server progress." | ||
| 779 | :initform `(nil nil t) :accessor eglot--spinner) | ||
| 780 | (inhibit-autoreconnect | ||
| 781 | :initform t | ||
| 782 | :documentation "Generalized boolean inhibiting auto-reconnection if true." | ||
| 783 | :accessor eglot--inhibit-autoreconnect) | ||
| 784 | (file-watches | ||
| 785 | :documentation "Map ID to list of WATCHES for `didChangeWatchedFiles'." | ||
| 786 | :initform (make-hash-table :test #'equal) :accessor eglot--file-watches) | ||
| 787 | (managed-buffers | ||
| 788 | :documentation "List of buffers managed by server." | ||
| 789 | :accessor eglot--managed-buffers) | ||
| 790 | (saved-initargs | ||
| 791 | :documentation "Saved initargs for reconnection purposes." | ||
| 792 | :accessor eglot--saved-initargs) | ||
| 793 | (inferior-process | ||
| 794 | :documentation "Server subprocess started automatically." | ||
| 795 | :accessor eglot--inferior-process)) | ||
| 796 | :documentation | ||
| 797 | "Represents a server. Wraps a process for LSP communication.") | ||
| 798 | |||
| 799 | (cl-defmethod initialize-instance :before ((_server eglot-lsp-server) &optional args) | ||
| 800 | (cl-remf args :initializationOptions)) | ||
| 801 | |||
| 802 | |||
| 803 | ;;; Process management | ||
| 804 | (defvar eglot--servers-by-project (make-hash-table :test #'equal) | ||
| 805 | "Keys are projects. Values are lists of processes.") | ||
| 806 | |||
| 807 | (defun eglot-shutdown (server &optional _interactive timeout preserve-buffers) | ||
| 808 | "Politely ask SERVER to quit. | ||
| 809 | Interactively, read SERVER from the minibuffer unless there is | ||
| 810 | only one and it's managing the current buffer. | ||
| 811 | |||
| 812 | Forcefully quit it if it doesn't respond within TIMEOUT seconds. | ||
| 813 | TIMEOUT defaults to 1.5 seconds. Don't leave this function with | ||
| 814 | the server still running. | ||
| 815 | |||
| 816 | If PRESERVE-BUFFERS is non-nil (interactively, when called with a | ||
| 817 | prefix argument), do not kill events and output buffers of | ||
| 818 | SERVER." | ||
| 819 | (interactive (list (eglot--read-server "Shutdown which server" | ||
| 820 | (eglot-current-server)) | ||
| 821 | t nil current-prefix-arg)) | ||
| 822 | (eglot--message "Asking %s politely to terminate" (jsonrpc-name server)) | ||
| 823 | (unwind-protect | ||
| 824 | (progn | ||
| 825 | (setf (eglot--shutdown-requested server) t) | ||
| 826 | (jsonrpc-request server :shutdown nil :timeout (or timeout 1.5)) | ||
| 827 | (jsonrpc-notify server :exit nil)) | ||
| 828 | ;; Now ask jsonrpc.el to shut down the server. | ||
| 829 | (jsonrpc-shutdown server (not preserve-buffers)) | ||
| 830 | (unless preserve-buffers (kill-buffer (jsonrpc-events-buffer server))))) | ||
| 831 | |||
| 832 | (defun eglot-shutdown-all (&optional preserve-buffers) | ||
| 833 | "Politely ask all language servers to quit, in order. | ||
| 834 | PRESERVE-BUFFERS as in `eglot-shutdown', which see." | ||
| 835 | (interactive (list current-prefix-arg)) | ||
| 836 | (cl-loop for ss being the hash-values of eglot--servers-by-project | ||
| 837 | do (cl-loop for s in ss do (eglot-shutdown s nil preserve-buffers)))) | ||
| 838 | |||
| 839 | (defun eglot--on-shutdown (server) | ||
| 840 | "Called by jsonrpc.el when SERVER is already dead." | ||
| 841 | ;; Turn off `eglot--managed-mode' where appropriate. | ||
| 842 | (dolist (buffer (eglot--managed-buffers server)) | ||
| 843 | (let (;; Avoid duplicate shutdowns (github#389) | ||
| 844 | (eglot-autoshutdown nil)) | ||
| 845 | (eglot--when-live-buffer buffer (eglot--managed-mode-off)))) | ||
| 846 | ;; Kill any expensive watches | ||
| 847 | (maphash (lambda (_id watches) | ||
| 848 | (mapcar #'file-notify-rm-watch watches)) | ||
| 849 | (eglot--file-watches server)) | ||
| 850 | ;; Kill any autostarted inferior processes | ||
| 851 | (when-let (proc (eglot--inferior-process server)) | ||
| 852 | (delete-process proc)) | ||
| 853 | ;; Sever the project/server relationship for `server' | ||
| 854 | (setf (gethash (eglot--project server) eglot--servers-by-project) | ||
| 855 | (delq server | ||
| 856 | (gethash (eglot--project server) eglot--servers-by-project))) | ||
| 857 | (cond ((eglot--shutdown-requested server) | ||
| 858 | t) | ||
| 859 | ((not (eglot--inhibit-autoreconnect server)) | ||
| 860 | (eglot--warn "Reconnecting after unexpected server exit.") | ||
| 861 | (eglot-reconnect server)) | ||
| 862 | ((timerp (eglot--inhibit-autoreconnect server)) | ||
| 863 | (eglot--warn "Not auto-reconnecting, last one didn't last long.")))) | ||
| 864 | |||
| 865 | (defun eglot--all-major-modes () | ||
| 866 | "Return all known major modes." | ||
| 867 | (let ((retval)) | ||
| 868 | (mapatoms (lambda (sym) | ||
| 869 | (when (plist-member (symbol-plist sym) 'derived-mode-parent) | ||
| 870 | (push sym retval)))) | ||
| 871 | retval)) | ||
| 872 | |||
| 873 | (defvar eglot--command-history nil | ||
| 874 | "History of CONTACT arguments to `eglot'.") | ||
| 875 | |||
| 876 | (defun eglot--lookup-mode (mode) | ||
| 877 | "Lookup `eglot-server-programs' for MODE. | ||
| 878 | Return (LANGUAGE-ID . CONTACT-PROXY). If not specified, | ||
| 879 | LANGUAGE-ID is determined from MODE." | ||
| 880 | (cl-loop | ||
| 881 | for (modes . contact) in eglot-server-programs | ||
| 882 | thereis (cl-some | ||
| 883 | (lambda (spec) | ||
| 884 | (cl-destructuring-bind (probe &key language-id &allow-other-keys) | ||
| 885 | (if (consp spec) spec (list spec)) | ||
| 886 | (and (provided-mode-derived-p mode probe) | ||
| 887 | (cons | ||
| 888 | (or language-id | ||
| 889 | (or (get mode 'eglot-language-id) | ||
| 890 | (get spec 'eglot-language-id) | ||
| 891 | (string-remove-suffix "-mode" (symbol-name mode)))) | ||
| 892 | contact)))) | ||
| 893 | (if (or (symbolp modes) (keywordp (cadr modes))) | ||
| 894 | (list modes) modes)))) | ||
| 895 | |||
| 896 | (defun eglot--guess-contact (&optional interactive) | ||
| 897 | "Helper for `eglot'. | ||
| 898 | Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is | ||
| 899 | non-nil, maybe prompt user, else error as soon as something can't | ||
| 900 | be guessed." | ||
| 901 | (let* ((guessed-mode (if buffer-file-name major-mode)) | ||
| 902 | (managed-mode | ||
| 903 | (cond | ||
| 904 | ((and interactive | ||
| 905 | (or (>= (prefix-numeric-value current-prefix-arg) 16) | ||
| 906 | (not guessed-mode))) | ||
| 907 | (intern | ||
| 908 | (completing-read | ||
| 909 | "[eglot] Start a server to manage buffers of what major mode? " | ||
| 910 | (mapcar #'symbol-name (eglot--all-major-modes)) nil t | ||
| 911 | (symbol-name guessed-mode) nil (symbol-name guessed-mode) nil))) | ||
| 912 | ((not guessed-mode) | ||
| 913 | (eglot--error "Can't guess mode to manage for `%s'" (current-buffer))) | ||
| 914 | (t guessed-mode))) | ||
| 915 | (lang-id-and-guess (eglot--lookup-mode guessed-mode)) | ||
| 916 | (language-id (or (car lang-id-and-guess) | ||
| 917 | (string-remove-suffix "-mode" (symbol-name guessed-mode)))) | ||
| 918 | (guess (cdr lang-id-and-guess)) | ||
| 919 | (guess (if (functionp guess) | ||
| 920 | (funcall guess interactive) | ||
| 921 | guess)) | ||
| 922 | (class (or (and (consp guess) (symbolp (car guess)) | ||
| 923 | (prog1 (unless current-prefix-arg (car guess)) | ||
| 924 | (setq guess (cdr guess)))) | ||
| 925 | 'eglot-lsp-server)) | ||
| 926 | (program (and (listp guess) | ||
| 927 | (stringp (car guess)) | ||
| 928 | ;; A second element might be the port of a (host, port) | ||
| 929 | ;; pair, but in that case it is not a string. | ||
| 930 | (or (null (cdr guess)) (stringp (cadr guess))) | ||
| 931 | (car guess))) | ||
| 932 | (base-prompt | ||
| 933 | (and interactive | ||
| 934 | "Enter program to execute (or <host>:<port>): ")) | ||
| 935 | (full-program-invocation | ||
| 936 | (and program | ||
| 937 | (cl-every #'stringp guess) | ||
| 938 | (combine-and-quote-strings guess))) | ||
| 939 | (prompt | ||
| 940 | (and base-prompt | ||
| 941 | (cond (current-prefix-arg base-prompt) | ||
| 942 | ((null guess) | ||
| 943 | (format "[eglot] Sorry, couldn't guess for `%s'!\n%s" | ||
| 944 | managed-mode base-prompt)) | ||
| 945 | ((and program | ||
| 946 | (not (file-name-absolute-p program)) | ||
| 947 | (not (eglot--executable-find program t))) | ||
| 948 | (if full-program-invocation | ||
| 949 | (concat (format "[eglot] I guess you want to run `%s'" | ||
| 950 | full-program-invocation) | ||
| 951 | (format ", but I can't find `%s' in PATH!" | ||
| 952 | program) | ||
| 953 | "\n" base-prompt) | ||
| 954 | (eglot--error | ||
| 955 | (concat "`%s' not found in PATH, but can't form" | ||
| 956 | " an interactive prompt for to fix %s!") | ||
| 957 | program guess)))))) | ||
| 958 | (contact | ||
| 959 | (or (and prompt | ||
| 960 | (read-shell-command | ||
| 961 | prompt | ||
| 962 | full-program-invocation | ||
| 963 | 'eglot-command-history)) | ||
| 964 | guess))) | ||
| 965 | (list managed-mode (eglot--current-project) class contact language-id))) | ||
| 966 | |||
| 967 | (defvar eglot-lsp-context) | ||
| 968 | (put 'eglot-lsp-context 'variable-documentation | ||
| 969 | "Dynamically non-nil when searching for projects in LSP context.") | ||
| 970 | |||
| 971 | (defvar eglot--servers-by-xrefed-file | ||
| 972 | (make-hash-table :test 'equal :weakness 'value)) | ||
| 973 | |||
| 974 | (defun eglot--current-project () | ||
| 975 | "Return a project object for Eglot's LSP purposes. | ||
| 976 | This relies on `project-current' and thus on | ||
| 977 | `project-find-functions'. Functions in the latter | ||
| 978 | variable (which see) can query the value `eglot-lsp-context' to | ||
| 979 | decide whether a given directory is a project containing a | ||
| 980 | suitable root directory for a given LSP server's purposes." | ||
| 981 | (let ((eglot-lsp-context t)) | ||
| 982 | (or (project-current) `(transient . ,default-directory)))) | ||
| 983 | |||
| 984 | ;;;###autoload | ||
| 985 | (defun eglot (managed-major-mode project class contact language-id | ||
| 986 | &optional interactive) | ||
| 987 | "Manage a project with a Language Server Protocol (LSP) server. | ||
| 988 | |||
| 989 | The LSP server of CLASS is started (or contacted) via CONTACT. | ||
| 990 | If this operation is successful, current *and future* file | ||
| 991 | buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\" | ||
| 992 | by the LSP server, meaning information about their contents is | ||
| 993 | exchanged periodically to provide enhanced code-analysis via | ||
| 994 | `xref-find-definitions', `flymake-mode', `eldoc-mode', | ||
| 995 | `completion-at-point', among others. | ||
| 996 | |||
| 997 | Interactively, the command attempts to guess MANAGED-MAJOR-MODE | ||
| 998 | from current buffer, CLASS and CONTACT from | ||
| 999 | `eglot-server-programs' and PROJECT from | ||
| 1000 | `project-find-functions'. The search for active projects in this | ||
| 1001 | context binds `eglot-lsp-context' (which see). | ||
| 1002 | |||
| 1003 | If it can't guess, the user is prompted. With a single | ||
| 1004 | \\[universal-argument] prefix arg, it always prompt for COMMAND. | ||
| 1005 | With two \\[universal-argument] prefix args, also prompts for | ||
| 1006 | MANAGED-MAJOR-MODE. | ||
| 1007 | |||
| 1008 | PROJECT is a project object as returned by `project-current'. | ||
| 1009 | |||
| 1010 | CLASS is a subclass of `eglot-lsp-server'. | ||
| 1011 | |||
| 1012 | CONTACT specifies how to contact the server. It is a | ||
| 1013 | keyword-value plist used to initialize CLASS or a plain list as | ||
| 1014 | described in `eglot-server-programs', which see. | ||
| 1015 | |||
| 1016 | LANGUAGE-ID is the language ID string to send to the server for | ||
| 1017 | MANAGED-MAJOR-MODE, which matters to a minority of servers. | ||
| 1018 | |||
| 1019 | INTERACTIVE is t if called interactively." | ||
| 1020 | (interactive (append (eglot--guess-contact t) '(t))) | ||
| 1021 | (let* ((current-server (eglot-current-server)) | ||
| 1022 | (live-p (and current-server (jsonrpc-running-p current-server)))) | ||
| 1023 | (if (and live-p | ||
| 1024 | interactive | ||
| 1025 | (y-or-n-p "[eglot] Live process found, reconnect instead? ")) | ||
| 1026 | (eglot-reconnect current-server interactive) | ||
| 1027 | (when live-p (ignore-errors (eglot-shutdown current-server))) | ||
| 1028 | (eglot--connect managed-major-mode project class contact language-id)))) | ||
| 1029 | |||
| 1030 | (defun eglot-reconnect (server &optional interactive) | ||
| 1031 | "Reconnect to SERVER. | ||
| 1032 | INTERACTIVE is t if called interactively." | ||
| 1033 | (interactive (list (eglot--current-server-or-lose) t)) | ||
| 1034 | (when (jsonrpc-running-p server) | ||
| 1035 | (ignore-errors (eglot-shutdown server interactive nil 'preserve-buffers))) | ||
| 1036 | (eglot--connect (eglot--major-mode server) | ||
| 1037 | (eglot--project server) | ||
| 1038 | (eieio-object-class-name server) | ||
| 1039 | (eglot--saved-initargs server) | ||
| 1040 | (eglot--language-id server)) | ||
| 1041 | (eglot--message "Reconnected!")) | ||
| 1042 | |||
| 1043 | (defvar eglot--managed-mode) ; forward decl | ||
| 1044 | |||
| 1045 | ;;;###autoload | ||
| 1046 | (defun eglot-ensure () | ||
| 1047 | "Start Eglot session for current buffer if there isn't one." | ||
| 1048 | (let ((buffer (current-buffer))) | ||
| 1049 | (cl-labels | ||
| 1050 | ((maybe-connect | ||
| 1051 | () | ||
| 1052 | (remove-hook 'post-command-hook #'maybe-connect nil) | ||
| 1053 | (eglot--when-live-buffer buffer | ||
| 1054 | (unless eglot--managed-mode | ||
| 1055 | (apply #'eglot--connect (eglot--guess-contact)))))) | ||
| 1056 | (when buffer-file-name | ||
| 1057 | (add-hook 'post-command-hook #'maybe-connect 'append nil))))) | ||
| 1058 | |||
| 1059 | (defun eglot-events-buffer (server) | ||
| 1060 | "Display events buffer for SERVER. | ||
| 1061 | Use current server's or first available Eglot events buffer." | ||
| 1062 | (interactive (list (eglot-current-server))) | ||
| 1063 | (let ((buffer (if server (jsonrpc-events-buffer server) | ||
| 1064 | (cl-find "\\*EGLOT.*events\\*" | ||
| 1065 | (buffer-list) | ||
| 1066 | :key #'buffer-name :test #'string-match)))) | ||
| 1067 | (if buffer (display-buffer buffer) | ||
| 1068 | (eglot--error "Can't find an Eglot events buffer!")))) | ||
| 1069 | |||
| 1070 | (defun eglot-stderr-buffer (server) | ||
| 1071 | "Display stderr buffer for SERVER." | ||
| 1072 | (interactive (list (eglot--current-server-or-lose))) | ||
| 1073 | (display-buffer (jsonrpc-stderr-buffer server))) | ||
| 1074 | |||
| 1075 | (defun eglot-forget-pending-continuations (server) | ||
| 1076 | "Forget pending requests for SERVER." | ||
| 1077 | (interactive (list (eglot--current-server-or-lose))) | ||
| 1078 | (jsonrpc-forget-pending-continuations server)) | ||
| 1079 | |||
| 1080 | (defvar eglot-connect-hook | ||
| 1081 | '(eglot-signal-didChangeConfiguration) | ||
| 1082 | "Hook run after connecting in `eglot--connect'.") | ||
| 1083 | |||
| 1084 | (defvar eglot-server-initialized-hook | ||
| 1085 | '() | ||
| 1086 | "Hook run after a `eglot-lsp-server' instance is created. | ||
| 1087 | |||
| 1088 | That is before a connection was established. Use | ||
| 1089 | `eglot-connect-hook' to hook into when a connection was | ||
| 1090 | successfully established and the server on the other side has | ||
| 1091 | received the initializing configuration. | ||
| 1092 | |||
| 1093 | Each function is passed the server as an argument") | ||
| 1094 | |||
| 1095 | (defun eglot--cmd (contact) | ||
| 1096 | "Helper for `eglot--connect'." | ||
| 1097 | (if (file-remote-p default-directory) | ||
| 1098 | ;; TODO: this seems like a bug, although it’s everywhere. For | ||
| 1099 | ;; some reason, for remote connections only, over a pipe, we | ||
| 1100 | ;; need to turn off line buffering on the tty. | ||
| 1101 | ;; | ||
| 1102 | ;; Not only does this seem like there should be a better way, | ||
| 1103 | ;; but it almost certainly doesn’t work on non-unix systems. | ||
| 1104 | (list "sh" "-c" | ||
| 1105 | (string-join (cons "stty raw > /dev/null;" | ||
| 1106 | (mapcar #'shell-quote-argument contact)) | ||
| 1107 | " ")) | ||
| 1108 | contact)) | ||
| 1109 | |||
| 1110 | (defvar-local eglot--cached-server nil | ||
| 1111 | "A cached reference to the current EGLOT server.") | ||
| 1112 | |||
| 1113 | (defun eglot--connect (managed-major-mode project class contact language-id) | ||
| 1114 | "Connect to MANAGED-MAJOR-MODE, LANGUAGE-ID, PROJECT, CLASS and CONTACT. | ||
| 1115 | This docstring appeases checkdoc, that's all." | ||
| 1116 | (let* ((default-directory (project-root project)) | ||
| 1117 | (nickname (file-name-base (directory-file-name default-directory))) | ||
| 1118 | (readable-name (format "EGLOT (%s/%s)" nickname managed-major-mode)) | ||
| 1119 | autostart-inferior-process | ||
| 1120 | server-info | ||
| 1121 | (contact (if (functionp contact) (funcall contact) contact)) | ||
| 1122 | (initargs | ||
| 1123 | (cond ((keywordp (car contact)) contact) | ||
| 1124 | ((integerp (cadr contact)) | ||
| 1125 | (setq server-info (list (format "%s:%s" (car contact) | ||
| 1126 | (cadr contact)))) | ||
| 1127 | `(:process ,(lambda () | ||
| 1128 | (apply #'open-network-stream | ||
| 1129 | readable-name nil | ||
| 1130 | (car contact) (cadr contact) | ||
| 1131 | (cddr contact))))) | ||
| 1132 | ((and (stringp (car contact)) (memq :autoport contact)) | ||
| 1133 | (setq server-info (list "<inferior process>")) | ||
| 1134 | `(:process ,(lambda () | ||
| 1135 | (pcase-let ((`(,connection . ,inferior) | ||
| 1136 | (eglot--inferior-bootstrap | ||
| 1137 | readable-name | ||
| 1138 | contact))) | ||
| 1139 | (setq autostart-inferior-process inferior) | ||
| 1140 | connection)))) | ||
| 1141 | ((stringp (car contact)) | ||
| 1142 | (let* ((probe (cl-position-if #'keywordp contact)) | ||
| 1143 | (more-initargs (and probe (cl-subseq contact probe))) | ||
| 1144 | (contact (cl-subseq contact 0 probe))) | ||
| 1145 | `(:process | ||
| 1146 | ,(lambda () | ||
| 1147 | (let ((default-directory default-directory)) | ||
| 1148 | (make-process | ||
| 1149 | :name readable-name | ||
| 1150 | :command (setq server-info (eglot--cmd contact)) | ||
| 1151 | :connection-type 'pipe | ||
| 1152 | :coding 'utf-8-emacs-unix | ||
| 1153 | :noquery t | ||
| 1154 | :stderr (get-buffer-create | ||
| 1155 | (format "*%s stderr*" readable-name)) | ||
| 1156 | :file-handler t))) | ||
| 1157 | ,@more-initargs))))) | ||
| 1158 | (spread (lambda (fn) (lambda (server method params) | ||
| 1159 | (let ((eglot--cached-server server)) | ||
| 1160 | (apply fn server method (append params nil)))))) | ||
| 1161 | (server | ||
| 1162 | (apply | ||
| 1163 | #'make-instance class | ||
| 1164 | :name readable-name | ||
| 1165 | :events-buffer-scrollback-size eglot-events-buffer-size | ||
| 1166 | :notification-dispatcher (funcall spread #'eglot-handle-notification) | ||
| 1167 | :request-dispatcher (funcall spread #'eglot-handle-request) | ||
| 1168 | :on-shutdown #'eglot--on-shutdown | ||
| 1169 | initargs)) | ||
| 1170 | (cancelled nil) | ||
| 1171 | (tag (make-symbol "connected-catch-tag"))) | ||
| 1172 | (when server-info | ||
| 1173 | (jsonrpc--debug server "Running language server: %s" | ||
| 1174 | (string-join server-info " "))) | ||
| 1175 | (setf (eglot--saved-initargs server) initargs) | ||
| 1176 | (setf (eglot--project server) project) | ||
| 1177 | (setf (eglot--project-nickname server) nickname) | ||
| 1178 | (setf (eglot--major-mode server) managed-major-mode) | ||
| 1179 | (setf (eglot--language-id server) language-id) | ||
| 1180 | (setf (eglot--inferior-process server) autostart-inferior-process) | ||
| 1181 | (run-hook-with-args 'eglot-server-initialized-hook server) | ||
| 1182 | ;; Now start the handshake. To honour `eglot-sync-connect' | ||
| 1183 | ;; maybe-sync-maybe-async semantics we use `jsonrpc-async-request' | ||
| 1184 | ;; and mimic most of `jsonrpc-request'. | ||
| 1185 | (unwind-protect | ||
| 1186 | (condition-case _quit | ||
| 1187 | (let ((retval | ||
| 1188 | (catch tag | ||
| 1189 | (jsonrpc-async-request | ||
| 1190 | server | ||
| 1191 | :initialize | ||
| 1192 | (list :processId | ||
| 1193 | (unless (or eglot-withhold-process-id | ||
| 1194 | (file-remote-p default-directory) | ||
| 1195 | (eq (jsonrpc-process-type server) | ||
| 1196 | 'network)) | ||
| 1197 | (emacs-pid)) | ||
| 1198 | ;; Maybe turn trampy `/ssh:foo@bar:/path/to/baz.py' | ||
| 1199 | ;; into `/path/to/baz.py', so LSP groks it. | ||
| 1200 | :rootPath (file-local-name | ||
| 1201 | (expand-file-name default-directory)) | ||
| 1202 | :rootUri (eglot--path-to-uri default-directory) | ||
| 1203 | :initializationOptions (eglot-initialization-options | ||
| 1204 | server) | ||
| 1205 | :capabilities (eglot-client-capabilities server) | ||
| 1206 | :workspaceFolders (eglot-workspace-folders server)) | ||
| 1207 | :success-fn | ||
| 1208 | (eglot--lambda ((InitializeResult) capabilities serverInfo) | ||
| 1209 | (unless cancelled | ||
| 1210 | (push server | ||
| 1211 | (gethash project eglot--servers-by-project)) | ||
| 1212 | (setf (eglot--capabilities server) capabilities) | ||
| 1213 | (setf (eglot--server-info server) serverInfo) | ||
| 1214 | (jsonrpc-notify server :initialized eglot--{}) | ||
| 1215 | (dolist (buffer (buffer-list)) | ||
| 1216 | (with-current-buffer buffer | ||
| 1217 | ;; No need to pass SERVER as an argument: it has | ||
| 1218 | ;; been registered in `eglot--servers-by-project', | ||
| 1219 | ;; so that it can be found (and cached) from | ||
| 1220 | ;; `eglot--maybe-activate-editing-mode' in any | ||
| 1221 | ;; managed buffer. | ||
| 1222 | (eglot--maybe-activate-editing-mode))) | ||
| 1223 | (setf (eglot--inhibit-autoreconnect server) | ||
| 1224 | (cond | ||
| 1225 | ((booleanp eglot-autoreconnect) | ||
| 1226 | (not eglot-autoreconnect)) | ||
| 1227 | ((cl-plusp eglot-autoreconnect) | ||
| 1228 | (run-with-timer | ||
| 1229 | eglot-autoreconnect nil | ||
| 1230 | (lambda () | ||
| 1231 | (setf (eglot--inhibit-autoreconnect server) | ||
| 1232 | (null eglot-autoreconnect))))))) | ||
| 1233 | (let ((default-directory (project-root project)) | ||
| 1234 | (major-mode managed-major-mode)) | ||
| 1235 | (hack-dir-local-variables-non-file-buffer) | ||
| 1236 | (run-hook-with-args 'eglot-connect-hook server)) | ||
| 1237 | (eglot--message | ||
| 1238 | "Connected! Server `%s' now managing `%s' buffers \ | ||
| 1239 | in project `%s'." | ||
| 1240 | (or (plist-get serverInfo :name) | ||
| 1241 | (jsonrpc-name server)) | ||
| 1242 | managed-major-mode | ||
| 1243 | (eglot-project-nickname server)) | ||
| 1244 | (when tag (throw tag t)))) | ||
| 1245 | :timeout eglot-connect-timeout | ||
| 1246 | :error-fn (eglot--lambda ((ResponseError) code message) | ||
| 1247 | (unless cancelled | ||
| 1248 | (jsonrpc-shutdown server) | ||
| 1249 | (let ((msg (format "%s: %s" code message))) | ||
| 1250 | (if tag (throw tag `(error . ,msg)) | ||
| 1251 | (eglot--error msg))))) | ||
| 1252 | :timeout-fn (lambda () | ||
| 1253 | (unless cancelled | ||
| 1254 | (jsonrpc-shutdown server) | ||
| 1255 | (let ((msg (format "Timed out after %s seconds" | ||
| 1256 | eglot-connect-timeout))) | ||
| 1257 | (if tag (throw tag `(error . ,msg)) | ||
| 1258 | (eglot--error msg)))))) | ||
| 1259 | (cond ((numberp eglot-sync-connect) | ||
| 1260 | (accept-process-output nil eglot-sync-connect)) | ||
| 1261 | (eglot-sync-connect | ||
| 1262 | (while t (accept-process-output nil 30))))))) | ||
| 1263 | (pcase retval | ||
| 1264 | (`(error . ,msg) (eglot--error msg)) | ||
| 1265 | (`nil (eglot--message "Waiting in background for server `%s'" | ||
| 1266 | (jsonrpc-name server)) | ||
| 1267 | nil) | ||
| 1268 | (_ server))) | ||
| 1269 | (quit (jsonrpc-shutdown server) (setq cancelled 'quit))) | ||
| 1270 | (setq tag nil)))) | ||
| 1271 | |||
| 1272 | (defun eglot--inferior-bootstrap (name contact &optional connect-args) | ||
| 1273 | "Use CONTACT to start a server, then connect to it. | ||
| 1274 | Return a cons of two process objects (CONNECTION . INFERIOR). | ||
| 1275 | Name both based on NAME. | ||
| 1276 | CONNECT-ARGS are passed as additional arguments to | ||
| 1277 | `open-network-stream'." | ||
| 1278 | (let* ((port-probe (make-network-process :name "eglot-port-probe-dummy" | ||
| 1279 | :server t | ||
| 1280 | :host "localhost" | ||
| 1281 | :service 0)) | ||
| 1282 | (port-number (unwind-protect | ||
| 1283 | (process-contact port-probe :service) | ||
| 1284 | (delete-process port-probe))) | ||
| 1285 | inferior connection) | ||
| 1286 | (unwind-protect | ||
| 1287 | (progn | ||
| 1288 | (setq inferior | ||
| 1289 | (make-process | ||
| 1290 | :name (format "autostart-inferior-%s" name) | ||
| 1291 | :stderr (format "*%s stderr*" name) | ||
| 1292 | :noquery t | ||
| 1293 | :command (cl-subst | ||
| 1294 | (format "%s" port-number) :autoport contact))) | ||
| 1295 | (setq connection | ||
| 1296 | (cl-loop | ||
| 1297 | repeat 10 for i from 1 | ||
| 1298 | do (accept-process-output nil 0.5) | ||
| 1299 | while (process-live-p inferior) | ||
| 1300 | do (eglot--message | ||
| 1301 | "Trying to connect to localhost and port %s (attempt %s)" | ||
| 1302 | port-number i) | ||
| 1303 | thereis (ignore-errors | ||
| 1304 | (apply #'open-network-stream | ||
| 1305 | (format "autoconnect-%s" name) | ||
| 1306 | nil | ||
| 1307 | "localhost" port-number connect-args)))) | ||
| 1308 | (cons connection inferior)) | ||
| 1309 | (cond ((and (process-live-p connection) | ||
| 1310 | (process-live-p inferior)) | ||
| 1311 | (eglot--message "Done, connected to %s!" port-number)) | ||
| 1312 | (t | ||
| 1313 | (when inferior (delete-process inferior)) | ||
| 1314 | (when connection (delete-process connection)) | ||
| 1315 | (eglot--error "Could not start and connect to server%s" | ||
| 1316 | (if inferior | ||
| 1317 | (format " started with %s" | ||
| 1318 | (process-command inferior)) | ||
| 1319 | "!"))))))) | ||
| 1320 | |||
| 1321 | |||
| 1322 | ;;; Helpers (move these to API?) | ||
| 1323 | ;;; | ||
| 1324 | (defun eglot--error (format &rest args) | ||
| 1325 | "Error out with FORMAT with ARGS." | ||
| 1326 | (error "[eglot] %s" (apply #'format format args))) | ||
| 1327 | |||
| 1328 | (defun eglot--message (format &rest args) | ||
| 1329 | "Message out with FORMAT with ARGS." | ||
| 1330 | (message "[eglot] %s" (apply #'format format args))) | ||
| 1331 | |||
| 1332 | (defun eglot--warn (format &rest args) | ||
| 1333 | "Warning message with FORMAT and ARGS." | ||
| 1334 | (apply #'eglot--message (concat "(warning) " format) args) | ||
| 1335 | (let ((warning-minimum-level :error)) | ||
| 1336 | (display-warning 'eglot (apply #'format format args) :warning))) | ||
| 1337 | |||
| 1338 | (defun eglot-current-column () (- (point) (point-at-bol))) | ||
| 1339 | |||
| 1340 | (defvar eglot-current-column-function #'eglot-lsp-abiding-column | ||
| 1341 | "Function to calculate the current column. | ||
| 1342 | |||
| 1343 | This is the inverse operation of | ||
| 1344 | `eglot-move-to-column-function' (which see). It is a function of | ||
| 1345 | no arguments returning a column number. For buffers managed by | ||
| 1346 | fully LSP-compliant servers, this should be set to | ||
| 1347 | `eglot-lsp-abiding-column' (the default), and | ||
| 1348 | `eglot-current-column' for all others.") | ||
| 1349 | |||
| 1350 | (defun eglot-lsp-abiding-column (&optional lbp) | ||
| 1351 | "Calculate current COLUMN as defined by the LSP spec. | ||
| 1352 | LBP defaults to `line-beginning-position'." | ||
| 1353 | (/ (- (length (encode-coding-region (or lbp (line-beginning-position)) | ||
| 1354 | ;; Fix github#860 | ||
| 1355 | (min (point) (point-max)) 'utf-16 t)) | ||
| 1356 | 2) | ||
| 1357 | 2)) | ||
| 1358 | |||
| 1359 | (defun eglot--pos-to-lsp-position (&optional pos) | ||
| 1360 | "Convert point POS to LSP position." | ||
| 1361 | (eglot--widening | ||
| 1362 | (list :line (1- (line-number-at-pos pos t)) ; F!@&#$CKING OFF-BY-ONE | ||
| 1363 | :character (progn (when pos (goto-char pos)) | ||
| 1364 | (funcall eglot-current-column-function))))) | ||
| 1365 | |||
| 1366 | (defvar eglot-move-to-column-function #'eglot-move-to-lsp-abiding-column | ||
| 1367 | "Function to move to a column reported by the LSP server. | ||
| 1368 | |||
| 1369 | According to the standard, LSP column/character offsets are based | ||
| 1370 | on a count of UTF-16 code units, not actual visual columns. So | ||
| 1371 | when LSP says position 3 of a line containing just \"aXbc\", | ||
| 1372 | where X is a multi-byte character, it actually means `b', not | ||
| 1373 | `c'. However, many servers don't follow the spec this closely. | ||
| 1374 | |||
| 1375 | For buffers managed by fully LSP-compliant servers, this should | ||
| 1376 | be set to `eglot-move-to-lsp-abiding-column' (the default), and | ||
| 1377 | `eglot-move-to-column' for all others.") | ||
| 1378 | |||
| 1379 | (defun eglot-move-to-column (column) | ||
| 1380 | "Move to COLUMN without closely following the LSP spec." | ||
| 1381 | ;; We cannot use `move-to-column' here, because it moves to *visual* | ||
| 1382 | ;; columns, which can be different from LSP columns in case of | ||
| 1383 | ;; `whitespace-mode', `prettify-symbols-mode', etc. (github#296, | ||
| 1384 | ;; github#297) | ||
| 1385 | (goto-char (min (+ (line-beginning-position) column) | ||
| 1386 | (line-end-position)))) | ||
| 1387 | |||
| 1388 | (defun eglot-move-to-lsp-abiding-column (column) | ||
| 1389 | "Move to COLUMN abiding by the LSP spec." | ||
| 1390 | (save-restriction | ||
| 1391 | (cl-loop | ||
| 1392 | with lbp = (line-beginning-position) | ||
| 1393 | initially | ||
| 1394 | (narrow-to-region lbp (line-end-position)) | ||
| 1395 | (move-to-column column) | ||
| 1396 | for diff = (- column | ||
| 1397 | (eglot-lsp-abiding-column lbp)) | ||
| 1398 | until (zerop diff) | ||
| 1399 | do (condition-case eob-err | ||
| 1400 | (forward-char (/ (if (> diff 0) (1+ diff) (1- diff)) 2)) | ||
| 1401 | (end-of-buffer (cl-return eob-err)))))) | ||
| 1402 | |||
| 1403 | (defun eglot--lsp-position-to-point (pos-plist &optional marker) | ||
| 1404 | "Convert LSP position POS-PLIST to Emacs point. | ||
| 1405 | If optional MARKER, return a marker instead" | ||
| 1406 | (save-excursion | ||
| 1407 | (save-restriction | ||
| 1408 | (widen) | ||
| 1409 | (goto-char (point-min)) | ||
| 1410 | (forward-line (min most-positive-fixnum | ||
| 1411 | (plist-get pos-plist :line))) | ||
| 1412 | (unless (eobp) ;; if line was excessive leave point at eob | ||
| 1413 | (let ((tab-width 1) | ||
| 1414 | (col (plist-get pos-plist :character))) | ||
| 1415 | (unless (wholenump col) | ||
| 1416 | (eglot--warn | ||
| 1417 | "Caution: LSP server sent invalid character position %s. Using 0 instead." | ||
| 1418 | col) | ||
| 1419 | (setq col 0)) | ||
| 1420 | (funcall eglot-move-to-column-function col))) | ||
| 1421 | (if marker (copy-marker (point-marker)) (point))))) | ||
| 1422 | |||
| 1423 | (defconst eglot--uri-path-allowed-chars | ||
| 1424 | (let ((vec (copy-sequence url-path-allowed-chars))) | ||
| 1425 | (aset vec ?: nil) ;; see github#639 | ||
| 1426 | vec) | ||
| 1427 | "Like `url-path-allows-chars' but more restrictive.") | ||
| 1428 | |||
| 1429 | (defun eglot--path-to-uri (path) | ||
| 1430 | "URIfy PATH." | ||
| 1431 | (let ((truepath (file-truename path))) | ||
| 1432 | (concat "file://" | ||
| 1433 | ;; Add a leading "/" for local MS Windows-style paths. | ||
| 1434 | (if (and (eq system-type 'windows-nt) | ||
| 1435 | (not (file-remote-p truepath))) | ||
| 1436 | "/") | ||
| 1437 | (url-hexify-string | ||
| 1438 | ;; Again watch out for trampy paths. | ||
| 1439 | (directory-file-name (file-local-name truepath)) | ||
| 1440 | eglot--uri-path-allowed-chars)))) | ||
| 1441 | |||
| 1442 | (defun eglot--uri-to-path (uri) | ||
| 1443 | "Convert URI to file path, helped by `eglot--current-server'." | ||
| 1444 | (when (keywordp uri) (setq uri (substring (symbol-name uri) 1))) | ||
| 1445 | (let* ((server (eglot-current-server)) | ||
| 1446 | (remote-prefix (and server (eglot--trampish-p server))) | ||
| 1447 | (retval (url-unhex-string (url-filename (url-generic-parse-url uri)))) | ||
| 1448 | ;; Remove the leading "/" for local MS Windows-style paths. | ||
| 1449 | (normalized (if (and (not remote-prefix) | ||
| 1450 | (eq system-type 'windows-nt) | ||
| 1451 | (cl-plusp (length retval))) | ||
| 1452 | (substring retval 1) | ||
| 1453 | retval))) | ||
| 1454 | (concat remote-prefix normalized))) | ||
| 1455 | |||
| 1456 | (defun eglot--snippet-expansion-fn () | ||
| 1457 | "Compute a function to expand snippets. | ||
| 1458 | Doubles as an indicator of snippet support." | ||
| 1459 | (and (boundp 'yas-minor-mode) | ||
| 1460 | (symbol-value 'yas-minor-mode) | ||
| 1461 | 'yas-expand-snippet)) | ||
| 1462 | |||
| 1463 | (defun eglot--format-markup (markup) | ||
| 1464 | "Format MARKUP according to LSP's spec." | ||
| 1465 | (pcase-let ((`(,string ,mode) | ||
| 1466 | (if (stringp markup) (list markup 'gfm-view-mode) | ||
| 1467 | (list (plist-get markup :value) | ||
| 1468 | (pcase (plist-get markup :kind) | ||
| 1469 | ("markdown" 'gfm-view-mode) | ||
| 1470 | ("plaintext" 'text-mode) | ||
| 1471 | (_ major-mode)))))) | ||
| 1472 | (with-temp-buffer | ||
| 1473 | (setq-local markdown-fontify-code-blocks-natively t) | ||
| 1474 | (insert string) | ||
| 1475 | (let ((inhibit-message t) | ||
| 1476 | (message-log-max nil)) | ||
| 1477 | (ignore-errors (delay-mode-hooks (funcall mode)))) | ||
| 1478 | (font-lock-ensure) | ||
| 1479 | (string-trim (buffer-string))))) | ||
| 1480 | |||
| 1481 | (define-obsolete-variable-alias 'eglot-ignored-server-capabilites | ||
| 1482 | 'eglot-ignored-server-capabilities "1.8") | ||
| 1483 | |||
| 1484 | (defcustom eglot-ignored-server-capabilities (list) | ||
| 1485 | "LSP server capabilities that Eglot could use, but won't. | ||
| 1486 | You could add, for instance, the symbol | ||
| 1487 | `:documentHighlightProvider' to prevent automatic highlighting | ||
| 1488 | under cursor." | ||
| 1489 | :type '(set | ||
| 1490 | :tag "Tick the ones you're not interested in" | ||
| 1491 | (const :tag "Documentation on hover" :hoverProvider) | ||
| 1492 | (const :tag "Code completion" :completionProvider) | ||
| 1493 | (const :tag "Function signature help" :signatureHelpProvider) | ||
| 1494 | (const :tag "Go to definition" :definitionProvider) | ||
| 1495 | (const :tag "Go to type definition" :typeDefinitionProvider) | ||
| 1496 | (const :tag "Go to implementation" :implementationProvider) | ||
| 1497 | (const :tag "Go to declaration" :implementationProvider) | ||
| 1498 | (const :tag "Find references" :referencesProvider) | ||
| 1499 | (const :tag "Highlight symbols automatically" :documentHighlightProvider) | ||
| 1500 | (const :tag "List symbols in buffer" :documentSymbolProvider) | ||
| 1501 | (const :tag "List symbols in workspace" :workspaceSymbolProvider) | ||
| 1502 | (const :tag "Execute code actions" :codeActionProvider) | ||
| 1503 | (const :tag "Code lens" :codeLensProvider) | ||
| 1504 | (const :tag "Format buffer" :documentFormattingProvider) | ||
| 1505 | (const :tag "Format portion of buffer" :documentRangeFormattingProvider) | ||
| 1506 | (const :tag "On-type formatting" :documentOnTypeFormattingProvider) | ||
| 1507 | (const :tag "Rename symbol" :renameProvider) | ||
| 1508 | (const :tag "Highlight links in document" :documentLinkProvider) | ||
| 1509 | (const :tag "Decorate color references" :colorProvider) | ||
| 1510 | (const :tag "Fold regions of buffer" :foldingRangeProvider) | ||
| 1511 | (const :tag "Execute custom commands" :executeCommandProvider))) | ||
| 1512 | |||
| 1513 | (defun eglot--server-capable (&rest feats) | ||
| 1514 | "Determine if current server is capable of FEATS." | ||
| 1515 | (unless (cl-some (lambda (feat) | ||
| 1516 | (memq feat eglot-ignored-server-capabilities)) | ||
| 1517 | feats) | ||
| 1518 | (cl-loop for caps = (eglot--capabilities (eglot--current-server-or-lose)) | ||
| 1519 | then (cadr probe) | ||
| 1520 | for (feat . more) on feats | ||
| 1521 | for probe = (plist-member caps feat) | ||
| 1522 | if (not probe) do (cl-return nil) | ||
| 1523 | if (eq (cadr probe) :json-false) do (cl-return nil) | ||
| 1524 | if (not (listp (cadr probe))) do (cl-return (if more nil (cadr probe))) | ||
| 1525 | finally (cl-return (or (cadr probe) t))))) | ||
| 1526 | |||
| 1527 | (defun eglot--range-region (range &optional markers) | ||
| 1528 | "Return region (BEG . END) that represents LSP RANGE. | ||
| 1529 | If optional MARKERS, make markers." | ||
| 1530 | (let* ((st (plist-get range :start)) | ||
| 1531 | (beg (eglot--lsp-position-to-point st markers)) | ||
| 1532 | (end (eglot--lsp-position-to-point (plist-get range :end) markers))) | ||
| 1533 | (cons beg end))) | ||
| 1534 | |||
| 1535 | (defun eglot--read-server (prompt &optional dont-if-just-the-one) | ||
| 1536 | "Read a running Eglot server from minibuffer using PROMPT. | ||
| 1537 | If DONT-IF-JUST-THE-ONE and there's only one server, don't prompt | ||
| 1538 | and just return it. PROMPT shouldn't end with a question mark." | ||
| 1539 | (let ((servers (cl-loop for servers | ||
| 1540 | being hash-values of eglot--servers-by-project | ||
| 1541 | append servers)) | ||
| 1542 | (name (lambda (srv) | ||
| 1543 | (format "%s/%s" (eglot-project-nickname srv) | ||
| 1544 | (eglot--major-mode srv))))) | ||
| 1545 | (cond ((null servers) | ||
| 1546 | (eglot--error "No servers!")) | ||
| 1547 | ((or (cdr servers) (not dont-if-just-the-one)) | ||
| 1548 | (let* ((default (when-let ((current (eglot-current-server))) | ||
| 1549 | (funcall name current))) | ||
| 1550 | (read (completing-read | ||
| 1551 | (if default | ||
| 1552 | (format "%s (default %s)? " prompt default) | ||
| 1553 | (concat prompt "? ")) | ||
| 1554 | (mapcar name servers) | ||
| 1555 | nil t | ||
| 1556 | nil nil | ||
| 1557 | default))) | ||
| 1558 | (cl-find read servers :key name :test #'equal))) | ||
| 1559 | (t (car servers))))) | ||
| 1560 | |||
| 1561 | (defun eglot--trampish-p (server) | ||
| 1562 | "Tell if SERVER's project root is `file-remote-p'." | ||
| 1563 | (file-remote-p (project-root (eglot--project server)))) | ||
| 1564 | |||
| 1565 | (defun eglot--plist-keys (plist) "Get keys of a plist." | ||
| 1566 | (cl-loop for (k _v) on plist by #'cddr collect k)) | ||
| 1567 | |||
| 1568 | |||
| 1569 | ;;; Minor modes | ||
| 1570 | ;;; | ||
| 1571 | (defvar eglot-mode-map | ||
| 1572 | (let ((map (make-sparse-keymap))) | ||
| 1573 | (define-key map [remap display-local-help] #'eldoc-doc-buffer) | ||
| 1574 | map)) | ||
| 1575 | |||
| 1576 | (defvar-local eglot--current-flymake-report-fn nil | ||
| 1577 | "Current flymake report function for this buffer.") | ||
| 1578 | |||
| 1579 | (defvar-local eglot--saved-bindings nil | ||
| 1580 | "Bindings saved by `eglot--setq-saving'.") | ||
| 1581 | |||
| 1582 | (defvar eglot-stay-out-of '() | ||
| 1583 | "List of Emacs things that Eglot should try to stay of. | ||
| 1584 | Each element is a string, a symbol, or a regexp which is matched | ||
| 1585 | against a variable's name. Examples include the string | ||
| 1586 | \"company\" or the symbol `xref'. | ||
| 1587 | |||
| 1588 | Before Eglot starts \"managing\" a particular buffer, it | ||
| 1589 | opinionatedly sets some peripheral Emacs facilities, such as | ||
| 1590 | Flymake, Xref and Company. These overriding settings help ensure | ||
| 1591 | consistent Eglot behaviour and only stay in place until | ||
| 1592 | \"managing\" stops (usually via `eglot-shutdown'), whereupon the | ||
| 1593 | previous settings are restored. | ||
| 1594 | |||
| 1595 | However, if you wish for Eglot to stay out of a particular Emacs | ||
| 1596 | facility that you'd like to keep control of add an element to | ||
| 1597 | this list and Eglot will refrain from setting it. | ||
| 1598 | |||
| 1599 | For example, to keep your Company customization, add the symbol | ||
| 1600 | `company' to this variable.") | ||
| 1601 | |||
| 1602 | (defun eglot--stay-out-of-p (symbol) | ||
| 1603 | "Tell if EGLOT should stay of of SYMBOL." | ||
| 1604 | (cl-find (symbol-name symbol) eglot-stay-out-of | ||
| 1605 | :test (lambda (s thing) | ||
| 1606 | (let ((re (if (symbolp thing) (symbol-name thing) thing))) | ||
| 1607 | (string-match re s))))) | ||
| 1608 | |||
| 1609 | (defmacro eglot--setq-saving (symbol binding) | ||
| 1610 | `(unless (or (not (boundp ',symbol)) (eglot--stay-out-of-p ',symbol)) | ||
| 1611 | (push (cons ',symbol (symbol-value ',symbol)) eglot--saved-bindings) | ||
| 1612 | (setq-local ,symbol ,binding))) | ||
| 1613 | |||
| 1614 | (defun eglot-managed-p () | ||
| 1615 | "Tell if current buffer is managed by EGLOT." | ||
| 1616 | eglot--managed-mode) | ||
| 1617 | |||
| 1618 | (defvar eglot-managed-mode-hook nil | ||
| 1619 | "A hook run by EGLOT after it started/stopped managing a buffer. | ||
| 1620 | Use `eglot-managed-p' to determine if current buffer is managed.") | ||
| 1621 | |||
| 1622 | (define-minor-mode eglot--managed-mode | ||
| 1623 | "Mode for source buffers managed by some EGLOT project." | ||
| 1624 | :init-value nil :lighter nil :keymap eglot-mode-map | ||
| 1625 | (cond | ||
| 1626 | (eglot--managed-mode | ||
| 1627 | (add-hook 'after-change-functions 'eglot--after-change nil t) | ||
| 1628 | (add-hook 'before-change-functions 'eglot--before-change nil t) | ||
| 1629 | (add-hook 'kill-buffer-hook #'eglot--managed-mode-off nil t) | ||
| 1630 | ;; Prepend "didClose" to the hook after the "nonoff", so it will run first | ||
| 1631 | (add-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose nil t) | ||
| 1632 | (add-hook 'before-revert-hook 'eglot--signal-textDocument/didClose nil t) | ||
| 1633 | (add-hook 'after-revert-hook 'eglot--after-revert-hook nil t) | ||
| 1634 | (add-hook 'before-save-hook 'eglot--signal-textDocument/willSave nil t) | ||
| 1635 | (add-hook 'after-save-hook 'eglot--signal-textDocument/didSave nil t) | ||
| 1636 | (unless (eglot--stay-out-of-p 'xref) | ||
| 1637 | (add-hook 'xref-backend-functions 'eglot-xref-backend nil t)) | ||
| 1638 | (add-hook 'completion-at-point-functions #'eglot-completion-at-point nil t) | ||
| 1639 | (add-hook 'change-major-mode-hook #'eglot--managed-mode-off nil t) | ||
| 1640 | (add-hook 'post-self-insert-hook 'eglot--post-self-insert-hook nil t) | ||
| 1641 | (add-hook 'pre-command-hook 'eglot--pre-command-hook nil t) | ||
| 1642 | (eglot--setq-saving eldoc-documentation-functions | ||
| 1643 | '(eglot-signature-eldoc-function | ||
| 1644 | eglot-hover-eldoc-function)) | ||
| 1645 | (eglot--setq-saving eldoc-documentation-strategy | ||
| 1646 | #'eldoc-documentation-enthusiast) | ||
| 1647 | (eglot--setq-saving xref-prompt-for-identifier nil) | ||
| 1648 | (eglot--setq-saving flymake-diagnostic-functions '(eglot-flymake-backend)) | ||
| 1649 | (eglot--setq-saving company-backends '(company-capf)) | ||
| 1650 | (eglot--setq-saving company-tooltip-align-annotations t) | ||
| 1651 | (unless (eglot--stay-out-of-p 'imenu) | ||
| 1652 | (add-function :before-until (local 'imenu-create-index-function) | ||
| 1653 | #'eglot-imenu)) | ||
| 1654 | (unless (eglot--stay-out-of-p 'flymake) (flymake-mode 1)) | ||
| 1655 | (unless (eglot--stay-out-of-p 'eldoc) (eldoc-mode 1)) | ||
| 1656 | (cl-pushnew (current-buffer) (eglot--managed-buffers (eglot-current-server)))) | ||
| 1657 | (t | ||
| 1658 | (remove-hook 'after-change-functions 'eglot--after-change t) | ||
| 1659 | (remove-hook 'before-change-functions 'eglot--before-change t) | ||
| 1660 | (remove-hook 'kill-buffer-hook #'eglot--managed-mode-off t) | ||
| 1661 | (remove-hook 'kill-buffer-hook 'eglot--signal-textDocument/didClose t) | ||
| 1662 | (remove-hook 'before-revert-hook 'eglot--signal-textDocument/didClose t) | ||
| 1663 | (remove-hook 'after-revert-hook 'eglot--after-revert-hook t) | ||
| 1664 | (remove-hook 'before-save-hook 'eglot--signal-textDocument/willSave t) | ||
| 1665 | (remove-hook 'after-save-hook 'eglot--signal-textDocument/didSave t) | ||
| 1666 | (remove-hook 'xref-backend-functions 'eglot-xref-backend t) | ||
| 1667 | (remove-hook 'completion-at-point-functions #'eglot-completion-at-point t) | ||
| 1668 | (remove-hook 'change-major-mode-hook #'eglot--managed-mode-off t) | ||
| 1669 | (remove-hook 'post-self-insert-hook 'eglot--post-self-insert-hook t) | ||
| 1670 | (remove-hook 'pre-command-hook 'eglot--pre-command-hook t) | ||
| 1671 | (cl-loop for (var . saved-binding) in eglot--saved-bindings | ||
| 1672 | do (set (make-local-variable var) saved-binding)) | ||
| 1673 | (remove-function (local 'imenu-create-index-function) #'eglot-imenu) | ||
| 1674 | (when eglot--current-flymake-report-fn | ||
| 1675 | (eglot--report-to-flymake nil) | ||
| 1676 | (setq eglot--current-flymake-report-fn nil)) | ||
| 1677 | (let ((server eglot--cached-server)) | ||
| 1678 | (setq eglot--cached-server nil) | ||
| 1679 | (when server | ||
| 1680 | (setf (eglot--managed-buffers server) | ||
| 1681 | (delq (current-buffer) (eglot--managed-buffers server))) | ||
| 1682 | (when (and eglot-autoshutdown | ||
| 1683 | (null (eglot--managed-buffers server))) | ||
| 1684 | (eglot-shutdown server)))))) | ||
| 1685 | ;; Note: the public hook runs before the internal eglot--managed-mode-hook. | ||
| 1686 | (run-hooks 'eglot-managed-mode-hook)) | ||
| 1687 | |||
| 1688 | (defun eglot--managed-mode-off () | ||
| 1689 | "Turn off `eglot--managed-mode' unconditionally." | ||
| 1690 | (eglot--managed-mode -1)) | ||
| 1691 | |||
| 1692 | (defun eglot-current-server () | ||
| 1693 | "Return logical EGLOT server for current buffer, nil if none." | ||
| 1694 | (setq eglot--cached-server | ||
| 1695 | (or eglot--cached-server | ||
| 1696 | (cl-find major-mode | ||
| 1697 | (gethash (eglot--current-project) eglot--servers-by-project) | ||
| 1698 | :key #'eglot--major-mode) | ||
| 1699 | (and eglot-extend-to-xref | ||
| 1700 | buffer-file-name | ||
| 1701 | (gethash (expand-file-name buffer-file-name) | ||
| 1702 | eglot--servers-by-xrefed-file))))) | ||
| 1703 | |||
| 1704 | (defun eglot--current-server-or-lose () | ||
| 1705 | "Return current logical EGLOT server connection or error." | ||
| 1706 | (or (eglot-current-server) | ||
| 1707 | (jsonrpc-error "No current JSON-RPC connection"))) | ||
| 1708 | |||
| 1709 | (defvar-local eglot--diagnostics nil | ||
| 1710 | "Flymake diagnostics for this buffer.") | ||
| 1711 | |||
| 1712 | (defvar revert-buffer-preserve-modes) | ||
| 1713 | (defun eglot--after-revert-hook () | ||
| 1714 | "Eglot's `after-revert-hook'." | ||
| 1715 | (when revert-buffer-preserve-modes (eglot--signal-textDocument/didOpen))) | ||
| 1716 | |||
| 1717 | (defun eglot--maybe-activate-editing-mode () | ||
| 1718 | "Maybe activate `eglot--managed-mode'. | ||
| 1719 | |||
| 1720 | If it is activated, also signal textDocument/didOpen." | ||
| 1721 | (unless eglot--managed-mode | ||
| 1722 | ;; Called when `revert-buffer-in-progress-p' is t but | ||
| 1723 | ;; `revert-buffer-preserve-modes' is nil. | ||
| 1724 | (when (and buffer-file-name (eglot-current-server)) | ||
| 1725 | (setq eglot--diagnostics nil) | ||
| 1726 | (eglot--managed-mode) | ||
| 1727 | (eglot--signal-textDocument/didOpen)))) | ||
| 1728 | |||
| 1729 | (add-hook 'find-file-hook 'eglot--maybe-activate-editing-mode) | ||
| 1730 | (add-hook 'after-change-major-mode-hook 'eglot--maybe-activate-editing-mode) | ||
| 1731 | |||
| 1732 | (defun eglot-clear-status (server) | ||
| 1733 | "Clear the last JSONRPC error for SERVER." | ||
| 1734 | (interactive (list (eglot--current-server-or-lose))) | ||
| 1735 | (setf (jsonrpc-last-error server) nil)) | ||
| 1736 | |||
| 1737 | |||
| 1738 | ;;; Mode-line, menu and other sugar | ||
| 1739 | ;;; | ||
| 1740 | (defvar eglot--mode-line-format `(:eval (eglot--mode-line-format))) | ||
| 1741 | |||
| 1742 | (put 'eglot--mode-line-format 'risky-local-variable t) | ||
| 1743 | |||
| 1744 | (defun eglot--mouse-call (what) | ||
| 1745 | "Make an interactive lambda for calling WHAT from mode-line." | ||
| 1746 | (lambda (event) | ||
| 1747 | (interactive "e") | ||
| 1748 | (let ((start (event-start event))) (with-selected-window (posn-window start) | ||
| 1749 | (save-excursion | ||
| 1750 | (goto-char (or (posn-point start) | ||
| 1751 | (point))) | ||
| 1752 | (call-interactively what) | ||
| 1753 | (force-mode-line-update t)))))) | ||
| 1754 | |||
| 1755 | (defun eglot-manual () "Open on-line documentation." | ||
| 1756 | (interactive) (browse-url "https://github.com/joaotavora/eglot#readme")) | ||
| 1757 | |||
| 1758 | (easy-menu-define eglot-menu nil "Eglot" | ||
| 1759 | `("Eglot" | ||
| 1760 | ;; Commands for getting information and customization. | ||
| 1761 | ["Read manual" eglot-manual] | ||
| 1762 | ["Customize Eglot" (lambda () (interactive) (customize-group "eglot"))] | ||
| 1763 | "--" | ||
| 1764 | ;; xref like commands. | ||
| 1765 | ["Find definitions" xref-find-definitions | ||
| 1766 | :help "Find definitions of identifier at point" | ||
| 1767 | :active (eglot--server-capable :definitionProvider)] | ||
| 1768 | ["Find references" xref-find-references | ||
| 1769 | :help "Find references to identifier at point" | ||
| 1770 | :active (eglot--server-capable :referencesProvider)] | ||
| 1771 | ["Find symbols in workspace (apropos)" xref-find-apropos | ||
| 1772 | :help "Find symbols matching a query" | ||
| 1773 | :active (eglot--server-capable :workspaceSymbolProvider)] | ||
| 1774 | ["Find declaration" eglot-find-declaration | ||
| 1775 | :help "Find declaration for identifier at point" | ||
| 1776 | :active (eglot--server-capable :declarationProvider)] | ||
| 1777 | ["Find implementation" eglot-find-implementation | ||
| 1778 | :help "Find implementation for identifier at point" | ||
| 1779 | :active (eglot--server-capable :implementationProvider)] | ||
| 1780 | ["Find type definition" eglot-find-typeDefinition | ||
| 1781 | :help "Find type definition for identifier at point" | ||
| 1782 | :active (eglot--server-capable :typeDefinitionProvider)] | ||
| 1783 | "--" | ||
| 1784 | ;; LSP-related commands (mostly Eglot's own commands). | ||
| 1785 | ["Rename symbol" eglot-rename | ||
| 1786 | :active (eglot--server-capable :renameProvider)] | ||
| 1787 | ["Format buffer" eglot-format-buffer | ||
| 1788 | :active (eglot--server-capable :documentFormattingProvider)] | ||
| 1789 | ["Format active region" eglot-format | ||
| 1790 | :active (and (region-active-p) | ||
| 1791 | (eglot--server-capable :documentRangeFormattingProvider))] | ||
| 1792 | ["Show Flymake diagnostics for buffer" flymake-show-buffer-diagnostics] | ||
| 1793 | ["Show Flymake diagnostics for project" flymake-show-project-diagnostics] | ||
| 1794 | ["Show Eldoc documentation at point" eldoc-doc-buffer] | ||
| 1795 | "--" | ||
| 1796 | ["All possible code actions" eglot-code-actions | ||
| 1797 | :active (eglot--server-capable :codeActionProvider)] | ||
| 1798 | ["Organize imports" eglot-code-action-organize-imports | ||
| 1799 | :visible (eglot--server-capable :codeActionProvider)] | ||
| 1800 | ["Extract" eglot-code-action-extract | ||
| 1801 | :visible (eglot--server-capable :codeActionProvider)] | ||
| 1802 | ["Inline" eglot-code-action-inline | ||
| 1803 | :visible (eglot--server-capable :codeActionProvider)] | ||
| 1804 | ["Rewrite" eglot-code-action-rewrite | ||
| 1805 | :visible (eglot--server-capable :codeActionProvider)] | ||
| 1806 | ["Quickfix" eglot-code-action-quickfix | ||
| 1807 | :visible (eglot--server-capable :codeActionProvider)])) | ||
| 1808 | |||
| 1809 | (easy-menu-define eglot-server-menu nil "Monitor server communication" | ||
| 1810 | '("Debugging the server communication" | ||
| 1811 | ["Reconnect to server" eglot-reconnect] | ||
| 1812 | ["Quit server" eglot-shutdown] | ||
| 1813 | "--" | ||
| 1814 | ["LSP events buffer" eglot-events-buffer] | ||
| 1815 | ["Server stderr buffer" eglot-stderr-buffer] | ||
| 1816 | ["Customize event buffer size" | ||
| 1817 | (lambda () | ||
| 1818 | (interactive) | ||
| 1819 | (customize-variable 'eglot-events-buffer-size))])) | ||
| 1820 | |||
| 1821 | (defun eglot--mode-line-props (thing face defs &optional prepend) | ||
| 1822 | "Helper for function `eglot--mode-line-format'. | ||
| 1823 | Uses THING, FACE, DEFS and PREPEND." | ||
| 1824 | (cl-loop with map = (make-sparse-keymap) | ||
| 1825 | for (elem . rest) on defs | ||
| 1826 | for (key def help) = elem | ||
| 1827 | do (define-key map `[mode-line ,key] (eglot--mouse-call def)) | ||
| 1828 | concat (format "%s: %s" key help) into blurb | ||
| 1829 | when rest concat "\n" into blurb | ||
| 1830 | finally (return `(:propertize ,thing | ||
| 1831 | face ,face | ||
| 1832 | keymap ,map help-echo ,(concat prepend blurb) | ||
| 1833 | mouse-face mode-line-highlight)))) | ||
| 1834 | |||
| 1835 | (defun eglot--mode-line-format () | ||
| 1836 | "Compose the EGLOT's mode-line." | ||
| 1837 | (pcase-let* ((server (eglot-current-server)) | ||
| 1838 | (nick (and server (eglot-project-nickname server))) | ||
| 1839 | (pending (and server (hash-table-count | ||
| 1840 | (jsonrpc--request-continuations server)))) | ||
| 1841 | (`(,_id ,doing ,done-p ,_detail) (and server (eglot--spinner server))) | ||
| 1842 | (last-error (and server (jsonrpc-last-error server)))) | ||
| 1843 | (append | ||
| 1844 | `(,(propertize | ||
| 1845 | eglot-menu-string | ||
| 1846 | 'face 'eglot-mode-line | ||
| 1847 | 'mouse-face 'mode-line-highlight | ||
| 1848 | 'help-echo "Eglot: Emacs LSP client\nmouse-1: Display minor mode menu" | ||
| 1849 | 'keymap (let ((map (make-sparse-keymap))) | ||
| 1850 | (define-key map [mode-line down-mouse-1] eglot-menu) | ||
| 1851 | map))) | ||
| 1852 | (when nick | ||
| 1853 | `(":" | ||
| 1854 | ,(propertize | ||
| 1855 | nick | ||
| 1856 | 'face 'eglot-mode-line | ||
| 1857 | 'mouse-face 'mode-line-highlight | ||
| 1858 | 'help-echo (format "Project '%s'\nmouse-1: LSP server control menu" nick) | ||
| 1859 | 'keymap (let ((map (make-sparse-keymap))) | ||
| 1860 | (define-key map [mode-line down-mouse-1] eglot-server-menu) | ||
| 1861 | map)) | ||
| 1862 | ,@(when last-error | ||
| 1863 | `("/" ,(eglot--mode-line-props | ||
| 1864 | "error" 'compilation-mode-line-fail | ||
| 1865 | '((mouse-3 eglot-clear-status "Clear this status")) | ||
| 1866 | (format "An error occurred: %s\n" (plist-get last-error | ||
| 1867 | :message))))) | ||
| 1868 | ,@(when (and doing (not done-p)) | ||
| 1869 | `("/" ,(eglot--mode-line-props doing | ||
| 1870 | 'compilation-mode-line-run '()))) | ||
| 1871 | ,@(when (cl-plusp pending) | ||
| 1872 | `("/" ,(eglot--mode-line-props | ||
| 1873 | (format "%d" pending) 'warning | ||
| 1874 | '((mouse-3 eglot-forget-pending-continuations | ||
| 1875 | "Forget pending continuations")) | ||
| 1876 | "Number of outgoing, \ | ||
| 1877 | still unanswered LSP requests to the server\n")))))))) | ||
| 1878 | |||
| 1879 | (add-to-list 'mode-line-misc-info | ||
| 1880 | `(eglot--managed-mode (" [" eglot--mode-line-format "] "))) | ||
| 1881 | |||
| 1882 | |||
| 1883 | ;;; Flymake customization | ||
| 1884 | ;;; | ||
| 1885 | (put 'eglot-note 'flymake-category 'flymake-note) | ||
| 1886 | (put 'eglot-warning 'flymake-category 'flymake-warning) | ||
| 1887 | (put 'eglot-error 'flymake-category 'flymake-error) | ||
| 1888 | |||
| 1889 | (defalias 'eglot--make-diag 'flymake-make-diagnostic) | ||
| 1890 | (defalias 'eglot--diag-data 'flymake-diagnostic-data) | ||
| 1891 | |||
| 1892 | (cl-loop for i from 1 | ||
| 1893 | for type in '(eglot-note eglot-warning eglot-error ) | ||
| 1894 | do (put type 'flymake-overlay-control | ||
| 1895 | `((mouse-face . highlight) | ||
| 1896 | (priority . ,(+ 50 i)) | ||
| 1897 | (keymap . ,(let ((map (make-sparse-keymap))) | ||
| 1898 | (define-key map [mouse-1] | ||
| 1899 | (eglot--mouse-call 'eglot-code-actions)) | ||
| 1900 | map))))) | ||
| 1901 | |||
| 1902 | |||
| 1903 | ;;; Protocol implementation (Requests, notifications, etc) | ||
| 1904 | ;;; | ||
| 1905 | (cl-defmethod eglot-handle-notification | ||
| 1906 | (_server method &key &allow-other-keys) | ||
| 1907 | "Handle unknown notification." | ||
| 1908 | (unless (or (string-prefix-p "$" (format "%s" method)) | ||
| 1909 | (not (memq 'disallow-unknown-methods eglot-strict-mode))) | ||
| 1910 | (eglot--warn "Server sent unknown notification method `%s'" method))) | ||
| 1911 | |||
| 1912 | (cl-defmethod eglot-handle-request | ||
| 1913 | (_server method &key &allow-other-keys) | ||
| 1914 | "Handle unknown request." | ||
| 1915 | (when (memq 'disallow-unknown-methods eglot-strict-mode) | ||
| 1916 | (jsonrpc-error "Unknown request method `%s'" method))) | ||
| 1917 | |||
| 1918 | (cl-defmethod eglot-execute-command | ||
| 1919 | (server command arguments) | ||
| 1920 | "Execute COMMAND on SERVER with `:workspace/executeCommand'. | ||
| 1921 | COMMAND is a symbol naming the command." | ||
| 1922 | (jsonrpc-request server :workspace/executeCommand | ||
| 1923 | `(:command ,(format "%s" command) :arguments ,arguments))) | ||
| 1924 | |||
| 1925 | (cl-defmethod eglot-handle-notification | ||
| 1926 | (_server (_method (eql window/showMessage)) &key type message) | ||
| 1927 | "Handle notification window/showMessage." | ||
| 1928 | (eglot--message (propertize "Server reports (type=%s): %s" | ||
| 1929 | 'face (if (<= type 1) 'error)) | ||
| 1930 | type message)) | ||
| 1931 | |||
| 1932 | (cl-defmethod eglot-handle-request | ||
| 1933 | (_server (_method (eql window/showMessageRequest)) &key type message actions) | ||
| 1934 | "Handle server request window/showMessageRequest." | ||
| 1935 | (let* ((actions (append actions nil)) ;; gh#627 | ||
| 1936 | (label (completing-read | ||
| 1937 | (concat | ||
| 1938 | (format (propertize "[eglot] Server reports (type=%s): %s" | ||
| 1939 | 'face (if (<= type 1) 'error)) | ||
| 1940 | type message) | ||
| 1941 | "\nChoose an option: ") | ||
| 1942 | (or (mapcar (lambda (obj) (plist-get obj :title)) actions) | ||
| 1943 | '("OK")) | ||
| 1944 | nil t (plist-get (elt actions 0) :title)))) | ||
| 1945 | (if label `(:title ,label) :null))) | ||
| 1946 | |||
| 1947 | (cl-defmethod eglot-handle-notification | ||
| 1948 | (_server (_method (eql window/logMessage)) &key _type _message) | ||
| 1949 | "Handle notification window/logMessage.") ;; noop, use events buffer | ||
| 1950 | |||
| 1951 | (cl-defmethod eglot-handle-notification | ||
| 1952 | (_server (_method (eql telemetry/event)) &rest _any) | ||
| 1953 | "Handle notification telemetry/event.") ;; noop, use events buffer | ||
| 1954 | |||
| 1955 | (cl-defmethod eglot-handle-notification | ||
| 1956 | (_server (_method (eql textDocument/publishDiagnostics)) &key uri diagnostics | ||
| 1957 | &allow-other-keys) ; FIXME: doesn't respect `eglot-strict-mode' | ||
| 1958 | "Handle notification publishDiagnostics." | ||
| 1959 | (cl-flet ((eglot--diag-type (sev) | ||
| 1960 | (cond ((null sev) 'eglot-error) | ||
| 1961 | ((<= sev 1) 'eglot-error) | ||
| 1962 | ((= sev 2) 'eglot-warning) | ||
| 1963 | (t 'eglot-note))) | ||
| 1964 | (mess (source code message) | ||
| 1965 | (concat source (and code (format " [%s]" code)) ": " message))) | ||
| 1966 | (if-let ((buffer (find-buffer-visiting (eglot--uri-to-path uri)))) | ||
| 1967 | (with-current-buffer buffer | ||
| 1968 | (cl-loop | ||
| 1969 | for diag-spec across diagnostics | ||
| 1970 | collect (eglot--dbind ((Diagnostic) range code message severity source tags) | ||
| 1971 | diag-spec | ||
| 1972 | (setq message (mess source code message)) | ||
| 1973 | (pcase-let | ||
| 1974 | ((`(,beg . ,end) (eglot--range-region range))) | ||
| 1975 | ;; Fallback to `flymake-diag-region' if server | ||
| 1976 | ;; botched the range | ||
| 1977 | (when (= beg end) | ||
| 1978 | (if-let* ((st (plist-get range :start)) | ||
| 1979 | (diag-region | ||
| 1980 | (flymake-diag-region | ||
| 1981 | (current-buffer) (1+ (plist-get st :line)) | ||
| 1982 | (plist-get st :character)))) | ||
| 1983 | (setq beg (car diag-region) end (cdr diag-region)) | ||
| 1984 | (eglot--widening | ||
| 1985 | (goto-char (point-min)) | ||
| 1986 | (setq beg | ||
| 1987 | (point-at-bol | ||
| 1988 | (1+ (plist-get (plist-get range :start) :line)))) | ||
| 1989 | (setq end | ||
| 1990 | (point-at-eol | ||
| 1991 | (1+ (plist-get (plist-get range :end) :line))))))) | ||
| 1992 | (eglot--make-diag | ||
| 1993 | (current-buffer) beg end | ||
| 1994 | (eglot--diag-type severity) | ||
| 1995 | message `((eglot-lsp-diag . ,diag-spec)) | ||
| 1996 | (when-let ((faces | ||
| 1997 | (cl-loop for tag across tags | ||
| 1998 | when (alist-get tag eglot--tag-faces) | ||
| 1999 | collect it))) | ||
| 2000 | `((face . ,faces)))))) | ||
| 2001 | into diags | ||
| 2002 | finally (cond ((and | ||
| 2003 | ;; only add to current report if Flymake | ||
| 2004 | ;; starts on idle-timer (github#958) | ||
| 2005 | (not (null flymake-no-changes-timeout)) | ||
| 2006 | eglot--current-flymake-report-fn) | ||
| 2007 | (eglot--report-to-flymake diags)) | ||
| 2008 | (t | ||
| 2009 | (setq eglot--diagnostics diags))))) | ||
| 2010 | (cl-loop | ||
| 2011 | with path = (expand-file-name (eglot--uri-to-path uri)) | ||
| 2012 | for diag-spec across diagnostics | ||
| 2013 | collect (eglot--dbind ((Diagnostic) code range message severity source) diag-spec | ||
| 2014 | (setq message (mess source code message)) | ||
| 2015 | (let* ((start (plist-get range :start)) | ||
| 2016 | (line (1+ (plist-get start :line))) | ||
| 2017 | (char (1+ (plist-get start :character)))) | ||
| 2018 | (eglot--make-diag | ||
| 2019 | path (cons line char) nil (eglot--diag-type severity) message))) | ||
| 2020 | into diags | ||
| 2021 | finally | ||
| 2022 | (setq flymake-list-only-diagnostics | ||
| 2023 | (assoc-delete-all path flymake-list-only-diagnostics #'string=)) | ||
| 2024 | (push (cons path diags) flymake-list-only-diagnostics))))) | ||
| 2025 | |||
| 2026 | (cl-defun eglot--register-unregister (server things how) | ||
| 2027 | "Helper for `registerCapability'. | ||
| 2028 | THINGS are either registrations or unregisterations (sic)." | ||
| 2029 | (cl-loop | ||
| 2030 | for thing in (cl-coerce things 'list) | ||
| 2031 | do (eglot--dbind ((Registration) id method registerOptions) thing | ||
| 2032 | (apply (cl-ecase how | ||
| 2033 | (register 'eglot-register-capability) | ||
| 2034 | (unregister 'eglot-unregister-capability)) | ||
| 2035 | server (intern method) id registerOptions)))) | ||
| 2036 | |||
| 2037 | (cl-defmethod eglot-handle-request | ||
| 2038 | (server (_method (eql client/registerCapability)) &key registrations) | ||
| 2039 | "Handle server request client/registerCapability." | ||
| 2040 | (eglot--register-unregister server registrations 'register)) | ||
| 2041 | |||
| 2042 | (cl-defmethod eglot-handle-request | ||
| 2043 | (server (_method (eql client/unregisterCapability)) | ||
| 2044 | &key unregisterations) ;; XXX: "unregisterations" (sic) | ||
| 2045 | "Handle server request client/unregisterCapability." | ||
| 2046 | (eglot--register-unregister server unregisterations 'unregister)) | ||
| 2047 | |||
| 2048 | (cl-defmethod eglot-handle-request | ||
| 2049 | (_server (_method (eql workspace/applyEdit)) &key _label edit) | ||
| 2050 | "Handle server request workspace/applyEdit." | ||
| 2051 | (eglot--apply-workspace-edit edit eglot-confirm-server-initiated-edits) | ||
| 2052 | `(:applied t)) | ||
| 2053 | |||
| 2054 | (cl-defmethod eglot-handle-request | ||
| 2055 | (server (_method (eql workspace/workspaceFolders))) | ||
| 2056 | "Handle server request workspace/workspaceFolders." | ||
| 2057 | (eglot-workspace-folders server)) | ||
| 2058 | |||
| 2059 | (defun eglot--TextDocumentIdentifier () | ||
| 2060 | "Compute TextDocumentIdentifier object for current buffer." | ||
| 2061 | `(:uri ,(eglot--path-to-uri (or buffer-file-name | ||
| 2062 | (ignore-errors | ||
| 2063 | (buffer-file-name | ||
| 2064 | (buffer-base-buffer))))))) | ||
| 2065 | |||
| 2066 | (defvar-local eglot--versioned-identifier 0) | ||
| 2067 | |||
| 2068 | (defun eglot--VersionedTextDocumentIdentifier () | ||
| 2069 | "Compute VersionedTextDocumentIdentifier object for current buffer." | ||
| 2070 | (append (eglot--TextDocumentIdentifier) | ||
| 2071 | `(:version ,eglot--versioned-identifier))) | ||
| 2072 | |||
| 2073 | (defun eglot--TextDocumentItem () | ||
| 2074 | "Compute TextDocumentItem object for current buffer." | ||
| 2075 | (append | ||
| 2076 | (eglot--VersionedTextDocumentIdentifier) | ||
| 2077 | (list :languageId | ||
| 2078 | (eglot--language-id (eglot--current-server-or-lose)) | ||
| 2079 | :text | ||
| 2080 | (eglot--widening | ||
| 2081 | (buffer-substring-no-properties (point-min) (point-max)))))) | ||
| 2082 | |||
| 2083 | (defun eglot--TextDocumentPositionParams () | ||
| 2084 | "Compute TextDocumentPositionParams." | ||
| 2085 | (list :textDocument (eglot--TextDocumentIdentifier) | ||
| 2086 | :position (eglot--pos-to-lsp-position))) | ||
| 2087 | |||
| 2088 | (defvar-local eglot--last-inserted-char nil | ||
| 2089 | "If non-nil, value of the last inserted character in buffer.") | ||
| 2090 | |||
| 2091 | (defun eglot--post-self-insert-hook () | ||
| 2092 | "Set `eglot--last-inserted-char', maybe call on-type-formatting." | ||
| 2093 | (setq eglot--last-inserted-char last-input-event) | ||
| 2094 | (let ((ot-provider (eglot--server-capable :documentOnTypeFormattingProvider))) | ||
| 2095 | (when (and ot-provider | ||
| 2096 | (ignore-errors ; github#906, some LS's send empty strings | ||
| 2097 | (or (eq last-input-event | ||
| 2098 | (seq-first (plist-get ot-provider :firstTriggerCharacter))) | ||
| 2099 | (cl-find last-input-event | ||
| 2100 | (plist-get ot-provider :moreTriggerCharacter) | ||
| 2101 | :key #'seq-first)))) | ||
| 2102 | (eglot-format (point) nil last-input-event)))) | ||
| 2103 | |||
| 2104 | (defvar eglot--workspace-symbols-cache (make-hash-table :test #'equal) | ||
| 2105 | "Cache of `workspace/Symbol' results used by `xref-find-definitions'.") | ||
| 2106 | |||
| 2107 | (defun eglot--pre-command-hook () | ||
| 2108 | "Reset some temporary variables." | ||
| 2109 | (clrhash eglot--workspace-symbols-cache) | ||
| 2110 | (setq eglot--last-inserted-char nil)) | ||
| 2111 | |||
| 2112 | (defun eglot--CompletionParams () | ||
| 2113 | (append | ||
| 2114 | (eglot--TextDocumentPositionParams) | ||
| 2115 | `(:context | ||
| 2116 | ,(if-let (trigger (and (characterp eglot--last-inserted-char) | ||
| 2117 | (cl-find eglot--last-inserted-char | ||
| 2118 | (eglot--server-capable :completionProvider | ||
| 2119 | :triggerCharacters) | ||
| 2120 | :key (lambda (str) (aref str 0)) | ||
| 2121 | :test #'char-equal))) | ||
| 2122 | `(:triggerKind 2 :triggerCharacter ,trigger) `(:triggerKind 1))))) | ||
| 2123 | |||
| 2124 | (defvar-local eglot--recent-changes nil | ||
| 2125 | "Recent buffer changes as collected by `eglot--before-change'.") | ||
| 2126 | |||
| 2127 | (cl-defmethod jsonrpc-connection-ready-p ((_server eglot-lsp-server) _what) | ||
| 2128 | "Tell if SERVER is ready for WHAT in current buffer." | ||
| 2129 | (and (cl-call-next-method) (not eglot--recent-changes))) | ||
| 2130 | |||
| 2131 | (defvar-local eglot--change-idle-timer nil "Idle timer for didChange signals.") | ||
| 2132 | |||
| 2133 | (defun eglot--before-change (beg end) | ||
| 2134 | "Hook onto `before-change-functions' with BEG and END." | ||
| 2135 | (when (listp eglot--recent-changes) | ||
| 2136 | ;; Records BEG and END, crucially convert them into LSP | ||
| 2137 | ;; (line/char) positions before that information is lost (because | ||
| 2138 | ;; the after-change thingy doesn't know if newlines were | ||
| 2139 | ;; deleted/added). Also record markers of BEG and END | ||
| 2140 | ;; (github#259) | ||
| 2141 | (push `(,(eglot--pos-to-lsp-position beg) | ||
| 2142 | ,(eglot--pos-to-lsp-position end) | ||
| 2143 | (,beg . ,(copy-marker beg nil)) | ||
| 2144 | (,end . ,(copy-marker end t))) | ||
| 2145 | eglot--recent-changes))) | ||
| 2146 | |||
| 2147 | (defun eglot--after-change (beg end pre-change-length) | ||
| 2148 | "Hook onto `after-change-functions'. | ||
| 2149 | Records BEG, END and PRE-CHANGE-LENGTH locally." | ||
| 2150 | (cl-incf eglot--versioned-identifier) | ||
| 2151 | (pcase (and (listp eglot--recent-changes) | ||
| 2152 | (car eglot--recent-changes)) | ||
| 2153 | (`(,lsp-beg ,lsp-end | ||
| 2154 | (,b-beg . ,b-beg-marker) | ||
| 2155 | (,b-end . ,b-end-marker)) | ||
| 2156 | ;; github#259 and github#367: With `capitalize-word' or somesuch, | ||
| 2157 | ;; `before-change-functions' always records the whole word's | ||
| 2158 | ;; `b-beg' and `b-end'. Similarly, when coalescing two lines | ||
| 2159 | ;; into one, `fill-paragraph' they mark the end of the first line | ||
| 2160 | ;; up to the end of the second line. In both situations, args | ||
| 2161 | ;; received here contradict that information: `beg' and `end' | ||
| 2162 | ;; will differ by 1 and will likely only encompass the letter | ||
| 2163 | ;; that was capitalized or, in the sentence-joining situation, | ||
| 2164 | ;; the replacement of the newline with a space. That's we keep | ||
| 2165 | ;; markers _and_ positions so we're able to detect and correct | ||
| 2166 | ;; this. We ignore `beg', `len' and `pre-change-len' and send | ||
| 2167 | ;; "fuller" information about the region from the markers. I've | ||
| 2168 | ;; also experimented with doing this unconditionally but it seems | ||
| 2169 | ;; to break when newlines are added. | ||
| 2170 | (if (and (= b-end b-end-marker) (= b-beg b-beg-marker) | ||
| 2171 | (or (/= beg b-beg) (/= end b-end))) | ||
| 2172 | (setcar eglot--recent-changes | ||
| 2173 | `(,lsp-beg ,lsp-end ,(- b-end-marker b-beg-marker) | ||
| 2174 | ,(buffer-substring-no-properties b-beg-marker | ||
| 2175 | b-end-marker))) | ||
| 2176 | (setcar eglot--recent-changes | ||
| 2177 | `(,lsp-beg ,lsp-end ,pre-change-length | ||
| 2178 | ,(buffer-substring-no-properties beg end))))) | ||
| 2179 | (_ (setf eglot--recent-changes :emacs-messup))) | ||
| 2180 | (when eglot--change-idle-timer (cancel-timer eglot--change-idle-timer)) | ||
| 2181 | (let ((buf (current-buffer))) | ||
| 2182 | (setq eglot--change-idle-timer | ||
| 2183 | (run-with-idle-timer | ||
| 2184 | eglot-send-changes-idle-time | ||
| 2185 | nil (lambda () (eglot--when-live-buffer buf | ||
| 2186 | (when eglot--managed-mode | ||
| 2187 | (eglot--signal-textDocument/didChange) | ||
| 2188 | (setq eglot--change-idle-timer nil)))))))) | ||
| 2189 | |||
| 2190 | ;; HACK! Launching a deferred sync request with outstanding changes is a | ||
| 2191 | ;; bad idea, since that might lead to the request never having a | ||
| 2192 | ;; chance to run, because `jsonrpc-connection-ready-p'. | ||
| 2193 | (advice-add #'jsonrpc-request :before | ||
| 2194 | (cl-function (lambda (_proc _method _params &key | ||
| 2195 | deferred &allow-other-keys) | ||
| 2196 | (when (and eglot--managed-mode deferred) | ||
| 2197 | (eglot--signal-textDocument/didChange)))) | ||
| 2198 | '((name . eglot--signal-textDocument/didChange))) | ||
| 2199 | |||
| 2200 | (defvar-local eglot-workspace-configuration () | ||
| 2201 | "Configure LSP servers specifically for a given project. | ||
| 2202 | |||
| 2203 | This variable's value should be a plist (SECTION VALUE ...). | ||
| 2204 | SECTION is a keyword naming a parameter section relevant to a | ||
| 2205 | particular server. VALUE is a plist or a primitive type | ||
| 2206 | converted to JSON also understood by that server. | ||
| 2207 | |||
| 2208 | Instead of a plist, an alist ((SECTION . VALUE) ...) can be used | ||
| 2209 | instead, but this variant is less reliable and not recommended. | ||
| 2210 | |||
| 2211 | This variable should be set as a directory-local variable. See | ||
| 2212 | See info node `(emacs)Directory Variables' for various ways to to | ||
| 2213 | that. | ||
| 2214 | |||
| 2215 | Here's an example value that establishes two sections relevant to | ||
| 2216 | the Pylsp and Gopls LSP servers: | ||
| 2217 | |||
| 2218 | (:pylsp (:plugins (:jedi_completion (:include_params t | ||
| 2219 | :fuzzy t) | ||
| 2220 | :pylint (:enabled :json-false))) | ||
| 2221 | :gopls (:usePlaceholders t)) | ||
| 2222 | |||
| 2223 | The value of this variable can also be a unary function of a | ||
| 2224 | single argument, which will be a connected `eglot-lsp-server' | ||
| 2225 | instance. The function runs with `default-directory' set to the | ||
| 2226 | root of the current project. It should return an object of the | ||
| 2227 | format described above.") | ||
| 2228 | |||
| 2229 | ;;;###autoload | ||
| 2230 | (put 'eglot-workspace-configuration 'safe-local-variable 'listp) | ||
| 2231 | |||
| 2232 | (defun eglot-show-workspace-configuration (&optional server) | ||
| 2233 | "Dump `eglot-workspace-configuration' as JSON for debugging." | ||
| 2234 | (interactive (list (and (eglot-current-server) | ||
| 2235 | (eglot--read-server "Server configuration" | ||
| 2236 | (eglot-current-server))))) | ||
| 2237 | (let ((conf (eglot--workspace-configuration-plist server))) | ||
| 2238 | (with-current-buffer (get-buffer-create "*EGLOT workspace configuration*") | ||
| 2239 | (erase-buffer) | ||
| 2240 | (insert (jsonrpc--json-encode conf)) | ||
| 2241 | (with-no-warnings | ||
| 2242 | (require 'json) | ||
| 2243 | (when (require 'json-mode nil t) (json-mode)) | ||
| 2244 | (json-pretty-print-buffer)) | ||
| 2245 | (pop-to-buffer (current-buffer))))) | ||
| 2246 | |||
| 2247 | (defun eglot--workspace-configuration (server) | ||
| 2248 | (if (functionp eglot-workspace-configuration) | ||
| 2249 | (funcall eglot-workspace-configuration server) | ||
| 2250 | eglot-workspace-configuration)) | ||
| 2251 | |||
| 2252 | (defun eglot--workspace-configuration-plist (server) | ||
| 2253 | "Returns `eglot-workspace-configuration' suitable for serialization." | ||
| 2254 | (let ((val (eglot--workspace-configuration server))) | ||
| 2255 | (or (and (consp (car val)) | ||
| 2256 | (cl-loop for (section . v) in val | ||
| 2257 | collect (if (keywordp section) section | ||
| 2258 | (intern (format ":%s" section))) | ||
| 2259 | collect v)) | ||
| 2260 | val))) | ||
| 2261 | |||
| 2262 | (defun eglot-signal-didChangeConfiguration (server) | ||
| 2263 | "Send a `:workspace/didChangeConfiguration' signal to SERVER. | ||
| 2264 | When called interactively, use the currently active server" | ||
| 2265 | (interactive (list (eglot--current-server-or-lose))) | ||
| 2266 | (jsonrpc-notify | ||
| 2267 | server :workspace/didChangeConfiguration | ||
| 2268 | (list | ||
| 2269 | :settings | ||
| 2270 | (or (eglot--workspace-configuration-plist server) | ||
| 2271 | eglot--{})))) | ||
| 2272 | |||
| 2273 | (cl-defmethod eglot-handle-request | ||
| 2274 | (server (_method (eql workspace/configuration)) &key items) | ||
| 2275 | "Handle server request workspace/configuration." | ||
| 2276 | (apply #'vector | ||
| 2277 | (mapcar | ||
| 2278 | (eglot--lambda ((ConfigurationItem) scopeUri section) | ||
| 2279 | (with-temp-buffer | ||
| 2280 | (let* ((uri-path (eglot--uri-to-path scopeUri)) | ||
| 2281 | (default-directory | ||
| 2282 | (if (and (not (string-empty-p uri-path)) | ||
| 2283 | (file-directory-p uri-path)) | ||
| 2284 | (file-name-as-directory uri-path) | ||
| 2285 | (project-root (eglot--project server))))) | ||
| 2286 | (setq-local major-mode (eglot--major-mode server)) | ||
| 2287 | (hack-dir-local-variables-non-file-buffer) | ||
| 2288 | (cl-loop for (wsection o) | ||
| 2289 | on (eglot--workspace-configuration-plist server) | ||
| 2290 | by #'cddr | ||
| 2291 | when (string= | ||
| 2292 | (if (keywordp wsection) | ||
| 2293 | (substring (symbol-name wsection) 1) | ||
| 2294 | wsection) | ||
| 2295 | section) | ||
| 2296 | return o)))) | ||
| 2297 | items))) | ||
| 2298 | |||
| 2299 | (defun eglot--signal-textDocument/didChange () | ||
| 2300 | "Send textDocument/didChange to server." | ||
| 2301 | (when eglot--recent-changes | ||
| 2302 | (let* ((server (eglot--current-server-or-lose)) | ||
| 2303 | (sync-capability (eglot--server-capable :textDocumentSync)) | ||
| 2304 | (sync-kind (if (numberp sync-capability) sync-capability | ||
| 2305 | (plist-get sync-capability :change))) | ||
| 2306 | (full-sync-p (or (eq sync-kind 1) | ||
| 2307 | (eq :emacs-messup eglot--recent-changes)))) | ||
| 2308 | (jsonrpc-notify | ||
| 2309 | server :textDocument/didChange | ||
| 2310 | (list | ||
| 2311 | :textDocument (eglot--VersionedTextDocumentIdentifier) | ||
| 2312 | :contentChanges | ||
| 2313 | (if full-sync-p | ||
| 2314 | (vector `(:text ,(eglot--widening | ||
| 2315 | (buffer-substring-no-properties (point-min) | ||
| 2316 | (point-max))))) | ||
| 2317 | (cl-loop for (beg end len text) in (reverse eglot--recent-changes) | ||
| 2318 | ;; github#259: `capitalize-word' and commands based | ||
| 2319 | ;; on `casify_region' will cause multiple duplicate | ||
| 2320 | ;; empty entries in `eglot--before-change' calls | ||
| 2321 | ;; without an `eglot--after-change' reciprocal. | ||
| 2322 | ;; Weed them out here. | ||
| 2323 | when (numberp len) | ||
| 2324 | vconcat `[,(list :range `(:start ,beg :end ,end) | ||
| 2325 | :rangeLength len :text text)])))) | ||
| 2326 | (setq eglot--recent-changes nil) | ||
| 2327 | (setf (eglot--spinner server) (list nil :textDocument/didChange t)) | ||
| 2328 | (jsonrpc--call-deferred server)))) | ||
| 2329 | |||
| 2330 | (defun eglot--signal-textDocument/didOpen () | ||
| 2331 | "Send textDocument/didOpen to server." | ||
| 2332 | (setq eglot--recent-changes nil eglot--versioned-identifier 0) | ||
| 2333 | (jsonrpc-notify | ||
| 2334 | (eglot--current-server-or-lose) | ||
| 2335 | :textDocument/didOpen `(:textDocument ,(eglot--TextDocumentItem)))) | ||
| 2336 | |||
| 2337 | (defun eglot--signal-textDocument/didClose () | ||
| 2338 | "Send textDocument/didClose to server." | ||
| 2339 | (with-demoted-errors | ||
| 2340 | "[eglot] error sending textDocument/didClose: %s" | ||
| 2341 | (jsonrpc-notify | ||
| 2342 | (eglot--current-server-or-lose) | ||
| 2343 | :textDocument/didClose `(:textDocument ,(eglot--TextDocumentIdentifier))))) | ||
| 2344 | |||
| 2345 | (defun eglot--signal-textDocument/willSave () | ||
| 2346 | "Send textDocument/willSave to server." | ||
| 2347 | (let ((server (eglot--current-server-or-lose)) | ||
| 2348 | (params `(:reason 1 :textDocument ,(eglot--TextDocumentIdentifier)))) | ||
| 2349 | (when (eglot--server-capable :textDocumentSync :willSave) | ||
| 2350 | (jsonrpc-notify server :textDocument/willSave params)) | ||
| 2351 | (when (eglot--server-capable :textDocumentSync :willSaveWaitUntil) | ||
| 2352 | (ignore-errors | ||
| 2353 | (eglot--apply-text-edits | ||
| 2354 | (jsonrpc-request server :textDocument/willSaveWaitUntil params | ||
| 2355 | :timeout 0.5)))))) | ||
| 2356 | |||
| 2357 | (defun eglot--signal-textDocument/didSave () | ||
| 2358 | "Send textDocument/didSave to server." | ||
| 2359 | (eglot--signal-textDocument/didChange) | ||
| 2360 | (jsonrpc-notify | ||
| 2361 | (eglot--current-server-or-lose) | ||
| 2362 | :textDocument/didSave | ||
| 2363 | (list | ||
| 2364 | ;; TODO: Handle TextDocumentSaveRegistrationOptions to control this. | ||
| 2365 | :text (buffer-substring-no-properties (point-min) (point-max)) | ||
| 2366 | :textDocument (eglot--TextDocumentIdentifier)))) | ||
| 2367 | |||
| 2368 | (defun eglot-flymake-backend (report-fn &rest _more) | ||
| 2369 | "A Flymake backend for Eglot. | ||
| 2370 | Calls REPORT-FN (or arranges for it to be called) when the server | ||
| 2371 | publishes diagnostics. Between calls to this function, REPORT-FN | ||
| 2372 | may be called multiple times (respecting the protocol of | ||
| 2373 | `flymake-backend-functions')." | ||
| 2374 | (cond (eglot--managed-mode | ||
| 2375 | (setq eglot--current-flymake-report-fn report-fn) | ||
| 2376 | (eglot--report-to-flymake eglot--diagnostics)) | ||
| 2377 | (t | ||
| 2378 | (funcall report-fn nil)))) | ||
| 2379 | |||
| 2380 | (defun eglot--report-to-flymake (diags) | ||
| 2381 | "Internal helper for `eglot-flymake-backend'." | ||
| 2382 | (save-restriction | ||
| 2383 | (widen) | ||
| 2384 | (funcall eglot--current-flymake-report-fn diags | ||
| 2385 | ;; If the buffer hasn't changed since last | ||
| 2386 | ;; call to the report function, flymake won't | ||
| 2387 | ;; delete old diagnostics. Using :region | ||
| 2388 | ;; keyword forces flymake to delete | ||
| 2389 | ;; them (github#159). | ||
| 2390 | :region (cons (point-min) (point-max)))) | ||
| 2391 | (setq eglot--diagnostics diags)) | ||
| 2392 | |||
| 2393 | (defun eglot-xref-backend () "EGLOT xref backend." 'eglot) | ||
| 2394 | |||
| 2395 | (defvar eglot--temp-location-buffers (make-hash-table :test #'equal) | ||
| 2396 | "Helper variable for `eglot--handling-xrefs'.") | ||
| 2397 | |||
| 2398 | (defvar eglot-xref-lessp-function #'ignore | ||
| 2399 | "Compare two `xref-item' objects for sorting.") | ||
| 2400 | |||
| 2401 | (cl-defmacro eglot--collecting-xrefs ((collector) &rest body) | ||
| 2402 | "Sort and handle xrefs collected with COLLECTOR in BODY." | ||
| 2403 | (declare (indent 1) (debug (sexp &rest form))) | ||
| 2404 | (let ((collected (cl-gensym "collected"))) | ||
| 2405 | `(unwind-protect | ||
| 2406 | (let (,collected) | ||
| 2407 | (cl-flet ((,collector (xref) (push xref ,collected))) | ||
| 2408 | ,@body) | ||
| 2409 | (setq ,collected (nreverse ,collected)) | ||
| 2410 | (sort ,collected eglot-xref-lessp-function)) | ||
| 2411 | (maphash (lambda (_uri buf) (kill-buffer buf)) eglot--temp-location-buffers) | ||
| 2412 | (clrhash eglot--temp-location-buffers)))) | ||
| 2413 | |||
| 2414 | (defun eglot--xref-make-match (name uri range) | ||
| 2415 | "Like `xref-make-match' but with LSP's NAME, URI and RANGE. | ||
| 2416 | Try to visit the target file for a richer summary line." | ||
| 2417 | (pcase-let* | ||
| 2418 | ((file (eglot--uri-to-path uri)) | ||
| 2419 | (visiting (or (find-buffer-visiting file) | ||
| 2420 | (gethash uri eglot--temp-location-buffers))) | ||
| 2421 | (collect (lambda () | ||
| 2422 | (eglot--widening | ||
| 2423 | (pcase-let* ((`(,beg . ,end) (eglot--range-region range)) | ||
| 2424 | (bol (progn (goto-char beg) (point-at-bol))) | ||
| 2425 | (substring (buffer-substring bol (point-at-eol))) | ||
| 2426 | (hi-beg (- beg bol)) | ||
| 2427 | (hi-end (- (min (point-at-eol) end) bol))) | ||
| 2428 | (add-face-text-property hi-beg hi-end 'xref-match | ||
| 2429 | t substring) | ||
| 2430 | (list substring (1+ (current-line)) (eglot-current-column) | ||
| 2431 | (- end beg)))))) | ||
| 2432 | (`(,summary ,line ,column ,length) | ||
| 2433 | (cond | ||
| 2434 | (visiting (with-current-buffer visiting (funcall collect))) | ||
| 2435 | ((file-readable-p file) (with-current-buffer | ||
| 2436 | (puthash uri (generate-new-buffer " *temp*") | ||
| 2437 | eglot--temp-location-buffers) | ||
| 2438 | (insert-file-contents file) | ||
| 2439 | (funcall collect))) | ||
| 2440 | (t ;; fall back to the "dumb strategy" | ||
| 2441 | (let* ((start (cl-getf range :start)) | ||
| 2442 | (line (1+ (cl-getf start :line))) | ||
| 2443 | (start-pos (cl-getf start :character)) | ||
| 2444 | (end-pos (cl-getf (cl-getf range :end) :character))) | ||
| 2445 | (list name line start-pos (- end-pos start-pos))))))) | ||
| 2446 | (setf (gethash (expand-file-name file) eglot--servers-by-xrefed-file) | ||
| 2447 | (eglot--current-server-or-lose)) | ||
| 2448 | (xref-make-match summary (xref-make-file-location file line column) length))) | ||
| 2449 | |||
| 2450 | (defun eglot--workspace-symbols (pat &optional buffer) | ||
| 2451 | "Ask for :workspace/symbol on PAT, return list of formatted strings. | ||
| 2452 | If BUFFER, switch to it before." | ||
| 2453 | (with-current-buffer (or buffer (current-buffer)) | ||
| 2454 | (unless (eglot--server-capable :workspaceSymbolProvider) | ||
| 2455 | (eglot--error "This LSP server isn't a :workspaceSymbolProvider")) | ||
| 2456 | (mapcar | ||
| 2457 | (lambda (wss) | ||
| 2458 | (eglot--dbind ((WorkspaceSymbol) name containerName kind) wss | ||
| 2459 | (propertize | ||
| 2460 | (format "%s%s %s" | ||
| 2461 | (if (zerop (length containerName)) "" | ||
| 2462 | (concat (propertize containerName 'face 'shadow) " ")) | ||
| 2463 | name | ||
| 2464 | (propertize (alist-get kind eglot--symbol-kind-names "Unknown") | ||
| 2465 | 'face 'shadow)) | ||
| 2466 | 'eglot--lsp-workspaceSymbol wss))) | ||
| 2467 | (jsonrpc-request (eglot--current-server-or-lose) :workspace/symbol | ||
| 2468 | `(:query ,pat))))) | ||
| 2469 | |||
| 2470 | (cl-defmethod xref-backend-identifier-completion-table ((_backend (eql eglot))) | ||
| 2471 | "Yet another tricky connection between LSP and Elisp completion semantics." | ||
| 2472 | (let ((buf (current-buffer)) (cache eglot--workspace-symbols-cache)) | ||
| 2473 | (cl-labels ((refresh (pat) (eglot--workspace-symbols pat buf)) | ||
| 2474 | (lookup-1 (pat) ;; check cache, else refresh | ||
| 2475 | (let ((probe (gethash pat cache :missing))) | ||
| 2476 | (if (eq probe :missing) (puthash pat (refresh pat) cache) | ||
| 2477 | probe))) | ||
| 2478 | (lookup (pat) | ||
| 2479 | (let ((res (lookup-1 pat)) | ||
| 2480 | (def (and (string= pat "") (gethash :default cache)))) | ||
| 2481 | (append def res nil))) | ||
| 2482 | (score (c) | ||
| 2483 | (cl-getf (get-text-property | ||
| 2484 | 0 'eglot--lsp-workspaceSymbol c) | ||
| 2485 | :score 0))) | ||
| 2486 | (lambda (string _pred action) | ||
| 2487 | (pcase action | ||
| 2488 | (`metadata `(metadata | ||
| 2489 | (cycle-sort-function | ||
| 2490 | . ,(lambda (completions) | ||
| 2491 | (cl-sort completions #'> :key #'score))) | ||
| 2492 | (category . eglot-indirection-joy))) | ||
| 2493 | (`(eglot--lsp-tryc . ,point) `(eglot--lsp-tryc . (,string . ,point))) | ||
| 2494 | (`(eglot--lsp-allc . ,_point) `(eglot--lsp-allc . ,(lookup string))) | ||
| 2495 | (_ nil)))))) | ||
| 2496 | |||
| 2497 | (defun eglot--recover-workspace-symbol-meta (string) | ||
| 2498 | "Search `eglot--workspace-symbols-cache' for rich entry of STRING." | ||
| 2499 | (catch 'found | ||
| 2500 | (maphash (lambda (_k v) | ||
| 2501 | (while (consp v) | ||
| 2502 | ;; Like mess? Ask minibuffer.el about improper lists. | ||
| 2503 | (when (equal (car v) string) (throw 'found (car v))) | ||
| 2504 | (setq v (cdr v)))) | ||
| 2505 | eglot--workspace-symbols-cache))) | ||
| 2506 | |||
| 2507 | (add-to-list 'completion-category-overrides | ||
| 2508 | '(eglot-indirection-joy (styles . (eglot--lsp-backend-style)))) | ||
| 2509 | |||
| 2510 | (cl-defmethod xref-backend-identifier-at-point ((_backend (eql eglot))) | ||
| 2511 | (let ((attempt | ||
| 2512 | (and (xref--prompt-p this-command) | ||
| 2513 | (puthash :default | ||
| 2514 | (ignore-errors | ||
| 2515 | (eglot--workspace-symbols (symbol-name (symbol-at-point)))) | ||
| 2516 | eglot--workspace-symbols-cache)))) | ||
| 2517 | (if attempt (car attempt) "LSP identifier at point"))) | ||
| 2518 | |||
| 2519 | (defvar eglot--lsp-xref-refs nil | ||
| 2520 | "`xref' objects for overriding `xref-backend-references''s.") | ||
| 2521 | |||
| 2522 | (cl-defun eglot--lsp-xrefs-for-method (method &key extra-params capability) | ||
| 2523 | "Make `xref''s for METHOD, EXTRA-PARAMS, check CAPABILITY." | ||
| 2524 | (unless (eglot--server-capable | ||
| 2525 | (or capability | ||
| 2526 | (intern | ||
| 2527 | (format ":%sProvider" | ||
| 2528 | (cadr (split-string (symbol-name method) | ||
| 2529 | "/")))))) | ||
| 2530 | (eglot--error "Sorry, this server doesn't do %s" method)) | ||
| 2531 | (let ((response | ||
| 2532 | (jsonrpc-request | ||
| 2533 | (eglot--current-server-or-lose) | ||
| 2534 | method (append (eglot--TextDocumentPositionParams) extra-params)))) | ||
| 2535 | (eglot--collecting-xrefs (collect) | ||
| 2536 | (mapc | ||
| 2537 | (lambda (loc-or-loc-link) | ||
| 2538 | (let ((sym-name (symbol-name (symbol-at-point)))) | ||
| 2539 | (eglot--dcase loc-or-loc-link | ||
| 2540 | (((LocationLink) targetUri targetSelectionRange) | ||
| 2541 | (collect (eglot--xref-make-match sym-name | ||
| 2542 | targetUri targetSelectionRange))) | ||
| 2543 | (((Location) uri range) | ||
| 2544 | (collect (eglot--xref-make-match sym-name | ||
| 2545 | uri range)))))) | ||
| 2546 | (if (vectorp response) response (and response (list response))))))) | ||
| 2547 | |||
| 2548 | (cl-defun eglot--lsp-xref-helper (method &key extra-params capability ) | ||
| 2549 | "Helper for `eglot-find-declaration' & friends." | ||
| 2550 | (let ((eglot--lsp-xref-refs (eglot--lsp-xrefs-for-method | ||
| 2551 | method | ||
| 2552 | :extra-params extra-params | ||
| 2553 | :capability capability))) | ||
| 2554 | (if eglot--lsp-xref-refs | ||
| 2555 | (xref-find-references "LSP identifier at point.") | ||
| 2556 | (eglot--message "%s returned no references" method)))) | ||
| 2557 | |||
| 2558 | (defun eglot-find-declaration () | ||
| 2559 | "Find declaration for SYM, the identifier at point." | ||
| 2560 | (interactive) | ||
| 2561 | (eglot--lsp-xref-helper :textDocument/declaration)) | ||
| 2562 | |||
| 2563 | (defun eglot-find-implementation () | ||
| 2564 | "Find implementation for SYM, the identifier at point." | ||
| 2565 | (interactive) | ||
| 2566 | (eglot--lsp-xref-helper :textDocument/implementation)) | ||
| 2567 | |||
| 2568 | (defun eglot-find-typeDefinition () | ||
| 2569 | "Find type definition for SYM, the identifier at point." | ||
| 2570 | (interactive) | ||
| 2571 | (eglot--lsp-xref-helper :textDocument/typeDefinition)) | ||
| 2572 | |||
| 2573 | (cl-defmethod xref-backend-definitions ((_backend (eql eglot)) id) | ||
| 2574 | (let ((probe (eglot--recover-workspace-symbol-meta id))) | ||
| 2575 | (if probe | ||
| 2576 | (eglot--dbind ((WorkspaceSymbol) name location) | ||
| 2577 | (get-text-property 0 'eglot--lsp-workspaceSymbol probe) | ||
| 2578 | (eglot--dbind ((Location) uri range) location | ||
| 2579 | (list (eglot--xref-make-match name uri range)))) | ||
| 2580 | (eglot--lsp-xrefs-for-method :textDocument/definition)))) | ||
| 2581 | |||
| 2582 | (cl-defmethod xref-backend-references ((_backend (eql eglot)) _identifier) | ||
| 2583 | (or | ||
| 2584 | eglot--lsp-xref-refs | ||
| 2585 | (eglot--lsp-xrefs-for-method | ||
| 2586 | :textDocument/references :extra-params `(:context (:includeDeclaration t))))) | ||
| 2587 | |||
| 2588 | (cl-defmethod xref-backend-apropos ((_backend (eql eglot)) pattern) | ||
| 2589 | (when (eglot--server-capable :workspaceSymbolProvider) | ||
| 2590 | (eglot--collecting-xrefs (collect) | ||
| 2591 | (mapc | ||
| 2592 | (eglot--lambda ((SymbolInformation) name location) | ||
| 2593 | (eglot--dbind ((Location) uri range) location | ||
| 2594 | (collect (eglot--xref-make-match name uri range)))) | ||
| 2595 | (jsonrpc-request (eglot--current-server-or-lose) | ||
| 2596 | :workspace/symbol | ||
| 2597 | `(:query ,pattern)))))) | ||
| 2598 | |||
| 2599 | (defun eglot-format-buffer () | ||
| 2600 | "Format contents of current buffer." | ||
| 2601 | (interactive) | ||
| 2602 | (eglot-format nil nil)) | ||
| 2603 | |||
| 2604 | (defun eglot-format (&optional beg end on-type-format) | ||
| 2605 | "Format region BEG END. | ||
| 2606 | If either BEG or END is nil, format entire buffer. | ||
| 2607 | Interactively, format active region, or entire buffer if region | ||
| 2608 | is not active. | ||
| 2609 | |||
| 2610 | If non-nil, ON-TYPE-FORMAT is a character just inserted at BEG | ||
| 2611 | for which LSP on-type-formatting should be requested." | ||
| 2612 | (interactive (and (region-active-p) (list (region-beginning) (region-end)))) | ||
| 2613 | (pcase-let ((`(,method ,cap ,args) | ||
| 2614 | (cond | ||
| 2615 | ((and beg on-type-format) | ||
| 2616 | `(:textDocument/onTypeFormatting | ||
| 2617 | :documentOnTypeFormattingProvider | ||
| 2618 | ,`(:position ,(eglot--pos-to-lsp-position beg) | ||
| 2619 | :ch ,(string on-type-format)))) | ||
| 2620 | ((and beg end) | ||
| 2621 | `(:textDocument/rangeFormatting | ||
| 2622 | :documentRangeFormattingProvider | ||
| 2623 | (:range ,(list :start (eglot--pos-to-lsp-position beg) | ||
| 2624 | :end (eglot--pos-to-lsp-position end))))) | ||
| 2625 | (t | ||
| 2626 | '(:textDocument/formatting :documentFormattingProvider nil))))) | ||
| 2627 | (unless (eglot--server-capable cap) | ||
| 2628 | (eglot--error "Server can't format!")) | ||
| 2629 | (eglot--apply-text-edits | ||
| 2630 | (jsonrpc-request | ||
| 2631 | (eglot--current-server-or-lose) | ||
| 2632 | method | ||
| 2633 | (cl-list* | ||
| 2634 | :textDocument (eglot--TextDocumentIdentifier) | ||
| 2635 | :options (list :tabSize tab-width | ||
| 2636 | :insertSpaces (if indent-tabs-mode :json-false t) | ||
| 2637 | :insertFinalNewline (if require-final-newline t :json-false) | ||
| 2638 | :trimFinalNewlines (if delete-trailing-lines t :json-false)) | ||
| 2639 | args) | ||
| 2640 | :deferred method)))) | ||
| 2641 | |||
| 2642 | (defun eglot-completion-at-point () | ||
| 2643 | "EGLOT's `completion-at-point' function." | ||
| 2644 | ;; Commit logs for this function help understand what's going on. | ||
| 2645 | (when-let (completion-capability (eglot--server-capable :completionProvider)) | ||
| 2646 | (let* ((server (eglot--current-server-or-lose)) | ||
| 2647 | (sort-completions | ||
| 2648 | (lambda (completions) | ||
| 2649 | (cl-sort completions | ||
| 2650 | #'string-lessp | ||
| 2651 | :key (lambda (c) | ||
| 2652 | (or (plist-get | ||
| 2653 | (get-text-property 0 'eglot--lsp-item c) | ||
| 2654 | :sortText) | ||
| 2655 | ""))))) | ||
| 2656 | (metadata `(metadata (category . eglot) | ||
| 2657 | (display-sort-function . ,sort-completions))) | ||
| 2658 | resp items (cached-proxies :none) | ||
| 2659 | (proxies | ||
| 2660 | (lambda () | ||
| 2661 | (if (listp cached-proxies) cached-proxies | ||
| 2662 | (setq resp | ||
| 2663 | (jsonrpc-request server | ||
| 2664 | :textDocument/completion | ||
| 2665 | (eglot--CompletionParams) | ||
| 2666 | :deferred :textDocument/completion | ||
| 2667 | :cancel-on-input t)) | ||
| 2668 | (setq items (append | ||
| 2669 | (if (vectorp resp) resp (plist-get resp :items)) | ||
| 2670 | nil)) | ||
| 2671 | (setq cached-proxies | ||
| 2672 | (mapcar | ||
| 2673 | (jsonrpc-lambda | ||
| 2674 | (&rest item &key label insertText insertTextFormat | ||
| 2675 | &allow-other-keys) | ||
| 2676 | (let ((proxy | ||
| 2677 | (cond ((and (eql insertTextFormat 2) | ||
| 2678 | (eglot--snippet-expansion-fn)) | ||
| 2679 | (string-trim-left label)) | ||
| 2680 | ((and insertText | ||
| 2681 | (not (string-empty-p insertText))) | ||
| 2682 | insertText) | ||
| 2683 | (t | ||
| 2684 | (string-trim-left label))))) | ||
| 2685 | (unless (zerop (length proxy)) | ||
| 2686 | (put-text-property 0 1 'eglot--lsp-item item proxy)) | ||
| 2687 | proxy)) | ||
| 2688 | items))))) | ||
| 2689 | (resolved (make-hash-table)) | ||
| 2690 | (resolve-maybe | ||
| 2691 | ;; Maybe completion/resolve JSON object `lsp-comp' into | ||
| 2692 | ;; another JSON object, if at all possible. Otherwise, | ||
| 2693 | ;; just return lsp-comp. | ||
| 2694 | (lambda (lsp-comp) | ||
| 2695 | (or (gethash lsp-comp resolved) | ||
| 2696 | (setf (gethash lsp-comp resolved) | ||
| 2697 | (if (and (eglot--server-capable :completionProvider | ||
| 2698 | :resolveProvider) | ||
| 2699 | (plist-get lsp-comp :data)) | ||
| 2700 | (jsonrpc-request server :completionItem/resolve | ||
| 2701 | lsp-comp :cancel-on-input t) | ||
| 2702 | lsp-comp))))) | ||
| 2703 | (bounds (bounds-of-thing-at-point 'symbol))) | ||
| 2704 | (list | ||
| 2705 | (or (car bounds) (point)) | ||
| 2706 | (or (cdr bounds) (point)) | ||
| 2707 | (lambda (probe pred action) | ||
| 2708 | (cond | ||
| 2709 | ((eq action 'metadata) metadata) ; metadata | ||
| 2710 | ((eq action 'lambda) ; test-completion | ||
| 2711 | (test-completion probe (funcall proxies))) | ||
| 2712 | ((eq (car-safe action) 'boundaries) nil) ; boundaries | ||
| 2713 | ((null action) ; try-completion | ||
| 2714 | (try-completion probe (funcall proxies))) | ||
| 2715 | ((eq action t) ; all-completions | ||
| 2716 | (all-completions | ||
| 2717 | "" | ||
| 2718 | (funcall proxies) | ||
| 2719 | (lambda (proxy) | ||
| 2720 | (let* ((item (get-text-property 0 'eglot--lsp-item proxy)) | ||
| 2721 | (filterText (plist-get item :filterText))) | ||
| 2722 | (and (or (null pred) (funcall pred proxy)) | ||
| 2723 | (string-prefix-p | ||
| 2724 | probe (or filterText proxy) completion-ignore-case)))))))) | ||
| 2725 | :annotation-function | ||
| 2726 | (lambda (proxy) | ||
| 2727 | (eglot--dbind ((CompletionItem) detail kind) | ||
| 2728 | (get-text-property 0 'eglot--lsp-item proxy) | ||
| 2729 | (let* ((detail (and (stringp detail) | ||
| 2730 | (not (string= detail "")) | ||
| 2731 | detail)) | ||
| 2732 | (annotation | ||
| 2733 | (or detail | ||
| 2734 | (cdr (assoc kind eglot--kind-names))))) | ||
| 2735 | (when annotation | ||
| 2736 | (concat " " | ||
| 2737 | (propertize annotation | ||
| 2738 | 'face 'font-lock-function-name-face)))))) | ||
| 2739 | :company-kind | ||
| 2740 | ;; Associate each lsp-item with a lsp-kind symbol. | ||
| 2741 | (lambda (proxy) | ||
| 2742 | (when-let* ((lsp-item (get-text-property 0 'eglot--lsp-item proxy)) | ||
| 2743 | (kind (alist-get (plist-get lsp-item :kind) | ||
| 2744 | eglot--kind-names))) | ||
| 2745 | (intern (downcase kind)))) | ||
| 2746 | :company-deprecated | ||
| 2747 | (lambda (proxy) | ||
| 2748 | (when-let ((lsp-item (get-text-property 0 'eglot--lsp-item proxy))) | ||
| 2749 | (or (seq-contains-p (plist-get lsp-item :tags) | ||
| 2750 | 1) | ||
| 2751 | (eq t (plist-get lsp-item :deprecated))))) | ||
| 2752 | :company-docsig | ||
| 2753 | ;; FIXME: autoImportText is specific to the pyright language server | ||
| 2754 | (lambda (proxy) | ||
| 2755 | (when-let* ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy)) | ||
| 2756 | (data (plist-get (funcall resolve-maybe lsp-comp) :data)) | ||
| 2757 | (import-text (plist-get data :autoImportText))) | ||
| 2758 | import-text)) | ||
| 2759 | :company-doc-buffer | ||
| 2760 | (lambda (proxy) | ||
| 2761 | (let* ((documentation | ||
| 2762 | (let ((lsp-comp (get-text-property 0 'eglot--lsp-item proxy))) | ||
| 2763 | (plist-get (funcall resolve-maybe lsp-comp) :documentation))) | ||
| 2764 | (formatted (and documentation | ||
| 2765 | (eglot--format-markup documentation)))) | ||
| 2766 | (when formatted | ||
| 2767 | (with-current-buffer (get-buffer-create " *eglot doc*") | ||
| 2768 | (erase-buffer) | ||
| 2769 | (insert formatted) | ||
| 2770 | (current-buffer))))) | ||
| 2771 | :company-require-match 'never | ||
| 2772 | :company-prefix-length | ||
| 2773 | (save-excursion | ||
| 2774 | (when (car bounds) (goto-char (car bounds))) | ||
| 2775 | (when (listp completion-capability) | ||
| 2776 | (looking-back | ||
| 2777 | (regexp-opt | ||
| 2778 | (cl-coerce (cl-getf completion-capability :triggerCharacters) 'list)) | ||
| 2779 | (line-beginning-position)))) | ||
| 2780 | :exit-function | ||
| 2781 | (lambda (proxy status) | ||
| 2782 | (when (memq status '(finished exact)) | ||
| 2783 | ;; To assist in using this whole `completion-at-point' | ||
| 2784 | ;; function inside `completion-in-region', ensure the exit | ||
| 2785 | ;; function runs in the buffer where the completion was | ||
| 2786 | ;; triggered from. This should probably be in Emacs itself. | ||
| 2787 | ;; (github#505) | ||
| 2788 | (with-current-buffer (if (minibufferp) | ||
| 2789 | (window-buffer (minibuffer-selected-window)) | ||
| 2790 | (current-buffer)) | ||
| 2791 | (eglot--dbind ((CompletionItem) insertTextFormat | ||
| 2792 | insertText textEdit additionalTextEdits label) | ||
| 2793 | (funcall | ||
| 2794 | resolve-maybe | ||
| 2795 | (or (get-text-property 0 'eglot--lsp-item proxy) | ||
| 2796 | ;; When selecting from the *Completions* | ||
| 2797 | ;; buffer, `proxy' won't have any properties. | ||
| 2798 | ;; A lookup should fix that (github#148) | ||
| 2799 | (get-text-property | ||
| 2800 | 0 'eglot--lsp-item | ||
| 2801 | (cl-find proxy (funcall proxies) :test #'string=)))) | ||
| 2802 | (let ((snippet-fn (and (eql insertTextFormat 2) | ||
| 2803 | (eglot--snippet-expansion-fn)))) | ||
| 2804 | (cond (textEdit | ||
| 2805 | ;; Undo (yes, undo) the newly inserted completion. | ||
| 2806 | ;; If before completion the buffer was "foo.b" and | ||
| 2807 | ;; now is "foo.bar", `proxy' will be "bar". We | ||
| 2808 | ;; want to delete only "ar" (`proxy' minus the | ||
| 2809 | ;; symbol whose bounds we've calculated before) | ||
| 2810 | ;; (github#160). | ||
| 2811 | (delete-region (+ (- (point) (length proxy)) | ||
| 2812 | (if bounds | ||
| 2813 | (- (cdr bounds) (car bounds)) | ||
| 2814 | 0)) | ||
| 2815 | (point)) | ||
| 2816 | (eglot--dbind ((TextEdit) range newText) textEdit | ||
| 2817 | (pcase-let ((`(,beg . ,end) | ||
| 2818 | (eglot--range-region range))) | ||
| 2819 | (delete-region beg end) | ||
| 2820 | (goto-char beg) | ||
| 2821 | (funcall (or snippet-fn #'insert) newText)))) | ||
| 2822 | (snippet-fn | ||
| 2823 | ;; A snippet should be inserted, but using plain | ||
| 2824 | ;; `insertText'. This requires us to delete the | ||
| 2825 | ;; whole completion, since `insertText' is the full | ||
| 2826 | ;; completion's text. | ||
| 2827 | (delete-region (- (point) (length proxy)) (point)) | ||
| 2828 | (funcall snippet-fn (or insertText label)))) | ||
| 2829 | (when (cl-plusp (length additionalTextEdits)) | ||
| 2830 | (eglot--apply-text-edits additionalTextEdits))) | ||
| 2831 | (eglot--signal-textDocument/didChange) | ||
| 2832 | (eldoc))))))))) | ||
| 2833 | |||
| 2834 | (defun eglot--hover-info (contents &optional _range) | ||
| 2835 | (mapconcat #'eglot--format-markup | ||
| 2836 | (if (vectorp contents) contents (list contents)) "\n")) | ||
| 2837 | |||
| 2838 | (defun eglot--sig-info (sigs active-sig sig-help-active-param) | ||
| 2839 | (cl-loop | ||
| 2840 | for (sig . moresigs) on (append sigs nil) for i from 0 | ||
| 2841 | concat | ||
| 2842 | (eglot--dbind ((SignatureInformation) label documentation parameters activeParameter) sig | ||
| 2843 | (with-temp-buffer | ||
| 2844 | (save-excursion (insert label)) | ||
| 2845 | (let ((active-param (or activeParameter sig-help-active-param)) | ||
| 2846 | params-start params-end) | ||
| 2847 | ;; Ad-hoc attempt to parse label as <name>(<params>) | ||
| 2848 | (when (looking-at "\\([^(]+\\)(\\([^)]+\\))") | ||
| 2849 | (setq params-start (match-beginning 2) params-end (match-end 2)) | ||
| 2850 | (add-face-text-property (match-beginning 1) (match-end 1) | ||
| 2851 | 'font-lock-function-name-face)) | ||
| 2852 | (when (eql i active-sig) | ||
| 2853 | ;; Decide whether to add one-line-summary to signature line | ||
| 2854 | (when (and (stringp documentation) | ||
| 2855 | (string-match "[[:space:]]*\\([^.\r\n]+[.]?\\)" | ||
| 2856 | documentation)) | ||
| 2857 | (setq documentation (match-string 1 documentation)) | ||
| 2858 | (unless (string-prefix-p (string-trim documentation) label) | ||
| 2859 | (goto-char (point-max)) | ||
| 2860 | (insert ": " (eglot--format-markup documentation)))) | ||
| 2861 | ;; Decide what to do with the active parameter... | ||
| 2862 | (when (and (eql i active-sig) active-param | ||
| 2863 | (< -1 active-param (length parameters))) | ||
| 2864 | (eglot--dbind ((ParameterInformation) label documentation) | ||
| 2865 | (aref parameters active-param) | ||
| 2866 | ;; ...perhaps highlight it in the formals list | ||
| 2867 | (when params-start | ||
| 2868 | (goto-char params-start) | ||
| 2869 | (pcase-let | ||
| 2870 | ((`(,beg ,end) | ||
| 2871 | (if (stringp label) | ||
| 2872 | (let ((case-fold-search nil)) | ||
| 2873 | (and (re-search-forward | ||
| 2874 | (concat "\\<" (regexp-quote label) "\\>") | ||
| 2875 | params-end t) | ||
| 2876 | (list (match-beginning 0) (match-end 0)))) | ||
| 2877 | (mapcar #'1+ (append label nil))))) | ||
| 2878 | (if (and beg end) | ||
| 2879 | (add-face-text-property | ||
| 2880 | beg end | ||
| 2881 | 'eldoc-highlight-function-argument)))) | ||
| 2882 | ;; ...and/or maybe add its doc on a line by its own. | ||
| 2883 | (when documentation | ||
| 2884 | (goto-char (point-max)) | ||
| 2885 | (insert "\n" | ||
| 2886 | (propertize | ||
| 2887 | (if (stringp label) | ||
| 2888 | label | ||
| 2889 | (apply #'buffer-substring (mapcar #'1+ label))) | ||
| 2890 | 'face 'eldoc-highlight-function-argument) | ||
| 2891 | ": " (eglot--format-markup documentation)))))) | ||
| 2892 | (buffer-string)))) | ||
| 2893 | when moresigs concat "\n")) | ||
| 2894 | |||
| 2895 | (defun eglot-signature-eldoc-function (cb) | ||
| 2896 | "A member of `eldoc-documentation-functions', for signatures." | ||
| 2897 | (when (eglot--server-capable :signatureHelpProvider) | ||
| 2898 | (let ((buf (current-buffer))) | ||
| 2899 | (jsonrpc-async-request | ||
| 2900 | (eglot--current-server-or-lose) | ||
| 2901 | :textDocument/signatureHelp (eglot--TextDocumentPositionParams) | ||
| 2902 | :success-fn | ||
| 2903 | (eglot--lambda ((SignatureHelp) | ||
| 2904 | signatures activeSignature activeParameter) | ||
| 2905 | (eglot--when-buffer-window buf | ||
| 2906 | (funcall cb | ||
| 2907 | (unless (seq-empty-p signatures) | ||
| 2908 | (eglot--sig-info signatures | ||
| 2909 | activeSignature | ||
| 2910 | activeParameter))))) | ||
| 2911 | :deferred :textDocument/signatureHelp)) | ||
| 2912 | t)) | ||
| 2913 | |||
| 2914 | (defun eglot-hover-eldoc-function (cb) | ||
| 2915 | "A member of `eldoc-documentation-functions', for hover." | ||
| 2916 | (when (eglot--server-capable :hoverProvider) | ||
| 2917 | (let ((buf (current-buffer))) | ||
| 2918 | (jsonrpc-async-request | ||
| 2919 | (eglot--current-server-or-lose) | ||
| 2920 | :textDocument/hover (eglot--TextDocumentPositionParams) | ||
| 2921 | :success-fn (eglot--lambda ((Hover) contents range) | ||
| 2922 | (eglot--when-buffer-window buf | ||
| 2923 | (let ((info (unless (seq-empty-p contents) | ||
| 2924 | (eglot--hover-info contents range)))) | ||
| 2925 | (funcall cb info :buffer t)))) | ||
| 2926 | :deferred :textDocument/hover)) | ||
| 2927 | (eglot--highlight-piggyback cb) | ||
| 2928 | t)) | ||
| 2929 | |||
| 2930 | (defvar eglot--highlights nil "Overlays for textDocument/documentHighlight.") | ||
| 2931 | |||
| 2932 | (defun eglot--highlight-piggyback (_cb) | ||
| 2933 | "Request and handle `:textDocument/documentHighlight'." | ||
| 2934 | ;; FIXME: Obviously, this is just piggy backing on eldoc's calls for | ||
| 2935 | ;; convenience, as shown by the fact that we just ignore cb. | ||
| 2936 | (let ((buf (current-buffer))) | ||
| 2937 | (when (eglot--server-capable :documentHighlightProvider) | ||
| 2938 | (jsonrpc-async-request | ||
| 2939 | (eglot--current-server-or-lose) | ||
| 2940 | :textDocument/documentHighlight (eglot--TextDocumentPositionParams) | ||
| 2941 | :success-fn | ||
| 2942 | (lambda (highlights) | ||
| 2943 | (mapc #'delete-overlay eglot--highlights) | ||
| 2944 | (setq eglot--highlights | ||
| 2945 | (eglot--when-buffer-window buf | ||
| 2946 | (mapcar | ||
| 2947 | (eglot--lambda ((DocumentHighlight) range) | ||
| 2948 | (pcase-let ((`(,beg . ,end) | ||
| 2949 | (eglot--range-region range))) | ||
| 2950 | (let ((ov (make-overlay beg end))) | ||
| 2951 | (overlay-put ov 'face 'eglot-highlight-symbol-face) | ||
| 2952 | (overlay-put ov 'modification-hooks | ||
| 2953 | `(,(lambda (o &rest _) (delete-overlay o)))) | ||
| 2954 | ov))) | ||
| 2955 | highlights)))) | ||
| 2956 | :deferred :textDocument/documentHighlight) | ||
| 2957 | nil))) | ||
| 2958 | |||
| 2959 | (defun eglot-imenu () | ||
| 2960 | "EGLOT's `imenu-create-index-function'. | ||
| 2961 | Returns a list as described in docstring of `imenu--index-alist'." | ||
| 2962 | (cl-labels | ||
| 2963 | ((unfurl (obj) | ||
| 2964 | (eglot--dcase obj | ||
| 2965 | (((SymbolInformation)) (list obj)) | ||
| 2966 | (((DocumentSymbol) name children) | ||
| 2967 | (cons obj | ||
| 2968 | (mapcar | ||
| 2969 | (lambda (c) | ||
| 2970 | (plist-put | ||
| 2971 | c :containerName | ||
| 2972 | (let ((existing (plist-get c :containerName))) | ||
| 2973 | (if existing (format "%s::%s" name existing) | ||
| 2974 | name)))) | ||
| 2975 | (mapcan #'unfurl children))))))) | ||
| 2976 | (mapcar | ||
| 2977 | (pcase-lambda (`(,kind . ,objs)) | ||
| 2978 | (cons | ||
| 2979 | (alist-get kind eglot--symbol-kind-names "Unknown") | ||
| 2980 | (mapcan (pcase-lambda (`(,container . ,objs)) | ||
| 2981 | (let ((elems (mapcar | ||
| 2982 | (lambda (obj) | ||
| 2983 | (cons (plist-get obj :name) | ||
| 2984 | (car (eglot--range-region | ||
| 2985 | (eglot--dcase obj | ||
| 2986 | (((SymbolInformation) location) | ||
| 2987 | (plist-get location :range)) | ||
| 2988 | (((DocumentSymbol) selectionRange) | ||
| 2989 | selectionRange)))))) | ||
| 2990 | objs))) | ||
| 2991 | (if container (list (cons container elems)) elems))) | ||
| 2992 | (seq-group-by | ||
| 2993 | (lambda (e) (plist-get e :containerName)) objs)))) | ||
| 2994 | (seq-group-by | ||
| 2995 | (lambda (obj) (plist-get obj :kind)) | ||
| 2996 | (mapcan #'unfurl | ||
| 2997 | (jsonrpc-request (eglot--current-server-or-lose) | ||
| 2998 | :textDocument/documentSymbol | ||
| 2999 | `(:textDocument | ||
| 3000 | ,(eglot--TextDocumentIdentifier)) | ||
| 3001 | :cancel-on-input non-essential)))))) | ||
| 3002 | |||
| 3003 | (defun eglot--apply-text-edits (edits &optional version) | ||
| 3004 | "Apply EDITS for current buffer if at VERSION, or if it's nil." | ||
| 3005 | (unless (or (not version) (equal version eglot--versioned-identifier)) | ||
| 3006 | (jsonrpc-error "Edits on `%s' require version %d, you have %d" | ||
| 3007 | (current-buffer) version eglot--versioned-identifier)) | ||
| 3008 | (atomic-change-group | ||
| 3009 | (let* ((change-group (prepare-change-group)) | ||
| 3010 | (howmany (length edits)) | ||
| 3011 | (reporter (make-progress-reporter | ||
| 3012 | (format "[eglot] applying %s edits to `%s'..." | ||
| 3013 | howmany (current-buffer)) | ||
| 3014 | 0 howmany)) | ||
| 3015 | (done 0)) | ||
| 3016 | (mapc (pcase-lambda (`(,newText ,beg . ,end)) | ||
| 3017 | (let ((source (current-buffer))) | ||
| 3018 | (with-temp-buffer | ||
| 3019 | (insert newText) | ||
| 3020 | (let ((temp (current-buffer))) | ||
| 3021 | (with-current-buffer source | ||
| 3022 | (save-excursion | ||
| 3023 | (save-restriction | ||
| 3024 | (narrow-to-region beg end) | ||
| 3025 | |||
| 3026 | ;; On emacs versions < 26.2, | ||
| 3027 | ;; `replace-buffer-contents' is buggy - it calls | ||
| 3028 | ;; change functions with invalid arguments - so we | ||
| 3029 | ;; manually call the change functions here. | ||
| 3030 | ;; | ||
| 3031 | ;; See emacs bugs #32237, #32278: | ||
| 3032 | ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32237 | ||
| 3033 | ;; https://debbugs.gnu.org/cgi/bugreport.cgi?bug=32278 | ||
| 3034 | (let ((inhibit-modification-hooks t) | ||
| 3035 | (length (- end beg)) | ||
| 3036 | (beg (marker-position beg)) | ||
| 3037 | (end (marker-position end))) | ||
| 3038 | (run-hook-with-args 'before-change-functions | ||
| 3039 | beg end) | ||
| 3040 | (replace-buffer-contents temp) | ||
| 3041 | (run-hook-with-args 'after-change-functions | ||
| 3042 | beg (+ beg (length newText)) | ||
| 3043 | length)))) | ||
| 3044 | (progress-reporter-update reporter (cl-incf done))))))) | ||
| 3045 | (mapcar (eglot--lambda ((TextEdit) range newText) | ||
| 3046 | (cons newText (eglot--range-region range 'markers))) | ||
| 3047 | (reverse edits))) | ||
| 3048 | (undo-amalgamate-change-group change-group) | ||
| 3049 | (progress-reporter-done reporter)))) | ||
| 3050 | |||
| 3051 | (defun eglot--apply-workspace-edit (wedit &optional confirm) | ||
| 3052 | "Apply the workspace edit WEDIT. If CONFIRM, ask user first." | ||
| 3053 | (eglot--dbind ((WorkspaceEdit) changes documentChanges) wedit | ||
| 3054 | (let ((prepared | ||
| 3055 | (mapcar (eglot--lambda ((TextDocumentEdit) textDocument edits) | ||
| 3056 | (eglot--dbind ((VersionedTextDocumentIdentifier) uri version) | ||
| 3057 | textDocument | ||
| 3058 | (list (eglot--uri-to-path uri) edits version))) | ||
| 3059 | documentChanges))) | ||
| 3060 | (unless (and changes documentChanges) | ||
| 3061 | ;; We don't want double edits, and some servers send both | ||
| 3062 | ;; changes and documentChanges. This unless ensures that we | ||
| 3063 | ;; prefer documentChanges over changes. | ||
| 3064 | (cl-loop for (uri edits) on changes by #'cddr | ||
| 3065 | do (push (list (eglot--uri-to-path uri) edits) prepared))) | ||
| 3066 | (if (or confirm | ||
| 3067 | (cl-notevery #'find-buffer-visiting | ||
| 3068 | (mapcar #'car prepared))) | ||
| 3069 | (unless (y-or-n-p | ||
| 3070 | (format "[eglot] Server wants to edit:\n %s\n Proceed? " | ||
| 3071 | (mapconcat #'identity (mapcar #'car prepared) "\n "))) | ||
| 3072 | (jsonrpc-error "User cancelled server edit"))) | ||
| 3073 | (cl-loop for edit in prepared | ||
| 3074 | for (path edits version) = edit | ||
| 3075 | do (with-current-buffer (find-file-noselect path) | ||
| 3076 | (eglot--apply-text-edits edits version)) | ||
| 3077 | finally (eldoc) (eglot--message "Edit successful!"))))) | ||
| 3078 | |||
| 3079 | (defun eglot-rename (newname) | ||
| 3080 | "Rename the current symbol to NEWNAME." | ||
| 3081 | (interactive | ||
| 3082 | (list (read-from-minibuffer | ||
| 3083 | (format "Rename `%s' to: " (or (thing-at-point 'symbol t) | ||
| 3084 | "unknown symbol")) | ||
| 3085 | nil nil nil nil | ||
| 3086 | (symbol-name (symbol-at-point))))) | ||
| 3087 | (unless (eglot--server-capable :renameProvider) | ||
| 3088 | (eglot--error "Server can't rename!")) | ||
| 3089 | (eglot--apply-workspace-edit | ||
| 3090 | (jsonrpc-request (eglot--current-server-or-lose) | ||
| 3091 | :textDocument/rename `(,@(eglot--TextDocumentPositionParams) | ||
| 3092 | :newName ,newname)) | ||
| 3093 | current-prefix-arg)) | ||
| 3094 | |||
| 3095 | (defun eglot--region-bounds () | ||
| 3096 | "Region bounds if active, else bounds of things at point." | ||
| 3097 | (if (use-region-p) `(,(region-beginning) ,(region-end)) | ||
| 3098 | (let ((boftap (bounds-of-thing-at-point 'sexp))) | ||
| 3099 | (list (car boftap) (cdr boftap))))) | ||
| 3100 | |||
| 3101 | (defun eglot-code-actions (beg &optional end action-kind) | ||
| 3102 | "Offer to execute actions of ACTION-KIND between BEG and END. | ||
| 3103 | If ACTION-KIND is nil, consider all kinds of actions. | ||
| 3104 | Interactively, default BEG and END to region's bounds else BEG is | ||
| 3105 | point and END is nil, which results in a request for code actions | ||
| 3106 | at point. With prefix argument, prompt for ACTION-KIND." | ||
| 3107 | (interactive | ||
| 3108 | `(,@(eglot--region-bounds) | ||
| 3109 | ,(and current-prefix-arg | ||
| 3110 | (completing-read "[eglot] Action kind: " | ||
| 3111 | '("quickfix" "refactor.extract" "refactor.inline" | ||
| 3112 | "refactor.rewrite" "source.organizeImports"))))) | ||
| 3113 | (unless (eglot--server-capable :codeActionProvider) | ||
| 3114 | (eglot--error "Server can't execute code actions!")) | ||
| 3115 | (let* ((server (eglot--current-server-or-lose)) | ||
| 3116 | (actions | ||
| 3117 | (jsonrpc-request | ||
| 3118 | server | ||
| 3119 | :textDocument/codeAction | ||
| 3120 | (list :textDocument (eglot--TextDocumentIdentifier) | ||
| 3121 | :range (list :start (eglot--pos-to-lsp-position beg) | ||
| 3122 | :end (eglot--pos-to-lsp-position end)) | ||
| 3123 | :context | ||
| 3124 | `(:diagnostics | ||
| 3125 | [,@(cl-loop for diag in (flymake-diagnostics beg end) | ||
| 3126 | when (cdr (assoc 'eglot-lsp-diag | ||
| 3127 | (eglot--diag-data diag))) | ||
| 3128 | collect it)] | ||
| 3129 | ,@(when action-kind `(:only [,action-kind])))) | ||
| 3130 | :deferred t)) | ||
| 3131 | (menu-items | ||
| 3132 | (or (cl-loop for action across actions | ||
| 3133 | ;; Do filtering ourselves, in case the `:only' | ||
| 3134 | ;; didn't go through. | ||
| 3135 | when (or (not action-kind) | ||
| 3136 | (equal action-kind (plist-get action :kind))) | ||
| 3137 | collect (cons (plist-get action :title) action)) | ||
| 3138 | (apply #'eglot--error | ||
| 3139 | (if action-kind `("No \"%s\" code actions here" ,action-kind) | ||
| 3140 | `("No code actions here"))))) | ||
| 3141 | (preferred-action (cl-find-if | ||
| 3142 | (lambda (menu-item) | ||
| 3143 | (plist-get (cdr menu-item) :isPreferred)) | ||
| 3144 | menu-items)) | ||
| 3145 | (default-action (car (or preferred-action (car menu-items)))) | ||
| 3146 | (action (if (and action-kind (null (cadr menu-items))) | ||
| 3147 | (cdr (car menu-items)) | ||
| 3148 | (if (listp last-nonmenu-event) | ||
| 3149 | (x-popup-menu last-nonmenu-event `("Eglot code actions:" | ||
| 3150 | ("dummy" ,@menu-items))) | ||
| 3151 | (cdr (assoc (completing-read | ||
| 3152 | (format "[eglot] Pick an action (default %s): " | ||
| 3153 | default-action) | ||
| 3154 | menu-items nil t nil nil default-action) | ||
| 3155 | menu-items)))))) | ||
| 3156 | (eglot--dcase action | ||
| 3157 | (((Command) command arguments) | ||
| 3158 | (eglot-execute-command server (intern command) arguments)) | ||
| 3159 | (((CodeAction) edit command) | ||
| 3160 | (when edit (eglot--apply-workspace-edit edit)) | ||
| 3161 | (when command | ||
| 3162 | (eglot--dbind ((Command) command arguments) command | ||
| 3163 | (eglot-execute-command server (intern command) arguments))))))) | ||
| 3164 | |||
| 3165 | (defmacro eglot--code-action (name kind) | ||
| 3166 | "Define NAME to execute KIND code action." | ||
| 3167 | `(defun ,name (beg &optional end) | ||
| 3168 | ,(format "Execute `%s' code actions between BEG and END." kind) | ||
| 3169 | (interactive (eglot--region-bounds)) | ||
| 3170 | (eglot-code-actions beg end ,kind))) | ||
| 3171 | |||
| 3172 | (eglot--code-action eglot-code-action-organize-imports "source.organizeImports") | ||
| 3173 | (eglot--code-action eglot-code-action-extract "refactor.extract") | ||
| 3174 | (eglot--code-action eglot-code-action-inline "refactor.inline") | ||
| 3175 | (eglot--code-action eglot-code-action-rewrite "refactor.rewrite") | ||
| 3176 | (eglot--code-action eglot-code-action-quickfix "quickfix") | ||
| 3177 | |||
| 3178 | |||
| 3179 | ;;; Dynamic registration | ||
| 3180 | ;;; | ||
| 3181 | (cl-defmethod eglot-register-capability | ||
| 3182 | (server (method (eql workspace/didChangeWatchedFiles)) id &key watchers) | ||
| 3183 | "Handle dynamic registration of workspace/didChangeWatchedFiles." | ||
| 3184 | (eglot-unregister-capability server method id) | ||
| 3185 | (let* (success | ||
| 3186 | (globs (mapcar | ||
| 3187 | (eglot--lambda ((FileSystemWatcher) globPattern) | ||
| 3188 | (eglot--glob-compile globPattern t t)) | ||
| 3189 | watchers)) | ||
| 3190 | (dirs-to-watch | ||
| 3191 | (delete-dups (mapcar #'file-name-directory | ||
| 3192 | (project-files | ||
| 3193 | (eglot--project server)))))) | ||
| 3194 | (cl-labels | ||
| 3195 | ((handle-event | ||
| 3196 | (event) | ||
| 3197 | (pcase-let ((`(,desc ,action ,file ,file1) event)) | ||
| 3198 | (cond | ||
| 3199 | ((and (memq action '(created changed deleted)) | ||
| 3200 | (cl-find file globs :test (lambda (f g) (funcall g f)))) | ||
| 3201 | (jsonrpc-notify | ||
| 3202 | server :workspace/didChangeWatchedFiles | ||
| 3203 | `(:changes ,(vector `(:uri ,(eglot--path-to-uri file) | ||
| 3204 | :type ,(cl-case action | ||
| 3205 | (created 1) | ||
| 3206 | (changed 2) | ||
| 3207 | (deleted 3))))))) | ||
| 3208 | ((eq action 'renamed) | ||
| 3209 | (handle-event `(,desc 'deleted ,file)) | ||
| 3210 | (handle-event `(,desc 'created ,file1))))))) | ||
| 3211 | (unwind-protect | ||
| 3212 | (progn | ||
| 3213 | (dolist (dir dirs-to-watch) | ||
| 3214 | (push (file-notify-add-watch dir '(change) #'handle-event) | ||
| 3215 | (gethash id (eglot--file-watches server)))) | ||
| 3216 | (setq | ||
| 3217 | success | ||
| 3218 | `(:message ,(format "OK, watching %s directories in %s watchers" | ||
| 3219 | (length dirs-to-watch) (length watchers))))) | ||
| 3220 | (unless success | ||
| 3221 | (eglot-unregister-capability server method id)))))) | ||
| 3222 | |||
| 3223 | (cl-defmethod eglot-unregister-capability | ||
| 3224 | (server (_method (eql workspace/didChangeWatchedFiles)) id) | ||
| 3225 | "Handle dynamic unregistration of workspace/didChangeWatchedFiles." | ||
| 3226 | (mapc #'file-notify-rm-watch (gethash id (eglot--file-watches server))) | ||
| 3227 | (remhash id (eglot--file-watches server)) | ||
| 3228 | (list t "OK")) | ||
| 3229 | |||
| 3230 | |||
| 3231 | ;;; Glob heroics | ||
| 3232 | ;;; | ||
| 3233 | (defun eglot--glob-parse (glob) | ||
| 3234 | "Compute list of (STATE-SYM EMITTER-FN PATTERN)." | ||
| 3235 | (with-temp-buffer | ||
| 3236 | (save-excursion (insert glob)) | ||
| 3237 | (cl-loop | ||
| 3238 | with grammar = '((:** "\\*\\*/?" eglot--glob-emit-**) | ||
| 3239 | (:* "\\*" eglot--glob-emit-*) | ||
| 3240 | (:? "\\?" eglot--glob-emit-?) | ||
| 3241 | (:{} "{[^][*{}]+}" eglot--glob-emit-{}) | ||
| 3242 | (:range "\\[\\^?[^][/,*{}]+\\]" eglot--glob-emit-range) | ||
| 3243 | (:literal "[^][,*?{}]+" eglot--glob-emit-self)) | ||
| 3244 | until (eobp) | ||
| 3245 | collect (cl-loop | ||
| 3246 | for (_token regexp emitter) in grammar | ||
| 3247 | thereis (and (re-search-forward (concat "\\=" regexp) nil t) | ||
| 3248 | (list (cl-gensym "state-") emitter (match-string 0))) | ||
| 3249 | finally (error "Glob '%s' invalid at %s" (buffer-string) (point)))))) | ||
| 3250 | |||
| 3251 | (defun eglot--glob-compile (glob &optional byte-compile noerror) | ||
| 3252 | "Convert GLOB into Elisp function. Maybe BYTE-COMPILE it. | ||
| 3253 | If NOERROR, return predicate, else erroring function." | ||
| 3254 | (let* ((states (eglot--glob-parse glob)) | ||
| 3255 | (body `(with-current-buffer (get-buffer-create " *eglot-glob-matcher*") | ||
| 3256 | (erase-buffer) | ||
| 3257 | (save-excursion (insert string)) | ||
| 3258 | (cl-labels ,(cl-loop for (this that) on states | ||
| 3259 | for (self emit text) = this | ||
| 3260 | for next = (or (car that) 'eobp) | ||
| 3261 | collect (funcall emit text self next)) | ||
| 3262 | (or (,(caar states)) | ||
| 3263 | (error "Glob done but more unmatched text: '%s'" | ||
| 3264 | (buffer-substring (point) (point-max))))))) | ||
| 3265 | (form `(lambda (string) ,(if noerror `(ignore-errors ,body) body)))) | ||
| 3266 | (if byte-compile (byte-compile form) form))) | ||
| 3267 | |||
| 3268 | (defun eglot--glob-emit-self (text self next) | ||
| 3269 | `(,self () (re-search-forward ,(concat "\\=" (regexp-quote text))) (,next))) | ||
| 3270 | |||
| 3271 | (defun eglot--glob-emit-** (_ self next) | ||
| 3272 | `(,self () (or (ignore-errors (save-excursion (,next))) | ||
| 3273 | (and (re-search-forward "\\=/?[^/]+/?") (,self))))) | ||
| 3274 | |||
| 3275 | (defun eglot--glob-emit-* (_ self next) | ||
| 3276 | `(,self () (re-search-forward "\\=[^/]") | ||
| 3277 | (or (ignore-errors (save-excursion (,next))) (,self)))) | ||
| 3278 | |||
| 3279 | (defun eglot--glob-emit-? (_ self next) | ||
| 3280 | `(,self () (re-search-forward "\\=[^/]") (,next))) | ||
| 3281 | |||
| 3282 | (defun eglot--glob-emit-{} (arg self next) | ||
| 3283 | (let ((alternatives (split-string (substring arg 1 (1- (length arg))) ","))) | ||
| 3284 | `(,self () | ||
| 3285 | (or (re-search-forward ,(concat "\\=" (regexp-opt alternatives)) nil t) | ||
| 3286 | (error "Failed matching any of %s" ',alternatives)) | ||
| 3287 | (,next)))) | ||
| 3288 | |||
| 3289 | (defun eglot--glob-emit-range (arg self next) | ||
| 3290 | (when (eq ?! (aref arg 1)) (aset arg 1 ?^)) | ||
| 3291 | `(,self () (re-search-forward ,(concat "\\=" arg)) (,next))) | ||
| 3292 | |||
| 3293 | |||
| 3294 | ;;; Hacks | ||
| 3295 | ;;; | ||
| 3296 | ;; FIXME: Although desktop.el compatibility is Emacs bug#56407, the | ||
| 3297 | ;; optimal solution agreed to there is a bit more work than what I | ||
| 3298 | ;; have time to right now. See | ||
| 3299 | ;; e.g. https://debbugs.gnu.org/cgi/bugreport.cgi?bug=bug%2356407#68. | ||
| 3300 | ;; For now, just use `with-eval-after-load' | ||
| 3301 | (with-eval-after-load 'desktop | ||
| 3302 | (add-to-list 'desktop-minor-mode-handlers '(eglot--managed-mode . ignore))) | ||
| 3303 | |||
| 3304 | |||
| 3305 | ;;; Obsolete | ||
| 3306 | ;;; | ||
| 3307 | |||
| 3308 | (make-obsolete-variable 'eglot--managed-mode-hook | ||
| 3309 | 'eglot-managed-mode-hook "1.6") | ||
| 3310 | (provide 'eglot) | ||
| 3311 | |||
| 3312 | |||
| 3313 | ;;; Backend completion | ||
| 3314 | |||
| 3315 | ;; Written by Stefan Monnier circa 2016. Something to move to | ||
| 3316 | ;; minibuffer.el "ASAP" (with all the `eglot--lsp-' replaced by | ||
| 3317 | ;; something else. The very same code already in SLY and stable for a | ||
| 3318 | ;; long time. | ||
| 3319 | |||
| 3320 | ;; This "completion style" delegates all the work to the "programmable | ||
| 3321 | ;; completion" table which is then free to implement its own | ||
| 3322 | ;; completion style. Typically this is used to take advantage of some | ||
| 3323 | ;; external tool which already has its own completion system and | ||
| 3324 | ;; doesn't give you efficient access to the prefix completion needed | ||
| 3325 | ;; by other completion styles. The table should recognize the symbols | ||
| 3326 | ;; 'eglot--lsp-tryc and 'eglot--lsp-allc as ACTION, reply with | ||
| 3327 | ;; (eglot--lsp-tryc COMP...) or (eglot--lsp-allc . (STRING . POINT)), | ||
| 3328 | ;; accordingly. tryc/allc names made akward/recognizable on purpose. | ||
| 3329 | |||
| 3330 | (add-to-list 'completion-styles-alist | ||
| 3331 | '(eglot--lsp-backend-style | ||
| 3332 | eglot--lsp-backend-style-try-completion | ||
| 3333 | eglot--lsp-backend-style-all-completions | ||
| 3334 | "Ad-hoc completion style provided by the completion table.")) | ||
| 3335 | |||
| 3336 | (defun eglot--lsp-backend-style-call (op string table pred point) | ||
| 3337 | (when (functionp table) | ||
| 3338 | (let ((res (funcall table string pred (cons op point)))) | ||
| 3339 | (when (eq op (car-safe res)) | ||
| 3340 | (cdr res))))) | ||
| 3341 | |||
| 3342 | (defun eglot--lsp-backend-style-try-completion (string table pred point) | ||
| 3343 | (eglot--lsp-backend-style-call 'eglot--lsp-tryc string table pred point)) | ||
| 3344 | |||
| 3345 | (defun eglot--lsp-backend-style-all-completions (string table pred point) | ||
| 3346 | (eglot--lsp-backend-style-call 'eglot--lsp-allc string table pred point)) | ||
| 3347 | |||
| 3348 | |||
| 3349 | ;; Local Variables: | ||
| 3350 | ;; bug-reference-bug-regexp: "\\(github#\\([0-9]+\\)\\)" | ||
| 3351 | ;; bug-reference-url-format: "https://github.com/joaotavora/eglot/issues/%s" | ||
| 3352 | ;; checkdoc-force-docstrings-flag: nil | ||
| 3353 | ;; End: | ||
| 3354 | |||
| 3355 | ;;; eglot.el ends here | ||