Skip to content

Commit

Permalink
diff support with marked ranges and custom diff buffer
Browse files Browse the repository at this point in the history
vundo-diff adds support for computing the diff between a marked and
the current node, displaying in a custom vundo-diff buffer with
color-matched timestamped modifications.  Minor associated
improvements to timestamp calculation are included.
  • Loading branch information
jdtsmith committed Dec 9, 2023
1 parent 56c0863 commit dc55a9b
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 33 deletions.
12 changes: 12 additions & 0 deletions NEWS.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
<2023-12-08 Fri>: Version 2.5.0

vundo-diff introduced, providing on-demand diff functionality. Diff's
are evaluated between the current node and either its parent node, or,
if any, a marked node. New key commands:

(m)ark - mark a node for diff
(u)nmark - unmark any marked node
(d)iff - diff between current and marked or parent node

The (d)ebug command has been moved to (D)ebug.

<2022-04-23 Sat>: Version 2.0.0

Breaking change:
Expand Down
8 changes: 5 additions & 3 deletions README.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ Comparing to undo-tree:

Vundo doesn’t need to be turned on all the time nor replace the undo
commands like undo-tree does. Vundo displays the tree horizontally,
whereas undo-tree displays a tree vertically. Vundo doesn’t have many
advanced features that undo-tree does (like showing diff), and most
probably will not add those features in the future.
whereas undo-tree displays a tree vertically. Diff is provided
on-demand between any nodes, not just the node immediately prior.

Tests:

Expand All @@ -89,6 +88,9 @@ to run those tests interactively, or use the following batch command:

Changelog (full changelog in NEWS.txt):

<2023-12-08 Fri>: Version 2.5.0: vundo-diff introduced, supporting
on-demand diff to parent or any marked node.

<2022-04-04 Mon>: Version 1.0.0

<2022-03-29 Tue>: vundo--mode and vundo--mode-map are now vundo-mode
Expand Down
177 changes: 177 additions & 0 deletions vundo-diff.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
;;; vundo-diff.el --- buffer diff for vundo -*- lexical-binding: t; -*-

;; Copyright (C) 2023 Free Software Foundation, Inc.
;;
;; Author: JD Smith <[email protected]>
;; Maintainer: Yuan Fu <[email protected]>
;; URL: https://github.com/casouri/vundo
;; Version: 0.1
;; Package-Requires: ((emacs "28.1"))
;;
;; This file is part of GNU Emacs.
;;
;; GNU Emacs is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; GNU Emacs is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.

;;; Commentary:

;; vundo-diff provides simple on-demand diff between arbitray undo
;; states in the vundo tree.

