branch: externals/vundo
commit 258d99e3342454b9b87ee8de2cf0465771adc0bd
Author: JD Smith <93749+jdtsm...@users.noreply.github.com>
Commit: Yuan Fu <caso...@gmail.com>

    Highlight the previously saved nodes (issue#62)
    
    * vundo.el (vundo-saved):
    (vundo-last-saved): New faces.
    (vundo-highlight-saved-nodes): New option.
    (vundo-m): A new field timestamp.
    (vundo--mod-list-from): Set the timestamp field when creating a mod.
    (vundo--mod-timestamp): New function.
    (vundo--draw-tree): Add a parameter. Highlight nodes that represent
    saved buffer state with vundo-saved face. Also calculate the last save
    node's index, and highlight the last saved node in vundo-last-saved
    face.
    (vundo-mode-map): New binding for vundo-goto-last-saved.
    (vundo--last-saved-idx): New local state.
    (vundo--refresh-buffer): Set vundo--last-saved-idx when clearing local
    state.
    (vundo--highlight-node): Give current node's highlight a higher
    priority.
    (vundo--highlight-last-saved-node): New function.
    (vundo-goto-last-saved): New command.
    (vundo-save): Update vundo--last-saved-idx and re-highlight.
    (vundo--inspect): Also print the timestamp.
---
 vundo.el | 180 ++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 150 insertions(+), 30 deletions(-)

diff --git a/vundo.el b/vundo.el
index 0ad10a566b..1e50b99878 100644
--- a/vundo.el
+++ b/vundo.el
@@ -156,6 +156,7 @@
 (require 'pcase)
 (require 'cl-lib)
 (require 'seq)
+(require 'subr-x)
 
 ;;; Customization
 
@@ -179,10 +180,27 @@
      (:inherit vundo-node :weight bold :foreground "yellow")))
   "Face for the highlighted node in the undo tree.")
 
+(defface vundo-saved
+  '((((background light)) .
+     (:inherit vundo-node :foreground "dark green"))
+    (((background dark)) .
+     (:inherit vundo-node  :foreground "light green")))
+  "Face for saved nodes in the undo tree.")
+
+(defface vundo-last-saved
+  '((t (:inherit vundo-saved :weight bold)))
+  "Face for the last saved node in the undo tree.")
+
 (defcustom vundo-roll-back-on-quit t
   "If non-nil, vundo will roll back the change when it quits."
   :type 'boolean)
 
+(defcustom vundo-highlight-saved-nodes t
+  "If non-nil, vundo will highlight nodes which have been saved and then 
modified.
+The face `vundo-saved' is used for saved nodes, except for the
+most recent such node, which receives the face `vundo-last-saved'."
+  :type 'boolean)
+
 (defcustom vundo-window-max-height 3
   "The maximum height of the vundo window."
   :type 'integer)
@@ -300,7 +318,15 @@ modification."
   (point
    nil
    :type integer
-   :documentation "Marks the text node in the vundo buffer if drawn."))
+   :documentation "Marks the text node in the vundo buffer if drawn.")
+  (timestamp
+   nil
+   :type timestamp
+   :documentation
+   "Timestamp at which this mod altered a saved buffer state.
+If this field is non-nil, the mod contains a timestamp entry in
+the undo list, meaning the previous state was saved to file. This
+field records that timestamp."))
 
 (defun vundo--position-only-p (undo-list)
   "Check if the records at the start of UNDO-LIST are position-only.
@@ -330,18 +356,27 @@ If MOD-LIST non-nil, extend on MOD-LIST."
       ;; It's possible the index was exceeded stepping over nil.
       (when (or (null n) (< uidx n))
         ;; Add modification.
-        (unless (vundo--position-only-p undo-list)
-          ;; If this record is position-only, we skip it and don’t add a
-          ;; mod for it. Effectively taking it out of the undo tree.
-          ;; Read ‘Position-only records’ section in Commentary for more
-          ;; explanation.
-          (cl-assert (not (null (car undo-list))))
-          (push (make-vundo-m :undo-list undo-list)
-                new-mlist))
-        ;; Skip through the content of this modification.
-        (while (car undo-list)
-          (setq undo-list (cdr undo-list))
-          (cl-incf uidx))))
+        (let ((pos-only (vundo--position-only-p undo-list))
+              (mod-timestamp nil))
+          (unless pos-only
+            ;; If this record is position-only, we skip it and don’t
+            ;; add a mod for it. Effectively taking it out of the undo
+            ;; tree. Read ‘Position-only records’ section in
+            ;; Commentary for more explanation.
+            (cl-assert (not (null (car undo-list))))
+            (push (make-vundo-m :undo-list undo-list)
+                  new-mlist))
+          ;; Skip through the content of this modification.
+          (while (car undo-list)
+            ;; Is this entry a timestamp?
+            (when (and (consp (car undo-list)) (eq (caar undo-list) t))
+              (setq mod-timestamp (cdar undo-list)))
+            (setq undo-list (cdr undo-list))
+            (cl-incf uidx))
+          ;; If this modification contains a timestamp, the previous
+          ;; state is saved to file.
+          (when (and mod-timestamp (not pos-only))
+            (setf (vundo-m-timestamp (car new-mlist)) mod-timestamp)))))
     ;; Convert to vector.
     (vconcat mod-list new-mlist)))
 
