The keymap arms race
31 Dec 2014Sometimes new Emacs packages have to fight for their place in the sun, as all the good bindings and huge keymap areas are already taken by the older packages. This post will cover some practical problems that you may encounter when your package needs to be aware of another active package.
ace-window
vs. helm
helm is a wonderful package, it's my goto-package when I need completion. All of the following packages use it:
But helm
is super-greedy: once you're in the helm
-minibuffer,
there's no way out except either a successful completion or a cancel.
But exiting the minibuffer for a short while may be useful. For
instance, you could copy some text and yank it in the
helm
-minibuffer. The default minibuffer functions, as well as ido
easily allow it.
I started to investigate into this when I got
issue #15: Does not work with helm in minibuffer
for ace-window
. I checked it, and indeed you could not ace-window
out of a helm
-minibuffer. In fact, it was not possible to exit with
other-window
either. This looked like good news, since I had an
inkling that it used to work at some point. So I checked out a
year-old version of helm
and it did work.
magit-bisect
to the rescue!
It's very simple:
- Check out the master of
helm
- Call
magit-bisect-start
and markHEAD
as bad magit
will automatically check out a revision that is halfway between a bad state and the initial commit. Now I exit Emacs and tryhelm
again to see if I can exit from the minibuffer. It's kind of lame to have to exit Emacs, but somehow I don't trustunload-feature
to do the right thing. Anyway, if the thing works, move tohelm
's repository and callmagit-bisect-good
, otherwise callmagit-bisect-bad
.- Continue this process until termination. I needed 10 iterations in this case.
The culprit commit was this one:
(let* ((source (helm-get-current-source)) (kmap (and (listp source) ; Check if source is empty. (assoc-default 'keymap source)))) - (when kmap (setq overriding-local-map kmap))))) + (when kmap (set-transient-map kmap)))))
the priority of maps
In Emacs, set-transient-map
has priority over
overriding-local-map
, which was exactly what ace-window
was using.
It's also funny that ace-window
used to work with helm
for a week,
since it was published on Mar 26
and helm
switched to
set-transient-map
on Apr 2
. And I found out that it's not working only
now.
Here's what helm
is using currently:
(if (fboundp 'set-transient-map)
(set-transient-map it)
(set-temporary-overlay-map it))
So I've amended ace-window
with similar code. The way
set-transient-map
works, the last call to it overrides the previous
one, so I thought that it would work out since ace-window
is always
called after helm
.
It didn't work out, because helm
adds the code that calls
set-transient-map
to post-command-hook
.
In the end, this finally worked:
(remove-hook 'post-command-hook 'helm--maybe-update-keymap)
lispy
vs. SLIME
and CIDER
I thought that enabling lispy-mode
for slime-repl-mode
and
cider-repl-mode
might be a good idea.
Extra navigation options are always good, and the ability to
call raise
is just so useful.
For instance, you start with:
; SLIME 2014-11-28 CL-USER> (expt (expt 2 10) 3) 1073741824 CL-USER>
With lispy-mode
on you can:
- M-p to get the previous input
- f to move the cursor after
(expt 2 10)
- r to raise
(expt 2 10)
Here's the final state:
; SLIME 2014-11-28 CL-USER> (expt (expt 2 10) 3) 1073741824 CL-USER> (expt 2 10)
But the problem was that SLIME has slime-repl-map-mode
minor mode on
that competes for lispy
's shortcuts, and CIDER has something similar
as well.
When two minor modes bind the same keys, which one wins?
The answer is the first one on minor-mode-map-alist
.
This function I've found on the wiki:
(defun lispy-raise-minor-mode (mode)
"Make MODE the first on `minor-mode-map-alist'."
(let ((x (assq mode minor-mode-map-alist)))
(when x
(setq minor-mode-map-alist
(cons x (delq mode minor-mode-map-alist))))))
I didn't want lispy-mode
to mess with minor-mode-map-alist
too
actively, so I went with this approach: if lispy-mode
is called
interactively, i.e. via a key binding or M-x, put it ahead:
(when (and lispy-mode (called-interactively-p 'any))
(mapc #'lispy-raise-minor-mode
(cons 'lispy-mode lispy-known-verbs)))
It's still a work in progress, currently only lispy
's RET
properly yields to call cider-repl-return
and slime-repl-return
respectively. I'll see if there are more key bindings that need to
yield. Happy coding in the New Year!