Posts tagged "eglot":

25 Jan 2026

More on the switch to eglot

Since the switching to eglot I've ended up making a few related changes.

Replacing flycheck with flymake

Since eglot it's written to work with other packages in core, which means it integrates with flymake. The switch comprised

  • Use :ensure nil to make sure elpaca knows there's nothing to download.
  • Add a call to flymake-mode to prog-mode-hook.
  • Define two functions to toggle showing a list of diagnostics for the current buffer and the project.
  • Redefine the relevant keybindings.

The two functions for toggling showing diagnostics look like this

(defun mes/toggle-flymake-buffer-diagnostics ()
  (interactive)
  (if-let* ((window (get-buffer-window (flymake--diagnostics-buffer-name))))
      (save-selected-window (quit-window nil window))
    (flymake-show-buffer-diagnostics)))

(defun mes/toggle-flymake-project-diagnostics ()
  (interactive)
  (if-let* ((window (get-buffer-window (flymake--project-diagnostics-buffer (projectile-project-root)))))
      (save-selected-window (quit-window nil window))
    (flymake-show-project-diagnostics)))

And the changed keybindings are

flycheck flymake
flycheck-next-error flymake-goto-next-error
flycheck-previous-error flymake-goto-prev-error
mes/toggle-flycheck-error-list mes/toggle-flymake-buffer-diagnostics
mes/toggle-flycheck-projectile-error-list mes/toggle-flymake-project-diagnostics

Using with-eval-after-load instead of :after eglot

When it comes to use-package I keep on being surprised, and after the switch to elpaca I've found some new surprises. One of them was that using :after eglot like this

(use-package haskell-ng-mode
  :afer eglot
  :ensure (:type git
           :repo "git@gitlab.com:magus/haskell-ng-mode.git"
           :branch "main")
  :init
  (add-to-list 'major-mode-remap-alist '(haskell-mode . haskell-ng-mode))
  (add-to-list 'eglot-server-programs '(haskell-ng-mode "haskell-language-server-wrapper" "--lsp"))
  (setq-default eglot-workspace-configuration
                (plist-put eglot-workspace-configuration
                           :haskell
                           '(:formattingProvider "fourmolu"
                             :plugin (:stan (:global-on :json-false)))))
  ...
  :hook
  (haskell-ng-mode . eglot-ensure)
  ...)

would delay initialisation until after eglot had been loaded. However, it turned out that nothing in :init ... seemed to run and upon opening a haskell file no mode was loaded.

After a bit of thinking and tinkering I got it working by removing :after eglot and using with-eval-after-load

(use-package haskell-ng-mode
  :ensure (:type git
           :repo "git@gitlab.com:magus/haskell-ng-mode.git"
           :branch "main")
  :init
  (add-to-list 'major-mode-remap-alist '(haskell-mode . haskell-ng-mode))
  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs '(haskell-ng-mode "haskell-language-server-wrapper" "--lsp"))
    (setq-default eglot-workspace-configuration
                  (plist-put eglot-workspace-configuration
                             :haskell
                             '(:formattingProvider "fourmolu"
                               :plugin (:stan (:global-on :json-false))))))
  ...
  :hook
  (haskell-ng-mode . eglot-ensure)
  ...)

That change worked for haskell, and it seemed to work for python too, but after a little while I realised that python needed a bit more attention.

Getting the configuration for Python to work properly

The python setup looked like this

