Emacs Lisp: Prompting for new file creation, part 3

This is all about working towards a way to handle interactive input of a perl module name and path, as described at the end of Part II. We want to get these two pieces of information from the user with a single prompt, something like:

   /home/doom/lib/Text::Munger::Fancy

Emacs is full of lower-level functions you can use to implement your own variations of the higher-level functions, so I didn't anticipate a huge amount of trouble in solving this one.

A false lead: completing-read

The first problem I encountered was that The GNU Emacs Lisp Reference Manual seemed to me to imply that the "completing-read" function was the key. Typically, you feed completing-read an alist of possible completions, but it does have a customization feature: you can pass it the symbol for a function that let's you implement some sort of "virtual alist", and "completely take control" of the response from "completing-read".

The documentation for this stuff looks complete, but on close inspection has many gaps. For one thing, it's pretty clearly in an odd order, and I went back and forth through Chapter 20 ("Minibuffers") many times while trying to figure this out.

For another thing: what values do you need to supply to the keys of this alist of completions? The examples all seem to use serial integers, but nothing explains if this is required. (In all of my experiments, I was careful to always supply an alist of strings with associated interger values sorted in order -- which meant using "reverse" a lot: cargo-cult programming at it's finest.)

So, when you're having trouble understanding the emacs docs, what do you do? Look at the source! But completing-read, as well as read-file-name, are built-in functions, and I wasn't willing to delve into the c-code. Okay, so let's look through the installed code base for examples of using completing-read... but that turns out to be harder than you'd think. Grepping for "completing-read" turns up the simple form, more often than not, using an ordinary alist, rather than a "virtual" one. It occured to me after a while that it wasn't hard to write a pattern that would look for the more complex case only ("completing-read" followed by white space and then an apostrophe), but that turned up very few hits... Maybe no one actually uses this feature? Maybe it's not actually good for anything?

Eventually, playing around with some simple examples, I concluded that there wasn't really any way to get completing-read to do what I needed to do... if you hit space or tab in the minibuffer while doing a completing read, you were kicked off into the standard bindings for those keys, and control never gets returned to completing-read.

The workable solution: read-from-minibuffer

So the actual solution to this customization problem, is to change the binding of SPC and TAB, so that they call routines that work the way you want them to... and we'll do the read with a primitive one step down the chain from completing-read: read-from-minibuffer.

The important feature of read-from-minibuffer, is that it let's you enter a custom keymap as an argument. Building a keymap isn't all that difficult, but a lot of the examples you see in The GNU Emacs Lisp Reference Manual do it in a somewhat archaic way. I settled on doing it like this:

(defvar perlnow-read-minibuffer-map
   (let ((map (make-sparse-keymap)))
     (define-key map "?"       'perlnow-read-minibuffer-completion-help)
     (define-key map " "       'perlnow-read-minibuffer-complete-word)
     (define-key map [tab]     'perlnow-read-minibuffer-complete)
     (define-key map "\C-g"    'abort-recursive-edit)
     (define-key map [return]  'exit-minibuffer)
     (define-key map [newline] 'exit-minibuffer)
     (define-key map [down]    'next-history-element)
     (define-key map [up]      'previous-history-element)
     (define-key map "\M-n"    'next-history-element)
     (define-key map "\M-p"    'previous-history-element)
     map)
   "Keymap for reading a perl module name via the minibuffer.")
(put 'perlnow-read-minibuffer-map  'risky-local-variable t)

Note that this is probably still a little on the simple side, e.g. there's no support for menu-bar features in this map. (Never touch the thing, myself.)

Given that keymap, we can use it like this:

  (interactive
     (setq result
           (read-from-minibuffer
            "New module to create \(e.g. /tmp/dev/New::Mod\): "
                        perlnow-module-location        ; the inital value (like a default)
                        perlnow-read-minibuffer-map    ; new keymap is key
                        nil
                        'perlnow-package-name-history  ; maintain a separate history stack
                        nil
                        nil))
   (list result))

And the real problems get solved down in these fuctions:

   perlnow-read-minibuffer-complete
   perlnow-read-minibuffer-complete-word

Except that these are so similar, I just made them wrappers around one over-grown multi-purpose function. An example wrapper:

(defun perlnow-read-minibuffer-complete ()
  "Does automatic completion of up to an entire directory or file name.
Used in reading in path and name of a perl module.  Valid name
separators are \(\"/\" or \"::\"\)."
;;; codename: new tabby
  (interactive)
  (let ((restrict-to-word-completion nil))
        (perlnow-read-minibuffer-workhorse restrict-to-word-completion)
    ))

A walkthrough of the perlnow-read-minibuffer-workhorse function

The "workhorse" function ("perlnow-read-minibuffer-workhorse") itself is full of annoying details that I don't want to get into here. There's a lot of transforming double-colon separators to forward slashes, and vice-versa, a lot of piecing together paths and name fragments and doing completion calls with directory listings fed in as alists, and so on. It took quite a bit of hackery to tune this up to the point where it works as well as it does.

I'm not going to present it here in all it's infamy (you can look at it inside perlnow.el if you like, of course), but there might be some point in discussing some code fragments, and some of the problems that needed to be debugged.

For "workhorse", the current buffer is the minibuffer, and it leads off with a read of the entire contents of it:

    (setq raw_string (buffer-string))

Then it's necessary to peel off and throw-away whatever "Prompt: " string was used in the mini-buffer. We use the first ": " to identify the end of the prompt.

Thereafter the handling of "::" separators are cruicial. A single trailing ":" is not allowed, so those immediately get doubled. If there are any "::" found in the string, then that identifies the immediately previous level as the beginning of the module name space, and the default separator thereafter becomes a "::".

It gets a directory listing of whatever location we're up to in the mini-buffer input string, which then gets massaged into alist form. Completions are obtained with the "try-completion" primitive, which looks for the longest possible completion of the trailing fragment in the directory listing alist.

The try-completion function also returns logical values under some circumstances, and we have to watch for those and handle them correctly. No matches at all is indicated with a nil value and an exact match is indicated with a t.

If it *is* an exact match, we'll handle it slightly differently if it's a directory or a filename. Filenames will end with "*.pm" in this application, but we don't want to show that to the user (who is thinking in terms of perl module names: inside perl, the extension is always ellided). So the ".pm" extension is stripped from any completions, to keep it from appearing in the minibuffer.

After all of this massaging, if it looks like the "suggested-completion" we're coming up with is no change from what's already in the minibuffer, then we jump to our custom minibuffer-completion-help.

Otherwise, we append the new stuff that we've turned up to the minibuffer (by carefully deleting the accessible region -- outside of the read-only prompt area -- and doing an insert of the newly built-up string).

Getting this all working required a lot of testing, making sure that all the different possible little cases were handled correctly. It was not intuitively obvious that a "nil" value from try-completion meant I should use re-use the fragment fed to try-completion as the new portion to be added to the minibuffer. It only makes sense in retrospect: the trailing portion of the string the user enters must reflect something that doesn't exist yet (or else this command couldn't be used to create something new). You would not want the new portion you've typed in to suddenly disappear if you accidentally hit the space bar; but that's what used to happen in the early versions of this code.

Similarly, it did not occur to me in advance that I needed to double up any lone, trailing ":"s, but that's pretty essential. As written, if you type one ":" then hit space, you immediately get another ":", which is the only possible completion.

In general determining precisely how to read in the contents of the minibuffer like this took some playing around. Working down at this level, you read in the minibuffer the way you'd read in any buffer (and you get the prompt string along for the ride), but you can't just write the string back in the same way: the prompt section of the minibuffer is flagged as read-only. Any attempts at deleting it or over-writing it just yield errors, so you have to be very precise about determining where it ends, when you clear the buffer, you delete only the portion outside the prompt area, and then do an insert of a string that has had the prompt string removed from it.


Further discussion of Emacs Lisp: Devnotes


Joseph Brenner, 28 Feb 2004