Hung-Yi's LogoHung-Yi’s Journal

How to Group Multiple Emacs Commands Into a Single Undo

If you've got some Emacs Lisp to do a bunch of things, but you call it by mistake and want to be able to undo it all in one go.

It’s surprisingly hard to search for documentation on this concept. Point of view: you’ve written a clever custom command do-a-million-repetitive-things to save you time and effort by doing a million repetitive things on a simple key press. This works great most of the time, but one day you trigger it accidentally and you’ve ruined your document.

“Never mind”, you say to yourself, “I can just undo it”. So you hit C-x u and expect your document to be back in working order, but alas…Emacs only undoes one step. And there were 999,999 other ways in which your document was so cleverly mangled.

Was there a way you could have written do-a-million-repetitive-things to allow you to undo it all at once? Maybe Emacs Lisp has some incantation to let you amalgamate1 all those changes into one change group1?

Heck yeah!2, 3

(defmacro with-single-undo (&rest body)
  "Execute BODY as a single undo step."
  `(let ((marker (prepare-change-group)))
     (unwind-protect ,@body
       (undo-amalgamate-change-group marker))))

This defines a new macro called with-single-undo that lets you wrap any arbitrary Emacs Lisp code and have all changes to a buffer be amalgamated into one change group, which tells Emacs’ undo system to treat it as a single undo step.

Hey there! If you’re lucky enough to be using an up-to-date build of Emacs 29, there should be a macro with-undo-amalgamate that does this already.4 You should use it if you can!

To continue our example from above, this is how you might put it to use:

(defun do-a-million-repetitive-things ()
  (interactive)
  (with-single-undo
     (do-thing-1)
     (do-thing-2)
     (do-thing-3)
     ;; 999,997 more repetitive things
     ))

Of course, in normal usage nothing will change, but the next time you need to undo do-a-million-repetitive-things, it’ll be quick and painless.

Footnotes:

1

“Amalgamate” and “change group” are the concept keywords you need to be searching for, in order to read more about this functionality.

2

Thanks to this reddit post for pointing me in the right direction.

3

And big thanks to original code from oantolin for showing everyone how it’s done.