Quitting to command loop in Elisp
16 Jul 2015Today I'd like to share an interesting bit of Elisp that comes up every now and then for me. Imagine that you have this:
(defun useful-command ()
(interactive)
(do-thing-1)
(do-thing-2 (funcall callback-function))
(do-thing-3))
Sometimes, when you are in callback-function, you might want to
abandon the function that called you, useful-command in this case,
and call a different function, with the current context.
Here's what I've come up with to do just that:
(defmacro ivy-quit-and-run (&rest body)
"Quit the minibuffer and run BODY afterwards."
`(progn
(put 'quit 'error-message "")
(run-at-time nil nil
(lambda ()
(put 'quit 'error-message "Quit")
,@body))
(minibuffer-keyboard-quit)))
To break it up into parts:
minibuffer-keyboard-quitwill unwind the call stack all the way to the command loop, preventing e.g.do-thing-3from being called. Note that the call stack can be as deep as you like, e.g.useful-commandmight be called byutility-commandetc.run-at-timewith the argumentnilwill run the code as soon as possible, which is almost exactly after we're back into the command loop.The final trick is to prevent
Quitfrom being displayed in the minibuffer.
Sample application
Suppose that I've called find-file when ivy-mode is
active. Typically, I'd select a file and press C-m to open
it. However, sometimes I just want to see the selected file in dired
instead of opening it. This code is from before
ivy multi-action interface,
it plainly binds a command in ivy-minibuffer-map:
(define-key ivy-minibuffer-map (kbd "C-:") 'ivy-dired)
(defun ivy-dired ()
(interactive)
(if ivy--directory
(ivy-quit-and-run
(dired ivy--directory)
(when (re-search-forward
(regexp-quote
(substring ivy--current 0 -1)) nil t)
(goto-char (match-beginning 0))))
(user-error
"Not completing files currently")))
So at the moment when C-: is pressed, the call stack is:
- C-x C-f called
find-file. find-filecalledcompletion-read-functionwhich is set toivy-completing-read.ivy-completing-readcalledivy-read.ivy-readcalledread-from-minibuffer.
Thanks to the new macro, I can unwind all of that stuff, making sure
nothing extra will be executed that was supposed to be executed after
read-from-minibuffer had returned. In other words, the file will not
be opened. Instead, a dired buffer will be opened, centered on the
selected candidate.
What I described above can actually be a pretty common scenario, you
could adapt it to helm or avy or projectile. Basically to
anything that includes some form of completion (i.e. commands that
wait for input) and offers you a customizable keymap. Small
disclaimer: the above code falls into the quick-and-dirty category, I
don't recommend it if you can do something smarter instead. But if you
can't do anything smarter due to being constrained by an interface you
don't control, this macro could help you out.