The keymap arms race31 Dec 2014
Sometimes 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.
helm is a wonderful package, it's my goto-package when I need completion. All of the following packages use it:
helm is super-greedy: once you're in the
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
easily allow it.
I started to investigate into this when I got
issue #15: Does not work with helm in minibuffer
ace-window. I checked it, and indeed you could not
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
magitwill automatically check out a revision that is halfway between a bad state and the initial commit. Now I exit Emacs and try
helmagain to see if I can exit from the minibuffer. It's kind of lame to have to exit Emacs, but somehow I don't trust
unload-featureto do the right thing. Anyway, if the thing works, move to
helm's repository and call
magit-bisect-good, otherwise call
- 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
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
Apr 2. And I found out that it's not working only
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
It didn't work out, because
helm adds the code that calls
In the end, this finally worked:
(remove-hook 'post-command-hook 'helm--maybe-update-keymap)
I thought that enabling
cider-repl-mode might be a good idea.
Extra navigation options are always good, and the ability to
raise is just so useful.
For instance, you start with:
; SLIME 2014-11-28 CL-USER> (expt (expt 2 10) 3) 1073741824 CL-USER>
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
When two minor modes bind the same keys, which one wins?
The answer is the first one on
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
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
properly yields to call
respectively. I'll see if there are more key bindings that need to
yield. Happy coding in the New Year!