Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to prevent Emacs from setting an undo boundary?

Tags:

undo

emacs

elisp

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?

like image 425
Jason Blevins Avatar asked Feb 26 '13 18:02

Jason Blevins


2 Answers

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))
like image 42
Lionel Henry Avatar answered Sep 27 '22 18:09

Lionel Henry


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.

like image 58
lunaryorn Avatar answered Sep 27 '22 18:09

lunaryorn