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

From: "Paul D. Nelson" <[email-masked]>
Date: Sun, 06 Apr 2025 17:45:16 +0200

I'd like to propose a command 'ediff-undo' that compares the current
buffer with how it would look after undo.  It supports a prefix arg (to
specify multiple undo steps) and respects the active region (to restrict
undo operations to that region).

Some less obvious situations where I've found this helpful:

- When a buffer has been modified by an external program or a
  complicated Emacs command.

- After replacing part of a buffer with text obtained externally (e.g.,
  copied from an external email client).

- For a quick recap of recent edits to a buffer or region, via something
  like C-u C-u C-u M-x ediff-undo.

The implementation is a straightforward combination of parts of
'ediff-current-file' and 'undo'.  Any feedback would be welcome.

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

* lisp/vc/ediff.el (ediff-undo): New command to compare current
buffer and its undo state.
* doc/misc/ediff.texi (Major Entry Points): Document it.
---
 doc/misc/ediff.texi |  4 ++++
 etc/NEWS            |  6 ++++++
 lisp/vc/ediff.el    | 41 +++++++++++++++++++++++++++++++++++++++++
 3 files changed, 51 insertions(+)

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..261ff68605c 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -1288,6 +1288,12 @@ name of the directory now reverts the Dired buffer.
 With a new value of the prefix argument (1), this command copies file
 names relative to the root directory of the current project.
 
+*** 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.  With a prefix argument, it considers that many undo
+operations.  With an active region, it restricts to changes in that
+region.
+
 ** Grep
 
 +++
diff --git a/lisp/vc/ediff.el b/lisp/vc/ediff.el
index bc89b2025ff..2c389ca19af 100644
--- a/lisp/vc/ediff.el
+++ b/lisp/vc/ediff.el
@@ -400,6 +400,47 @@ ediff-current-file
       (funcall current-major))
     (ediff-buffers revert-buf (current-buffer) startup-hooks)))
 
+;;;###autoload
+(defun ediff-undo (&optional arg 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.
+If there is nothing to undo, then this command fails.
+
+Non-interactively, STARTUP-HOOKS is a list of functions that Emacs calls
+without arguments after setting up the Ediff buffers."
+  (interactive "p")
+  (unless (and buffer-undo-list (not (eq buffer-undo-list t)))
+    (error "Nothing to undo in the current buffer"))
+  (let* ((use-region (use-region-p))
+         (beg (and use-region (region-beginning)))
+         (end (and use-region (region-end)))
+         (arg (if (numberp arg) arg 1))
+         (orig-buf (current-buffer))
+         (current-major major-mode)
+         (undo-buf-name (concat "UNDO=" (buffer-name orig-buf)))
+         (undo-buf (get-buffer undo-buf-name)))
+    (unless (>= arg 0)
+      (error "Negative argument %d" arg))
+    (when undo-buf
+      (kill-buffer undo-buf)
+      (setq undo-buf nil))
+    (setq undo-buf (get-buffer-create undo-buf-name))
+    (with-current-buffer undo-buf
+      (insert-buffer-substring orig-buf)
+      (setq buffer-undo-list
+            (undo-copy-list (with-current-buffer orig-buf
+                              buffer-undo-list)))
+      (funcall current-major)
+      (if use-region
+          (let* ((undo-in-region t)
+                 (last-command nil)
+                 (this-command 'undo))
+            (undo-start beg end)
+            (undo-more (1+ arg)))
+        (undo arg)))
+    (ediff-buffers undo-buf orig-buf startup-hooks)))
 
 ;;;###autoload
 (defun ediff-backup (file)
-- 
2.39.3 (Apple Git-145)