Ivy usability improvements when dealing with directories
27 Jun 2019Introduction
When Ivy just started out as a completion framework, the functionality was supposed to be simple: select one string from a list of strings. The UI is simple enough:
- Show the list of strings that match the entered text,
- Use C-n and C-p to navigate them,
- Use C-m or C-j to submit.
Emacs has three key bindings that mean "Enter": RET, C-m, and
C-j. But in terminal mode, emacs -nw
, RET and C-m are the same
binding: Emacs can't distinguish them. So we have at most two bindings. Fortunately, the world of
completion is simple at the moment, and we only need one binding.
File name completion
Enter file name completion. When you're completing file names, you are selecting not one string, but many strings in succession while moving from one directory to the next. So we need at least two key bindings:
- Use C-m (
ivy-done
) to select the current candidate and exit completion. - Use C-j (
ivy-alt-done
) to change the current directory to the current candidate without exiting completion.
What to do when C-j is used on a file and not on a directory? Might as well open the file: same action as C-m. OK, we had two key bindings, and we have used them. Hopefully nothing else comes up.
Enter creating a new file, i.e. selecting something that's not on the list of strings.
Suppose I call find-file
, enter "do", and the only match is a directory named "doc":
- Pressing C-m will end completion with the "doc" directory selected.
- Pressing C-j will continue completion inside the "doc" directory.
So creating a file named "do" is the third action. Our two "Enter" keybindings are already taken by
the first two different useful actions. So we need a third key binding. The one I chose is
C-M-j (ivy-immediate-done
). It means: I don't care that the current input is not on
the list of candidate strings, submit it anyway.
Directory creation
Enter directory creation: dired-create-directory
and make-directory
. These built-in Emacs
commands request file name completion, but what they tell Ivy is no different from what find-file
tells: "I want to select a file". However, for these commands, the C-M-j action is the
one that makes most sense. Here it would be nice for Ivy to take the back seat and just act like an
interactive ls
, since the user will enter a completely new string that's not on the list of
candidates.
For a long time, you still had to use C-M-j with those commands, to much frustration of
new but also existing users, including myself. But a few days ago, I looked at the prompt that
dired-create-directory
uses: "Create directory: ". That prompt is passed to Ivy. Using the prompt
to detect the intention of the command is a bit of a hack, but I think in this case it's
justifiable. So now Ivy will recognize that the intention commands that request file name completion
and pass the "Create directory: " prompt is to create a directory, and all key bindings will do just
that: C-m, C-j, and C-M-j will behave the same in this case.
An alternative key binding scheme
Note that C-m and C-j behave differently only for directories. But thanks to
the fact that "." is always the first candidate, C-m for directories is equivalent to
C-j C-j. So we can get away with just using ivy-alt-done
, and bind C-m to
ivy-immediate-done
. Or swap the two meanings:
(define-key ivy-minibuffer-map (kbd "C-j") 'ivy-immediate-done)
(define-key ivy-minibuffer-map (kbd "C-m") 'ivy-alt-done)
The price to pay here is the extra context switch when we simply want to select a directory. We
could the bind ivy-done
to C-M-j and avoid the context switch, but then we're back to
three bindings once more. Still, I thought that swapping the bindings is an interesting idea worth
sharing.
Canceling dired-dwim-target
Setting dired-dwim-target
is a nice productivity boost. It allows to use Emacs in a similar way to
a two-pane file explorer, like mc(1). But it was really annoying when I was in dir-1
with the
intention to copy a file to a different name in dir-1
(e.g. create a backup, or copy a template),
but the current directory was set to dir-2
because of a random dired
window I had open. In that
case, I had to call delete-other-windows
, perform the copy, and then restore the window
configuration with winner-undo
.
I did the above many times over many years, until I finally dug into the code of dired.el
to see
how dired-dwim-target
worked. Turns out it was storing dir-1
in the minibuffer-defaults
variable. So now Ivy will use that variable when I press M-n. All in all, it was a five
minute fix. But rather than regret that I didn't do it years ago, I'm glad I did it now. It only
remains to build some muscle memory to press M-n in that situation.
I'm guesstimating that dired-dwim-target
works to my advantage 90% of the time when I press
C in dired
. For the other 10% of the times, I can now press M-n.
Outro
I hope you find the new functionality useful. I'm always open to new ideas and pull requests. Happy hacking!