Clementson's Blog

Bits and pieces (mostly Lisp-related) that I collect from the ether.

January 2007
Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Dec  Feb

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:
  1. Minor Mode keymap(s)
  2. Major Mode keymap (which is a "local" keymap)
  3. Global keymap
The Emacs commands current-minor-mode-maps, current-local-map, and current-global-map let you see which bindings are in place in each of these.

The relevant SLIME scratch buffer setup code in slime.el was (annotated with numbers for the comments below):
;;;; Scratch
(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))))
So, we can see that:
  1. The slime-scratch-mode-map is created as a copy of slime-mode-map
  2. Only one key binding (C-j) is added to the keymap
  3. When a SLIME scratch buffer is created, the following occurs:
  4. lisp-mode is activated for the buffer (with the key bindings associated with lisp-mode)
  5. The slime-scratch-mode-map key bindings are activated as a "local" map
  6. slime-mode is activated for the buffer (with the key bindings associated with slime-mode)
Since lisp-mode is a major mode (and therefore uses a "local" key map) and slime-scratch-mode-map is also activated as a "local" map, the slime-scratch-mode-map key bindings are in effect in the buffer rather than the lisp-mode key bindings (since only one local map exists in a buffer). Therefore, any key bindings defined in lisp-mode are lost. A simplistic fix for this would be to add the necessary binding(s) to slime-scratch-mode-map:
(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!

emacs Copyright © 2007 by Bill Clementson