Wandering Thoughts archives

2023-10-17

(Minibuffer) completion categories in GNU Emacs and forcing them

Minibuffer completion is the most common type of completion in Emacs and it's invoked in all sorts of situations and thus to complete all sorts of different things. As part of this, Emacs completion has the concept of a completion category that can be used to customize aspects of completion in both basic Emacs (eg, also) and in and for third party packages like vertico (eg) and orderless. My personal experience is that this customization can be very useful to make me happy with third party packages; the default vertico experience is not what I want in some types of frequently used completions.

(Vertico can customize things on a per-command basis, but this can get tedious if you have a bunch of commands that all complete the same sort of thing and you want to adjust in the same way. And you can't adjust Emacs completion styles on a per-command basis in stock Emacs.)

In Emacs Lisp code you may write, the most straightforward way to do minibuffer completion is using completing-read with a big list of all of your completion choices. Often this is the most useful form as well, partly because it allows extensions like orderless to act at their most powerful, with a full view of all possible completion candidates. Unfortunately, when you invoke completing-read this way, as far as I know there is no normal way to provide a completion category. You can only provide a completion category through programmed completion, where you provide a completion function instead of a collection of choices and one of the things your completion function does is return completion metadata, including the category.

If we want to force the completion category anyway, the way I've found to do this (researched from marginalia) is to hook into the internals of the completion functions with advice-add. Specifically we need to hook into completion-metadata-get, which is what completion uses to extract a particular metadata property from a (nominal) blob of metadata:

(defvar cks/completion-category nil "Forced completion category.")
(defun cks/completion-force-category (metadata prop)
  (if (and cks/completion-category (eq prop 'category))
      cks/completion-category))

(advice-add 'completion-metadata-get :before-until
            'cks/completion-force-category)

;; used this way:
(defun cks/some-completion (msg)
  (let ((cks/completion-category 'my-special-category))
    (completing-read msg list-of-stuff ...)))

(I'm not sure where I picked up '<name>/' symbol name prefixes as a way to avoid name conflicts, or if it's the accepted Emacs style these days.)

As is traditional, we dynamically set the value we want for our (forced) completion category to something and then invoke completing-read. The dynamically scoped value will pass through to our added advice and be returned as the category value (overriding any category that's already in the metadata, because that's easier to code).

Possibly there's already a standard Emacs Lisp way of providing or setting the category of a basic completing-read. If not, my personal view is that there should be (and maybe there will be someday). Completion categories are neat and useful, so it should be easy to use them. In the mean time, well, this approach works for me in Emacs 29.1.

Sometimes you may have a more sophisticated completion environment where there's already a special completion function and some existing elisp code that calls it, but the special completion function doesn't implement providing metadata to the completion system. In that case you can advice-add the special completion function, which is simpler and normally doesn't need a special variable:

(defun cks/mh-folder-complete-note (name predicate flag)
  (if (eq flag 'metadata)
      '(metadata (category . mh-e-folder))
    nil))

(advice-add 'mh-folder-completion-function :before-until
            'cks/mh-folder-complete-note)

(You don't need to explicitly return nil, but this particular bit of code was written before I had internalized some bits of standard Lisp behavior.)

PS: In real usage these functions should have docstrings and perhaps comments, but I've omitted them for space reasons.

PPS: Since I looked up the code, a possible alternate approach would be to advice-add the completion-metadata function, which is what completion calls to obtain the metadata from a completion function in programmed completion. Getting the format right is up to you; see the Lisp code for completion-metadata.

Sidebar: marginalia and its category overrides

In theory marginalia has its own system for setting completion categories based on various things, including the current command. 'Command' here is Emacs jargon for the interactive function that was directly invoked either through a key binding or through M-x (and specifically marginalia bases this on the value of this-command, which is probably obvious to experienced Emacs people). Unfortunately, I was unable to get this marginalia feature to affect the completion category as recognized by vertico, so I eventually resorted to brute force (which did work with vertico).

Also, this way I don't have to maintain and update a list of all of my commands that call my core completion function. I can just modify the core completion function itself and automatically cover any future use of it I add as I think of more possibilities.

programming/EmacsCompletionForcingCategories written at 22:36:53;


Page tools: See As Normal.
Search:
Login: Password:

This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.