;;; Code:
(require 'vundo)
(require 'diff)
(require 'diff-mode)
(eval-when-compile (require 'cl-lib))

(defface vundo-diff-highlight
'((((background light)) .
(:inherit vundo-highlight :foreground "DodgerBlue4"))
(((background dark)) .
(:inherit vundo-highlight :foreground "DodgerBlue1")))
"Face for nodes marked for diff in the undo tree.")

(defvar-local vundo-diff--marked-node nil)
(defvar-local vundo-diff--highlight-overlay nil
"Overlay used to highlight the selected node.")

(defun vundo-diff--cleanup-diff-buffer (orig-name buf current from to)
"Update diff headers in BUF.
Headers are updated to indicate the diff in the contents of
buffer named ORIG-NAME, between nodes FROM and TO, and given the
CURRENT node."
(let ((inhibit-read-only t)
(info (cl-loop for x in (list from to)
for idx = (vundo-m-idx x)
for ts = (vundo--node-timestamp vundo--prev-mod-list x)
for stat = (if (eq x current) "Current"
(if vundo-diff--marked-node "Marked" "Parent"))
collect
(list (format "[%d]" idx)
(format "<%s> [mod %d] (%s)" orig-name idx stat)
(when (consp ts) (format-time-string "%F %r" ts))))))
(with-current-buffer buf
(vundo-diff-mode)
(goto-char (point-min))
(insert (concat (propertize "vundo-diff: " 'font-lock-face 'diff-header)
(propertize orig-name 'font-lock-face
'(diff-file-header diff-header))
"\n"))
(let* ((change-files
(cl-loop for (name fullname ts) in info
for pat in '("---" "+++")
if (re-search-forward
(rx-to-string `(and bol ,pat (+ space)
(group (group (+ (not ?\t)))
(* any))
eol))
nil t)
collect (cons (match-string-no-properties 2) name)
and do (replace-match
(if ts (concat fullname "\t" ts) fullname)
t t nil 1)))
(lim (point)))
(when (eq (length change-files) 2)
(goto-char (point-min))
(dolist (c change-files) ; change the file names in the diff
(when (search-forward (car c) lim t)
(replace-match (cdr c)))))))))

;;;###autoload
(defun vundo-diff-mark (&optional node)
"Mark NODE for vundo diff.
NODE defaults to the current node."
(interactive)
(let* ((mod-list vundo--prev-mod-list)
(node (or node (vundo--current-node mod-list))))
(setq vundo-diff--marked-node node)
(unless vundo-diff--highlight-overlay
(setq vundo-diff--highlight-overlay
(make-overlay (1- (vundo-m-point node)) (vundo-m-point node)))
(overlay-put vundo-diff--highlight-overlay
'display (vundo--translate ""))
(overlay-put vundo-diff--highlight-overlay
'face 'vundo-diff-highlight)
(overlay-put vundo-diff--highlight-overlay 'priority 2))
(move-overlay vundo-diff--highlight-overlay
(1- (vundo-m-point node))
(vundo-m-point node))))

;;;###autoload
(defun vundo-diff-unmark ()
"Unmark the node marked for vundo diff."
(interactive)
(when vundo-diff--marked-node
(setq vundo-diff--marked-node nil)
(when vundo-diff--highlight-overlay
(delete-overlay vundo-diff--highlight-overlay)
(setq vundo-diff--highlight-overlay nil))))

;;;###autoload
(defun vundo-diff ()
"Perform diff between marked and current buffer state.
Displays in a separate diff buffer with name based on
the original buffer name."
(interactive)
(let* ((orig vundo--orig-buffer)
(oname (buffer-name orig))
(current (vundo--current-node vundo--prev-mod-list))
(marked (or vundo-diff--marked-node (vundo-m-parent current)))
(swapped (> (vundo-m-idx marked) (vundo-m-idx current)))
mrkbuf)
(if (or (not current) (not marked) (eq current marked))
(message "vundo diff not available.")
(setq mrkbuf (get-buffer-create
(make-temp-name (concat oname "-vundo-diff-marked"))))
(unwind-protect
(progn
(vundo--check-for-command
(vundo--move-to-node current marked orig vundo--prev-mod-list)
(with-current-buffer mrkbuf
(insert-buffer-substring-no-properties orig))
(vundo--refresh-buffer orig (current-buffer) 'incremental)
(vundo--move-to-node marked current orig vundo--prev-mod-list)
(vundo--trim-undo-list orig current vundo--prev-mod-list)
(vundo--refresh-buffer orig (current-buffer) 'incremental))
(let* ((a (if swapped current marked))
(b (if swapped marked current))
(abuf (if swapped orig mrkbuf))
(bbuf (if swapped mrkbuf orig))
(dbuf (diff-no-select
abuf bbuf nil t
(get-buffer-create
(concat "*vundo-diff-" oname "*")))))
(vundo-diff--cleanup-diff-buffer oname dbuf current a b)
(display-buffer dbuf)))
(kill-buffer mrkbuf)))))

(defconst vundo-diff-font-lock-keywords
`((,(rx bol (or "---" "+++") (* nonl) "[mod " (group (+ num)) ?\]
(+ ?\s) ?\((group (or "Parent" "Current")) ?\))
(1 'diff-index t)
(2 'vundo-highlight t))
(,(rx bol (or "---" "+++") (* nonl) "[mod " (group (+ num)) ?\]
(+ ?\s) ?\((group "Marked") ?\))
(1 'diff-index t)
(2 'vundo-diff-highlight t)))
"Additional font-lock keyword to fontify Parent/Current/Marked.")

(define-derived-mode vundo-diff-mode diff-mode "Vundo Diff"
:syntax-table nil
:abbrev-table nil
(setcar font-lock-defaults
(append diff-font-lock-keywords vundo-diff-font-lock-keywords)))

(provide 'vundo-diff)

;;; vundo-diff.el ends here
87 changes: 57 additions & 30 deletions vundo.el
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@
;;
;; a to go back to the last branching point
;; e to go forward to the end/tip of the branch
;; l to go to the last saved node
;;
;; m to mark the current node for diff
;; u to unmark the marked node
;; d to show a diff between the marked (or parent) and current nodes
;;
;; q to quit, you can also type C-g
;;
Expand Down Expand Up @@ -88,9 +93,7 @@
;;
;; Vundo doesn’t need to be turned on all the time nor replace the undo
;; commands like undo-tree does. Vundo displays the tree horizontally,
;; whereas undo-tree displays a tree vertically. Vundo doesn’t have many
;; advanced features that undo-tree does (like showing diff), and most
;; probably will not add those features in the future.
;; whereas undo-tree displays a tree vertically.

;;; Developer:
;;
Expand Down Expand Up @@ -513,6 +516,41 @@ If FROM non-nil, build from FORM-th modification in MOD-LIST."
(vundo--sort-mod (cons mod children)
'reverse))))))))))

;;; Timestamps
;; buffer-undo-list contains "timestamp entries" like (t . TIMESTAMP)
;; which capture the file modification time of the saved file which
;; an undo changed. During tree draw, we collect the last of these, and
;; indicated nodes which had been saved specially.

(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--orig-buffer nil
"Vundo buffer displays the undo tree for this buffer.")

(defun vundo--mod-timestamp (mod-list idx)
"Return a timestamp if the mod in MOD-LIST at IDX has a timestamp."
;; 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))))

(defun vundo--node-timestamp (mod-list node)
"Return a timestamp from MOD-LIST for NODE, if any.
In addition to undo-based timestamps, this includes the modtime of the
current buffer (if unmodified)."
(let* ((idx (vundo-m-idx node))
(current (vundo--current-node mod-list)))
(or (vundo--mod-timestamp mod-list idx)
(and (eq node current) (eq idx vundo--last-saved-idx)
(with-current-buffer vundo--orig-buffer
(and (buffer-file-name)
(not (buffer-modified-p))
(visited-file-modtime)))))))
;;; Draw tree

(defun vundo--put-node-at-point (node)
Expand Down Expand Up @@ -553,15 +591,6 @@ Translate according to `vundo-glyph-alist'."
vundo-glyph-alist)))
text 'string))

(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)
Expand All @@ -584,11 +613,11 @@ corresponding to the index of the last saved node."
(only-child-p (and parent (eq (length siblings) 1)))
(node-last-child-p (and parent (eq node (car (last siblings)))))
(node-idx (vundo-m-idx node))
(saved-p (and vundo-highlight-saved-nodes
(vundo--mod-timestamp mod-list node-idx)))
(mod-ts (vundo--mod-timestamp mod-list node-idx))
(saved-p (and vundo-highlight-saved-nodes mod-ts))
(node-face (if saved-p 'vundo-saved 'vundo-node))
(stem-face (if only-child-p 'vundo-stem 'vundo-branch-stem)))
(when (and saved-p (> node-idx last-saved-idx))
(when (and mod-ts (> node-idx last-saved-idx))
(setq last-saved-idx node-idx))
;; Go to parent.
(if parent (goto-char (vundo-m-point parent)))
Expand Down Expand Up @@ -647,8 +676,8 @@ corresponding to the index of the last saved node."
(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.
;; the last saved node even though it doesn’t (yet) 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)
Expand All @@ -673,6 +702,9 @@ WINDOW is the window that was/is displaying the vundo buffer."
(with-selected-window window
(kill-buffer-and-window))))

(declare-function vundo-diff "vundo-diff")
(declare-function vundo-diff-mark "vundo-diff")
(declare-function vundo-diff-unmark "vundo-diff")
(defvar vundo-mode-map
(let ((map (make-sparse-keymap)))
(define-key map (kbd "f") #'vundo-forward)
Expand All @@ -689,8 +721,11 @@ WINDOW is the window that was/is displaying the vundo buffer."
(define-key map (kbd "q") #'vundo-quit)
(define-key map (kbd "C-g") #'vundo-quit)
(define-key map (kbd "RET") #'vundo-confirm)
(define-key map (kbd "m") #'vundo-diff-mark)
(define-key map (kbd "u") #'vundo-diff-unmark)
(define-key map (kbd "d") #'vundo-diff)
(define-key map (kbd "i") #'vundo--inspect)
(define-key map (kbd "d") #'vundo--debug)
(define-key map (kbd "D") #'vundo--debug)

(define-key map [remap save-buffer] #'vundo-save)
map)
Expand All @@ -716,18 +751,12 @@ WINDOW is the window that was/is displaying the vundo buffer."
"Modification hash table generated by `vundo--update-mapping'.")
(defvar-local vundo--prev-undo-list nil
"Original buffer's `buffer-undo-list'.")
(defvar-local vundo--orig-buffer nil
"Vundo buffer displays the undo tree for this buffer.")
(defvar-local vundo--message nil
"If non-nil, print information when moving between nodes.")
(defvar-local vundo--roll-back-to-this nil
"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.")

Expand Down Expand Up @@ -1269,9 +1298,9 @@ Accepts the same interactive arfument ARG as ‘save-buffer’."
(vundo--check-for-command
(with-current-buffer vundo--orig-buffer
(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))
(let* ((cur-node (vundo--current-node vundo--prev-mod-list)))
(setq vundo--last-saved-idx (vundo-m-idx cur-node))
(when vundo-highlight-saved-nodes
(vundo--highlight-last-saved-node cur-node))))

;;; Debug
Expand All @@ -1295,9 +1324,7 @@ TYPE is the type of buffer you want."
(mapcar #'vundo-m-idx (vundo--eqv-list-of node))
(and (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)))
(if-let* ((ts (vundo--node-timestamp vundo--prev-mod-list node))
((consp ts)))
(format " Saved: %s" (format-time-string "%F %r" ts))
""))))
Expand Down

0 comments on commit dc55a9b

Please sign in to comment.