An Alternate Completing Read

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:

alt-completing-read-01.png

However, just about everyone extends this function using Avy, Helm, and others that add fuzzy-matching, regular expressions, more actions:

alt-completing-read-02.png

The 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:

alt-completing-read-03.png

But within the code, the interactive function would set the hostname variable to something like 192.168.5.20.

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.

Notice 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 cons.

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.

Date: 2020-12-22 December

Created: 2020-12-23 Wed 10:35