-
Notifications
You must be signed in to change notification settings - Fork 23
/
vundo.el
1462 lines (1344 loc) · 56.9 KB
/
vundo.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
;;; vundo.el --- Visual undo tree -*- lexical-binding: t; -*-
;; Copyright (C) 2019-2023 Free Software Foundation, Inc.
;;
;; Author: Yuan Fu <[email protected]>
;; Maintainer: Yuan Fu <[email protected]>
;; URL: https://github.com/casouri/vundo
;; Version: 2.3.0
;; Keywords: undo, text, editing
;; 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 (visual undo) displays the undo history as a tree and lets you
;; move in the tree to go back to previous buffer states. To use vundo,
;; type M-x vundo RET in the buffer you want to undo. An 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
;;
;; 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
;; r to go to the next 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
;;
;; n/p may need some more explanation. In the following tree, n/p can
;; move between A and B because they share a parent (thus at a branching
;; point), but not C and D.
;;
;; A C
;; ──○──○──○──○──○
;; │ ↕
;; └──○──○──○
;; B D
;;
;; By default, you need to press RET to “commit” your change and if you
;; quit with q or C-g, the changes made by vundo are rolled back. You can
;; set `vundo-roll-back-on-quit' to nil to disable rolling back.
;;
;; Note: vundo.el requires Emacs 28.
;;
;; Customizable faces:
;;
;; - vundo-default
;; - vundo-node
;; - vundo-stem
;; - vundo-highlight
;;
;; If you want to use prettier Unicode characters to draw the tree like
;; this:
;;
;; ○──○──○
;; │ └──●
;; ├──○
;; └──○
;;
;; set vundo-glyph-alist by
;;
;; (setq vundo-glyph-alist vundo-unicode-symbols)
;;
;; Your default font needs to contain these Unicode characters, otherwise
;; they look terrible and don’t align. You can find a font that covers
;; these characters (eg, Symbola, Unifont), and set `vundo-default' face
;; to use that font:
;;
;; (set-face-attribute 'vundo-default nil :family "Symbola")
;;
;; 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.
;;; Developer:
;;
;; In the comments, when I say node, modification, mod, buffer state,
;; they all mean one thing: `vundo-m'. Ie, `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 vector of `vundo-m'. This vector
;; 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
;; associate equivalent nodes.
;;
;; Once we have generated the data structure and drawn the tree, vundo
;; commands can move around in that tree by calling
;; `vundo--move-to-node'. It will construct the correct undo-list and
;; feed it to `primitive-undo'. `vundo--trim-undo-list' can trim the
;; undo list when possible.
;;
;; Finally, to avoid generating everything from scratch every time we
;; move 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.
;;
;; Position-only records
;;
;; We know how undo works: when undoing, `primitive-undo' looks at
;; each record in `pending-undo-list' and modifies the buffer
;; accordingly, and that modification itself pushes new undo records
;; into `buffer-undo-list'. However, not all undo records introduce
;; modification, if the record is an integer, `primitive-undo' simply
;; `goto' that position, which introduces no modification to the
;; buffer and pushes no undo record to `buffer-undo-list'. Normally
;; position records accompany other buffer-modifying records, but if a
;; particular record consists of only position records, we have
;; trouble: after an undo step, `buffer-undo-list' didn’t grow, as far
;; as vundo tree-folding algorithm is concerned, we didn’t move.
;; Assertions expecting to see new undo records in `buffer-undo-list'
;; are also violated. To avoid all these complications, we ignore
;; position-only records when generating mod-list in
;; `vundo--mod-list-from'. These records are not removed, but they
;; can’t harm us now.
;;; Code:
(require 'pcase)
(require 'cl-lib)
(require 'seq)
(require 'subr-x)
;;; 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-branch-stem
'((t (:inherit vundo-stem :weight bold)))
"Face for branching stems in the undo tree.")
(defface vundo-highlight
'((((background light)) .
(:inherit vundo-node :weight bold :foreground "red"))
(((background dark)) .
(: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)
(defcustom vundo-window-side 'bottom
"The vundo window pops up on this side."
:type '(choice (const :tag "Bottom" bottom)
(const :tag "Top" top)))
;;;###autoload
(defconst vundo-ascii-symbols
'((selected-node . ?x)
(node . ?o)
(horizontal-stem . ?-)
(vertical-stem . ?|)
(branch . ?|)
(last-branch . ?`))
"ASCII symbols to draw vundo tree.")
;;;###autoload
(defconst vundo-unicode-symbols
'((selected-node . ?●)
(node . ?○)
(horizontal-stem . ?─)
(vertical-stem . ?│)
(branch . ?├)
(last-branch . ?└))
"Unicode symbols to draw vundo tree.")
(defcustom vundo-compact-display nil
"Show a more compact tree display if non-nil.
Basically we display
○─○─○ instead of ○──○──○
│ └─● │ └──●
├─○ ├──○
└─○ └──○"
:type 'boolean)
(defcustom vundo-glyph-alist vundo-ascii-symbols
"Alist mapping tree parts to characters used to draw a tree.
Keys are names for different parts of a tree, values are
characters for that part. Possible keys include
node which represents ○
selected-node which represents ●
horizontal-stem which represents ─
vertical-stem which represents │
branch which represents ├
last-branch which represents └
in a tree like
○──○──○
│ └──●
├──○
└──○
By default, the tree is drawn with ASCII characters like this:
o--o--o
| \\=`--x
|--o
\\=`--o
Set this variable to `vundo-unicode-symbols' to use Unicode
characters."
:type `(alist :tag "Translation alist"
:key-type (symbol :tag "Part of tree")
:value-type (character :tag "Draw using")
:options ,(mapcar #'car vundo-unicode-symbols)))
(defcustom vundo-pre-enter-hook nil
"List of functions to call when entering vundo.
This hook runs immediately after ‘vundo’ is called, in the buffer
the user invoked ‘vundo’, before every setup ‘vundo’ does."
:type 'hook)
(defcustom vundo-post-exit-hook nil
"List of functions to call when exiting vundo.
This hook runs in the original buffer the user invoked ‘vundo’,
after all the clean up the exiting function does. Ie, it is the
very last thing that happens when vundo exists."
:type 'hook)
(defcustom vundo-diff-setup-hook nil
"List of functions to call after creating a diff buffer.
This hook runs in the ‘vundo-diff’ buffer immediately after it's setup,
both for new or existing buffers. This may be used to
manipulate the diff or transform it's contents."
:type 'hook)
;;; 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.")
(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.
Position-only means all records until to the next undo
boundary are position records. Position record is just an
integer (see `buffer-undo-list'). Assumes the first element
of UNDO-LIST is not nil."
(let ((pos-only t))
(while (car undo-list)
(when (not (integerp (pop undo-list)))
(setq pos-only nil)
(setq undo-list nil)))
pos-only))
(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 ((uidx 0)
(mod-list (or mod-list (vector (make-vundo-m))))
new-mlist)
(while (and undo-list (or (null n) (< uidx n)))
;; Skip leading nils.
(while (and undo-list (null (car undo-list)))
(setq undo-list (cdr undo-list))
(cl-incf uidx))
;; It's possible the index was exceeded stepping over nil.
(when (or (null n) (< uidx n))
;; Add modification.
(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)))
(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 midx from (or n 0) to (1- (length mod-list))
for mod = (aref mod-list 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--master-eqv-mod-of (mod)
"Return the master mod in the eqv-list of MOD.
Master mod is the mod with the smallest index in the eqv-list.
This function is equivalent to (car (vundo--eqv-list-of mod))."
(while (vundo-m-prev-eqv mod)
(cl-assert (not (eq mod (vundo-m-prev-eqv mod))))
(setq mod (vundo-m-prev-eqv mod)))
mod)
(defun vundo--eqv-list-of (mod)
"Return all the modifications equivalent to MOD."
(while (vundo-m-next-eqv mod)
(cl-assert (not (eq mod (vundo-m-next-eqv mod))))
(setq mod (vundo-m-next-eqv mod)))
;; Start at the last mod in the equiv chain, walk back to the first.
(let ((eqv-list (list mod)))
(while (vundo-m-prev-eqv mod)
(cl-assert (not (eq mod (vundo-m-prev-eqv mod))))
(setq mod (vundo-m-prev-eqv mod))
(push mod eqv-list))
eqv-list))
(defun vundo--eqv-merge (mlist)
"Connect modifications in MLIST to be in the same equivalence list.
Order is reserved."
;; Basically, for MLIST = (A B C), set
;; A.prev = nil A.next = B
;; B.prev = A B.next = C
;; C.prev = B C.next = nil
(cl-loop for this-tail = mlist then (cdr this-tail)
for next-tail = (cdr mlist) then (cdr next-tail)
for prev-tail = (cons nil mlist) then (cdr prev-tail)
while this-tail
do (setf (vundo-m-prev-eqv (car this-tail)) (car prev-tail))
do (setf (vundo-m-next-eqv (car this-tail)) (car next-tail))))
(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 = (aref mod-list m)
;; 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 (aref mod-list 0) 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 (aref mod-list (1- m)))
(min-eqv-mod (vundo--master-eqv-mod-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, ie, later mod
;; comes first. Later in `vundo--build-tree' we
;; draw the tree depth-first.
(vundo--sort-mod (cons mod children)
'reverse))))))))))
;;; Timestamps
;; buffer-undo-list contains "timestamp entries" within a record like
;; (t . TIMESTAMP). These capture the file modification time of the
;; saved file which that undo changed (i.e. the TIMESTAMP applies to
;; the prior state). While reading the undo list, we collect these,
;; sort them, and during tree draw, indicate nodes which had been
;; saved specially. Note that the buffer associated with the current
;; node can be saved, but not yet modified by an undo/redo; this is
;; handled specially.
(defvar-local vundo--timestamps nil
"An alist mapping mods to modification times.
When there are multiple mods corresponding to the same node in
the undo tree, use the master equivalent mod as the
key (‘vundo--master-eqv-mod-of’).
Sorted by time, with latest saved mods first. Only undo-based
modification times are included; see `vundo--node-timestamp'.")
(defun vundo--record-timestamps (mod-list)
"Return an alist mapping mods in MOD-LIST to timestamps.
The alist is sorted by time, with latest saved mods first."
(let ((timestamps ()))
(cl-loop for idx from 1 below (length mod-list)
for ts = (vundo-m-timestamp (aref mod-list idx))
if ts do
(let* ((mod-node (aref mod-list (1- idx)))
(master (vundo--master-eqv-mod-of mod-node))
(entry (assq master timestamps))
(old-ts (cdr entry)))
(when (and old-ts (time-less-p ts old-ts))
;; Equivalent node modified again? take the newer time.
(setq ts old-ts))
(if entry (setcdr entry ts)
(push (cons master ts) timestamps))))
(sort timestamps ; Sort latest first.
(lambda (a b) (time-less-p (cdr b) (cdr a))))))
(defun vundo--find-last-saved (node &optional arg)
"Return the last saved node prior to NODE.
ARG (default 1) specifies the number of saved nodes to move
backwards in history. ARG<0 indicates moving that many saved
nodes forward in history. Returns nil if no such saved node
exists."
(let* ((arg (or arg 1))
(past (>= arg 0))
(cnt (abs arg))
(master (vundo--master-eqv-mod-of node))
(midx (vundo-m-idx master))
last-node)
(if (assq master vundo--timestamps)
(setq last-node master)
;; No timestamp here, find closest master idx on saved list in
;; the direction indicated by ARG.
(cl-loop with val = (if past -1 most-positive-fixnum)
with between = (if past #'< #'>)
for (n . _) in vundo--timestamps
for idx = (vundo-m-idx n)
if (funcall between val idx midx)
do (setq val idx last-node n))
;; Use up one count when getting started.
(when last-node (setq cnt (1- cnt))))
;; Found one, but more to go.
(if (and last-node (> cnt 0))
(let ((vt (if past vundo--timestamps
(reverse vundo--timestamps))))
(while (and vt (not (eq (caar vt) last-node)))
(setq vt (cdr vt)))
(caar (nthcdr cnt vt)))
last-node)))
(defvar vundo--orig-buffer)
(defun vundo--node-timestamp (mod-list node &optional no-buffer)
"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 it has an associated file which is
unmodified and NO-BUFFER is non-nil)."
(when-let ((master (vundo--master-eqv-mod-of node)))
(or (alist-get master vundo--timestamps nil nil #'eq)
(and (eq node (vundo--current-node mod-list))
(with-current-buffer vundo--orig-buffer
(and (not no-buffer) (buffer-file-name)
(not (buffer-modified-p))
(visited-file-modtime)))))))
;;; Draw tree
(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 (eobp)))
(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 translated TEXT.
Translate according to `vundo-glyph-alist'."
(seq-mapcat (lambda (ch)
(char-to-string
(alist-get
(pcase ch
(?○ 'node)
(?● 'selected-node)
(?─ 'horizontal-stem)
(?│ 'vertical-stem)
(?├ 'branch)
(?└ 'last-branch))
vundo-glyph-alist)))
text 'string))
(defun vundo--draw-tree (mod-list)
"Draw the tree in MOD-LIST in current buffer."
(let* ((root (aref mod-list 0))
(node-queue (list root))
(inhibit-read-only t)
(inhibit-modification-hooks t))
(erase-buffer)
(while node-queue
(let* ((node (pop node-queue))
(children (vundo-m-children node))
(parent (vundo-m-parent node))
(siblings (and parent (vundo-m-children parent)))
(only-child-p (and parent (eq (length siblings) 1)))
(node-last-child-p (and parent (eq node (car (last siblings)))))
(mod-ts (vundo--node-timestamp mod-list node 'no-buffer))
(node-face (if (and vundo-highlight-saved-nodes mod-ts)
'vundo-saved 'vundo-node))
(stem-face (if only-child-p 'vundo-stem 'vundo-branch-stem)))
;; Go to parent.
(if parent (goto-char (vundo-m-point parent)))
(let ((room-for-another-rx
(rx-to-string
`(or (>= ,(if vundo-compact-display 3 4) ?\s) eol))))
(if (null parent)
(insert (propertize (vundo--translate "○")
'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
;; | child to 1 but is blocked
;; +--4 by that plus sign.
(while (not (looking-at room-for-another-rx))
(vundo--next-line-at-column (max 0 (1- (current-column))))
;; When we go down, we could encounter space, EOL, │,
;; ├, or └. Space and EOL should be replaced by │, ├
;; and └ should be replaced by ├.
(let ((replace-char
(if (looking-at
(rx-to-string
`(or ,(vundo--translate "├")
,(vundo--translate "└"))))
(vundo--translate "├")
(vundo--translate "│"))))
(unless (eolp) (delete-char 1))
(insert (propertize replace-char 'face stem-face))))
;; Make room for inserting the new node.
(unless (looking-at "$")
(delete-char (if vundo-compact-display 2 3)))
;; Insert the new node.
(if (eq (point) planned-point)
(insert (propertize
(vundo--translate
(if vundo-compact-display "─" "──"))
'face stem-face)
(propertize (vundo--translate "○")
'face node-face))
;; We must break the line. Delete the previously
;; inserted char.
(delete-char -1)
(insert (propertize
(vundo--translate
(if node-last-child-p
(if vundo-compact-display "└─" "└──")
(if vundo-compact-display "├─" "├──")))
'face stem-face))
(insert (propertize (vundo--translate "○")
'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))))))
;;; 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))))
(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)
(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 "w") #'vundo-next-root)
(define-key map (kbd "e") #'vundo-stem-end)
(define-key map (kbd "l") #'vundo-goto-last-saved)
(define-key map (kbd "r") #'vundo-goto-next-saved)
(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 [remap save-buffer] #'vundo-save)
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 nil)
(face-remap-add-relative 'default 'vundo-default)
;; Disable evil-mode, as normal-mode
;; key bindings override the ones set by vundo.
(when (and (boundp 'evil-emacs-state-modes)
(not (memq 'vundo-mode evil-emacs-state-modes)))
(push 'vundo-mode evil-emacs-state-modes)))
(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.")
(defvar-local vundo--highlight-overlay nil
"Overlay used to highlight the selected 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.
Keep the first N modifications."
(cl-loop for midx from (1+ n) to (1- (length mod-list))
for mod = (aref mod-list midx)
do (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 existing mod-list and mod-hash.
INCREMENTAL is only applicable when entries are either added or
removed from undo-list. On the other hand, if some entries are
removed and some added, do not use INCREMENTAL.
This function modifies `vundo--prev-mod-list',
`vundo--prev-mod-hash', `vundo--prev-undo-list',
`vundo--orig-buffer'."
(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))
;; 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))))
;; 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)
;; Record timestamps
(setq vundo--timestamps (vundo--record-timestamps 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--highlight-node (vundo--current-node mod-list))
(goto-char (vundo-m-point (vundo--current-node mod-list)))
;; Highlight the last saved node extra specially
(when vundo-highlight-saved-nodes
(vundo--highlight-last-saved-node mod-list vundo--timestamps)))))
(defun vundo--current-node (mod-list)
"Return the currently highlighted node in MOD-LIST."
(vundo--master-eqv-mod-of (aref mod-list (1- (length mod-list)))))
(defun vundo--highlight-node (node)
"Highlight NODE as current node."
(unless vundo--highlight-overlay
(setq vundo--highlight-overlay
(make-overlay (1- (vundo-m-point node)) (vundo-m-point node)))
(overlay-put vundo--highlight-overlay
'display (vundo--translate "●"))
(overlay-put vundo--highlight-overlay
'face 'vundo-highlight)
;; Make current node’s highlight override last saved node’s
;; highlight, should they collide.
(overlay-put vundo--highlight-overlay 'priority 2))
(move-overlay vundo--highlight-overlay
(1- (vundo-m-point node))
(vundo-m-point node)))
(defun vundo--highlight-last-saved-node (mod-list timestamps)
"Highlight the last (latest) saved node on MOD-LIST.
Consults the alist of TIMESTAMPS. This moves the overlay
`vundo--highlight-last-saved-overlay'."
(let* ((last-saved (car timestamps))
(cur (vundo--current-node mod-list))
(cur-ts (vundo--node-timestamp mod-list cur))
(node (cond ((and last-saved cur-ts)
(if (time-less-p (cdr last-saved) cur-ts)
cur (car last-saved)))
(last-saved (car last-saved))
(cur-ts cur)
(t nil)))
(node-pt (and node (vundo-m-point node))))
(when node-pt
(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."
(interactive)
(when (not (consp buffer-undo-list))
(user-error "There is no undo history"))
(when buffer-read-only
(user-error "Buffer is read-only"))
(run-hooks 'vundo-pre-enter-hook)
(let ((vundo-buf (vundo-1 (current-buffer))))
(select-window
(display-buffer-in-side-window
vundo-buf
`((side . ,vundo-window-side)
(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."
(declare (debug (&rest form)))
`(progn
(when (not (derived-mode-p 'vundo-mode))
(user-error "Not in vundo buffer"))
(when (not (buffer-live-p vundo--orig-buffer))
(when (y-or-n-p "Original buffer is gone, kill vundo buffer? ")
(kill-buffer-and-window))
;; Non-local exit.
(user-error ""))
;; If ORIG-BUFFER changed since we last synced the vundo buffer
;; (eg, 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)