@@ -514,13 +549,29 @@ Translate according to `vundo-glyph-alist'."
                   vundo-glyph-alist)))
               text 'string))
 
-(defun vundo--draw-tree (mod-list)
-  "Draw the tree in MOD-LIST in current buffer."
+(defun vundo--mod-timestamp (mod-list idx)
+  "Return a timestamp if the mod in MOD-LIST at IDX has a timestramp."
+  ;; If the next mod’s timestamp is non-nil, this mod/node
+  ;; represents a saved state.
+  (let* ((next-mod-idx (1+ idx))
+         (next-mod (when (< next-mod-idx (length mod-list))
+                     (aref mod-list next-mod-idx))))
+    (and next-mod (vundo-m-timestamp next-mod))))
+
+(defvar vundo--last-saved-idx)
+
+(defun vundo--draw-tree (mod-list orig-buffer-modified)
+  "Draw the tree in MOD-LIST in current buffer.
+ORIG-BUFFER-MODIFIED is t if the original buffer is not saved to
+the disk. Set `vundo--last-saved-idx' by side-effect,
+corresponding to the index of the last saved node."
   (let* ((root (aref mod-list 0))
          (node-queue (list root))
          (inhibit-read-only t)
-         (inhibit-modification-hooks t))
+         (inhibit-modification-hooks t)
+         (last-saved-idx -1))
     (erase-buffer)
