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.
PROMPTis a string to prompt with; normally it ends in a colon and a space.
COLLECTIONcan be a list of strings, an alist, an obarray or a hash table.
COLLECTIONcan also be a function to do the completion itself.
PREDICATElimits completion to a subset of
completion-boundaries, for more details on completion,
PREDICATE. See also Info node (elisp)Basic Completion for the details about completion, and Info node (elisp)Programmed Completion for expectations from
COLLECTIONwhen it's a function.
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:
The completion function should accept three arguments:
- The string to be completed.
- A predicate function with which to filter possible matches, or ‘nil’ if none. The function should call the predicate for each possible match, and ignore the match if the predicate returns ‘nil’.
A flag specifying the type of completion operation to perform […] This flag may be one of the following values.
- This specifies a request for information about the state of the current completion. The return value should have the form ‘(metadata . ALIST)’, where ALIST is an alist whose elements are described below.
If the flag has any other value, the completion function should return ‘nil’.
The following is a list of metadata entries that a completion function may return in response to a ‘metadata’ flag argument:
- 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,
;; 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--file-name-table. Let's look at
(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
(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.
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
It can then be used like this:
(completing-read "Prompt: " (k//mark-category '("/usr" "/tmp" "/home") 'file))
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))