Subject: bug#77580: [PATCH] New command ediff-undo

From: "Paul D. Nelson" <[email-masked]>
Date: Mon, 07 Apr 2025 08:20:16 +0200

Hi Sean, thanks for your comments.

> - Why ediff?  Why not plain diff?  The latter is lighter weight, and is
>   what diff-buffer-with-file uses, and your new command feels similar to
>   diff-buffer-with-file, to me.

"ediff" because that's what I've typically used more, but I agree that
it makes sense to have a "diff" analogue, so I've added one (attached).

> - Have you thought about automatically detecting how far to go back,
>   somehow?  We already have a feature whereby autosaves are disabled if
>   the buffer text has shrunk a lot; maybe that heuristic could help
>   here.  Probably this would want to be a separate command so the manual
>   version was still available.
>

In practice, I've just spammed C-u enough times.  I considered allowing
a negative argument to signal "undo everything", which would be most
useful with an active region; how does that sound?

I hadn't thought about automatic detection along the lines that you
suggest.  I think I'm happy to leave that for now, either to a
refinement of this command or a future one.

>> +         (orig-buf (current-buffer))
>> +         (current-major major-mode)
>> +         (undo-buf-name (concat "UNDO=" (buffer-name orig-buf)))
>
> A more usual name would be " *ediff-undo*<N>", I think.  The space means
> it is considered an internal buffer.  (I know you're copying other ediff
> practice with this FOO= thing but let's not introduce more.)

I'm hesitant to mark it as internal since other Ediff functions don't
automatically delete modified non-indirect buffers without user-defined
cleanup hooks.  In particular, I think it would be confusing if this
function behaved differently from ediff-current-file, although I see
your point about naming conventions.  Perhaps "*ediff-undo*<N>" would be
more suitable?

>> +      (funcall current-major)
>
> Could you explain why the temporary buffer needs to be set to the major
> mode?  Is it for font lock/syntax highlighting, essentially?

Another reason is that users might edit the "undo" buffer during the
Ediff session, in which case standard keybindings are useful.  Other
suggestions would be welcome here.

Paul

>From 9e3782e855909f6170faa5078b65c0d46e91e7a6 Mon Sep 17 00:00:00 2001
From: Paul Nelson <[email-masked]>
Date: Sun, 6 Apr 2025 15:19:50 +0200
Subject: [PATCH] Add diff-with-undo and ediff-undo commands

* lisp/vc/diff.el (diff-with-undo):
* lisp/vc/ediff.el (ediff-undo): New commands to compare current
buffer and its undo state.
* doc/emacs/files.texi (Comparing Files):
* doc/misc/ediff.texi (Major Entry Points): Document them.
* lisp/simple.el (undo-to-buffer): New helper function, used to
implement the above.
---
 doc/emacs/files.texi |  4 ++++
 doc/misc/ediff.texi  |  4 ++++
 etc/NEWS             | 10 ++++++++++
 lisp/simple.el       | 23 +++++++++++++++++++++++
 lisp/vc/diff.el      | 15 +++++++++++++++
 lisp/vc/ediff.el     | 16 ++++++++++++++++
 6 files changed, 72 insertions(+)

diff --git a/doc/emacs/files.texi b/doc/emacs/files.texi
index 6128be8f128..8dee84ed1fa 100644
--- a/doc/emacs/files.texi
+++ b/doc/emacs/files.texi
@@ -1680,6 +1680,10 @@ Comparing Files
   The command @kbd{M-x diff-buffers} compares the contents of two
 specified buffers.
 
+@findex diff-with-undo
+  The command @kbd{M-x diff-with-undo} compares the current buffer with
+its undo states.
+
 @findex compare-windows
   The command @kbd{M-x compare-windows} compares the text in the
 current window with that in the window that was the selected window
diff --git a/doc/misc/ediff.texi b/doc/misc/ediff.texi
index 73f751f3a4b..080d7e0c432 100644
--- a/doc/misc/ediff.texi
+++ b/doc/misc/ediff.texi
@@ -165,6 +165,10 @@ Major Entry Points
 Compare the buffer with its file on disk.  This function can be used as a
 safe version of @code{revert-buffer}.
 
+@item ediff-undo
+@findex ediff-undo
+Compare the buffer or region with its undo states.
+
 @item ediff-buffers
 @findex ediff-buffers
 Compare two buffers.
diff --git a/etc/NEWS b/etc/NEWS
index 35e6edcd712..f7bed99af8f 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1207,6 +1207,12 @@ overlap.
 
 *** 'diff-apply-hunk' now supports creating and deleting files.
 
+*** New command 'diff-with-undo' to compare buffer with its undo states.
+This command compares the current buffer and how it would look if undo
+were performed.  With a prefix argument, it considers that many undo
+operations.  With an active region, it restricts itself to changes in
+that region.
+
 ---
 *** 'vc-version-diff' and 'vc-root-version-diff' changed default for REV1.
 They suggest the previous revision as the default for REV1, not the last
@@ -1256,6 +1262,10 @@ and control windows can be left undisplayed and the corresponding
 variable set to nil.  This change enables custom layouts without
 a control panel window.
 
+*** New command 'ediff-undo' to compare buffer with its undo states.
+This command compares the current buffer and how it would look if undo
+were performed (see similar entry for 'diff-with-undo').
+
 ** Dired
 
 +++
diff --git a/lisp/simple.el b/lisp/simple.el
index 7037158df8d..32accb473ac 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -4254,6 +4254,29 @@ undo-outer-limit-truncate
 		     :warning)
     (setq buffer-undo-list nil)
     t))
