1use std::collections::VecDeque;
9use std::time::{Duration, Instant};
10
11const MAX_DEPTH: usize = 64;
13const COALESCE_WINDOW: Duration = Duration::from_millis(1_200);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum EditKind {
19 Typing,
21 Atomic,
23}
24
25#[derive(Debug, Clone)]
27pub struct History<T> {
28 undo: VecDeque<T>,
29 redo: VecDeque<T>,
30 last_kind: Option<EditKind>,
31 last_at: Option<Instant>,
32}
33
34impl<T> Default for History<T> {
35 fn default() -> Self {
36 Self {
37 undo: VecDeque::new(),
38 redo: VecDeque::new(),
39 last_kind: None,
40 last_at: None,
41 }
42 }
43}
44
45impl<T> History<T> {
46 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn will_coalesce(&self, kind: EditKind) -> bool {
52 kind == EditKind::Typing
53 && self.last_kind == Some(EditKind::Typing)
54 && self.last_at.is_some_and(|t| t.elapsed() < COALESCE_WINDOW)
55 && !self.undo.is_empty()
56 }
57
58 pub fn touch_coalesce(&mut self) {
60 self.last_at = Some(Instant::now());
61 }
62
63 pub fn push(&mut self, current: T, kind: EditKind) {
65 self.undo.push_back(current);
66 while self.undo.len() > MAX_DEPTH {
67 self.undo.pop_front();
68 }
69 self.redo.clear();
70 self.last_kind = Some(kind);
71 self.last_at = Some(Instant::now());
72 }
73
74 pub fn before_edit_with(&mut self, kind: EditKind, current: impl FnOnce() -> T) {
76 if self.will_coalesce(kind) {
77 self.touch_coalesce();
78 return;
79 }
80 self.push(current(), kind);
81 }
82
83 pub fn break_coalesce(&mut self) {
85 self.last_kind = None;
86 self.last_at = None;
87 }
88
89 pub fn undo(&mut self, current: T) -> Option<T> {
91 let prev = self.undo.pop_back()?;
92 self.redo.push_back(current);
93 self.break_coalesce();
94 Some(prev)
95 }
96
97 pub fn redo(&mut self, current: T) -> Option<T> {
99 let next = self.redo.pop_back()?;
100 self.undo.push_back(current);
101 self.break_coalesce();
102 Some(next)
103 }
104
105 pub fn can_redo(&self) -> bool {
106 !self.redo.is_empty()
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn typing_coalesces_into_one_step() {
116 let mut h: History<String> = History::new();
117 h.before_edit_with(EditKind::Typing, || "a".into());
118 h.before_edit_with(EditKind::Typing, || "ab".into());
119 h.before_edit_with(EditKind::Typing, || "abc".into());
120 assert_eq!(h.undo.len(), 1);
121 let restored = h.undo("abc".into()).unwrap();
122 assert_eq!(restored, "a");
123 }
124
125 #[test]
126 fn atomic_always_splits() {
127 let mut h: History<String> = History::new();
128 h.before_edit_with(EditKind::Typing, || "a".into());
129 h.before_edit_with(EditKind::Atomic, || "ab".into());
130 h.before_edit_with(EditKind::Typing, || "abc".into());
131 assert_eq!(h.undo.len(), 3);
132 }
133
134 #[test]
135 fn redo_clears_on_new_edit() {
136 let mut h: History<String> = History::new();
137 h.before_edit_with(EditKind::Atomic, || "0".into());
138 let _ = h.undo("1".into());
139 assert!(h.can_redo());
140 h.before_edit_with(EditKind::Atomic, || "2".into());
141 assert!(!h.can_redo());
142 }
143
144 #[test]
145 fn coalesce_skips_snapshot_fn() {
146 let mut h: History<String> = History::new();
147 h.before_edit_with(EditKind::Typing, || "a".into());
148 let mut built = 0;
149 h.before_edit_with(EditKind::Typing, || {
150 built += 1;
151 "ab".into()
152 });
153 assert_eq!(built, 0);
154 assert_eq!(h.undo.len(), 1);
155 }
156}