Outside of editing a buffer, one of the most common user interface in Emacs is
completing-read, which allows you to select an item from a list of choices.
(let ((choices '("First" "Second" "Third"))) (completing-read "Choose: " choices))
Which can look like this in the mini buffer area:
However, just about everyone extends this function using Avy, Helm, and others that add fuzzy-matching, regular expressions, more actions:
completing-read can also take more interesting types of lists, like associative lists.
(let ((choices '(("First" . 'first-choice) ("Second" . 'second-choice) ("Third" . 'third-choice)))) (completing-read "Choose: " choices))
Even though it can take such a beastie, it only displays the first element of each tuple, and returns this first element.
I sometimes have a list of identifiers the program needs, but I want to display more user-friendly choices. In other words, in the example above, I would like to display
Second, but have the call return
'second-choice instead. We have always just called
assoc after the fact, as in:
(let ((choices '(("First" . 'first-choice) ("Second" . 'second-choice) ("Third" . 'third-choice)))) (alist-get (completing-read "Choose: " choices) choices nil nil 'equal))
That seems like a bit of extra code when your goal is to add this code as part of the
interactive macro. For instance, this is what I would like to see:
(defvar favorite-hosts '(("Glamdring" . "192.168.5.12") ("Orcrist" . "192.168.5.10") ("Sting" . "192.168.5.220") ("Gungnir" . "192.168.5.25"))) (defun favorite-ssh (hostname) "Start a SSH session to a given HOSTNAME." (interactive (list (alt-completing-read "Host: " favorite-hosts))) (message "Rockin' and rollin' to %s" hostname))
Which would look like this in the minibuffer:
But within the code, the
interactive function would set the
hostname variable to something like
I have found the following
alt-completing-read function to make my code a little more readable.
(defun alt-completing-read (prompt collection &optional predicate require-match initial-input hist def inherit-input-method) "Calls `completing-read' but returns the value from COLLECTION. Simple wrapper around the `completing-read' function that assumes the collection is either an alist, or a hash-table, and returns the _value_ of the choice, not the selected choice. For instance, give a variable of choices like: (defvar favorite-hosts '((\"Glamdring\" . \"192.168.5.12\") (\"Orcrist\" . \"192.168.5.10\") (\"Sting\" . \"192.168.5.220\") (\"Gungnir\" . \"192.168.5.25\"))) We can use this function to `interactive' without needing to call `alist-get' afterwards: (defun favorite-ssh (hostname) \"Start a SSH session to a given HOSTNAME.\" (interactive (list (alt-completing-read \"Host: \" favorite-hosts))) (message \"Rockin' and rollin' to %s\" hostname))" ;; Yes, Emacs really should have an `alistp' predicate to make this code more readable: (cl-flet ((assoc-list-p (obj) (and (listp obj) (consp (car obj))))) (let* ((choice (completing-read prompt collection predicate require-match initial-input hist def inherit-input-method)) (results (cond ((hash-table-p collection) (gethash choice collection)) ((assoc-list-p collection) (alist-get choice collection def nil 'equal)) (t choice)))) (if (listp results) (first results) results))))
The purpose of this essay is actually to explain a bit about programming in Emacs Lisp.
cl-flet allows me to easily define a small helper function inside a function. Yeah, one could use a
lambda, but since Emacs is a Lisp-2, I wanted to call it directly to make it obvious that
assoc-list-p is a predicate similar to
hash-table-p. The gnarly
and is the only way I’ve found to distinguish an associative list from a property or normal list.
Next, it calls
completing-read with all the parameters passed in, and assigns it to a local variable,
choice, that I can use in the following
cond to see how to get the value (based on its type). Notice the final
if call, and we need this because there are two types of associative lists, one where the elements are two-element lists, and the other are (as the examples shown above) joined with
Let’s write some tests to verify to show the possibilities:
(lexical-let ((data-set '(("Associative List, form 1:" . (("First" . first-choice) ("Second" . second-choice) ("Third" . third-choice))) ("Associative List, form 2:" . (("First" first-choice) ("Second" second-choice) ("Third" third-choice))) ("Hash Table:" . #s(hash-table size 3 test equal data ("First" first-choice "Second" second-choice "Third" third-choice))) ("Normal List:" . ("First" "Second" "Third"))))) (dolist (data data-set) (let ((prompt (car data)) (choices (cdr data))) (cl-flet ((completing-read (&rest ignored) "Second")) (message "Choice from %s was %s" prompt (alt-completing-read prompt choices))))))
Why yes, I should mock the
completing-read function in that test case, then this doesn’t have to be interactive.
Reach out to me if you’d like to see more discussion about this.