eglot

I use eglot as my language client since it is available out of the box and integrates nicely with core packages like xref

init.el
emacs r1
(use-package alc-eglot
  :bind (("<f2>" . eglot-rename)
         ("C-c e" . alc-eglot-menu))
  :custom
  (eglot-autoshutdown t)
  (eglot-extend-to-xref t)
  (eglot-events-buffer-config '(:size 0)) ;; I use `lsp-devtools` to inspect traffic.
  :config
  (advice-add 'eglot--workspace-configuration-plist :around #'alc-eglot-fix-workspace-configuration-scope)
  (add-to-list 'display-buffer-alist
               '("\\*EGLOT workspace configuration\\*"
                 (display-buffer-below-selected)
                 (dedicated . t))))

You see advice out there for bumping the default value of read-process-output-max to reduce the overhead of reading output from LSP servers etc. According to the docstring this should not exceed the value of /proc/sys/fs/pipe-max-size

init.el
emacs r1
(setq read-process-output-max
      (max 1048576 ; 1MB
           (string-to-number (with-temp-buffer
                               (ignore-errors (insert-file "/proc/sys/fs/pipe-max-size"))
                               (buffer-string)))))

Transient

It’s nice to have a transient menu from which we can launch things.

lisp/alc-eglot.el
emacs r1
(require 'eglot)
(require 'transient)

(transient-define-prefix alc-eglot-menu ()
  "Transient menu for launching eglot things."
  ["Code Actions"
   ("a" "All"              eglot-code-actions)
   ("i" "Organise Imports" eglot-code-action-organize-imports)
   ("q" "Quickfix"         eglot-code-action-quickfix)]
  ["Server Process"
   ("c" "Config"  eglot-show-workspace-configuration)
   ("E" "Stderr"  eglot-stderr-buffer)
   ("r" "Restart" alc-eglot-restart)])

lsp-devtools

Often I want to wrap some server with the lsp-devtools agent. What’s nice is that we can simply advise the function eglot uses to determine how to connect with the server

lisp/alc-eglot.el
emacs r1
(defun alc-eglot-wrap-server-in-lsp-devtools (oldfun &rest args)
  "Wrap the server config returned from eglot in the lsp-devtools agent."
  (if-let ((lsp-devtools (executable-find "lsp-devtools")))
     (pcase-let ((`(,modes ,project ,class ,contact ,lang-ids) (apply oldfun args)))
       `(,modes ,project ,class ,(append `(,lsp-devtools "agent" "--") contact) ,lang-ids))
    (apply oldfun args)))

I’m sure there’s loads of fun to be had coming up with a nice workflow to determine which projects/servers I wish to wrap at any given time however, for now I just run the following commands to enable/disable wrapping as needed.

emacs r1
(advice-add #'eglot--guess-contact :around #'alc-eglot-wrap-server-in-lsp-devtools)

(advice-remove #'eglot--guess-contact #'alc-eglot-wrap-server-in-lsp-devtools)

Workspace Configuration

Convincing eglot to apply the correct workspace/configuration settings for the current server and project is the single most frustrating part of using it…

One issue is making sure eglot evaluates settings at the right scope

lisp/alc-eglot.el
emacs r1
(require 's)

(defun alc-eglot-fix-workspace-configuration-scope (orig-fun server &optional path)
  "Fix the scopeUri of a workspace/configuration request.

When eglot handles a workspace/configuration request with an
associated scopeUri it uses the `file-name-directory' function to
determine the directory from which to resolve the configuration
values.

This causes an issue for servers like esbonio and pyright which
set the scopeUri to a directory. For `file-name-directory' to
treat the path as a directory it must include a trailing slash, a
convention which these servers do not follow.

Therefore `file-name-directory' treats the path as a file and
removes the final component, causing eglot to resolve the
configuration relative to the wrong directory.

This function fixes the issue by advising the
`eglot--workspace-configuration-plist' function, ensuring that
paths referencing directories include the trailing slash."
  (if (and path
           (file-directory-p path)
           (not (s-ends-with? "/" path)))
      (funcall orig-fun server (concat path "/"))
    (funcall orig-fun server path)))

Utilities

Helper function to (re)start the server in the current buffer.

lisp/alc-eglot.el
emacs r1
(defun alc-eglot-restart ()
  "(Re)start the server in the current buffer."
  (interactive)
  (if (eglot-current-server)
      (call-interactively 'eglot-reconnect)
    (call-interactively 'eglot)))