Subject: Re: bug#60467: 30.0.50; primitive-undo: Changes to be undone by function different from announced

From: Gregory Heytings <[email-masked]>
Date: Thu, 10 Aug 2023 13:55:56 +0000

>> It's not clear to me what you want: do you want to fix this issue on 
>> the emacs-29 branch or on the master branch?
>
> I suggested to install your fix on master.
>

Okay, but see below.

>> We now have two bug reports on this (the one by Frédéric Giquel in this 
>> bug, and the one by Eason Huang in bug#64989).  I just verified (again) 
>> that the Fix-combine-change-call-with-timestamps patch fixes these two 
>> recipes.
>
> If bug#64989 could be fixed by a simpler, safer change (which remains to 
> be seen), we might do that on emacs-29.  If not, at least we will have 
> this fixed in Emacs 30.
>

Bug#64989 is the same bug, only the recipe differs.  And the change I 
suggested six months ago is safe, it replaces an obscure and bugged piece 
of code with a refactored and clear piece of code.  I attach it again 
(that patch is for emacs-29, the one for master would be similar).

If Stefan (or anyone else) fixes the bug in a safer way, that patch is not 
necessary.
From 169b2064a4c5200fbfdfd7c6f43cb18e4f854b92 Mon Sep 17 00:00:00 2001
From: Gregory Heytings <[email-masked]>
Date: Thu, 10 Aug 2023 13:49:15 +0000
Subject: [PATCH] Fix combine-change-call

* lisp/subr.el (combine-change-calls-1): Rewrite and document
the part which creates the undo-list element.  Fixes bug#60467
and bug#64989.
---
 lisp/subr.el | 60 ++++++++++++++++++++++++++++++----------------------
 1 file changed, 35 insertions(+), 25 deletions(-)

diff --git a/lisp/subr.el b/lisp/subr.el
index 872c701dbe7..d0760e92474 100644
--- a/lisp/subr.el
+++ b/lisp/subr.el
@@ -4978,31 +4978,41 @@ combine-change-calls-1
 	      (kill-local-variable 'before-change-functions))
 	    (if local-acf (setq after-change-functions acf)
 	      (kill-local-variable 'after-change-functions))))
-        (when (not (eq buffer-undo-list t))
-          (let ((ap-elt
-		 (list 'apply
-		       (- end end-marker)
-		       beg
-		       (marker-position end-marker)
-		       #'undo--wrap-and-run-primitive-undo
-		       beg (marker-position end-marker) buffer-undo-list))
-		(ptr buffer-undo-list))
-	    (if (not (eq buffer-undo-list old-bul))
-		(progn
-		  (while (and (not (eq (cdr ptr) old-bul))
-			      ;; In case garbage collection has removed OLD-BUL.
-			      (cdr ptr))
-		    (if (and (consp (cdr ptr))
-			     (consp (cadr ptr))
-			     (eq (caadr ptr) t))
-			;; Don't include a timestamp entry.
-			(setcdr ptr (cddr ptr))
-		      (setq ptr (cdr ptr))))
-		  (unless (cdr ptr)
-		    (message "combine-change-calls: buffer-undo-list broken"))
-		  (setcdr ptr nil)
-		  (push ap-elt buffer-undo-list)
-		  (setcdr buffer-undo-list old-bul)))))
+	;; If buffer-undo-list is neither t (in which case undo
+	;; information is not recorded) nor equal to buffer-undo-list
+	;; before body was funcalled (in which case (funcall body) did
+	;; not add items to buffer-undo-list) ...
+	(unless (or (eq buffer-undo-list t)
+		    (eq buffer-undo-list old-bul))
+	  (let ((ptr buffer-undo-list) body-undo-list)
+	    ;; ... then loop over buffer-undo-list, until the head of
+	    ;; buffer-undo-list before body was funcalled is found, or
+	    ;; ptr is nil (which may happen if garbage-collect has
+	    ;; been called after (funcall body) and has removed
+	    ;; entries of buffer-undo-list that were added by (funcall
+	    ;; body)), and add these entries to body-undo-list.
+	    (while (and ptr (not (eq ptr old-bul)))
+	      (push (car ptr) body-undo-list)
+	      (setq ptr (cdr ptr)))
+	    (setq body-undo-list (nreverse body-undo-list))
+	    ;; Warn if garbage-collect has truncated buffer-undo-list
+	    ;; behind our back.
+	    (when (and old-bul (not ptr))
+	      (message
+               "combine-change-calls: buffer-undo-list has been truncated"))
+	    ;; Add an (apply ...) entry to buffer-undo-list, using
+	    ;; body-undo-list ...
+	    (push (list 'apply
+			(- end end-marker)
+			beg
+			(marker-position end-marker)
+			#'undo--wrap-and-run-primitive-undo
+			beg (marker-position end-marker)
+			body-undo-list)
+		  buffer-undo-list)
+	    ;; ... and set the cdr of buffer-undo-list to
+	    ;; buffer-undo-list before body was funcalled.
+	    (setcdr buffer-undo-list old-bul)))
 	(if (not inhibit-modification-hooks)
 	    (run-hook-with-args 'after-change-functions
 				beg (marker-position end-marker)
-- 
2.39.2