mirror of
https://git.savannah.gnu.org/git/emacs.git
synced 2025-01-30 19:53:09 +00:00
Eglot: add new chapter about Elisp extensions to Eglot manual
bug#65418 Co-authored-by: Filippo Argiolas <filippo.argiolas@gmail.com> * doc/misc/eglot.texi (Extending Eglot): New chapter.
This commit is contained in:
parent
dc171d5efa
commit
59c35bf16f
@ -99,6 +99,7 @@ This manual documents how to configure, use, and customize Eglot.
|
||||
* Using Eglot:: Important Eglot commands and variables.
|
||||
* Customizing Eglot:: Eglot customization and advanced features.
|
||||
* Advanced server configuration:: Fine-tune a specific language server
|
||||
* Extending Eglot:: Writing Eglot extensions in Elisp
|
||||
* Troubleshooting Eglot:: Troubleshooting and reporting bugs.
|
||||
* GNU Free Documentation License:: The license for this manual.
|
||||
* Index::
|
||||
@ -1264,6 +1265,154 @@ is serialized by Eglot to the following JSON text:
|
||||
@}
|
||||
@end example
|
||||
|
||||
@node Extending Eglot
|
||||
@chapter Extending Eglot
|
||||
|
||||
Sometimes it may be useful to extend existing Eglot functionality
|
||||
using Elisp its public methods. A good example of when this need may
|
||||
arise is adding support for a custom LSP protocol extension only
|
||||
implemented by a specific server.
|
||||
|
||||
The best source of documentation for this is probably Eglot source
|
||||
code itself, particularly the section marked ``API''.
|
||||
|
||||
Most of the functionality is implemented with Common-Lisp style
|
||||
generic functions (@pxref{Generics,,,eieio,EIEIO}) that can be easily
|
||||
extended or overridden. The Eglot code itself is an example on how to
|
||||
do this.
|
||||
|
||||
The following is a relatively simple example that adds support for the
|
||||
@code{inactiveRegions} experimental feature introduced in version 17
|
||||
of the @command{clangd} C/C++ language server++.
|
||||
|
||||
Summarily, the feature works by first having the server detect the
|
||||
Eglot's advertisement of the @code{inactiveRegions} client capability
|
||||
during startup, whereupon the language server will report a list of
|
||||
regions of inactive code for each buffer. This is usually code
|
||||
surrounded by C/C++ @code{#ifdef} macros that the preprocessor removes
|
||||
based on compile-time information.
|
||||
|
||||
The language server reports the regions by periodically sending a
|
||||
@code{textDocument/inactiveRegions} notification for each managed
|
||||
buffer (@pxref{Eglot and Buffers}). Normally, unknown server
|
||||
notifications are ignored by Eglot, but we're going change that.
|
||||
|
||||
Both the announcement of the client capability and the handling of the
|
||||
new notification is done by adding methods to generic functions.
|
||||
|
||||
@itemize @bullet
|
||||
@item
|
||||
The first method extends @code{eglot-client-capabilities} using a
|
||||
simple heuristic to detect if current server is @command{clangd} and
|
||||
enables the @code{inactiveRegion} capability.
|
||||
|
||||
@lisp
|
||||
(cl-defmethod eglot-client-capabilities :around (server)
|
||||
(let ((base (cl-call-next-method)))
|
||||
(when (cl-find "clangd" (process-command
|
||||
(jsonrpc--process server))
|
||||
:test #'string-match)
|
||||
(setf (cl-getf (cl-getf base :textDocument)
|
||||
:inactiveRegionsCapabilities)
|
||||
'(:inactiveRegions t)))
|
||||
base))
|
||||
@end lisp
|
||||
|
||||
Notice we use an internal function of the @code{jsonrpc.el} library,
|
||||
and a regexp search to detect @command{clangd}. An alternative would
|
||||
be to define a new EIEIO subclass of @code{eglot-lsp-server}, maybe
|
||||
called @code{eglot-clangd}, so that the method would be simplified:
|
||||
|
||||
@lisp
|
||||
(cl-defmethod eglot-client-capabilities :around ((_s eglot-clangd))
|
||||
(let ((base (cl-call-next-method)))
|
||||
(setf (cl-getf (cl-getf base :textDocument)
|
||||
:inactiveRegionsCapabilities)
|
||||
'(:inactiveRegions t))))
|
||||
@end lisp
|
||||
|
||||
However, this would require that users tweak
|
||||
@code{eglot-server-program} to tell Eglot instantiate such sub-classes
|
||||
instead of the generic @code{eglot-lsp-server} (@pxref{Setting Up LSP
|
||||
Servers}). For the purposes of this particular demonstration, we're
|
||||
going to use the more hacky regexp route which doesn't require that.
|
||||
|
||||
Note, however, that detecting server versions before announcing new
|
||||
capabilities is generally not needed, as both server and client are
|
||||
required by LSP to ignore unknown capabilities advertised by their
|
||||
counterparts.
|
||||
|
||||
@item
|
||||
The second method implements @code{eglot-handle-notification} to
|
||||
process the server notification for the LSP method
|
||||
@code{textDocument/inactiveRegions}. For each region received it
|
||||
creates an overlay applying the @code{shadow} face to the region.
|
||||
Overlays are recreated every time a new notification of this kind is
|
||||
received.
|
||||
|
||||
To learn about how @command{clangd}'s special JSONRPC notification
|
||||
message is structured in detail you could consult that server's
|
||||
documentation. Another possibility is to evaluate the first
|
||||
capability-announcing method, reconnect to the server and peek in the
|
||||
events buffer (@pxref{Eglot Commands, eglot-events-buffer}). You
|
||||
could find something like:
|
||||
|
||||
@lisp
|
||||
[server-notification] Mon Sep 4 01:10:04 2023:
|
||||
(:jsonrpc "2.0" :method "textDocument/inactiveRegions" :params
|
||||
(:textDocument
|
||||
(:uri "file:///path/to/file.cpp")
|
||||
:regions
|
||||
[(:start (:character 0 :line 18)
|
||||
:end (:character 58 :line 19))
|
||||
(:start (:character 0 :line 36)
|
||||
:end (:character 1 :line 38))]))
|
||||
@end lisp
|
||||
|
||||
This reveals that the @code{textDocument/inactiveRegions} notification
|
||||
contains a @code{:textDocument} property to designate the managed
|
||||
buffer and an array of LSP regions under the @code{:regions} property.
|
||||
Notice how the message (originally in JSON format), is represented as
|
||||
Elisp plists (@pxref{JSONRPC objects in Elisp}).
|
||||
|
||||
The Eglot generic function machinery will automatically destructure
|
||||
the incoming message, so these two properties can simply be added to
|
||||
the new method's lambda list as @code{&key} arguments. Also, the
|
||||
@code{eglot-uri-to-path} and@code{eglot-range-region} may be used to
|
||||
easily parse the LSP @code{:uri} and @code{:start ... :end ...}
|
||||
objects to obtain Emacs objects for file names and positions.
|
||||
|
||||
The remainder of the implementation consists of standard Elisp
|
||||
techniques to loop over arrays, manage buffers and overlays.
|
||||
|
||||
@lisp
|
||||
(defvar-local eglot-clangd-inactive-region-overlays '())
|
||||
|
||||
(cl-defmethod eglot-handle-notification
|
||||
(_server (_method (eql textDocument/inactiveRegions))
|
||||
&key regions textDocument &allow-other-keys)
|
||||
(if-let* ((path (expand-file-name (eglot-uri-to-path
|
||||
(cl-getf textDocument :uri))))
|
||||
(buffer (find-buffer-visiting path)))
|
||||
(with-current-buffer buffer
|
||||
(mapc #'delete-overlay eglot-clangd-inactive-region-overlays)
|
||||
(cl-loop
|
||||
for r across regions
|
||||
for (beg . end) = (eglot-range-region r)
|
||||
for ov = (make-overlay beg end)
|
||||
do
|
||||
(overlay-put ov 'face 'shadow)
|
||||
(push ov eglot-clangd-inactive-region-overlays)))))
|
||||
@end lisp
|
||||
|
||||
@end itemize
|
||||
|
||||
After evaluating these two additions and reconnecting to the
|
||||
@command{clangd} language server (version 17), the result will be that
|
||||
all the inactive code in the buffer will be nicely grayed out using
|
||||
the LSP server knowledge about current compile time preprocessor
|
||||
defines.
|
||||
|
||||
@node Troubleshooting Eglot
|
||||
@chapter Troubleshooting Eglot
|
||||
@cindex troubleshooting Eglot
|
||||
|
Loading…
Reference in New Issue
Block a user