(use-package python
  :init
  (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))
  (with-eval-after-load 'eglot
    (assoc-delete-all '(python-mode python-ts-mode) eglot-server-programs)
    (add-to-list 'eglot-server-programs
                 `((python-mode python-ts-mode) . ,(eglot-alternatives
                                                    '(("rass" "python") "pylsp")))))
  ...
  :hook (python-ts-mode . eglot-ensure)
  ...)

and it worked all right, but then I visited the package (using elpaca-visit) and realised that the downloaded package was all of emacs. That's a bit of overkill, I'd say.

However, adding :ensure nil didn't have the expected effect of just using the version that's in core. Instead the whole configuration seemed to never take effect and again I was back to the situation where I had to jump to python-ts-mode manually.

The documentation for use-package says that :init is for

Code to run before PACKAGE-NAME has been loaded.

but I'm guessing "before" isn't quite before enough. Then I noticed :preface with the description

Code to be run before everything except :disabled; this can be used to define functions for use in :if, or that should be seen by the byte-compiler.

and yes, "before everything" is early enough. The final python configuration looks like this

(use-package python
  :ensure nil
  :preface
  (add-to-list 'major-mode-remap-alist '(python-mode . python-ts-mode))
  :init
  (with-eval-after-load 'eglot
    (assoc-delete-all '(python-mode python-ts-mode) eglot-server-programs)
    (add-to-list 'eglot-server-programs
                 `((python-mode python-ts-mode) . ,(eglot-alternatives
                                                    '(("rass" "python") "pylsp")))))
  ...
  :hook (python-ts-mode . eglot-ensure)
  ...)

Closing remark

I'm still not sure I have the correct intuition about how to use use-package, but hopefully it's more correct now than before. I have a growing suspicion that use-package changes behaviour based on the package manager I use. Or maybe it's just that some package managers make use-package more forgiving of bad use.

Tags: eglot emacs
19 Jan 2026

Trying eglot, again

I've been using lsp-mode since I switched to Emacs several years ago. When eglot made into Emacs core I used it very briefly but quickly switched back. Mainly I found eglot a bit too bare-bones; I liked some of the bells and whistles of lsp-ui. Fast-forward a few years and I've grown a bit tired of those bells and whistles. Specifically that it's difficult to make lsp-ui-sideline and lsp-ui-doc work well together. lsp-ui-sidedline is shown on the right side, which is good, but combining it with lsp-ui-doc leads to situations where the popup covers the sideline. What I've done so far is centre the line to bring the sideline text out. I was playing a little bit with making the setting of lsp-ui-doc-position change depending on the location of the current position. It didn't work that well though so I decided to try to find a simpler setup. Instead of simplifying the setup of lsp-config I thought I'd give eglot another shot.

Basic setup

I removed the statements pulling in lsp-mode, lsp-ui, and all language-specific packages like lsp-haskell. Then I added this to configure eglot

(use-package eglot
  :ensure nil
  :custom
  (eglot-autoshutdown t)
  (eglot-confirm-server-edits '((eglot-rename . nil)
                                (t . diff))))

The rest was mainly just switching lsp-mode functions for eglot functions.

lsp-mode function eglot function
lsp-deferred eglot-ensure
lsp-describe-thing-at-point eldoc
lsp-execute-code-action eglot-code-actions
lsp-find-type-definition eglot-find-typeDefinition
lsp-format-buffer eglot-format-buffer
lsp-format-region eglot-format
lsp-organize-imports eglot-code-action-organize-imports
lsp-rename eglot-rename
lsp-workspace-restart eglot-reconnect
lsp-workspace-shutdown eglot-shutdown

I haven't verified that the list is fully correct yet, but it looks good so far.

The one thing I might miss is lenses, and using lsp-avy-lens. However, everything that I use lenses for can be done using actions, and to be honest I don't think I'll miss the huge lens texts from missing type annotations in Haskell.

Configuration

One good thing about lsp-mode's use of language-specific packages is that configuration of the various servers is performed through functions. This makes it easy to discover what options are available, though it also means not all options may be available. In eglot configuration is less organised, I have to know about the options for each language server and put the options into eglot-workspace-configuration myself. It's not always easy to track down what options are available, and I've found no easy way to verify the settings. For instance, with lsp-mode I configures HLS like this

(lsp-haskell-formatting-provider "fourmolu")
(lsp-haskell-plugin-stan-global-on nil)

which translates to this for eglot

(setq-default eglot-workspace-configuration
              (plist-put eglot-workspace-configuration
                         :haskell
                         '(:formattingProvider "fourmolu"
                           :plugin (:stan (:global-on :json-false)))))

and I can verify that this configuration has taken effect because I know enough about the Haskell tools.

I do some development in Python and I used to configure pylsp like this

(lsp-pylsp-plugins-mypy-enabled t)
(lsp-pylsp-plugins-ruff-enabled t)

which I think translates to this for eglot

(setq-default eglot-workspace-configuration
              (plist-put eglot-workspace-configuration
                         :pylsp
                         '(:plugins (:ruff (:enabled t)
                                     :mypy (:enabled t)))))

but I don't know any convenient way of verifying these settings. I'm simply not familiar enough with the Python tools. I can check the value of eglot-workspace-configuration by inspecting it or calling eglot-show-workspace-configuration but is there really no way of asking the language server for its active configuration?

Closing remark

The last time I gave up on eglot very quickly, probably too quickly to be honest. I made these changes to my configuration over the weekend, so the real test of eglot starts when I'm back in the office. I have a feeling I'll stick to it longer this time.

Tags: eglot emacs lsp-mode
Other posts