It Bears Repeating: Emacs 28 & Repeat Mode
Are you tired of pressing C-x o
repeatedly to switch to the window you want in Emacs? Or M-g n
and M-g p
to cycle through compile errors or grep matches? How about navigating outline headings with (yuck) C-c @ C-n
and C-c @ C-p
?
The correct answer to the latter is that no one traverses headings this way, because these keybindings are atrocious. Even the shorter ones start to grate when you need to use them repeatedly in an editing session.
Depending on their levels of comfort with Emacs, users typically deal with them in one of three ways:
- They aren’t aware of or don’t use these commands, or use the menu to access them.
- They rebind the ones they need elsewhere, to an easier to press but equally idiosyncratic location. After a few such short non-prefixed keybindings they run out of room on their keyboards.
- They use one of the many, many available helpers written with the Hydra or Transient packages for these.1
But there’s a fourth option. Emacs 28 bundles a simple and hassle-free way to take the tedium out of these keybindings: Repeat Mode.
Repeat Mode lets you call multiple commands in a keymap without repeating the prefix each time. So you can press
C-x o
,o
,o
, … to switch windows repeatedly.O
will cycle backwards.C-x u
,u
,u
, … to undo repeatedly,C-x @ C-n
,n
,p
,f
,b
,… to traverse headings,M-g n
,n
,p
,n
,… to navigate errors or grep matches, and so on.
With repeat-mode
active, calling the prefix (M-g
) the first time “activates” the keymap, after which only the “base” key for a command (n
or p
)is needed.
Naming things is hard
In this write-up I’ll call all but the final part of a keybinding the “prefix”, and the final key or chord the “base” key.
Example: org-next-link
, bound to C-c C-x C-n
:
- The prefix is
C-c C-x
. - The base key is
C-n
.
Repeating a single command
If you want to call a single command repeatedly, repeat-mode
is not required. You can just call M-x repeat
, bound to C-x z
by default. This has been part of Emacs for ages. You can repeat invocations of the repeat command itself with just z
, so it’s C-x z z z...
to repeat the last command multiple times.
In comparison to M-x repeat
, repeat-mode
shines when there’s a whole keymap of related commands with keys (like n
, p
, f
, b
, u
for outline navigation) under the same prefix (like C-c @
for outline-minor-mode
). In this video I switch windows, jump through Occur (grep) matches, navigate git-diff hunks
With diff-hl-mode
, which supports repeating commands out of the box.
and call undo repeatedly with single key presses. The buffer on the right lists the keystrokes. Commands with the same prefix or repeated commands are single key presses:
Play by play
- After the first invocation of
other-window
(C-x o
), I switch windows witho
(andO
). - Next I find
occur
-ences of the phrase “repeat-mode” in the buffer. Thenext-error
(M-g n
) andprevious-error
(M-g p
) commands jump through the matches. I only need to type the prefix (M-g
) once. - I call
diff-hl-previous-hunk
from the end of the buffer to jump to modified hunks in this version-controlled document. This is provided by thediff-hl
package. Again, I only type in the prefix (C-x v
in my configuration) once. - I mark a modified hunk with
SPC
– corresponding to the full versionC-x v SPC
. Then I callundo
a few times. As a bonus, undo in Emacs is limited to the active region. - After the first invocation of the
C-x
prefix, undo is justu
. This is simpler than holding down Control to pressC-/
, the other undo binding, a bunch of times.
Repeat Mode comes with built-in support for a bunch of keymaps. There’s no learning curve, M-x repeat-mode
and you’re set. You’ll wonder how you lived without it.
What’s in a keymap
Before we dive into Repeat Mode proper, let’s address the issue of creating an easily repeatable command using Emacs’ base API.
A “keymap” is a data structure that maps a collection of keybindings (i.e. keyboard shortcuts) to commands. Let’s call each element of this map a “binding”. Any number of keymaps can be “active” at a time in Emacs, meaning that typing a keybinding will run some command that it’s mapped to in these keymaps. Which command runs is a question of which keymap is looked up first.
For our purposes, a “transient keymap” is one that
- takes priority over all other keymaps, and
- disappears after a keypress unless unless you explicitly ask it to stick around.
If we want to make a key run a command no matter what other keymaps are active, we can use a transient keymap. If we want this keymap to stick around until some arbitrary condition is met, we can do that too. Putting these ideas together lets us add a “repeat” feature to any command.
For example, I cycle through popup buffers in Emacs with the key C-M-`
, or Control, Alt and the grave key chorded together. This calls the popper-cycle
command. Needless to say, this isn’t very conducive to repeated invocations. So I activate a transient-keymap after each call to popper-cycle
:
(advice-add 'popper-cycle :after
(defun my/popper-cycle-repeated (&rest _)
"Continue to cycle popups with the grave key."
(set-transient-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "`") #'popper-cycle)
map))))
This keymap binds popper-cycle
to the grave key, and it stays active for exactly one keypress. Typing the grave key thus calls popper-cycle
again, or I can break this chain by doing anything else. (Note: This is a terrible way to do it!2)
The logic used by Repeat Mode is quite different, but the effect is similar. In its simplest form, binding a shared set of commands to a transient map is how you could add a “repeat” functionality to the set.
Adding repeat-mode
support to keymaps
Repeat Mode supports a few keymaps out of the box, including all the ones in my demo above.
To add Repeat Mode support to commands in a keymap, you add a symbol property to each command. Here’s support for the completely unpressable smerge-mode
commands:
See below for a demo.
(map-keymap
(lambda (_key cmd)
(when (symbolp cmd)
(put cmd 'repeat-map 'smerge-basic-map)))
smerge-basic-map)
This is simple enough that we can shove it into a function that repeat-izes keymaps:
(defun repeatize (keymap)
"Add `repeat-mode' support to a KEYMAP."
(map-keymap
(lambda (_key cmd)
(when (symbolp cmd)
(put cmd 'repeat-map keymap)))
(symbol-value keymap)))
The “repeat” state is shared between commands that are (i) bound in a keymap, and (ii) have the keymap as their repeat-map
symbol property. But what if the commands you want to repeat aren’t defined on top of a prefix, unlike all the above examples? Or if the commands we want to group aren’t part of a single keymap to begin with? Then this is a two step process:
- Bind your commands of interest in some keymap, to short keys if necessary.
- Attach the keymap name as a symbol property to all of them.
For instance, suppose you’d like to be able to Isearch repeatedly with just s
and r
instead of hammering C-s
over and over. To do this you can bind them to short keys in a keymap, and attach the keymap name to the commands’ repeat-map
symbol properties:
(defvar isearch-repeat-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "s") #'isearch-repeat-forward)
(define-key map (kbd "r") #'isearch-repeat-backward)
map))
(dolist (cmd '(isearch-repeat-forward isearch-repeat-backward))
(put cmd 'repeat-map 'isearch-repeat-map))
Note: You’re not going to have trouble typing “s” or “r” as part of the search string, even after you’ve begun the search with C-s
– it’s only the repeated invocations (isearch-repeat-forward) that are shortened!
This is a somewhat low-level (though simple) operation and the recipe is fairly constant. So we could package it in a macro for ease of use… but we don’t have to. There are already multiple packages that let you specify commands to be repeated along with their short keys. Here are a few:
- define-repeat-map by Case Duckworth
- repeaters by mmarshall540
- Hercules by jjzmajic, an external package that bypasses the whole
repeat-mode
system and lets you define repeat functionality for any keymap.
Making repeat-maps: Emacs 29 update
Emacs 29 introduces defvar-keymap
, making it easier to define keymaps with the repeat-map
property attached:
(defvar-keymap hl-todo-repeat-map
:repeat t
"n" #'hl-todo-next
"p" #'hl-todo-previous
"o" #'hl-todo-occur)
Or, for finer control over when the repeat-map is (de)activated,
(defvar-keymap hl-todo-repeat-map
:repeat (:enter (hl-todo-insert) :exit (hl-todo-occur))
"n" #'hl-todo-next
"p" #'hl-todo-previous
"o" #'hl-todo-occur)
which specifies that using hl-=todo-insert
should activate the repeat-map and hl-todo-occur
should deactivate it.
Note that turning an existing keymap into a repeat-map or adding this behavior to individual commands still requires one of the above helpers, like repeatize
.
With any of these packages you could develop the above idea into a complete modal editing environment – like God Mode, but with custom modes for specialized editing tasks. Even at a more basic level, you can go wild here, placing all commands into repeat maps: Why delete words with M-d
or M-DEL
five times when you can just do M-d d d d d
? Why cycle through the kill-ring with M-y
when you can just yank and cycle with C-y y y y...
?
I’m not sure about taking it that far. For one, it’s simpler to use digit arguments: M-5 M-d
is shorter and possibly faster to type. Second, many commands that involve more than three levels of “cycling” are better served by a menu with choices, i.e. a completing-read
interface or an Avy selection. I’ve found that the returns diminish and the common issues with modality start to surface as the repeat maps become more expansive.
Repeat maps appear to be best suited for families of related commands that are usually invoked in succession and are cumbersome to invoke. Pretty much exactly what folks use Hydras or (more lately) Transients for, which brings us to…
Repeat Mode vs Hydra, Transient and Hercules
Repeat Mode has a few advantages when compared to Hydras/Transients3 for repeating commands:
- There’s no special definition or code to write, maintain or copy: it just works.
- There’s only one set of keys defined for a command: You can use the same full keybinding with or without Repeat Mode active.
- You can rebind both the prefix and the base keys without losing the repeat behavior.
- Repeat Mode has support for many prefix maps out of the box, and adding support for new maps is easy, see below.
- It’s built into Emacs.
The Hercules package shares many of these advantages, since it too uses existing keymaps as the basis for a repeat interface. However, Repeat Mode is more minimal in its configuration and presentation, does not depend on Which Key, and does not offer a fancy key hinting system out of the box.
With all of the above alternatives, you still need to invoke the mode somehow. With repeat-mode
this requires pressing the unwieldy prefix key once: C-c @
for outline-minor-mode
, C-c ^
for smerge-mode
, and so on. This is cause enough for rebinding them somewhere convenient, perhaps to the user-reserved C-c o
(for outline) and C-c m
(for merge). Or under a leader key if you use evil-mode
. Note that you have to do this with a Hydra/Transient as well.
There are disadvantages to Repeat Mode too. First, Hydras/Transients are incredibly versatile, and repeating commands isn’t even in the top three problems they solve. Using them for repeating commands is like using an elephant gun to hunt a mouse.
They can be full fledged suites combining settings and bespoke commands that don’t have much in common. For example, here’s a Transient I use to toggle minor-modes in Emacs:
And another to resize or clip a video with ffmpeg
:
Basically, Transients have state.
In contrast, Repeat Mode only aims to save your fingers some work. You can gather disparate commands under the same umbrella map for use with repeat mode
, but it gets out of hand rather quickly. So we will limit ourselves to the intended patterns of Repeat Mode usage in this write-up.
Second, Hydras can stay alive while you press keys not in the keymap. This means you can (usually) move the cursor around to where you next want to call a Hydra command. You can add this behavior to Repeat Mode with some creative use of composed keymaps, but by default the prefix map does not stay active if you call a non-keymap command. Again, I’m going to hew to the default scope of Repeat Mode here.
Finally, repeat-mode
prompts you with available keys in the echo area, but not what commands they’re bound to:
This is generally sufficient, but I could use a Which Key style menu for rarely used keymaps with lots of keys (like smerge-mode
). As it turns out, this is quite straightforward to add.
Adding a Hydra-like prompt to Repeat Mode
If you prefer an explicit and persistent menu of available keys/commands – like a Hydra or Transient menu – you can produce a menu using Which Key or an Embark prompter when calling a relevant command:
Doing this is quite simple: we disable the built-in hint display and advise the function that sets the transient keymap after a “repeatable key” is pressed. Here’s the Which Key version: This code creates a closure, so remember to enable lexical-binding where it’s placed.
;; Disable the built-in repeat-mode hinting
(custom-set-variables repeat-echo-function #'ignore)
;; Spawn or hide a which-key popup
(advice-add 'repeat-post-hook :after
(defun repeat-help--which-key-popup ()
(if-let ((cmd (or this-command real-this-command))
(keymap (or repeat-map
(repeat--command-property 'repeat-map))))
(run-at-time
0 nil
(lambda ()
(which-key--create-buffer-and-show
nil (symbol-value keymap))))
(which-key--hide-popup))))
Specifically, we schedule the which-key popup on the main event loop. (Emacs: Making things happen when other things happen™.)
In action, applied to the smerge-mode
keymap when performing a Git merge:
Play by play
- Open a file with merge conflicts.
- Call
smerge-previous
(C-c m p
in my configuration) to go to the beginning of a hunk. This “activates” the keymap and the Which Key pop up. - Jump through conflict regions with
n
andp
. - Resolve conflicts by picking the upper (
u
) or lower (l
) regions. - Save the buffer. This quits the repeat map and the popup.
The code to produce a suitable Embark indicator involves a little more book-keeping, but the idea is the same: Schedule a keymap display when a repeatable command is invoked and remove it once it’s not relevant any more.
;; Disable the built-in repeat-mode hinting
(custom-set-variables repeat-echo-function #'ignore)
(defun repeat-help--embark-indicate ()
(if-let ((cmd (or this-command real-this-command))
(keymap (or repeat-map
(repeat--command-property 'repeat-map))))
(run-at-time
0 nil
(lambda ()
(let* ((bufname "*Repeat Commands*")
(embark-verbose-indicator-buffer-sections
'(bindings))
(embark--verbose-indicator-buffer bufname)
(embark-verbose-indicator-display-action
'(display-buffer-at-bottom
(window-height . fit-window-to-buffer)
(window-parameters . ((no-other-window . t)
(mode-line-format))))))
(funcall
(embark-verbose-indicator)
(symbol-value keymap))
(setq other-window-scroll-buffer (get-buffer bufname)))))
(when-let ((win
(get-buffer-window
"*Repeat Commands*" 'visible)))
(kill-buffer (window-buffer win))
(delete-window win))))
(advice-add 'repeat-post-hook :after #'repeat-help--embark-indicate)
Here’s what it produces:
If these are too busy, you might prefer to toggle the prompter on demand. There are only a few keymaps I need hints to use, so I bind the popup key to C-h
. Here I jump between modified hunks in a version controlled document and examine and stage them for committing using the Embark popup/indicator as a guide. Keep an eye on the key/command description at the top of the window:
Play by play
- Call
diff-hl-next-hunk
, which I’ve bound toC-x v n
. This is the only time I type the prefixC-x v
. - Jump between modified hunks in the document with
n
andp
, which would beC-x v n
andC-x v p
withoutrepeat-mode
. - Bring up the Embark key description popup with
C-h
. I toggle it a couple of times. - Call
diff-hl-show-hunk
with*
, and stage the previous hunk withS
. - Perform an action that ends the
repeat-mode
chain, in this case by yanking some text into the buffer.
The key description popup does not need to be cancelled: it automatically disappears when you run any command that’s not in the keymap, such as inserting text. (This is regular behavior for transient keymaps in Emacs.)
Both indicators (Embark and Which Key) and both kinds of behavior (auto-popup or toggle on demand) are available in Repeat Help, a package I wrote to get a simplified Hydra-like prompt for repeat maps. The prompt interface is basic but generic, so any function that can list a keymap’s entries can be plugged in.
Command smells
To find other commands or keymaps that could use repeat-izing, we can look for a code smell:
-
Any command that has “next”, “forward”, “previous” or “backward” in the name is fair game. For reasons alluded to above, this excludes common commands like
next-line
orforward-word
: jumping across text is better solved by Isearch or Avy. These do other useful things besides, like pushing the mark and letting you act on the region you jumped across. But something contextual likeorg-next-link
is fair game:(defvar org-link-repeat-map (let ((map (make-sparse-keymap))) (define-key map (kbd "n") 'org-next-link) (define-key map (kbd "p") 'org-previous-link) map)) (dolist (cmd '(org-next-link org-previous-link)) (put cmd 'repeat-map 'org-link-repeat-map))
It’s surprising that link navigation isn’t already part of Org Speed Keys, which is included in Org Mode.
-
Any predictable sequence of actions that forms a “task” is a good candidate. Smerge Mode is a good example. In its most basic usage you jump to each merge conflict, pick one of three choices and repeat. Critically, it has to be a sequence of actions carried out in (or across) an editable buffer. In
read-only-mode
buffers like help windows or mail clients, most relevant commands are already bound to single key presses at the top level. -
Any command that tweaks an analog setting will need to be repeated. Setting the text scale (
C-x C-=
) or window width (C-x {
), for instance. Fortunately these are already handled by Repeat Mode.But other built-in libraries like Windmove, for which Hydras are usually written, can be repeat-ized instead:
(defvar windmove-repeat-map (let ((map (make-sparse-keymap))) (define-key map (kbd "<left>") 'windmove-left) (define-key map (kbd "S-<left>") 'windmove-swap-states-left) (define-key map (kbd "<right>") 'windmove-right) (define-key map (kbd "S-<right>") 'windmove-swap-states-right) (define-key map (kbd "<up>") 'windmove-up) (define-key map (kbd "S-<up>") 'windmove-swap-states-up) (define-key map (kbd "<down>") 'windmove-down) (define-key map (kbd "S-<down>") 'windmove-swap-states-down) map)) (map-keymap (lambda (_key cmd) (when (symbolp cmd) (put cmd 'repeat-map 'windmove-repeat-map))) windmove-repeat-map)
Now you can continue to move across or rearrange windows with the arrow keys after calling any Windmove command.
…but I repeat myself
Keybindings are typically where modal editing paradigms have an advantage. Indeed, most repeated invocations in Vim – like searching forward with n
, f
or ;
– are single key presses. But here too I had to rebind keys to switch windows (C-w j
etc) to use it comfortably over a long editing session.
The question remained: Why do I need to press so many keys to do the same few things? After failing to gel with the Hydra paradigm, I had been manually setting up transient keymaps (with set-transient-map
) to speed up consecutive calls for a bunch of commands. Placing them behind a uniform interface is a welcome addition. Repeat Mode is a small feature that solves a small problem, but with a big cumulative impact on my experience of using Emacs.
-
Technically there’s one more approach: Use bespoke solutions like
ace-window
to switch windows. But in terms of a general approach it’s just these three. ↩︎ -
Setting a new transient keymap on each call is actually a wasteful way of producing this effect! Instead, you can set a transient keymap just once but instruct it to stay active as long as the only key pressed is grave. ↩︎
-
It makes sense that the Transient package is called that, since Transients (the menus Magit uses) are “just” souped-up transient maps, the way a Komodo dragon is just a lizard. But it causes no end of confusion when talking about them. Here I refer to a regular Emacs transient map in lowercase and the Magit-style popup-menu variety in uppercase. ↩︎