6

I am trying to make a keymap to apply to a buffer region where the map is a composed keymap of several modes. Then, in that map, I want to define C-c C-c to run the command org-ctrl-c-ctrl-c.

This is the code that I am using:

(setq scimax-src-block-keymaps
      `(("emacs-lisp" . 
         ,(let ((map (make-composed-keymap `(,lispy-mode-map
                                             ,emacs-lisp-mode-map
                                             ,outline-minor-mode-map)
                                           org-mode-map)))
            (define-key map (kbd "C-c C-c") 'org-ctrl-c-ctrl-c)
            map))))

This works fine, but I was surprised to learn that it "leaks". For example, if I open a Latex file, and turn on outline-minor-mode, then C-c C-c is bound to org-ctrl-c-ctrl-c (note that before I run that code it is bound to the right command for latex-mode, but after I run that code it changes).

Am I doing this the wrong way? I would have thought this would make a copy of the maps and that I was modifying that copy, not that I was modifying the original maps here.

  • Good question. The result from make-composed-keymap, should be a new keymap. But it contains existing keymaps as components. The doc of make-composed-keymap speaks only about key lookup, not what happens when you define a key in the new map. Can you tell which of the component keymaps got altered? Maybe the doc of make-composed-keymap should be beefed up a bit, to make the behavior more clear about what happens when you bind a key in the composed map. – Drew Nov 25 '19 at 17:42
  • I'm guessing that @Stefan will provide a clear answer to this, if no one beats him to it. – Drew Nov 25 '19 at 17:43
  • It is only the binding of C-cC-c in the outline-minor-mode-map that gets modified. It looks like if I make a copy-keymap around that composed map then the issue is avoided. Although the docstring of that command says it is almost never needed, this scenario is a little more complicated than the one in the docstring, so maybe it is warranted here. – John Kitchin Nov 25 '19 at 18:17
  • Doc string of what command? Or did you mean doc string of make-composed-keymap? And what did you mean by "it", in "it is almost never needed"? Does some doc string talk about copying the keymap? (I don't see that in the doc of make-composed-keymap.) – Drew Nov 25 '19 at 18:39
  • Sorry, in the docstring of copy-keymap it says "Note that this is almost never needed. " – John Kitchin Nov 25 '19 at 19:07
4

Let us have a short look at the implementation of make-composed-keymap (in Emacs 26.3):

(defun make-composed-keymap (maps &optional parent)
  "..."
  `(keymap
    ,@(if (keymapp maps) (list maps) maps)
    ,@parent))

The list maps is spliced in.

When called with two maps and one parent you get a list (keymap (keymap el11 ... el1A) (keymap el21 ... el2B) . PARENT).

The doc (info "(elisp)Format of Keymaps") says:

‘(keymap ...)’ If an element of a keymap is itself a keymap, it counts as if this inner keymap were inlined in the outer keymap. This is used for multiple-inheritance, such as in ‘make-composed-keymap’.

So the map is actually equivalent to (keymap el11 .. el1A el21 ... el2B . PARENT).

If that is so define-key is free to insert the new definition into (keymap el11 ... el1A) instead of the top-level list.

And it does so as the following demo code shows.

(let ((parent (make-sparse-keymap))
      (map1 (make-sparse-keymap))
      (map2 (make-sparse-keymap))
      composed)
  (define-key map1 [?1] 'sym1)
  (define-key map2 [?2] 'sym2)
  (setq composed (make-composed-keymap (list map1 map2) parent))
  (define-key composed [?3] 'sym3)
  (list :composed composed
    :map1 map1))

(:composed (keymap (keymap (51 . sym3) (49 . sym1)) (keymap (50 . sym2)) keymap)
 :map1 (keymap (51 . sym3) (49 . sym1)))

IMHO this is bad behavior of define-key and worth a bug-report.

What I actually would expect from define-key is to work like (setcdr composed (cons '(?3 . sym3) (cdr composed))) as shown in the following example:

(let ((parent (make-sparse-keymap))
      (map1 (make-sparse-keymap))
      (map2 (make-sparse-keymap))
      composed)
  (define-key map1 [?1] 'sym1)
  (define-key map2 [?2] 'sym2)
  (setq composed (make-composed-keymap (list map1 map2) parent))
  (setcdr composed (cons '(?3 . sym3) (cdr composed)))
  (list :composed composed
    :map1 map1))

(:composed (keymap (51 . sym3) (keymap (49 . sym1)) (keymap (50 . sym2)) keymap)
 :map1 (keymap (49 . sym1)))

The resulting map is working as it should be:

(lookup-key
  '(keymap (51 . sym3) (keymap (49 . sym1)) (keymap (50 . sym2)) keymap)
  [?3])

sym3

and it does not arbitrarily modify the inherited maps.


A workaround is to insert a sacrificial sparse keymap as first element into the MAP argument of make-composed-keymap:

(setq scimax-src-block-keymaps
      `(("emacs-lisp" . 
         ,(let ((map (make-composed-keymap (list
                                             (let ((map1 (make-sparse-keymap)))
                                               (define-key map1 (kbd "C-c") (make-sparse-keymap))
                                               map)
                                             lispy-mode-map
                                             emacs-lisp-mode-map
                                             outline-minor-mode-map)
                                           org-mode-map)))
            (define-key map (kbd "C-c C-c") 'org-ctrl-c-ctrl-c)
            map))))

Note that the sacrificial map should bind C-c as prefix such that C-c C-c is bound later there and not in outline-minor-mode-map.

Confer to the doc string for define-key in that regard:

If KEYMAP is a sparse keymap with a binding for KEY, the existing binding is altered. If there is no binding for KEY, the new pair binding KEY to DEF is added at the front of KEYMAP.

Our problem imposed by this doc is that the prefix key C-c is already bound in outline-minor-mode-map to a keymap. So the binding there is modified if we do not bind C-c again in our sacrificial map.

Alternatively, you can also immediately bind C-c C-c in the sacrificial keymap.

| improve this answer | |

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.