I've written an Emacs Lisp function which calls a shell command to
process a given string and return the resulting string. Here is a
simplified example which just calls tr
to convert text to uppercase:
(defun test-shell-command (str)
"Apply tr to STR to convert lowercase letters to uppercase."
(let ((buffer (generate-new-buffer "*temp*")))
(with-current-buffer buffer
(insert str)
(call-process-region (point-min) (point-max) "tr" t t nil "'a-z'" "'A-Z'")
(buffer-string))))
This function creates a temporary buffer, inserts the text, calls
tr
, replaces the text with the result, and returns the result.
The above function works as expected, however, when I write a wrapper around this function to apply the command to the region, two steps are being added to the undo history. Here's another example:
(defun test-shell-command-region (begin end)
"Apply tr to region from BEGIN to END."
(interactive "*r")
(insert (test-shell-command (delete-and-extract-region begin end))))
When I call M-x test-shell-command-on-region
, the region is replaced
with the uppercase text, but when I press C-_
(undo
), the first
step in the undo history is the state with the text deleted. Going
two steps back, the original text is restored.
My question is, how does one prevent the intermediate step from being added to the undo history? I've read the Emacs documentation on undo, but it doesn't seem to address this as far as I can tell.
Here's a function which accomplishes the same thing by calling the
built-in Emacs function upcase
, just as before: on the result of
delete-and-extract-region
with the result being handed off to
insert
:
(defun test-upcase-region (begin end)
"Apply upcase to region from BEGIN to END."
(interactive "*r")
(insert (upcase (delete-and-extract-region begin end))))
When calling M-x test-upcase-region
, there is only one step in the
undo history, as expected. So, it seems to be the case that calling
test-shell-command
creates an undo boundary. Can that be avoided
somehow?
There are many situations where using a temporary buffer is not practical. It's hard to debug what's going on for example.
In these cases you can let-bind undo-inhibit-record-point
to prevent Emacs from deciding where to put boundaries:
(let ((undo-inhibit-record-point t))
;; Then record boundaries manually
(undo-boundary)
(do-lots-of-stuff)
(undo-boundary))
The key is the buffer name. See Maintaining Undo:
Recording of undo information in a newly created buffer is normally enabled to start with; but if the buffer name starts with a space, the undo recording is initially disabled. You can explicitly enable or disable undo recording with the following two functions, or by setting buffer-undo-list yourself.
with-temp-buffer
creates a buffer named ␣*temp*
(note the leading whitespace), whereas your function uses *temp*
.
To remove the undo boundary in your code, either use a buffer name with a leading space, or explicitly disable undo recoding in the temporary buffer with buffer-disable-undo
.
But generally, use with-temp-buffer
, really. It's the standard way for such things in Emacs, making your intention clear to anyone who reads your code. Also, with-temp-buffer
tries hard to clean up the temporary buffer properly.
As for why undo in the temporary buffer creates an undo boundary in the current one: If the previous change was undoable and made in some other buffer (the temporary one in this case), an implicit boundary is created. From undo-boundary
:
All buffer modifications add a boundary whenever the previous undoable change was made in some other buffer. This is to ensure that each command makes a boundary in each buffer where it makes changes.
Hence, inhibiting undo in the temporary buffer removes the undo boundary in the current buffer, too: The previous change is simply not undoable anymore, and thus no implicit boundary is created.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With