Emacs Keymaps and the SLIME scratch buffer
Thursday, January 25, 2007
Recently, Ariel Badichi
kicked off a discussion on the
slime-devel mailing list when he asked why "C-M-q" (which is
normally bound to 'indent-sexp' in lisp-mode) was not bound in the
SLIME scratch buffer (Note: the SLIME scratch buffer is similar to
the normal Emacs *scratch* buffer but it lets you evaluate CL instead
of elisp code). This highlighted some interesting aspects of Emacs
keymaps that I hadn't fully understood previously.
First of all, let's digress to review some Emacs concepts before we come
back to this particular issue. When you're editing CL code
using SLIME, you are using both lisp-mode and slime-mode. While lisp-mode is a
Major Mode, SLIME itself is a
Minor Mode. This is because we want to add functionality to
lisp-mode rather than replace it completely with slime-mode. In general (although there are
ways around this),
there can be only one active Major Mode for a buffer but there can be
multiple active Minor Modes for that same buffer. When a Major or
Minor mode is created, it can be created with a custom keymap (e.g. -
a custom set of key bindings). These override the default "global" key
bindings that Emacs comes with. If you want to create a mode that has
essentially the same set of key bindings (plus or minus a few) that
some other mode has, you can
inherit key bindings when you create your keymap. So, if keymap definitions can come
from multiple places, what is the order of precedence in determining
which key binding is selected? The Emacs Lisp Reference Manual
states:
At any time, several primary keymaps are "active"--that is, in use for finding key bindings. These are the "global map", which is shared by all buffers; the "local keymap", which is usually associated with a specific major mode; and zero or more "minor mode keymaps", which belong to currently enabled minor modes. (Not all minor modes have keymaps.) The local keymap bindings shadow (i.e., take precedence over) the corresponding global bindings. The minor mode keymaps shadow both local and global keymaps.So, the precedence hierarchy is:
- Minor Mode keymap(s)
- Major Mode keymap (which is a "local" keymap)
- Global keymap
The relevant SLIME scratch buffer setup code in slime.el was (annotated with numbers for the comments below):
;;;; ScratchSo, we can see that:
(defvar slime-scratch-mode-map (let ((map (make-sparse-keymap))) (set-keymap-parent map slime-mode-map) #1 map))
(slime-define-keys slime-scratch-mode-map ("C-j" 'slime-eval-print-last-expression)) #2
(defun slime-scratch-buffer () "Return the scratch buffer, create it if necessary." (or (get-buffer "*slime-scratch*") (with-current-buffer (get-buffer-create "*slime-scratch*") (lisp-mode) #3 (use-local-map slime-scratch-mode-map) #4 (slime-mode t) #5 (current-buffer))))
- The slime-scratch-mode-map is created as a copy of slime-mode-map
- Only one key binding (C-j) is added to the keymap When a SLIME scratch buffer is created, the following occurs:
- lisp-mode is activated for the buffer (with the key bindings associated with lisp-mode)
- The slime-scratch-mode-map key bindings are activated as a "local" map
- slime-mode is activated for the buffer (with the key bindings associated with slime-mode)
(slime-define-keys slime-scratch-mode-map
("C-j" 'slime-eval-print-last-expression)
("C-M-q" 'indent-sexp))
This adds the "C-M-q" binding (which happens to be the only lisp-mode
binding that is of relevance for the SLIME scratch buffer); however,
this isn't really the "correct" fix. If we look at the definition of
slime-scratch-mode-map in #1 above, it is inheriting the bindings from
slime-mode-map. Then, in the slime-scratch-buffer function, slime-mode is being
activated as a minor mode in the SLIME scratch buffer. So,
effectively, the slime-mode bindings are being "inherited" for no reason
since slime-mode is being activated as a minor mode anyhow. Although we
only really need the "C-M-q" binding from lisp-mode, the "correct" fix
is to have the slime-scratch-mode-map inherit bindings from
lisp-mode. That way, the slime-scratch-buffer function uses bindings
from lisp-mode (inherited), slime-mode, and slime-scratch-mode-map
(which is what is wanted). So, the correct fix is:
(defvar slime-scratch-mode-map
(let ((map (make-sparse-keymap)))
(set-keymap-parent map lisp-mode-map)
map))
Thanks to Ariel Badichi for both the fix and his explanations!

