From 2a66334bada5173f351e452f40d4289b5e06f04c Mon Sep 17 00:00:00 2001 From: =?utf8?q?Jo=C3=A3o=20T=C3=A1vora?= Date: Mon, 4 Sep 2023 01:39:05 +0100 Subject: [PATCH] Eglot: add new chapter about Elisp extensions to Eglot manual bug#65418 Co-authored-by: Filippo Argiolas * doc/misc/eglot.texi (Extending Eglot): New chapter. --- doc/misc/eglot.texi | 149 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) diff --git a/doc/misc/eglot.texi b/doc/misc/eglot.texi index 3338756c63c..89813a17944 100644 --- a/doc/misc/eglot.texi +++ b/doc/misc/eglot.texi @@ -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 -- 2.39.2