02 May 2026

Follow-up on switching to eglot

Jan G sent me a two-part comment.

Part one

I was under the impression that when using elpaca you needed to disable use-package, and that when using elpaca-use-package, you were redefining the macro. I’m not 100% sure about this, but the documentation has an example of use-package and how it actually expands to an elpaca command.

I wouldn't know. All I can say is that it would be nice if package managers that hook into, or completely redefines use-package, would document if they deviate from the behaviour of "vanilla use-package" in some way.

Part two

Given that, use-package’s documentation is always going to be a little off, since elpaca is doing everything async. The only way I’ve found to reliably manage some dependencies is to use the elpaca-after-init hook, so they don’t even try to run until elpaca is finished loading everything.

I'd say it sometimes seems like the documentation for use-package is a little off for use-package itself πŸ™‚

The README for Elpaca says that

Add configuration which relies on after-init-hook, emacs-startup-hook, etc to elpaca-after-init-hook so it runs after Elpaca has activated all queued packages.

but that seems like a very big hammer and as I understand it I'd have to move the whole :init block for python-mode into the hook in that case. Playing around with the various blocks for use-package isn't too time consuming and I think it's a good first thing to try.

Tags: emacs
02 May 2026

Secrets when connecting to DBs

I should have dealt with comments I got to my posts on how I deal with secrets in my work notes, here, and here. Better late than never though, I hope.

Comment from Stefano R

The first one is a link to post titled How I use :dbconnection in org files. It describes a nice way of setting sql-connection-alist based on the contents of a file, in his case ~/.pgppass.

Comment from Harald J

The other starts with a function for searching ~/.authinfo.gpg for entries of the form

machine <host>/<dbname> login <username> password <password> port <port>

and then setting sql-password-search-wallet-function and sql-password-wallet to tell sql-mode to use it

(defun my/sql-auth-source-search-wallet (wallet product user server database port)
  "Read auth source WALLET to locate the USER secret.
Sets `auth-sources' to WALLET and uses `auth-source-search' to locate the entry.
The DATABASE and SERVER are concatenated with a slash between them as the
host key."
  (when-let (results (auth-source-search :host (concat server "/" database)
                                         :user user
                                         :port (number-to-string port)))
    (when (and (= (length results) 1)
               (plist-member (car results) :secret))
      (plist-get (car results) :secret))))

(setq sql-password-search-wallet-function #'my/sql-auth-source-search-wallet)
(setq sql-password-wallet "~/.authinfo.gpg")

The value for sql-connection-alist is then as normal

(setq sql-connection-alist
  '((some-dbname (sql-product 'oracle)
                 (sql-port 1521)
                 (sql-server ...)
                 ...))

and the blocks in orgmode looks like this

SRC sql-mode :product oracle :dbconnection i3v1e-ro :results raw
SELECT to_char(sysdate, 'YYYY-MM-DD HH24:ii:ss') AS today,
       to_char(sysdate + 1, 'YYYY-MM-DD HH24:ii:ss') AS tomorrow
FROM dual;
SRC

Thoughts

Both of these feel closer to the intent of sql-mode in a way. I'll have to try using sql-connection-alist at some point.

Tags: emacs
18 Feb 2026

Switching to project.el

I've used projectile ever since I created my own Emacs config. I have a vague memory choosing it because some other package only supported it. (It might have been lsp-mode, but I'm not sure.) Anyway, now that I'm trying out eglot, again, I thought I might as well see if I can switch to project.el, which is included in Emacs nowadays.

A non-VC project marker

Projectile allows using a file, .projectile, in the root of a project. This makes it possible to turn a folder into a project without having to use version control. It's possible to configure project.el to respect more VC markers than what's built-in. This can be used to define a non-VC marker.

