(or emacs irrelevant)

The keymap arms race

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.

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:

  1. Check out the master of helm
  2. Call magit-bisect-start and mark HEAD as bad
  3. magit will automatically check out a revision that is halfway between a bad state and the initial commit. Now I exit Emacs and try helm 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 trust unload-feature to do the right thing. Anyway, if the thing works, move to helm's repository and call magit-bisect-good, otherwise call magit-bisect-bad.
  4. 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:

  1. M-p to get the previous input
  2. f to move the cursor after (expt 2 10)
  3. 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!