diff --git a/NEWS.txt b/NEWS.txt index e037879..dc0ddc5 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -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: diff --git a/README.txt b/README.txt index 26291aa..4ff5a29 100644 --- a/README.txt +++ b/README.txt @@ -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: @@ -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 diff --git a/vundo-diff.el b/vundo-diff.el new file mode 100644 index 0000000..ea847e1 --- /dev/null +++ b/vundo-diff.el @@ -0,0 +1,177 @@ +;;; vundo-diff.el --- buffer diff for vundo -*- lexical-binding: t; -*- + +;; Copyright (C) 2023 Free Software Foundation, Inc. +;; +;; Author: JD Smith <jdtsmith@gmail.com> +;; Maintainer: Yuan Fu <casouri@gmail.com> +;; 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 diff --git a/vundo.el b/vundo.el index b99b585..2e5bba4 100644 --- a/vundo.el +++ b/vundo.el @@ -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 ;; @@ -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: ;; @@ -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) @@ -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) @@ -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))) @@ -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) @@ -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) @@ -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) @@ -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.") @@ -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 @@ -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)) ""))))