(setopt project-vc-extra-root-markers '(".projectile" ".git"))

Since I've set vc-handled-backends to nil (the default made VC interfere with magit, so I turned it off completely) I had to add ".git" to make git repos be recognised as projects too.

Xref history

The first thing to solve was that the xref stack wasn't per project. Somewhat disappointingly there only seems to be two options for xref-history-storage shipped with Emacs

xref-global-history
a single global history (the default)
xref-window-local-history
a history per window

I had the same issue with projectile, and ended up writing my own package for it. For project.el I settled on using xref-project-history.

(use-package xref-project-history
  :ensure (:type git
           :repo "https://codeberg.org/imarko/xref-project-history.git"
           :branch "master")
  :custom
  (xref-history-storage #'xref-project-history))

Jumping between implementation and test

Projectile has a function for jumping between implementation and test. Not too surprisingly it's called projectile-toggle-between-implementation-and-test. I found some old emails in an archive suggesting that project.el might have had something similar in the past, but if that's the case it's been removed by now. When searching for a package I came across this email comparing tools for finding related files. The author mentions two that are included with Emacs

ff-find-other-file
part of find-file.el, which a few other functions and a rather impressive set of settings to customise its behaviour.
find-sibling-file
a newer command, I believe, that also can be customised.

So, there are options, but neither of them are made to work nicely with project.el out of the box. My most complicated use case seems to be in Haskell projects where modules for implementation and test live in separate (mirrored) folder hierarchies, e.g.

src
└── Sider
    └── Data
        β”œβ”€β”€ Command.hs
        β”œβ”€β”€ Pipeline.hs
        └── Resp.hs
test
└── Sider
    └── Data
        β”œβ”€β”€ CommandSpec.hs
        β”œβ”€β”€ PipelineSpec.hs
        └── RespSpec.hs

I'm not really sure how I'd configure find-sibling-rules, which are regular expressions, to deal with folder hierarchies like this. To be honest, I didn't really see a way of configuring ff-find-other-file at first either. Then I happened on a post about switching between a module and its tests in Python. With its help I came up with the following

(defun mes/setup-hs-ff ()
  (when-let* ((proj-root (project-root (project-current)))
              (rel-proj-root (-some--> (buffer-file-name)
                               (file-name-directory it)
                               (f-relative proj-root it)))
              (sub-tree (car (f-split (f-relative (buffer-file-name) proj-root))))
              (search-dirs (--> '("src" "test")
                                (remove sub-tree it)
                                (-map (lambda (p) (f-join proj-root p)) it)
                                (-select #'f-directory? it)
                                (-mapcat (lambda (p) (f-directories p nil t)) it)
                                (-map (lambda (p) (f-relative p proj-root)) it)
                                (-map (lambda (p) (f-join rel-proj-root p)) it))))
    (setq-local ff-search-directories search-dirs
                ff-other-file-alist '(("Spec\\.hs$" (".hs"))
                                      ("\\.hs$" ("Spec.hs"))))))

A few things to note

  1. The order of rules in ff-other-file-alist is important, the first match is chosen.
  2. (buffer-file-name) can, and really does, return nil at times, and file-name-directory doesn't deal with anything but strings.
  3. The entries in ff-search-directories have to be relative to the file in the current buffer, hence the rather involved varlist in the when-let* expression.

With this in place I get the following values for ff-search-directories

src/Sider/Data/Command.hs
("../../../test/Sider" "../../../test/Sider/Data")
test/Sider/Data/CommandSpec.hs
("../../../src/Sider" "../../../src/Sider/Data")

And ff-find-other-file works beautifully.

Conclusion

My setup with project.el now covers everything I used from projectile so I'm fairly confident I'll be happy keeping it.

Tags: emacs project-el
16 Feb 2026

Using advice to limit lsp-ui-doc nuisance

I've switched back to lsp-mode temporarily until I've had time to fix a few things with my eglot setup. Returning prompted me to finally address an irritating behaviour with lsp-ui-doc.

No matter what I set lsp-ui-doc-position to it ends up covering information that I want to see. While waiting for a fix I decided to work around it. It seems to me that this is exactly what advice is for.

I came up with the following to make sure the frame appears on the half of the buffer where point isn't.

(defun my-lsp-ui-doc-wrapper (&rest _)
  (let* ((pos-line (- (line-number-at-pos (point))
                      (line-number-at-pos (window-start))))
         (pos (if (<= pos-line (/ (window-body-height) 2))
                  'bottom
                'top)))
    (setopt lsp-ui-doc-position pos)))

(advice-add 'lsp-ui-doc--move-frame :before #'my-lsp-ui-doc-wrapper)
Tags: emacs lsp-mode
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
Other posts