+
+(defun undo-to-buffer (target-buffer source-buffer arg &optional beg end)
+  "Apply ARG undo operations from SOURCE-BUFFER to TARGET-BUFFER.
+TARGET-BUFFER is set to the undone state of SOURCE-BUFFER.
+If BEG and END are non-nil, restrict undo operations to that region.
+
+ARG is the number of undo operations to apply (must be non-negative).
+The major mode of TARGET-BUFFER is set to match SOURCE-BUFFER."
+  (unless (>= arg 0)
+    (error "Negative argument %d" arg))
+  (with-current-buffer target-buffer
+    (erase-buffer)
+    (insert-buffer-substring source-buffer)
+    (setq buffer-undo-list
+          (undo-copy-list (buffer-local-value 'buffer-undo-list source-buffer)))
+    (funcall (buffer-local-value 'major-mode source-buffer))
+    (if (and beg end)
+        (let ((undo-in-region t)
+              (last-command nil)
+              (this-command 'undo))
+          (undo-start beg end)
+          (undo-more (1+ arg)))
+      (undo arg))))
 
 ;;;; Shell commands
 
diff --git a/lisp/vc/diff.el b/lisp/vc/diff.el
index c8a1b7c0efa..d7247959407 100644
--- a/lisp/vc/diff.el
+++ b/lisp/vc/diff.el
@@ -295,6 +295,21 @@ diff-buffers
      (list oldb newb (diff-switches))))
   (diff (get-buffer old) (get-buffer new) switches no-async))
 
+;;;###autoload
+(defun diff-with-undo (arg)
+  "View differences between current buffer and its undo state
+With prefix ARG, consider that many undo operations.
+With active region, restrict to operations in the region.
+
+This requires the external program `diff' to be in your `exec-path'."
+  (interactive "p")
+  (let* ((beg (use-region-beginning))
+         (end (use-region-end))
+         (orig-buf (current-buffer)))
+    (with-temp-buffer
+      (undo-to-buffer (current-buffer) orig-buf arg beg end)
+      (diff-buffers (current-buffer) orig-buf nil 'noasync))))
+
 (provide 'diff)
 
 ;;; diff.el ends here
diff --git a/lisp/vc/ediff.el b/lisp/vc/ediff.el
index bc89b2025ff..092f376ac98 100644
--- a/lisp/vc/ediff.el
+++ b/lisp/vc/ediff.el
@@ -400,6 +400,22 @@ ediff-current-file
       (funcall current-major))
     (ediff-buffers revert-buf (current-buffer) startup-hooks)))
 
+;;;###autoload
+(defun ediff-undo (arg &optional startup-hooks)
+  "Run Ediff on current buffer and its undo states.
+Compare buffer to how it would be if undo were performed.
+With prefix ARG, consider that many undo operations.
+With active region, restrict to operations in the region.
+
+Non-interactively, STARTUP-HOOKS is a list of functions that Emacs calls
+without arguments after setting up the Ediff buffers."
+  (interactive "p")
+  (let* ((beg (use-region-beginning))
+         (end (use-region-end))
+         (undo-buf-name (concat "UNDO=" (buffer-name)))
+         (undo-buf (get-buffer-create undo-buf-name)))
+    (undo-to-buffer undo-buf (current-buffer) arg beg end)
+    (ediff-buffers undo-buf (current-buffer) startup-hooks)))
 
 ;;;###autoload
 (defun ediff-backup (file)
-- 
2.39.3 (Apple Git-145)