+    (setq vundo--last-saved-idx -1)
     (while node-queue
       (let* ((node (pop node-queue))
              (children (vundo-m-children node))
@@ -528,13 +579,19 @@ Translate according to `vundo-glyph-alist'."
              ;; Is NODE the last child of PARENT?
              (node-last-child-p
               (if parent
-                  (eq node (car (last (vundo-m-children parent)))))))
+                  (eq node (car (last (vundo-m-children parent))))))
+             (node-idx (vundo-m-idx node))
+             (saved-p (and vundo-highlight-saved-nodes
+                           (vundo--mod-timestamp mod-list node-idx)))
+             (node-face  (if saved-p 'vundo-saved 'vundo-node)))
+        (when (and saved-p (> node-idx last-saved-idx))
+          (setq last-saved-idx node-idx))
         ;; Go to parent.
         (if parent (goto-char (vundo-m-point parent)))
         (let ((col (max 0 (1- (current-column)))))
           (if (null parent)
               (insert (propertize (vundo--translate "○")
-                                  'face 'vundo-node))
+                                  'face node-face))
             (let ((planned-point (point)))
               ;; If a node is blocking, try next line.
               ;; Example: 1--2--3  Here we want to add a
@@ -556,7 +613,7 @@ Translate according to `vundo-glyph-alist'."
                             (if vundo-compact-display "─" "──"))
                            'face 'vundo-stem)
                           (propertize (vundo--translate "○")
-                                      'face 'vundo-node))
+                                      'face node-face))
                 ;; Delete the previously inserted |.
                 (delete-char -1)
                 (insert (propertize
@@ -566,13 +623,26 @@ Translate according to `vundo-glyph-alist'."
                             (if vundo-compact-display "├─" "├──")))
                          'face 'vundo-stem))
                 (insert (propertize (vundo--translate "○")
-                                    'face 'vundo-node))))))
+                                    'face node-face))))))
         ;; Store point so we can later come back to this node.
         (setf (vundo-m-point node) (point))
         ;; Associate the text node in buffer with the node object.
         (vundo--put-node-at-point node)
         ;; Depth-first search.
-        (setq node-queue (append children node-queue))))))
+        (setq node-queue (append children node-queue))))
+
+    ;; If the associated buffer is unmodified, the last node must be
+    ;; the last saved nodel even though it doesn’t have a next node
+    ;; with a timestamp to indicate that.
+    (setq vundo--last-saved-idx
+          (if orig-buffer-modified
+              (if (> last-saved-idx 0) last-saved-idx nil)
+            (vundo-m-idx (vundo--current-node mod-list))))
+    ;; Update the face of the last saved node (if any).
+    (when (and vundo-highlight-saved-nodes
+               vundo--last-saved-idx)
+      (vundo--highlight-last-saved-node
+       (aref mod-list vundo--last-saved-idx)))))
 
 ;;; Vundo buffer and invocation
 
@@ -600,6 +670,7 @@ WINDOW is the window that was/is displaying the vundo 
buffer."
     (define-key map (kbd "<up>") #'vundo-previous)
     (define-key map (kbd "a") #'vundo-stem-root)
     (define-key map (kbd "e") #'vundo-stem-end)
+    (define-key map (kbd "l") #'vundo-goto-last-saved)
     (define-key map (kbd "q") #'vundo-quit)
     (define-key map (kbd "C-g") #'vundo-quit)
     (define-key map (kbd "RET") #'vundo-confirm)
@@ -638,6 +709,12 @@ WINDOW is the window that was/is displaying the vundo 
buffer."
   "Vundo will roll back to this node.")
 (defvar-local vundo--highlight-overlay nil
   "Overlay used to highlight the selected node.")
+(defvar-local vundo--last-saved-idx nil
+  "The last node index with a timestamp seen.
+This is set by ‘vundo--draw-tree’ and ‘vundo-save’, and used by
+‘vundo-goto-last-saved’ and ‘vundo--highlight-last-saved-node’.")
+(defvar-local vundo--highlight-last-saved-overlay nil
+  "Overlay used to highlight the last saved node.")
 
 (defun vundo--mod-list-trim (mod-list n)
   "Remove MODS from MOD-LIST.
@@ -671,7 +748,8 @@ This function modifies `vundo--prev-mod-list',
     (when (not incremental)
       (setq vundo--prev-undo-list nil
             vundo--prev-mod-list nil
-            vundo--prev-mod-hash nil)
+            vundo--prev-mod-hash nil
+            vundo--last-saved-idx nil)
       ;; Give the garbage collector a chance to release
       ;; `buffer-undo-list': GC cannot release cons cells when all
       ;; these stuff are referring to it.
@@ -712,12 +790,14 @@ This function modifies `vundo--prev-mod-list',
                              (length vundo--prev-mod-list))))
       ;; 3. Render buffer. We don't need to redraw the tree if there
       ;; is no change to the nodes.
-      (unless (eq (vundo--latest-buffer-state mod-list)
-                  latest-state)
-        (vundo--draw-tree mod-list))
+      (unless (eq (vundo--latest-buffer-state mod-list) latest-state)
+        (vundo--draw-tree mod-list (with-current-buffer orig-buffer
+                                     (buffer-modified-p))))
+
       ;; Highlight current node.
       (vundo--highlight-node (vundo--current-node mod-list))
       (goto-char (vundo-m-point (vundo--current-node mod-list)))
