avatar

Functions passed to completing-read do more than just returning a list to select


Published
Tags
emacs-lisp

completing-read accepts a function as the collection of candidates, not just lists. I've always thought of this as a small niche that's only useful for dynamic collections.

(completing-read PROMPT COLLECTION &optional PREDICATE REQUIRE-MATCH INITIAL-INPUT HIST DEF INHERIT-INPUT-METHOD)

Read a string in the minibuffer, with completion.

Last night I was trying out Embark and Marginalia after reading Fifteen ways to use Embark, and I learned that the built in completion system actually supports specifying categories. The built in completion system knows that find-file is looking for files, and describe-minor-mode is looking for a list of minor modes; Marginalia makes use of this to decide what annotations to show, while Embark makes use of it to decide which context menu to show.

It is this last fact that made me want to make use of these categories in Canrylog and my Org-roam v1 fork. For instance, org-roam-find-file only shows note titles and tags, but I want it to show file paths as well. To do this, I had to first figure out how to mark categories for my candidates.

The Info nodes referenced in the completing-read docstring provides some help:

Programmed Completion

[…]

The completion function should accept three arguments:

The following is a list of metadata entries that a completion function may return in response to a ‘metadata’ flag argument:

‘category’

The value should be a symbol describing what kind of text the completion function is trying to complete. If the symbol matches one of the keys in ‘completion-category-overrides’, the usual completion behavior is overridden. *Note Completion Variables::.

[…]

This means when collection is a function, Emacs will actually probe it for more information, including the category above.

Digging into find-file's collection functions

There are some builtin examples of this. For instance, find-file:

;; in `find-file'
(interactive
 (find-file-read-args "Find file: "
                      (confirm-nonexistent-file-or-buffer)))
(defun find-file-read-args (prompt mustmatch)
  (list (read-file-name prompt nil default-directory mustmatch)
	t))
(defun read-file-name (prompt &optional dir default-filename mustmatch initial predicate)
  ;; ...
  (funcall (or read-file-name-function #'read-file-name-default)
           ;; ...
           ))
;; in `read-file-name-default'
(completing-read prompt 'read-file-name-internal
                 pred mustmatch insdef
                 'file-name-history default-filename)))

Ah, here's the call to completing-read. The collection function is read-file-name-internal, which combines completion--embedded-envvar-table and completion--file-name-table. Let's look at completion--file-name-table:

(defalias 'completion--file-name-table
  (completion-table-with-quoting #'completion-file-name-table
                                 #'substitute-in-file-name
                                 #'completion--sifn-requote))

Another combined collection function. Let's look at completion-file-name-table then:

(defun completion-file-name-table (string pred action)
  "Completion table for file names."
  (condition-case nil
      (cond
       ((eq action 'metadata) '(metadata (category . file)))
       ((string-match-p "\\`~[^/\\]*\\'" string))
       ;; ...
       )))

There we go. The '(metadata (category . file)) is exactly the thing I was looking for; this is how you attach a category to a collection.

As an aside, these functions seem to be referred to as completion tables.

Applying this

So when a collection for completing-read is a function, it's able to provide some metadata, including its category. But most of the time we have a fixed list of candidates to select from. What should we do?

We can just do this:

(defun k//mark-category (seq category)
  "Mark SEQ as being in CATEGORY."
  (lambda (str pred flag)
    (pcase flag
      ('metadata
       `(metadata (category . ,category)))
      (_
       (all-completions str seq pred)))))

This function returns a completion table that responds to a probe of its category appropriately, as well as handing the actual completion to all-completions.

It can then be used like this:

(completing-read "Prompt: "
                 (k//mark-category '("/usr" "/tmp" "/home") 'file))
/20211008T062042+0900.png
Marginalia read the category, then showed the appropriate annotations for files.

For a list of existing categories, if Marginalia is installed, it can be seen in the variable marginalia-annotator-registry. These are the existing values on my installation:

'((command marginalia-annotate-command marginalia-annotate-binding builtin none)
  (embark-keybinding marginalia-annotate-embark-keybinding builtin none)
  (customize-group marginalia-annotate-customize-group builtin none)
  (variable marginalia-annotate-variable builtin none)
  (function marginalia-annotate-function builtin none)
  (face marginalia-annotate-face builtin none)
  (color marginalia-annotate-color builtin none)
  (unicode-name marginalia-annotate-char builtin none)
  (minor-mode marginalia-annotate-minor-mode builtin none)
  (symbol marginalia-annotate-symbol builtin none)
  (environment-variable marginalia-annotate-environment-variable builtin none)
  (input-method marginalia-annotate-input-method builtin none)
  (coding-system marginalia-annotate-coding-system builtin none)
  (charset marginalia-annotate-charset builtin none)
  (package marginalia-annotate-package builtin none)
  (imenu marginalia-annotate-imenu builtin none)
  (bookmark marginalia-annotate-bookmark builtin none)
  (file marginalia-annotate-file builtin none)
  (project-file marginalia-annotate-project-file builtin none)
  (buffer marginalia-annotate-buffer builtin none)
  (consult-multi marginalia-annotate-consult-multi builtin none))