Subject: Re: bug#23906: 25.0.95; Undo boundary after process output is not consistent

From: Stefan Monnier <[email-masked]>
Date: Mon, 18 Jul 2016 00:18:36 -0400

> Please let us use this opportunity to fix the more general case
> too. Stefan agreed that the following primitives would work:

>     -) undo-begin-transaction
>        Starts a new transaction.
>     -) undo-end-transaction
>        Ends the most recently started undo transaction.

> The effects of all commands between would be undone as a single unit.

How 'bout the patch below (see the viper part to get an idea about how
to use it in ediprolog)?

        Stefan

diff --git a/lisp/simple.el b/lisp/simple.el
index f114235..e54d4f2 100644
--- a/lisp/simple.el
+++ b/lisp/simple.el
@@ -3054,6 +3054,41 @@ undo-auto--undoable-change
   (undo-auto--boundary-ensure-timer))
 ;; End auto-boundary section
 
+(defun undo-amalgamate-change-group (handle)
+  "Amalgamate changes in change-group since HANDLE.
+Remove all undo boundaries between the state of HANDLE and now.
+HANDLE is as returned by `prepare-change-group'."
+  (dolist (elt handle)
+    (with-current-buffer (car elt)
+      (setq elt (cdr elt))
+      (when (consp buffer-undo-list)
+        (let ((old-car (car-safe elt))
+              (old-cdr (cdr-safe elt)))
+          (unwind-protect
+              (progn
+                ;; Temporarily truncate the undo log at ELT.
+                (when (consp elt)
+                  (setcar elt t) (setcdr elt nil))
+                (when
+                    (or (null elt)        ;The undo-log was empty.
+                        ;; `elt' is still in the log: normal case.
+                        (eq elt (last buffer-undo-list))
+                        ;; `elt' is not in the log any more, but that's because
+                        ;; the log is "all new", so we should remove all
+                        ;; boundaries from it.
+                        (not (eq (last buffer-undo-list) (last old-cdr))))
+                  (cl-callf (lambda (x) (delq nil x))
+                      (if (car buffer-undo-list)
+                          buffer-undo-list
+                        ;; Preserve the undo-boundaries at either ends of the
+                        ;; change-groups.
+                        (cdr buffer-undo-list)))))
+            ;; Reset the modified cons cell ELT to its original content.
+            (when (consp elt)
+              (setcar elt old-car)
+              (setcdr elt old-cdr))))))))
+
+
 (defcustom undo-ask-before-discard nil
   "If non-nil ask about discarding undo info for the current command.
 Normally, Emacs discards the undo info for the current command if
diff --git a/lisp/emulation/viper-cmd.el b/lisp/emulation/viper-cmd.el
index 3d9d1cc..3ce1b4d 100644
--- a/lisp/emulation/viper-cmd.el
+++ b/lisp/emulation/viper-cmd.el
@@ -1709,40 +1709,20 @@ viper-undo-more
 ;; The following two functions are used to set up undo properly.
 ;; In VI, unlike Emacs, if you open a line, say, and add a bunch of lines,
 ;; they are undone all at once.
-(defun viper-adjust-undo ()
-  (if viper-undo-needs-adjustment
-      (let ((inhibit-quit t)
-	    tmp tmp2)
-	(setq viper-undo-needs-adjustment nil)
-	(when (listp buffer-undo-list)
-          (let ((had-boundary (null (car buffer-undo-list))))
-            (if (setq tmp (memq viper-buffer-undo-list-mark buffer-undo-list))
-		(progn
-		  (setq tmp2 (cdr tmp)) ; the part after mark
-
-		  ;; cut tail from buffer-undo-list temporarily by direct
-		  ;; manipulation with pointers in buffer-undo-list
-		  (setcdr tmp nil)
-
-		  (setq buffer-undo-list (delq nil buffer-undo-list))
-		  (setq buffer-undo-list
-			(delq viper-buffer-undo-list-mark buffer-undo-list))
-		  ;; restore tail of buffer-undo-list
-		  (setq buffer-undo-list (nconc buffer-undo-list tmp2)))
-	      (setq buffer-undo-list (delq nil buffer-undo-list)))
-            ;; The top-level loop only adds boundaries if there has been
-            ;; modifications in the buffer, so make sure we don't accidentally
-            ;; drop the "final" boundary (bug#22295).
-	    (if had-boundary (undo-boundary)))))))
+(viper-deflocalvar viper--undo-change-group-handle nil)
+(put 'viper--undo-change-group-handle 'permanent-local t)
 
+(defun viper-adjust-undo ()
+  (when viper--undo-change-group-handle
+    (undo-amalgamate-change-group
+     (prog1 viper--undo-change-group-handle
+       (setq viper--undo-change-group-handle nil)))))
 
 (defun viper-set-complex-command-for-undo ()
-  (if (listp buffer-undo-list)
-      (if (not viper-undo-needs-adjustment)
-	  (let ((inhibit-quit t))
-	    (setq buffer-undo-list
-		  (cons viper-buffer-undo-list-mark buffer-undo-list))
-	    (setq viper-undo-needs-adjustment t)))))
+  (and (listp buffer-undo-list)
+       (not viper--undo-change-group-handle)
+       (setq viper--undo-change-group-handle
+             (prepare-change-group))))

 ;;; Viper's destructive Command ring utilities
diff --git a/lisp/emulation/viper-init.el b/lisp/emulation/viper-init.el
index 086daf2..838d846 100644
--- a/lisp/emulation/viper-init.el
+++ b/lisp/emulation/viper-init.el
@@ -369,15 +369,6 @@ viper-set-input-method
 
 ;; VI-style Undo
 
-;; Used to 'undo' complex commands, such as replace and insert commands.
-(viper-deflocalvar viper-undo-needs-adjustment nil)
-(put 'viper-undo-needs-adjustment 'permanent-local t)
-
-;; A mark that Viper puts on buffer-undo-list.  Marks the beginning of a
-;; complex command that must be undone atomically.  If inserted, it is
-;; erased by viper-change-state-to-vi and viper-repeat.
-(defconst viper-buffer-undo-list-mark 'viper)
-
 (defcustom viper-keep-point-on-undo nil
   "Non-nil means not to move point while undoing commands.
 This style is different from Emacs and Vi.  Try it to see if