cl-loop beats map-keymap
Did you ever need to make many lookups or changes in #emacs key bindings, but balked at the complexity of programming with map-keymap
?
Me too. Good news, you can skip that mess! There's a keyword in cl-loop
that is much more human-friendly, and it's still blisteringly fast. It's been around since 1993, but I've found no blog posts about it.
There's a gotcha I want to tell you about, I'll get to that in a moment.
First, typical usage goes like this example:
(Behold! Lisp gone ugly!)
(cl-loop
for seq being the key-seqs of KEYMAP
using (key-bindings cmd)
as key = (key-description seq)
do ...)
In the above loop, you'll get access to KEY and its bound command, CMD. And that brings me to the gotcha.
Here's what happened to me: if KEYMAP has many adjacent keys bound to the same command (which is typically either nil
or self-insert-command
), then (keymap-lookup KEYMAP key)
will error out saying that this key is invalid. Some example values of key
are SPC..~
, ESC i..j
, C-x (..*
, \200..\377
.
Yes, a string "SPC..~" is being provided as key
. Can you guess what's going on?
I can't, but these seem to be ranges of keys. I think it might only happen in global-map, actually.
Filter them out with a simple (not (string-search ".." key))
.
And it turns out there's a couple more more sanitizations you'll probably want.
- Command bindings that are actually
nil
. - Command bindings that are actually lists of the form
(menu-item ... ... ...)
. At least, I assume you have no interest in menu bindings.
Thus, sanitized form:
(cl-loop for seq being the key-seqs of KEYMAP using (key-bindings cmd) as key = (key-description seq) when cmd when (not (string-search ".." key)) when (or (not (listp cmd)) (member (car cmd) '(closure lambda))) do ...)
If you're like me and never bind a key to a lambda, the third when
can be simplified to just
when (not (listp cmd))
Warning for cleverness. You might think this could be
when (commandp cmd)
but many autoloaded commands won't satisfy commandp
nor functionp
until called the first time. At init time, they only positively satisfy symbolp
.
Anyway, that's it! No more keeping mental track of subkeymaps nor the finer details of recursing into them!
Bonus: get all keys for the current buffer
OK great, we can unnest a single keymap variable (such as org-mode-map
or global-map
) and its children into a flat list of key seqs. But what about listing all the current buffer's keys, not just one mode-map's keys?
You know that any given key can usually be found several times in multiple keymaps, where it's bound to different commands. The current buffer has a sort of keymap composite calculated by merging all relevant keymaps. That composite is not stored as a variable anywhere.
Fortunately we can know which binding is correct for the current buffer thanks to the fact that (current-active-maps)
seems to return maps in the order in which Emacs selects them. So what comes earlier in the list is what would in fact be used. Then we can run (delete-dups)
to declutter the list, which likewise keeps the first instance of each set of duplicates.
Presto – list current buffer keys:
(delete-dups (cl-loop for map in (current-active-maps) append (cl-loop for seq being the key-seqs of map using (key-bindings cmd) as key = (key-description seq) when cmd when (not (string-search ".." key)) when (or (not (listp cmd)) (member (car cmd) '(closure lambda))) collect key)))
Bonus: get all keys and bindings for the current buffer
To list the keys together with their command bindings, we need a bit more complexity as delete-dups
will not work. While we're at it, I'll throw in a key-valid-p
check to represent a somewhat compute-heavy filter:
(cl-loop for map in (current-active-maps) append (cl-loop for seq being the key-seqs of map using (key-bindings cmd) as key = (key-description seq) when cmd when (not (string-search ".." key)) when (or (not (listp cmd)) (member (car cmd) '(closure lambda))) unless (assoc key map-bindings) unless (assoc key buffer-bindings) when (ignore-errors (key-valid-p key)) collect (cons key cmd) into map-bindings finally return map-bindings) into buffer-bindings finally return buffer-bindings)
Uncompiled, this is somewhat slow (0.5 seconds on a Surface Pro 7 @ 1.1GHz). Compiled is faster, but we can also speed it up somewhat with caching. That trick really comes into its own when you want to filter the result with all sorts of inefficient checks; add your custom checks inside the defun seq-to-key-if-legal
in the following snippet.
Final version!
(defvar legal-key-cache (make-hash-table :size 2000 :test #'equal)) (defun seq-to-key-if-legal (seq) (let ((cached-value (gethash seq legal-key-cache 'not-found))) (if (eq 'not-found cached-value) (puthash seq (let ((key (key-description seq))) (when (and ;; Ignore menu-bar/tool-bar/tab-bar (not (string-search "-bar>" key)) ;; All anomalies with ".." like "ESC 3..9" are bound ;; to self-insert-command or nil, ok to filter out. (not (string-search ".." key)) (ignore-errors (key-valid-p key))) key)) legal-key-cache) cached-value))) (cl-loop for map in (current-active-maps) append (cl-loop for seq being the key-seqs of map using (key-bindings cmd) as key = (seq-to-key-if-legal seq) when (and key cmd) when (or (not (listp cmd)) (member (car cmd) '(closure lambda))) unless (assoc key map-bindings) unless (assoc key buffer-bindings) collect (cons key cmd) into map-bindings finally return map-bindings) into buffer-bindings finally return buffer-bindings)
Alternatives
Keymap-utils ships kmu-map-keymap
which seems easier to use than the builtin map-keymap
. Although it doesn't ship an explicit utility for buffer bindings. For that, you may be able to borrow which-key's which-key--get-current-bindings
.
What links here
- 2023-11-06
Updated (10 months ago)