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.
make-composed-keymap
, should be a new keymap. But it contains existing keymaps as components. The doc ofmake-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 ofmake-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.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 ofmake-composed-keymap
.)copy-keymap
it says "Note that this is almost never needed. "