How to undo call-last-kbd-macro in emacs
Asked Answered
P

3

10

In emacs, I sometimes invoke call-last-kbd-macro by mistake. When undoing I would have expected undo to undo the entire effect of the keyboard macro atomically, but that does not happen. Instead I find myself having to undo each step of the macro one at a time. How can I get emacs to return to the buffer state before the execution of the macro?

Pomfrey answered 27/10, 2010 at 16:26 Comment(0)
F
3

I'm afraid you can't do that with just the built-in 'undo mechanism. The macro system in Emacs actually plays things back as though the user were actually typing in the keystrokes (or mouse events), and so the undo history (buffer-undo-list) gets updated as normal.

Here's a suggestion of how to extend the current undo mechanism to do what you want.

  1. extend 'undo to understand a new entry in the undo list, a macro-begin marker and a macro-end element

  2. advise/change the macro playback to insert the markers at the beginning/end of the macro playback

  3. have the undo code treat all undo events between the two markers as a unit and undo them all (and add the appropriate markers on the end of the undo history so when you redo things, they're still treated as a single block)

Caveats:

  • This would only work for macros that operate in a single buffer, if your macro switched buffers (or had side effects in other buffers), those changes would not be treated as a block.
  • If your macro ended in a different buffer than it started, then you'd have to handle that cleanly - you don't want "unbalanced" macro-begin and macro-end markers in the undo list.

Needless to say, this is a complicated endeavor. I wish you luck.

Feliciafeliciano answered 28/10, 2010 at 3:54 Comment(2)
Thanks Trey. Indeed a complex answer. A simpler but more hackish solution in the same spirit would be to make call-last-kbd-macro (or a wrapper) insert some kind of no-op marker in the undo list. And then create a new function clear-to-macro-marker that would undo all changes until the marker was hit.Pomfrey
Along those lines is panix.com/~harley/elisp/undo-group.el which adds a marker namely two nils which it then uses as a boundary for an undo-group command.Housebreak
L
3

[13 years later...] Simply run the command (with-undo-amalgamate (call-last-kbd-macro)).

The with-undo-amalgamate command was introduced in Emacs 29.1; it makes this sort of task much easier.

To make prefix arguments and whatnot work, here's what I have in my .emacs:

(defun undoable-call-last-kbd-macro (&optional PREFIX LOOPFUNC)
  "Thin wrapper around `call-last-kbd-macro' so that the <undo> command undoes the entire macro."
  (interactive "p")
  (with-undo-amalgamate (call-last-kbd-macro PREFIX LOOPFUNC)))

This does exactly what you want. (Edge case: say you call it with a prefix command, eg M-5 call-last-kbd-macro. Then <undo> will undo all 5 applications of the macro, not just the fifth. This makes sense; it's consistent with how "undo" handles other commands with prefix arguments.) The same trick works for making kmacro-end-or-call-macro atomically undoable.


Sidenote: you can do the same thing with named macros. Eg, if you have the keybinding

(global-set-key (kbd "C-c C-c") 'my-macro)

you should replace it with

(global-set-key (kbd "C-c C-c") (lambda () (interactive) (with-undo-amalgamate (my-macro))))
Lyndy answered 5/1 at 5:53 Comment(0)
H
1

This can be done pretty easily by overriding undo-boundary with e.g. the noflet package.

Given a macro definition (as generated by insert-kbd-macro) as follows:

(fset 'my-macro
   [... keys ...])

or:

(fset 'my-macro
   (lambda (&optional arg) "Keyboard macro." (interactive "p") (kmacro-exec-ring-item (quote ([...keys...] 0 "%d")) arg)))

Edit it as such:

(require 'noflet)

(fset 'my-macro
      (lambda (&optional arg) "Keyboard macro." (interactive "p")
        (undo-boundary)
        (noflet ((undo-boundary ()))
          (kmacro-exec-ring-item (quote ([...keys...] 0 "%d")) arg))
        (undo-boundary)))

If you don't want to edit all your macro definitions, you could alternatively add a wrapper which invokes a macro given as an argument, while creating / suppressing undo boundaries as above.

Halinahalite answered 10/1, 2019 at 21:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.