4

I just noticed that modifying another buffer creates an undo barrier in the current.

For example:

(defun my-test-undo ()
  (interactive)
  (insert "A")
  (with-current-buffer (get-buffer-create "FOO")
    (insert "abc"))
  (insert "B"))

The command will print AB. When hitting undo, only the B is removed. A second undo is needed to remove the A.

This version logs buffer-undo-list:

(defvar my-test-undo-log nil)
(defun my-test-undo-with-log ()
  (interactive)
  (setq buffer-undo-list '())
  (let ((before buffer-undo-list))
    (insert "A")
    (with-current-buffer (get-buffer-create "FOO")
      (insert "abc"))
    (let ((mid buffer-undo-list))
      (insert "B")
      (setq my-test-undo-log (list before mid buffer-undo-list)))))

After running this my-test-undo-log contains three versions of the undo information. In the last a nil has been inserted, indicating an undo barrier.

I came up with the following to manually modifying the undo list, but it feels like a hack. (Of course, it can be slimlined into a macro.)

(defun my-delete-nil-until (list sublist)
  ;; Strip of leading nil:s
  (while (and (not (eq list sublist))
          (null (car list)))
    (setq list (cdr list)))
  ;; Destructively remove nil:s
  (let ((next list))
    (while (not (eq (cdr next) sublist))
      (if (car (cdr next))
      (setq next (cdr next))
    (setcdr next (cdr (cdr next))))))
  list)

(defun my-test-undo-with-workaround ()
  (interactive)
  (let ((before buffer-undo-list))
    (insert "A")
    (with-current-buffer (get-buffer-create "FOO")
      (insert "abc"))
    (insert "B")
    (setq buffer-undo-list (my-delete-nil-until buffer-undo-list before))))

Is there any rational reasons why Emacs should behave this way, or is it a bug?

Are there better ways to handle this, like a built-in macro I've missed?

1

2 Answers 2

3

I see this in the Elisp manual, node Undo:

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.

I'm guessing that that design is responsible for the behavior you see here.

Adding what you discovered: If you do (buffer-disable-undo) in the buffer switched to then no undo boundary is set (in the original buffer).


(Why do you quote nil? '())

4
  • Oh, I didn't know that. The problem is that the code that modifies the other buffer is installed into interprogram-cut-function so it silently modifies the behaviour of other commands. I will play around with this to see if it's possible to make the modification to the other buffer non-undoable and thus avoid the boundary. Commented Oct 22, 2015 at 5:54
  • I write nil as '() as a visual queue whenever I use it as a list, to make it consistent with how I would write longer lists, as in '(1 2 3), '(1 2), '(1), and '(). Whenever I write it as boolean value I write nil. I only write (let (var) ...) whenever I never use the initial value of var. I consider the fact that the empty list is represented using nil is an unfortunate implementation detail of the language which I don't want to rely upon in actual programs. Commented Oct 22, 2015 at 5:58
  • I just experimented a bit. If I run (buffer-disable-undo) in the other buffer, no undo boundary is inserted in the original. If you extend your answer to include this, I'll accept it. Thanks! Commented Oct 22, 2015 at 8:54
  • FWIW, I do the same as you wrt nil vs () and let binding initializations. The only difference is that I don't quote (). (And that is not important. The other conventions, which we apparently both follow, are helpful, though.)
    – Drew
    Commented Oct 22, 2015 at 14:53
2

This behaviour has changed now for Emacs 25.1.

The original behaviour was to ensure that every buffer got an undo boundary, but it has negative side effects. Emacs 25.1 uses a new heuristic -- changes are lot more limited now and should only be to those buffers where changes have happened as a result of the command, not just by side effect.

So, in more detail, originally, after each change, Emacs would check the last changed buffer, and unless this was current, it would add an undo-boundary. So, when you add the second "B", you get an undo boundary after "A" because the last change was in buffer "FOO" which is not current.

Now it works differently. Emacs adds undos only at the end of a command, and it adds a boundary too all buffers which have been changed during the course of the command. So, in this case, both current and FOO would get an undo boundary after the command finishes. So, in Emacs 25, "AB" undo with a single undo.

There's a timer that adds boundaries also, but that's a detail.

The motivation for this change was pretty much the one shown here - specifically, in this case, use of the after-change-functions variable. If functions on here change another buffer, they used to affect the undo behaviour of the current buffer. Now they don't.

2
  • If I understand correctly you say that the described behavior is only for versions prior to 25.1, but apart from that I don't really understand. It would be nice if you could elaborate a bit.
    – JeanPierre
    Commented Sep 22, 2016 at 18:47
  • I've expanded it.
    – Phil Lord
    Commented Sep 23, 2016 at 14:41

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.