+
       ;; Update cache.
       (setq vundo--prev-mod-list mod-list
             vundo--prev-mod-hash mod-hash
@@ -736,11 +816,24 @@ This function modifies `vundo--prev-mod-list',
     (overlay-put vundo--highlight-overlay
                  'display (vundo--translate "●"))
     (overlay-put vundo--highlight-overlay
-                 'face 'vundo-highlight))
+                 'face 'vundo-highlight)
+    ;; Make current node’s highlight override last saved node’s
+    ;; highlight, should they collide.
+    (overlay-put vundo--highlight-overlay 'priority 1))
   (move-overlay vundo--highlight-overlay
                 (1- (vundo-m-point node))
                 (vundo-m-point node)))
 
+(defun vundo--highlight-last-saved-node (node)
+  "Highlight NODE as the last saved.
+This moves the overlay `vundo--highlight-last-saved-overlay'."
+  (let ((node-pt (vundo-m-point node)))
+    (unless vundo--highlight-last-saved-overlay
+      (setq vundo--highlight-last-saved-overlay
+           (make-overlay (1- node-pt) node-pt))
+      (overlay-put vundo--highlight-last-saved-overlay 'face 
'vundo-last-saved))
+    (move-overlay vundo--highlight-last-saved-overlay (1- node-pt) node-pt)))
+
 ;;;###autoload
 (defun vundo ()
   "Display visual undo for the current buffer."
@@ -1131,12 +1224,33 @@ If ARG < 0, move forward."
       vundo--orig-buffer (current-buffer)
       'incremental))))
 
+(defun vundo-goto-last-saved ()
+  "Goto the last saved node, if any."
+  (interactive)
+  (when (and vundo--last-saved-idx (>= vundo--last-saved-idx 0))
+    (vundo--check-for-command
+     (when-let* ((this (vundo--current-node vundo--prev-mod-list))
+                 (dest (aref vundo--prev-mod-list vundo--last-saved-idx)))
+       (unless (eq this dest)
+         (vundo--move-to-node
+          this dest vundo--orig-buffer vundo--prev-mod-list)
+         (vundo--trim-undo-list
+          vundo--orig-buffer dest vundo--prev-mod-list)
+         (vundo--refresh-buffer
+          vundo--orig-buffer (current-buffer)
+          'incremental))))))
+
 (defun vundo-save (arg)
-  "Run `save-buffer' with the current buffer Vundo is operating on."
+  "Run `save-buffer' with the current buffer Vundo is operating on.
+Accepts the same interactive arfument ARG as ‘save-buffer’."
   (interactive "p")
   (vundo--check-for-command
    (with-current-buffer vundo--orig-buffer
-     (save-buffer arg))))
+     (save-buffer arg)))
+  (when vundo-highlight-saved-nodes
+    (let* ((cur-node (vundo--current-node vundo--prev-mod-list)))
+      (setq vundo--last-saved-idx (vundo-m-idx cur-node))
+      (vundo--highlight-last-saved-node cur-node))))
 
 ;;; Debug
 
@@ -1153,12 +1267,18 @@ TYPE is the type of buffer you want."
   "Print some useful info about the node at point."
   (interactive)
   (let ((node (vundo--get-node-at-point)))
-    (message "Parent: %s States: %s Children: %s"
+    (message "Parent: %s States: %s Children: %s%s"
              (and (vundo-m-parent node)
                   (vundo-m-idx (vundo-m-parent node)))
              (mapcar #'vundo-m-idx (vundo--eqv-list-of node))
              (and (vundo-m-children node)
-                  (mapcar #'vundo-m-idx (vundo-m-children node))))))
+                  (mapcar #'vundo-m-idx (vundo-m-children node)))
+             (if-let* ((vundo-highlight-saved-nodes)
+                       (ts (vundo--mod-timestamp vundo--prev-mod-list
+                                                 (vundo-m-idx node)))
+                       ((consp ts)))
+                 (format " Saved: %s" (format-time-string "%F %r" ts))
+               ""))))
 
 (defun vundo--debug ()
   "Make cursor visible and show debug information on movement."

Reply via email to