[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/vundo ab84e87522 01/58: init
From: |
ELPA Syncer |
Subject: |
[elpa] externals/vundo ab84e87522 01/58: init |
Date: |
Fri, 15 Apr 2022 12:58:09 -0400 (EDT) |
branch: externals/vundo
commit ab84e87522d5b34167bd6d044752e4edec93b356
Author: Yuan Fu <casouri@gmail.com>
Commit: Yuan Fu <casouri@gmail.com>
init
---
vundo.el | 905 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 905 insertions(+)
diff --git a/vundo.el b/vundo.el
new file mode 100644
index 0000000000..8e1241cc79
--- /dev/null
+++ b/vundo.el
@@ -0,0 +1,905 @@
+;;; vundo.el --- Visual undo tree -*- lexical-binding: t; -*-
+
+;; Author: Yuan Fu <casouri@gmail.com>
+;; Package-Requires: ((emacs "28.0"))
+
+;;; This file is NOT part of GNU Emacs
+
+;;; Commentary:
+;;
+;; To use vundo, type M-x vundo RET in the buffer you want to undo.
+;; A undo tree buffer should pop up. To move around, type:
+;;
+;; f to go forward
+;; b to go backward
+;; n to go to the node below when you at a branching point
+;; p to go to the node above
+;; q to quit, you can also type C-g
+;;
+;; By default, you need to press RET to “commit” your change and if
+;; you quit with q or C-g, the change made by vundo are rolled back.
+;; You can set ‘vundo-roll-back-on-quit’ to nil to disable rolling
+;; back.
+;;
+;; If you bring up the vundo buffer and make some modification in the
+;; original buffer, the tree in the vundo buffer doesn’t automatically
+;; update. Vundo catches up the next time you invoke any command:
+;; instead of performing that command, it updates the tree.
+
+;;; Developer:
+;;
+;; In the comments, when I say node, modification, mod, buffer state,
+;; they all mean one thing: `vundo-m'. I.e., `vundo-m' represents
+;; multiple things at once: it represents an modification recorded in
+;; `buffer-undo-list', it represents the state of the buffer after
+;; that modification took place, and it represents the node in the
+;; undo tree in the vundo buffer representing that buffer state.
+;;
+;; The basic flow of the program:
+;;
+;; `vundo' calls `vundo--refresh-buffer' to setup the tree structure
+;; and draw it in the buffer. We have two data structures:
+;; `vundo--prev-mod-list' which stores a list of `vundo-m'. This list
+;; is generated from `buffer-undo-list' by `vundo--mod-list-from'. We
+;; also have a hash table `vundo--prev-mod-hash' generated by
+;; `vundo--update-mapping', which maps undo-lists back to the
+;; `vundo-m' object corresponding to it. Once we have the mod-list and
+;; hash table, we connect the nodes in mod-list to form a tree in
+;; `vundo--build-tree'. We build the tree by a simple observation:
+;; only non-undo modifications creates new unique buffer states and
+;; need to be drawn in the tree. For undo modifications, they
+;; associates equivalent nodes.
+;;
+;; Once we have generated the data structure and drawn the tree, vundo
+;; commands can move around on that tree by calling
+;; `vundo--move-to-node'. It will construct the correct undo-list and
+;; feed it to `primitive-undo'. After each movement,
+;; `vundo--move-to-node' also trims the undo list when possible.
+;;
+;; Finally, to avoid generating everything from scratch every time we
+;; moves on the tree, `vundo--refresh-buffer' can incrementally update
+;; the data structures (`vundo--prev-mod-list' and
+;; `vundo--prev-mod-hash'). If the undo list expands, we only process
+;; the new entries, if the undo list shrinks (trimmed), we remove
+;; modifications accordingly.
+;;
+;; For a high-level explanation of how this package works, see
+;; https://archive.casouri.cat/note/2021/visual-undo-tree.
+
+;;; Code:
+
+(require 'pcase)
+(require 'cl-lib)
+(require 'seq)
+
+;;; Customization
+
+(defgroup vundo nil
+ "Visual undo tree."
+ :group 'undo)
+
+(defface vundo-default '((t . (:inherit 'default)))
+ "Default face used in vundo buffer.")
+
+(defface vundo-node '((t . (:inherit 'vundo-default)))
+ "Face for nodes in the undo tree.")
+
+(defface vundo-stem '((t . (:inherit 'vundo-default)))
+ "Face for stems between nodes in the undo tree.")
+
+(defface vundo-highlight '((t . (:inherit 'vundo-node)))
+ "Face for the highlighted 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--window-max-height 3
+ "The maximum height of the vundo window."
+ :type 'integer)
+
+(defvar vundo-translation-alist nil
+ "An alist mapping text to their translations.
+E.g., mapping ○ to o, ● to *. Keys and values must be characters,
+not strings.")
+
+;;;###autoload
+(define-minor-mode vundo-ascii-mode
+ "Display the undo tree with ASCII characters."
+ :global t
+ (if vundo-ascii-mode
+ (progn
+ (put 'vundo-translation-alist 'before-ascii
+ vundo-translation-alist)
+ (setq vundo-translation-alist
+ '((?○ . ?o)
+ (?● . ?*)
+ (?─ . ?-)
+ (?│ . ?|)
+ (?├ . ?|)
+ (?└ . ?+))))
+ (setq vundo-translation-alist
+ (get 'vundo-translation-alist 'before-ascii))))
+
+;;; Undo list to mod list
+
+(cl-defstruct vundo-m
+ "A modification in undo history.
+This object serves two purpose: it represents a modification in
+undo history, and it also represents the buffer state after the
+modification."
+ (idx
+ nil
+ :type integer
+ :documentation "The index of this modification in history.")
+ (children
+ nil
+ :type proper-list
+ :documentation "Children in tree.")
+ (parent
+ nil
+ :type vundo-m
+ :documentation "Parent in tree.")
+ (prev-eqv
+ nil
+ :type vundo-m
+ :documentation "The previous equivalent state.")
+ (next-eqv
+ nil
+ :type vundo-m
+ :documentation "The next equivalent state.")
+ (undo-list
+ nil
+ :type cons
+ :documentation "The undo-list at this modification.")
+ (point
+ nil
+ :type integer
+ :documentation "Marks the text node in the vundo buffer if drawn."))
+
+(defun vundo--mod-list-from (undo-list &optional n mod-list)
+ "Generate and return a modification list from UNDO-LIST.
+If N non-nil, only look at the first N entries in UNDO-LIST.
+If MOD-LIST non-nil, extend on MOD-LIST."
+ (let ((bound (or n (length undo-list)))
+ (uidx 0)
+ (mod-list (or mod-list (list (make-vundo-m))))
+ new-mlist)
+ (while (and (consp undo-list) (< uidx bound))
+ ;; Skip leading nils.
+ (while (and (< uidx bound) (null (nth uidx undo-list)))
+ (cl-incf uidx))
+ ;; Add modification.
+ (when (< uidx bound)
+ (cl-assert (not (null (nth uidx undo-list))))
+ (push (make-vundo-m :undo-list (nthcdr uidx undo-list))
+ new-mlist))
+ ;; Skip through the content of this modification.
+ (while (nth uidx undo-list)
+ (cl-incf uidx)))
+ (append mod-list new-mlist)))
+
+(defun vundo--update-mapping (mod-list &optional hash-table n)
+ "Update each modification in MOD-LIST.
+Add :idx for each modification, map :undo-list back to each
+modification in HASH-TABLE. If N non-nil, start from the Nth
+modification in MOD-LIST. Return HASH-TABLE."
+ (let ((hash-table (or hash-table
+ (make-hash-table :test #'eq :weakness t))))
+ (cl-loop for mod in (nthcdr (or n 0) mod-list)
+ for midx = (or n 0) then (1+ midx)
+ do (cl-assert (null (vundo-m-idx mod)))
+ do (cl-assert (null (gethash (vundo-m-undo-list mod)
+ hash-table)))
+ do (setf (vundo-m-idx mod) midx)
+ do (puthash (vundo-m-undo-list mod) mod hash-table))
+ hash-table))
+
+;;; Mod list to tree
+;;
+;; If node a, b, c are in the same equivalent list, they represents
+;; identical buffer states. For example, in the figure below, node 3
+;; and 5 are in the same equivalent list:
+;;
+;; |
+;; 3 5
+;; | /
+;; |/
+;; 4
+;;
+;; We know 3 and 5 are in the same equivalent list because 5 maps to 3
+;; in `undo-equiv-table' (basically).
+
+(defun vundo--eqv-list-of (mod)
+ "Return all the modifications equivalent to MOD."
+ (while (vundo-m-prev-eqv mod)
+ (cl-assert (not (eq mod (vundo-m-prev-eqv mod))))
+ (setq mod (vundo-m-prev-eqv mod)))
+ ;; At the first mod in the equiv chain.
+ (let ((eqv-list (list mod)))
+ (while (vundo-m-next-eqv mod)
+ (cl-assert (not (eq mod (vundo-m-next-eqv mod))))
+ (setq mod (vundo-m-next-eqv mod))
+ (push mod eqv-list))
+ (reverse eqv-list)))
+
+(defun vundo--eqv-merge (mlist)
+ "Connect modifications in MLIST to be in the same equivalence list.
+Order is reserved."
+ (cl-loop for idx from 0 to (1- (length mlist))
+ for this = (nth idx mlist)
+ for next = (nth (1+ idx) mlist)
+ for prev = nil then (nth (1- idx) mlist)
+ do (setf (vundo-m-prev-eqv this) prev)
+ do (setf (vundo-m-next-eqv this) next)))
+
+(defun vundo--sort-mod (mlist &optional reverse)
+ "Return sorted modifications in MLIST by their idx...
+...in ascending order. If REVERSE non-nil, sort in descending
+order."
+ (seq-sort (if reverse
+ (lambda (m1 m2)
+ (> (vundo-m-idx m1) (vundo-m-idx m2)))
+ (lambda (m1 m2)
+ (< (vundo-m-idx m1) (vundo-m-idx m2))))
+ mlist))
+
+(defun vundo--eqv-merge-mod (m1 m2)
+ "Put M1 and M2 into the same equivalence list."
+ (let ((l1 (vundo--eqv-list-of m1))
+ (l2 (vundo--eqv-list-of m2)))
+ (vundo--eqv-merge (vundo--sort-mod (cl-union l1 l2)))))
+
+(defun vundo--build-tree (mod-list mod-hash &optional from)
+ "Connect equivalent modifications and build the tree in MOD-LIST.
+MOD-HASH maps undo-lists to modifications.
+If FROM non-nil, build from FORM-th modification in MOD-LIST."
+ (cl-loop
+ for m from (or from 0) to (1- (length mod-list))
+ for mod = (nth m mod-list)
+ ;; If MOD is an undo, the buffer state it represents is equivalent
+ ;; to a previous one.
+ do (let ((prev-undo (undo--last-change-was-undo-p
+ (vundo-m-undo-list mod))))
+ (pcase prev-undo
+ ;; This is an undo. Merge it with its equivalent nodes.
+ ((and (pred consp)
+ ;; It is possible for us to not find the PREV-UNDO in
+ ;; our mod-list: if Emacs garbage collected prev-m,
+ ;; then it will not end up in mod-list. NOTE: Is it
+ ;; also possible that unable to find PREV-M is an
+ ;; error? Maybe, but I think that's highly unlikely.
+ (guard (gethash prev-undo mod-hash)))
+ (let ((prev-m (gethash prev-undo mod-hash)))
+ (vundo--eqv-merge-mod prev-m mod)))
+ ;; This undo undoes to root, merge with the root node.
+ ('t (vundo--eqv-merge-mod (nth 0 mod-list) mod))
+ ;; This modification either is a region-undo, nil undo, or
+ ;; not an undo. We treat them the same.
+ ((or 'undo-in-region 'empty _)
+ ;; If MOD isn't an undo, it represents a new buffer state,
+ ;; we connect M-1 with M, where M-1 is the parent and M is
+ ;; the child.
+ (unless (eq m 0)
+ (let* ((m-1 (nth (1- m) mod-list))
+ ;; TODO: may need to optimize.
+ (min-eqv-mod (car (vundo--eqv-list-of m-1))))
+ (setf (vundo-m-parent mod) min-eqv-mod)
+ (let ((children (vundo-m-children min-eqv-mod)))
+ ;; If everything goes right, we should never encounter
+ ;; this.
+ (cl-assert (not (memq mod children)))
+ (setf (vundo-m-children min-eqv-mod)
+ ;; We sort in reverse order, i.e., later mod
+ ;; comes first. Later in `vundo--build-tree' we
+ ;; draw the tree depth-first.
+ (vundo--sort-mod (cons mod children) 'reverse))
+ ))))))))
+
+;;; Draw tree
+
+(defun vundo--replace-at-col (from to col &optional until)
+ "Replace FROM at COL with TO in each line of current buffer.
+If a line is not COL columns long, skip that line."
+ (save-excursion
+ (let ((run t))
+ (goto-char (point-min))
+ (while run
+ (move-to-column col)
+ (if (and (eq (current-column) col)
+ (looking-at (regexp-quote from)))
+ (replace-match to))
+ ;; If ‘forward-line’ returns 0, we haven’t hit the end of
+ ;; buffer.
+ (setq run (and (eq (forward-line) 0)
+ (not (eq (point) (point-max)))
+ (< (point) (or until (point-max)))))))))
+
+(defun vundo--put-node-at-point (node)
+ "Store the corresponding NODE as text property at point."
+ (put-text-property (1- (point)) (point)
+ 'vundo-node
+ node))
+
+(defun vundo--get-node-at-point ()
+ "Retrieve the corresponding NODE as text property at point."
+ (plist-get (text-properties-at (1- (point)))
+ 'vundo-node))
+
+(defun vundo--next-line-at-column (col)
+ "Move point to next line column COL."
+ (unless (and (eq 0 (forward-line))
+ (not (eq (point) (point-max))))
+ (goto-char (point-max))
+ (insert "\n"))
+ (move-to-column col)
+ (unless (eq (current-column) col)
+ (let ((indent-tabs-mode nil))
+ (indent-to-column col))))
+
+(defun vundo--translate (text)
+ "Translate each character in TEXT and return it.
+Translate according to `vundo-translation-alist'."
+ (seq-mapcat (lambda (c)
+ (char-to-string
+ (alist-get c vundo-translation-alist c)))
+ text 'string))
+
+(defun vundo--put-face (beg end face)
+ "Add FACE to the text between (+ (point) BEG) and (+ (point) END)."
+ (put-text-property (+ (point) beg) (+ (point) end) 'face face))
+
+(defun vundo--draw-tree (mod-list)
+ "Draw the tree in MOD-LIST in current buffer."
+ (let* ((root (nth 0 mod-list))
+ (node-queue (list root))
+ (inhibit-read-only t))
+ (erase-buffer)
+ (while node-queue
+ (let* ((node (pop node-queue))
+ (children (vundo-m-children node))
+ (parent (vundo-m-parent node))
+ ;; Is NODE the last child of PARENT?
+ (node-last-child-p
+ (if parent
+ (eq node (car (last (vundo-m-children parent)))))))
+ ;; Go to parent.
+ (if parent (goto-char (vundo-m-point parent)))
+ (let ((col (max 0 (1- (current-column)))))
+ (if (null parent)
+ (progn (insert (vundo--translate "○"))
+ (vundo--put-face -1 0 'vundo-node))
+ (let ((planned-point (point)))
+ ;; If a node is blocking, try next line.
+ ;; Example: 1--2--3 Here we want to add a
+ ;; | child to 1 but is blocked
+ ;; +--4 by that plus sign.
+ (while (not (looking-at (rx (or " " eol))))
+ (vundo--next-line-at-column col)
+ (if (looking-at "$")
+ (insert (vundo--translate "│"))
+ (delete-char 1)
+ (insert (vundo--translate "│")))
+ (vundo--put-face -1 0 'vundo-stem))
+ ;; Make room for inserting the new node.
+ (unless (looking-at "$")
+ (delete-char 3))
+ ;; Insert the new node.
+ (if (eq (point) planned-point)
+ (insert (vundo--translate "──○"))
+ ;; Delete the previously inserted |.
+ (delete-char -1)
+ (if node-last-child-p
+ (insert (vundo--translate "└──○"))
+ (insert (vundo--translate "├──○"))))
+ (vundo--put-face -4 -1 'vundo-stem)
+ (vundo--put-face -1 0 'vundo-node))))
+ ;; 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))))))
+
+;;; Vundo buffer and invocation
+
+(defun vundo--buffer ()
+ "Return the vundo buffer."
+ (get-buffer-create " *vundo tree*"))
+
+(defun vundo--kill-buffer-if-point-left (window)
+ "Kill the vundo buffer if point left WINDOW.
+WINDOW is the window that was/is displaying the vundo buffer."
+ (if (and (eq (window-buffer window) (vundo--buffer))
+ (not (eq window (selected-window))))
+ (with-selected-window window
+ (kill-buffer-and-window))))
+
+(defvar vundo--mode-map
+ (let ((map (make-sparse-keymap)))
+ (define-key map (kbd "f") #'vundo-forward)
+ (define-key map (kbd "<right>") #'vundo-forward)
+ (define-key map (kbd "b") #'vundo-backward)
+ (define-key map (kbd "<left>") #'vundo-backward)
+ (define-key map (kbd "n") #'vundo-next)
+ (define-key map (kbd "<down>") #'vundo-next)
+ (define-key map (kbd "p") #'vundo-previous)
+ (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 "q") #'vundo-quit)
+ (define-key map (kbd "C-g") #'vundo-quit)
+ (define-key map (kbd "RET") #'kill-buffer-and-window)
+ (define-key map (kbd "i") #'vundo--inspect)
+ (define-key map (kbd "d") #'vundo--debug)
+ map)
+ "Keymap for ‘vundo--mode’.")
+
+(define-derived-mode vundo--mode special-mode
+ "Vundo" "Mode for displaying the undo tree."
+ (setq mode-line-format nil
+ truncate-lines t
+ cursor-type nil)
+ (jit-lock-mode -1)
+ (face-remap-add-relative 'default 'vundo-default))
+
+(defvar-local vundo--prev-mod-list nil
+ "Modification list generated by ‘vundo--mod-list-from’.")
+(defvar-local vundo--prev-mod-hash nil
+ "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.")
+
+(defun vundo--mod-list-trim (mod-list n)
+ "Remove MODS from MOD-LIST.
+Keep the first N modifications."
+ (dolist (mod (nthcdr (1+ n) mod-list))
+ (let ((parent (vundo-m-parent mod))
+ (eqv-list (vundo--eqv-list-of mod)))
+ (when parent
+ (setf (vundo-m-children parent)
+ (remove mod (vundo-m-children parent))))
+ (when eqv-list
+ (vundo--eqv-merge (remove mod eqv-list)))))
+ (seq-subseq mod-list 0 (1+ n)))
+
+(defun vundo--refresh-buffer
+ (orig-buffer vundo-buffer &optional incremental)
+ "Refresh VUNDO-BUFFER with the undo history of ORIG-BUFFER.
+If INCREMENTAL non-nil, reuse some date."
+ (with-current-buffer vundo-buffer
+ ;; 1. Setting these to nil makes `vundo--mod-list-from',
+ ;; `vundo--update-mapping' and `vundo--build-tree' starts from
+ ;; scratch.
+ (when (not incremental)
+ (setq vundo--prev-undo-list nil
+ vundo--prev-mod-list nil
+ vundo--prev-mod-hash 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.
+ (garbage-collect))
+ (let ((undo-list (buffer-local-value
+ 'buffer-undo-list orig-buffer))
+ mod-list
+ mod-hash
+ (latest-state (and vundo--prev-mod-list
+ (vundo--latest-buffer-state
+ vundo--prev-mod-list)))
+ (inhibit-read-only t))
+ ;; 1.5 De-highlight the current node before
+ ;; `vundo--prev-mod-list' changes.
+ (when vundo--prev-mod-list
+ (vundo--toggle-highlight
+ -1 (vundo--current-node vundo--prev-mod-list)))
+ ;; 2. Here we consider two cases, adding more nodes (or starting
+ ;; from scratch) or removing nodes. In both cases, we update and
+ ;; set MOD-LIST and MOD-HASH. We don't need to worry about the
+ ;; garbage collector trimming the end of `buffer-undo-list': if
+ ;; we are generating MOD-LIST from scratch, it will work as
+ ;; normal, if we are generating incrementally,
+ ;; `vundo--prev-undo-list' holds the untrimmed undo list.
+ (if-let ((new-tail (and vundo--prev-mod-hash
+ (gethash (vundo--sans-nil undo-list)
+ vundo--prev-mod-hash))))
+ ;; a) Removing.
+ (setq mod-list (vundo--mod-list-trim vundo--prev-mod-list
+ (vundo-m-idx new-tail))
+ mod-hash vundo--prev-mod-hash)
+ ;; b) Adding.
+ (let ((diff (- (length undo-list)
+ (length vundo--prev-undo-list))))
+ (cl-assert (eq vundo--prev-undo-list (nthcdr diff undo-list)))
+ (setq mod-list (vundo--mod-list-from
+ undo-list diff vundo--prev-mod-list)
+ mod-hash (vundo--update-mapping
+ mod-list vundo--prev-mod-hash
+ (length vundo--prev-mod-list)))
+ ;; Build tree.
+ (vundo--build-tree mod-list mod-hash
+ (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))
+ ;; Highlight current node.
+ (vundo--toggle-highlight 1 (vundo--current-node mod-list))
+ ;; Update cache.
+ (setq vundo--prev-mod-list mod-list
+ vundo--prev-mod-hash mod-hash
+ vundo--prev-undo-list undo-list
+ vundo--orig-buffer orig-buffer))))
+
+(defun vundo--current-node (mod-list)
+ "Return the currently highlighted node in MOD-LIST."
+ (car (vundo--eqv-list-of (car (last mod-list)))))
+
+(defun vundo--toggle-highlight (arg node)
+ "Toggle highlight of NODE.
+Highlight if ARG >= 0, de-highlight if ARG < 0."
+ (goto-char (vundo-m-point node))
+ (if (>= arg 0)
+ (add-text-properties (1- (point)) (point)
+ (list 'display (vundo--translate "●")
+ 'face 'vundo-highlight))
+ (add-text-properties (1- (point)) (point)
+ (list 'display nil 'face 'vundo-node))))
+
+;;;###autoload
+(defun vundo ()
+ "Display visual undo for the current buffer."
+ (interactive)
+ (when (not (consp buffer-undo-list))
+ (user-error "There is no undo history"))
+ (let ((vundo-buf (vundo-1 (current-buffer))))
+ (select-window
+ (display-buffer-in-side-window
+ vundo-buf
+ '((side . bottom)
+ (window-height . 3))))
+ (set-window-dedicated-p nil t)
+ (let ((window-min-height 3))
+ (fit-window-to-buffer nil vundo--window-max-height))
+ (goto-char
+ (vundo-m-point
+ (vundo--current-node vundo--prev-mod-list)))
+ (setq vundo--roll-back-to-this
+ (vundo--current-node vundo--prev-mod-list))))
+
+(defun vundo-1 (buffer)
+ "Return a vundo buffer for BUFFER.
+BUFFER must have a valid `buffer-undo-list'."
+ (with-current-buffer buffer
+ (let* ((vundo-buf (vundo--buffer))
+ (orig-buf (current-buffer)))
+ (with-current-buffer vundo-buf
+ ;; Enable major mode before refreshing the buffer.
+ ;; Because major modes kill local variables.
+ (unless (derived-mode-p 'vundo--mode)
+ (vundo--mode))
+ (vundo--refresh-buffer orig-buf vundo-buf)
+ vundo-buf))))
+
+(defmacro vundo--check-for-command (&rest body)
+ "Sanity check before running interactive commands.
+Do sanity check, then evaluate BODY."
+ `(progn
+ (when (not (derived-mode-p 'vundo--mode))
+ (user-error "Not in vundo buffer"))
+ (when (not (buffer-live-p vundo--orig-buffer))
+ (user-error "Original buffer is gone"))
+ ;; If ORIG-BUFFER changed since we last synced the vundo buffer
+ ;; (e.g., user left vundo buffer and did some edit in ORIG-BUFFER
+ ;; then comes back), refresh to catch up.
+ (let ((undo-list (buffer-local-value
+ 'buffer-undo-list vundo--orig-buffer)))
+ ;; 1. Refresh if the beginning is not the same.
+ (cond ((not (eq (vundo--sans-nil undo-list)
+ (vundo--sans-nil vundo--prev-undo-list)))
+ (vundo--refresh-buffer vundo--orig-buffer (current-buffer))
+ (message "Refresh"))
+ ;; 2. It is possible that GC trimmed the end of undo
+ ;; list, but that doesn't affect us:
+ ;; `vundo--prev-mod-list' and `vundo--prev-undo-list' are
+ ;; still perfectly fine. Run the command normally. Of
+ ;; course, the next time the user invokes `vundo', the
+ ;; new tree will reflect the trimmed undo list.
+ (t ,@body)))))
+
+(defun vundo-quit ()
+ "Quit buffer and window.
+Roll back changes if `vundo-roll-back-on-quit' is non-nil."
+ (interactive)
+ (vundo--check-for-command
+ (when (and vundo-roll-back-on-quit vundo--roll-back-to-this
+ (not (eq vundo--roll-back-to-this
+ (vundo--current-node vundo--prev-mod-list))))
+ (vundo--move-to-node
+ (vundo--current-node vundo--prev-mod-list)
+ vundo--roll-back-to-this
+ vundo--orig-buffer vundo--prev-mod-list))
+ (kill-buffer-and-window)))
+
+;;; Traverse undo tree
+
+(defun vundo--calculate-shortest-route (from to)
+ "Calculate the shortest route from FROM to TO node.
+Return (SOURCE STOP1 STOP2 ... DEST), meaning you should undo the
+modifications from DEST to SOURCE. Each STOP is an intermediate
+stop. E.g., (6 5 4 3). Return nil if no valid route."
+ (let (route-list)
+ ;; Find all valid routes.
+ (dolist (source (vundo--eqv-list-of from))
+ (dolist (dest (vundo--eqv-list-of to))
+ ;; We only allow route in this direction.
+ (if (> (vundo-m-idx source) (vundo-m-idx dest))
+ (push (cons (vundo-m-idx source)
+ (vundo-m-idx dest))
+ route-list))))
+ ;; Find the shortest route.
+ (setq route-list
+ (seq-sort
+ (lambda (r1 r2)
+ ;; I.e., distance between SOURCE and DEST in R1 compare
+ ;; against distance in R2.
+ (< (- (car r1) (cdr r1)) (- (car r2) (cdr r2))))
+ route-list))
+ (if-let* ((route (car route-list))
+ (source (car route))
+ (dest (cdr route)))
+ (number-sequence source dest -1))))
+
+(defun vundo--list-subtract (l1 l2)
+ "Return L1 - L2.
+
+\(vundo--list-subtract '(4 3 2 1) '(2 1))
+=> (4 3)"
+ (let ((len1 (length l1))
+ (len2 (length l2)))
+ (cl-assert (> len1 len2))
+ (seq-subseq l1 0 (- len1 len2))))
+
+(defun vundo--sans-nil (undo-list)
+ "Return UNDO-LIST sans leading nils.
+If UNDO-LIST is nil, return nil."
+ (while (and (consp undo-list) (null (car undo-list)))
+ (setq undo-list (cdr undo-list)))
+ undo-list)
+
+(defun vundo--latest-buffer-state (mod-list)
+ "Return the node representing the latest buffer state.
+Basically, return the latest non-undo modification in MOD-LIST."
+ (let ((max-node (car mod-list)))
+ (cl-loop for mod in (cdr mod-list)
+ do (if (and (null (vundo-m-prev-eqv mod))
+ (> (vundo-m-idx mod)
+ (vundo-m-idx max-node)))
+ (setq max-node mod)))
+ max-node))
+
+(defun vundo--move-to-node (current dest orig-buffer mod-list)
+ "Move from CURRENT node to DEST node by undoing in ORIG-BUFFER.
+ORIG-BUFFER must be at CURRENT state. MOD-LIST is the list you
+get from ‘vundo--mod-list-from’. You should refresh vundo buffer
+after calling this function."
+ (cl-assert (not (eq current dest)))
+ ;; 1. Find the route we want to take.
+ (if-let* ((route (vundo--calculate-shortest-route current dest)))
+ (let* ((source-idx (car route))
+ (dest-idx (car (last route)))
+ ;; The complete undo-list that stops at SOURCE.
+ (undo-list-at-source
+ (vundo-m-undo-list (nth source-idx mod-list)))
+ ;; The complete undo-list that stops at DEST.
+ (undo-list-at-dest
+ (vundo-m-undo-list (nth dest-idx mod-list)))
+ ;; We will undo these modifications.
+ (planned-undo (vundo--list-subtract
+ undo-list-at-source undo-list-at-dest))
+ trimmed)
+ (with-current-buffer orig-buffer
+ ;; 2. Undo. This will undo modifications in PLANNED-UNDO and
+ ;; add new entries to ‘buffer-undo-list’.
+ (let ((undo-in-progress t))
+ (cl-loop
+ for step = (- source-idx dest-idx)
+ then (1- step)
+ while (> step 0)
+ for stop = (1- source-idx) then (1- stop)
+ do
+ (progn
+ ;; Stop at each intermediate stop along the route to
+ ;; create trim points for future undo.
+ (setq planned-undo (primitive-undo 1 planned-undo))
+ (cl-assert (not (and (consp buffer-undo-list)
+ (null (car buffer-undo-list)))))
+ (let ((undo-list-at-stop
+ (vundo-m-undo-list (nth stop mod-list))))
+ (puthash buffer-undo-list (or undo-list-at-stop t)
+ undo-equiv-table))
+ (push nil buffer-undo-list))))
+ ;; 3. Now we may be able to trim the undo-list.
+ (let ((latest-buffer-state-idx
+ ;; Among all the MODs that represents a unique buffer
+ ;; state, we find the latest one. Because any node
+ ;; beyond that one is dispensable.
+ (vundo-m-idx
+ (vundo--latest-buffer-state mod-list))))
+ ;; Find a trim point between latest buffer state and
+ ;; current node.
+ (when-let ((possible-trim-point
+ (cl-loop for node in (vundo--eqv-list-of dest)
+ if (>= (vundo-m-idx node)
+ latest-buffer-state-idx)
+ return node
+ finally return nil)))
+ (setq buffer-undo-list
+ (vundo-m-undo-list possible-trim-point)
+ trimmed (vundo-m-idx possible-trim-point))))
+ ;; 4. Some misc work.
+ (when vundo--message
+ (message "%s -> %s Trim to: %s Steps: %s Undo-list len: %s"
+ (mapcar #'vundo-m-idx (vundo--eqv-list-of
+ (nth source-idx mod-list)))
+ (mapcar #'vundo-m-idx (vundo--eqv-list-of
+ (nth dest-idx mod-list)))
+ trimmed
+ (length planned-undo)
+ (length buffer-undo-list)))
+ (when-let ((win (get-buffer-window)))
+ (set-window-point win (point)))))
+ (error "No possible route")))
+
+(defun vundo-forward (arg)
+ "Move forward ARG nodes in the undo tree.
+If ARG < 0, move backward"
+ (interactive "p")
+ (vundo--check-for-command
+ (let ((step (abs arg)))
+ (let ((node (vundo--current-node vundo--prev-mod-list))
+ dest)
+ ;; Move to the dest node step-by-step, stop when no further
+ ;; node to go to.
+ (while (and node (> step 0))
+ (setq dest (if (> arg 0)
+ (car (vundo-m-children node))
+ (vundo-m-parent node)))
+ (when dest
+ (vundo--move-to-node
+ node dest vundo--orig-buffer vundo--prev-mod-list))
+ (setq node dest)
+ (cl-decf step))
+ ;; Refresh display.
+ (vundo--refresh-buffer
+ vundo--orig-buffer (current-buffer) 'incremental)))))
+
+(defun vundo-backward (arg)
+ "Move back ARG nodes in the undo tree.
+If ARG < 0, move forward."
+ (interactive "p")
+ (vundo-forward (- arg)))
+
+(defun vundo-next (arg)
+ "Move to node below the current one. Move ARG steps."
+ (interactive "p")
+ (vundo--check-for-command
+ (let* ((source (vundo--current-node vundo--prev-mod-list))
+ (parent (vundo-m-parent source)))
+ ;; Move to next/previous sibling.
+ (when parent
+ (let* ((siblings (vundo-m-children parent))
+ (idx (seq-position siblings source))
+ (new-idx (+ idx arg))
+ ;; TODO: Move as far as possible instead of not
+ ;; moving when ARG is too large.
+ (dest (nth new-idx siblings)))
+ (when (and dest (not (eq source dest)))
+ (vundo--move-to-node
+ source dest vundo--orig-buffer vundo--prev-mod-list)
+ (vundo--refresh-buffer
+ vundo--orig-buffer (current-buffer)
+ 'incremental)))))))
+
+(defun vundo-previous (arg)
+ "Move to node above the current one. Move ARG steps."
+ (interactive "p")
+ (vundo-next (- arg)))
+
+(defun vundo--stem-root-p (node)
+ "Return non-nil if NODE is the root of a stem."
+ ;; I.e., parent has more than one children.
+ (> (length (vundo-m-children (vundo-m-parent node))) 1))
+
+(defun vundo--stem-end-p (node)
+ "Return non-nil if NODE is the end of a stem."
+ ;; No children, or more than one children.
+ (let ((len (length (vundo-m-children node))))
+ (or (> len 1) (eq len 0))))
+
+(defun vundo-stem-root ()
+ "Move to the beginning of the current stem."
+ (interactive)
+ (vundo--check-for-command
+ (let* ((this (vundo--current-node vundo--prev-mod-list))
+ (next (vundo-m-parent this)))
+ (vundo--move-to-node
+ this next vundo--orig-buffer vundo--prev-mod-list)
+ (setq this next
+ next (vundo-m-parent this))
+ (while (and next (not (vundo--stem-root-p this)))
+ (vundo--move-to-node
+ this next vundo--orig-buffer vundo--prev-mod-list)
+ (setq this next
+ next (vundo-m-parent this)))
+ (vundo--refresh-buffer
+ vundo--orig-buffer (current-buffer)
+ 'incremental))))
+
+(defun vundo-stem-end ()
+ "Move to the end of the current stem."
+ (interactive)
+ (vundo--check-for-command
+ (let* ((this (vundo--current-node vundo--prev-mod-list))
+ (next (car (vundo-m-children this))))
+ (vundo--move-to-node
+ this next vundo--orig-buffer vundo--prev-mod-list)
+ (setq this next
+ next (car (vundo-m-children this)))
+ (while (and next (not (vundo--stem-end-p this)))
+ (vundo--move-to-node
+ this next vundo--orig-buffer vundo--prev-mod-list)
+ (setq this next
+ next (car (vundo-m-children this))))
+ (vundo--refresh-buffer
+ vundo--orig-buffer (current-buffer)
+ 'incremental))))
+
+;;; Debug
+
+(defun vundo--setup-test-buffer ()
+ "Setup and pop a testing buffer.
+TYPE is the type of buffer you want."
+ (interactive)
+ (let ((buf (get-buffer "*vundo-test*")))
+ (if buf (kill-buffer buf))
+ (setq buf (get-buffer-create "*vundo-test*"))
+ (pop-to-buffer buf)))
+
+(defun vundo--inspect ()
+ "Print some useful info about the node at point."
+ (interactive)
+ (let ((node (vundo--get-node-at-point)))
+ (message "Parent: %s States: %s Children: %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))))))
+
+(defun vundo--debug ()
+ "Make cursor visible and show debug information on movement."
+ (interactive)
+ (setq cursor-type t
+ vundo--message t))
+
+(defvar vundo--monitor nil
+ "Timer for catching bugs.")
+(defun vundo--start-monitor ()
+ "Run `vundo-1' in idle timer to try to catch bugs."
+ (interactive)
+ (setq vundo--monitor
+ (run-with-idle-timer 3 t (lambda ()
+ (unless (eq t buffer-undo-list)
+ (vundo-1 (current-buffer))
+ (message "SUCCESS"))))))
+
+(provide 'vundo)
+
+;;; vundo.el ends here
- [elpa] externals/vundo 06de574d4f 24/58: Ignore position-only records when generating mod-list, (continued)
- [elpa] externals/vundo 06de574d4f 24/58: Ignore position-only records when generating mod-list, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 6a408d000f 39/58: * vundo.el (vundo--check-for-command): Add declaration for edebug., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo ed0d7d42d8 38/58: Replace lists with vectors for the main vundo-m lists, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 22345f42b1 45/58: * vundo.el (vundo): Replace let* with let., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo b93e15e0c2 50/58: * vundo.el: Sync from README., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 8d7e80d149 40/58: * vundo.el (vundo--refresh-buffer): Update docstring., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 4341d838aa 44/58: * vundo.el (vundo-glyph-alist): Escape backtick in docstring., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 6acda40607 42/58: Fix vundo-forward, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 377194b705 58/58: * vundo.el: Fix dependency tag., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 9657909b43 51/58: Trim a nreverse call in vundo--eqv-list-of, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo ab84e87522 01/58: init,
ELPA Syncer <=
- [elpa] externals/vundo 549efe15c3 14/58: UI change: default to ASCII and more, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo b27ddfecfd 25/58: Handle user quit gracefully, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 8a2f09ae42 29/58: Merge branch 'pr-avoid-list-nth', ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 1b98c3708c 33/58: * vundo.el (vundo--eqv-merge): Replace nth for performance., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo d833570d47 49/58: Update README per request, ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 7ddb684eb5 53/58: * vundo.el (vundo--draw-tree): Set inhibit-modification-hooks to t., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 9edd0ca36b 46/58: * vundo.el (vundo--eqv-list-of): Replace reverse with nreverse., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo f8cd72b611 30/58: * test/vundo-test.el (vundo-test--skip-position-only): New test., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 76a54ce79d 36/58: * README.txt (Tests): New section., ELPA Syncer, 2022/04/15
- [elpa] externals/vundo 7cafb7aac5 37/58: Merge branch 'pr-readme-test', ELPA Syncer, 2022/04/15