Switching to Selectrum for incremental narrowing in Emacs

geekery emacs

My Emacs tweaking tends to go in waves. I keep an eye on the Emacs subreddit and the Doom Discord channel on a fairly regular basis, but I try not to jump on every cool new package I see discussed there. Every now and again though, I see something interesting which coincides with an itch to tinker with Emacs, and away I go… This time, it was reading discussions about a constellation of relatively new packages concerned with incrementally narrowing lists (and other related functions) that caught my eye. This sounds very niche, but for many people (me included) being presented with a list of things and being able to type to incrementally narrow the list and then select something is a core part of the Emacs UI. Since I use Doom, and it offers you an easy way to choose either Ivy or Helm, I had been using Ivy, for the sake of easy configuration. Both are fine packages, but having tried both, I preferred Ivy’s more minimal interface, and the fact that it used the minibuffer rather than a buffer for completions. However, it — and the related packages, Counsel and Swiper — are somewhat complex and difficult to get to grips with. I was also not using all the features that they provided, so was curious if I would enjoy using something even simpler. That’s why I tried out Selectrum.

The Selectrum README page on GitHub gives an admirably thorough and clear explanation about the niche that it is trying to fill, and how it compares to other similar packages. What I like about it is that it tries to stick as closely as possible to using Emacs’ standard APIs, so that once you enable it, many standard Emacs commands (like find-file) automatically use Selectrum. It also doesn’t try to do everything, so it really only provides incremental narrowing, leaving sorting and filtering (and actions to operate on the selected candidate) to other packages. This means that you have a bit more package installation and configuration to do up front, but it also means that you can pick and choose which elements you need and leave the others, so you end up with a simpler system. I chose to use the following additional packages:

Prescient.el

prescient.el sorts choices in a more intelligent way, remembering your recent selections and bubbling those to the top of the list. It also enables you to toggle on and off different ways of matching a candidate (for example by the initial letter of each word, or by regular expression). I have found prescient to work very well in practice. It also works with Ivy and Helm and other incremental narrowing packages, so you can use it even if you don’t use Selectrum.

Consult

consult works with any function that works with Emacs’ built-in completing-read function (including Selectrum). It is intended as a rough replacement for counsel, and provides some useful functions for searching a buffer interactively for text, searching headlines (in Org or Markdown files, or any mode that implements outline-mode). It has a very handy function consult-buffer. Despite the name, it is a multi-purpose function which enables you to search and select among a list of buffers, files and bookmarks by default, so you don’t need separate commands to search each of those categories. If you do want to filter the list because you know you are searching for a buffer, not a file, for example, you can prefix your search with ‘b’ (or ‘f’ for file, or ’m’ for bookmark). I’ve found this so handy that I have bound it to Doom’s SPC SPC binding for easy access. I previously often found myself searching for a buffer first, before realising that I needed to search for a file as the buffer containing that file wasn’t yet open. This stops me having to think about the distinction, but allows me to narrow the list if I am sure.

Marginalia

marginalia adds ‘annotations’ to some of the lists presented by Selectrum. For example, if you use M-x it can add key-bindings and also docstrings to the list of commands. Ivy did this too, and I found it extremely useful.

Embark

embark is the package I am least sure about whether I will keep, but it is handy. It enables you to hit a key-binding when you have a candidate selected in a list and apply some action to the item. It knows whether the item is a file or buffer or some other item, so it presents a list of appropriate actions for each, like opening a buffer in the other window, or renaming a file. There are of course many other ways to achieve this in Emacs, but I find that the method of selecting something first, then choosing how to act on it works well for me. It is also really simple to add new actions, and I added one to jump to the dired buffer for a file. That was as simple as the snippet below in my config.el file (swap use-package! for use-package if you are not using Doom):

(use-package! embark
  :after selectrum
  :bind (:map minibuffer-local-map
         ("C-o" . embark-act)
         ("C-S-o" . embark-act-noexit)
         :map embark-file-map
         ("j" . dired-jump)))

Is it better than Ivy/Counsel/Swiper?

This setup doesn’t do anything that that Ivy and friends didn’t. It does some things a bit differently, and in ways I prefer. The consult-outline command presents Org headings in a way I find much easier to navigate, for example. I am going to stick with these packages because I find the smaller, more focused packages easier to understand and configure, and potentially to extend as I need.

The other impressive thing about them is that the authors of each of the packages are talking to each other and trying to make their packages as inter-operable as possible and also as simple and discrete as possible. The Marginalia package was extracted from the Consult and Embark packages (which are maintained by different people), to reduce the overlap between them and ensure that you can mix and match the different components like Lego bricks to build your ideal setup. I find this an admirable position, which helps to keep users’ options open and improve all the tools. I’m going to watch the development of all these packages with interest.