Skip to main content

pnc_engine/
stack.rs

1use crate::value::Value;
2
3pub const DEFAULT_HISTORY_DEPTH: usize = 10;
4pub const DEFAULT_HISTORY_CAPACITY: usize = DEFAULT_HISTORY_DEPTH + 1;
5
6/// Bounded undo/redo history buffer carrying a position index.
7///
8/// Capacity is `history_depth + 1` snapshots: one for the current state plus
9/// `history_depth` undoable operations. `history_depth = 0` disables history.
10#[derive(Debug, Clone)]
11pub struct History {
12    snapshots: Vec<Vec<Value>>,
13    position: usize,
14    capacity: usize,
15}
16
17impl Default for History {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22
23impl History {
24    pub fn new() -> Self {
25        Self::with_capacity(DEFAULT_HISTORY_CAPACITY)
26    }
27
28    pub fn with_capacity(capacity: usize) -> Self {
29        Self {
30            snapshots: Vec::new(),
31            position: 0,
32            capacity,
33        }
34    }
35
36    pub fn capacity(&self) -> usize {
37        self.capacity
38    }
39
40    pub fn is_enabled(&self) -> bool {
41        self.capacity >= 2
42    }
43
44    pub fn position(&self) -> usize {
45        self.position
46    }
47
48    pub fn len(&self) -> usize {
49        self.snapshots.len()
50    }
51
52    pub fn is_empty(&self) -> bool {
53        self.snapshots.is_empty()
54    }
55
56    pub fn can_undo(&self) -> bool {
57        self.position > 0
58    }
59
60    pub fn can_redo(&self) -> bool {
61        self.position + 1 < self.snapshots.len()
62    }
63
64    pub fn undo_count(&self) -> usize {
65        self.position
66    }
67
68    pub fn redo_count(&self) -> usize {
69        if self.snapshots.is_empty() {
70            0
71        } else {
72            self.snapshots.len() - 1 - self.position
73        }
74    }
75
76    pub fn undo(&mut self) -> Option<Vec<Value>> {
77        if !self.is_enabled() || self.position == 0 {
78            return None;
79        }
80        self.position -= 1;
81        Some(self.snapshots[self.position].clone())
82    }
83
84    pub fn redo(&mut self) -> Option<Vec<Value>> {
85        if !self.is_enabled() || self.position + 1 >= self.snapshots.len() {
86            return None;
87        }
88        self.position += 1;
89        Some(self.snapshots[self.position].clone())
90    }
91
92    pub fn record(&mut self, snapshot: Vec<Value>) {
93        if !self.is_enabled() {
94            return;
95        }
96
97        // Truncate forward/redo history.
98        self.snapshots.truncate(self.position + 1);
99
100        // Skip append if equal to last snapshot.
101        if !self.snapshots.is_empty() && snapshot == *self.snapshots.last().unwrap() {
102            return;
103        }
104
105        // Evict oldest if at capacity.
106        if self.snapshots.len() == self.capacity && self.capacity > 0 {
107            self.snapshots.remove(0);
108            if self.position > 0 {
109                self.position -= 1;
110            }
111        }
112
113        self.snapshots.push(snapshot);
114        self.position = self.snapshots.len() - 1;
115    }
116
117    pub fn seed(&mut self, snapshot: Vec<Value>) {
118        if !self.is_enabled() {
119            return;
120        }
121        self.snapshots = vec![snapshot];
122        self.position = 0;
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::value::Value;
130
131    #[test]
132    fn undo_disabled_at_zero_capacity() {
133        let mut h = History::with_capacity(0);
134        assert!(!h.is_enabled());
135        h.record(vec![Value::Int(1.into())]);
136        assert_eq!(h.len(), 0);
137        assert!(h.undo().is_none());
138        assert!(h.redo().is_none());
139    }
140
141    #[test]
142    fn capacity_one_disables() {
143        let h = History::with_capacity(1);
144        assert!(!h.is_enabled());
145    }
146
147    #[test]
148    fn capacity_three_transition() {
149        let mut h = History::with_capacity(3);
150        h.seed(vec![Value::Int(0.into())]); // S0
151        h.record(vec![Value::Int(1.into())]); // S1
152        h.record(vec![Value::Int(2.into())]); // S2
153        assert_eq!(h.len(), 3);
154        assert_eq!(h.position(), 2);
155        h.record(vec![Value::Int(3.into())]); // S3 -> evict S0
156        assert_eq!(h.len(), 3);
157        assert_eq!(h.position(), 2);
158        // undo -> S2
159        let v = h.undo().unwrap();
160        assert_eq!(v, vec![Value::Int(2.into())]);
161        assert_eq!(h.position(), 1);
162        // redo -> S3
163        let v = h.redo().unwrap();
164        assert_eq!(v, vec![Value::Int(3.into())]);
165        assert_eq!(h.position(), 2);
166    }
167
168    #[test]
169    fn mutation_after_undo_drops_redo() {
170        let mut h = History::with_capacity(5);
171        h.seed(vec![Value::Int(0.into())]);
172        h.record(vec![Value::Int(1.into())]);
173        h.record(vec![Value::Int(2.into())]);
174        h.undo(); // position 1
175        h.record(vec![Value::Int(99.into())]); // truncate + append
176        assert_eq!(h.redo_count(), 0);
177    }
178
179    #[test]
180    fn no_op_result_truncates_redo_no_append() {
181        let mut h = History::with_capacity(5);
182        h.seed(vec![Value::Int(1.into())]);
183        h.record(vec![Value::Int(2.into())]);
184        h.undo(); // position 0
185        // record same as last before undo... actually record S1 again
186        // Simpler: record equal snapshot
187        h.record(vec![Value::Int(1.into())]);
188        // Should truncate redo (none here) and skip append since equal to S0
189        // Actually after undo position=0, snapshots=[S0], record S0 -> equal -> skip
190        assert_eq!(h.len(), 1);
191    }
192
193    #[test]
194    fn redo_count_empty_buffer() {
195        let h = History::with_capacity(5);
196        assert_eq!(h.redo_count(), 0);
197    }
198
199    #[test]
200    fn seed_empty_stack() {
201        let mut h = History::with_capacity(5);
202        h.seed(vec![]);
203        assert_eq!(h.len(), 1);
204        assert_eq!(h.position(), 0);
205        let v = h.undo();
206        assert!(v.is_none()); // position 0, can't undo
207    }
208
209    #[test]
210    fn new_delegates() {
211        let h = History::new();
212        assert_eq!(h.capacity(), DEFAULT_HISTORY_CAPACITY);
213    }
214}