aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lisp/progmodes/eglot.el3355
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'.
106Each element of ALTERNATIVES is a string PROGRAM or a list of
107strings (PROGRAM ARGS...) where program names an LSP server
108program to start with ARGS. Returns a function of one argument.
109When invoked, that function will return a list (ABSPATH ARGS),
110where ABSPATH is the absolute path of the PROGRAM that was
111chosen (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/\
159language-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.
202An association list of (MAJOR-MODE . CONTACT) pairs. MAJOR-MODE
203identifies the buffers that are to be managed by a specific
204language server. The associated CONTACT specifies how to connect
205to a server for those buffers.
206
207MAJOR-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
219CONTACT 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.
282If t, always reconnect automatically (not recommended). If nil,
283never reconnect automatically after unexpected server shutdowns,
284crashes or network failures. A positive integer number says to
285only autoreconnect if the previous successful connection attempt
286lasted 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.
292If nil, never time out."
293 :type 'number)
294
295(defcustom eglot-sync-connect 3
296 "Control blocking of LSP connection attempts.
297If t, block for `eglot-connect-timeout' seconds. A positive
298integer number means block for that many seconds, and then wait
299for the connection in the background. nil has the same meaning
300as 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.
314If a number, don't let the buffer grow larger than that many
315characters. If 0, don't use an event's buffer at all. If nil,
316let the buffer grow forever.
317
318For changes on this variable to take effect on a connection
319already started, you need to restart the connection. That can be
320done 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.
339This 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
424INTERFACE-NAME is a symbol designated by the spec as
425\"interface\". INTERFACE is a list (REQUIRED OPTIONAL) where
426REQUIRED and OPTIONAL are lists of KEYWORD designating field
427names that must be, or may be, respectively, present in a message
428adhering to that interface. KEY can be a keyword or a cons (SYM
429TYPE), where type is used by `cl-typep' to check types at
430runtime.
431
432Here'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
445Value is a list of symbols (if the list is empty, no checks are
446performed).
447
448If the symbol `disallow-non-standard-keys' is present, an error
449is raised if any extraneous fields are sent by the server. At
450compile-time, a warning is raised if a destructuring spec
451includes such a field.
452
453If the symbol `enforce-required-keys' is present, an error is
454raised if any required fields are missing from the message sent
455from the server. At compile-time, a warning is raised if a
456destructuring spec doesn't use such a field.
457
458If the symbol `enforce-optional-keys' is present, nothing special
459happens at run-time. At compile-time, a warning is raised if a
460destructuring spec doesn't use all optional fields.
461
462If the symbol `disallow-unknown-methods' is present, Eglot warns
463on 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.
542VARS is ([(INTERFACE)] SYMS...)
543Honour `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.
570Honour `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.
577CLAUSES is a list (DESTRUCTURE FORMS...) where DESTRUCTURE is
578treated 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.
809Interactively, read SERVER from the minibuffer unless there is
810only one and it's managing the current buffer.
811
812Forcefully quit it if it doesn't respond within TIMEOUT seconds.
813TIMEOUT defaults to 1.5 seconds. Don't leave this function with
814the server still running.
815
816If PRESERVE-BUFFERS is non-nil (interactively, when called with a
817prefix argument), do not kill events and output buffers of
818SERVER."
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.
834PRESERVE-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.
878Return (LANGUAGE-ID . CONTACT-PROXY). If not specified,
879LANGUAGE-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'.
898Return (MANAGED-MODE PROJECT CLASS CONTACT LANG-ID). If INTERACTIVE is
899non-nil, maybe prompt user, else error as soon as something can't
900be 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.
976This relies on `project-current' and thus on
977`project-find-functions'. Functions in the latter
978variable (which see) can query the value `eglot-lsp-context' to
979decide whether a given directory is a project containing a
980suitable 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
989The LSP server of CLASS is started (or contacted) via CONTACT.
990If this operation is successful, current *and future* file
991buffers of MANAGED-MAJOR-MODE inside PROJECT become \"managed\"
992by the LSP server, meaning information about their contents is
993exchanged periodically to provide enhanced code-analysis via
994`xref-find-definitions', `flymake-mode', `eldoc-mode',
995`completion-at-point', among others.
996
997Interactively, the command attempts to guess MANAGED-MAJOR-MODE
998from current buffer, CLASS and CONTACT from
999`eglot-server-programs' and PROJECT from
1000`project-find-functions'. The search for active projects in this
1001context binds `eglot-lsp-context' (which see).
1002
1003If it can't guess, the user is prompted. With a single
1004\\[universal-argument] prefix arg, it always prompt for COMMAND.
1005With two \\[universal-argument] prefix args, also prompts for
1006MANAGED-MAJOR-MODE.
1007
1008PROJECT is a project object as returned by `project-current'.
1009
1010CLASS is a subclass of `eglot-lsp-server'.
1011
1012CONTACT specifies how to contact the server. It is a
1013keyword-value plist used to initialize CLASS or a plain list as
1014described in `eglot-server-programs', which see.
1015
1016LANGUAGE-ID is the language ID string to send to the server for
1017MANAGED-MAJOR-MODE, which matters to a minority of servers.
1018
1019INTERACTIVE 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.
1032INTERACTIVE 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.
1061Use 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
1088That is before a connection was established. Use
1089`eglot-connect-hook' to hook into when a connection was
1090successfully established and the server on the other side has
1091received the initializing configuration.
1092
1093Each 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.
1115This 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 \
1239in 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.
1274Return a cons of two process objects (CONNECTION . INFERIOR).
1275Name both based on NAME.
1276CONNECT-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
1343This is the inverse operation of
1344`eglot-move-to-column-function' (which see). It is a function of
1345no arguments returning a column number. For buffers managed by
1346fully 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.
1352LBP 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
1369According to the standard, LSP column/character offsets are based
1370on a count of UTF-16 code units, not actual visual columns. So
1371when LSP says position 3 of a line containing just \"aXbc\",
1372where X is a multi-byte character, it actually means `b', not
1373`c'. However, many servers don't follow the spec this closely.
1374
1375For buffers managed by fully LSP-compliant servers, this should
1376be 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.
1405If 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.
1458Doubles 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.
1486You could add, for instance, the symbol
1487`:documentHighlightProvider' to prevent automatic highlighting
1488under 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.
1529If 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.
1537If DONT-IF-JUST-THE-ONE and there's only one server, don't prompt
1538and 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.
1584Each element is a string, a symbol, or a regexp which is matched
1585against a variable's name. Examples include the string
1586\"company\" or the symbol `xref'.
1587
1588Before Eglot starts \"managing\" a particular buffer, it
1589opinionatedly sets some peripheral Emacs facilities, such as
1590Flymake, Xref and Company. These overriding settings help ensure
1591consistent Eglot behaviour and only stay in place until
1592\"managing\" stops (usually via `eglot-shutdown'), whereupon the
1593previous settings are restored.
1594
1595However, if you wish for Eglot to stay out of a particular Emacs
1596facility that you'd like to keep control of add an element to
1597this list and Eglot will refrain from setting it.
1598
1599For 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.
1620Use `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
1720If 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'.
1823Uses 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, \
1877still 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'.
1921COMMAND 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'.
2028THINGS 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'.
2149Records 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
2203This variable's value should be a plist (SECTION VALUE ...).
2204SECTION is a keyword naming a parameter section relevant to a
2205particular server. VALUE is a plist or a primitive type
2206converted to JSON also understood by that server.
2207
2208Instead of a plist, an alist ((SECTION . VALUE) ...) can be used
2209instead, but this variant is less reliable and not recommended.
2210
2211This variable should be set as a directory-local variable. See
2212See info node `(emacs)Directory Variables' for various ways to to
2213that.
2214
2215Here's an example value that establishes two sections relevant to
2216the 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
2223The value of this variable can also be a unary function of a
2224single argument, which will be a connected `eglot-lsp-server'
2225instance. The function runs with `default-directory' set to the
2226root of the current project. It should return an object of the
2227format 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.
2264When 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.
2370Calls REPORT-FN (or arranges for it to be called) when the server
2371publishes diagnostics. Between calls to this function, REPORT-FN
2372may 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.
2416Try 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.
2452If 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.
2606If either BEG or END is nil, format entire buffer.
2607Interactively, format active region, or entire buffer if region
2608is not active.
2609
2610If non-nil, ON-TYPE-FORMAT is a character just inserted at BEG
2611for 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'.
2961Returns 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.
3103If ACTION-KIND is nil, consider all kinds of actions.
3104Interactively, default BEG and END to region's bounds else BEG is
3105point and END is nil, which results in a request for code actions
3106at 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.
3253If 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