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 (see Generics in 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 inactiveRegions experimental feature introduced in version 17 of the clangd C/C++ language server++.
Summarily, the feature works by first having the server detect the Eglot’s advertisement of the 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++ #ifdef macros that the preprocessor removes based on compile-time information.
The language server reports the regions by periodically sending a textDocument/inactiveRegions notification for each managed buffer (see Buffers, Projects, and Eglot). 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.
The first method extends
eglot-client-capabilitiesusing a simple heuristic to detect if current server isclangdand enables theinactiveRegioncapability.emacs-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))Notice we use an internal function of the
jsonrpc.ellibrary, and a regexp search to detectclangd. An alternative would be to define a new EIEIO subclass ofeglot-lsp-server, maybe calledeglot-clangd, so that the method would be simplified:emacs-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))))However, this would require that users tweak
eglot-server-programto tell Eglot instantiate such sub-classes instead of the genericeglot-lsp-server(see 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.
The second method implements
eglot-handle-notificationto process the server notification for the LSP methodtextDocument/inactiveRegions. For each region received it creates an overlay applying theshadowface to the region. Overlays are recreated every time a new notification of this kind is received.To learn about how
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 (see eglot-events-buffer). You could find something like:bash[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))]))This reveals that the
textDocument/inactiveRegionsnotification contains a:textDocumentproperty to designate the managed buffer and an array of LSP regions under the:regionsproperty. Notice how the message (originally in JSON format), is represented as Elisp plists (see 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
&keyarguments. Also, theeglot-uri-to-pathandeglot-range-regionmay be used to easily parse the LSP:uriand: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.
emacs-lisp(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 (remove-overlays nil nil 'inactive-code t) (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) (overlay-put ov 'inactive-code t)))))
After evaluating these two additions and reconnecting to the 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.