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
Comment here.