1
0
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:
João Távora 2023-09-04 01:39:05 +01:00
parent dc171d5efa
commit 59c35bf16f

View File

@ -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