ftui_runtime/undo/
snapshot_store.rs1#![forbid(unsafe_code)]
2
3use std::collections::VecDeque;
51use std::fmt;
52use std::sync::Arc;
53
54#[derive(Debug, Clone)]
56pub struct SnapshotConfig {
57 pub max_depth: usize,
60}
61
62impl Default for SnapshotConfig {
63 fn default() -> Self {
64 Self { max_depth: 100 }
65 }
66}
67
68impl SnapshotConfig {
69 #[must_use]
71 pub fn new(max_depth: usize) -> Self {
72 Self { max_depth }
73 }
74
75 #[must_use]
77 pub fn unlimited() -> Self {
78 Self {
79 max_depth: usize::MAX,
80 }
81 }
82}
83
84pub struct SnapshotStore<T> {
96 undo_stack: VecDeque<Arc<T>>,
98 redo_stack: VecDeque<Arc<T>>,
100 config: SnapshotConfig,
102}
103
104impl<T: fmt::Debug> fmt::Debug for SnapshotStore<T> {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 f.debug_struct("SnapshotStore")
107 .field("undo_depth", &self.undo_stack.len())
108 .field("redo_depth", &self.redo_stack.len())
109 .field("config", &self.config)
110 .finish()
111 }
112}
113
114impl<T> SnapshotStore<T> {
115 #[must_use]
117 pub fn new(config: SnapshotConfig) -> Self {
118 Self {
119 undo_stack: VecDeque::new(),
120 redo_stack: VecDeque::new(),
121 config,
122 }
123 }
124
125 #[must_use]
127 pub fn with_default_config() -> Self {
128 Self::new(SnapshotConfig::default())
129 }
130
131 pub fn push(&mut self, state: T) {
140 self.redo_stack.clear();
141 self.undo_stack.push_back(Arc::new(state));
142 self.enforce_depth();
143 }
144
145 pub fn push_arc(&mut self, state: Arc<T>) {
150 self.redo_stack.clear();
151 self.undo_stack.push_back(state);
152 self.enforce_depth();
153 }
154
155 pub fn undo(&mut self) -> Option<Arc<T>> {
161 if self.undo_stack.len() < 2 {
163 return None;
164 }
165 let current = self.undo_stack.pop_back()?;
166 self.redo_stack.push_back(current);
167 self.undo_stack.back().cloned()
168 }
169
170 pub fn redo(&mut self) -> Option<Arc<T>> {
174 let snapshot = self.redo_stack.pop_back()?;
175 self.undo_stack.push_back(snapshot);
176 self.undo_stack.back().cloned()
177 }
178
179 #[must_use]
183 pub fn current(&self) -> Option<&Arc<T>> {
184 self.undo_stack.back()
185 }
186
187 #[must_use]
193 pub fn can_undo(&self) -> bool {
194 self.undo_stack.len() >= 2
195 }
196
197 #[must_use]
199 pub fn can_redo(&self) -> bool {
200 !self.redo_stack.is_empty()
201 }
202
203 #[must_use]
205 pub fn undo_depth(&self) -> usize {
206 self.undo_stack.len()
207 }
208
209 #[must_use]
211 pub fn redo_depth(&self) -> usize {
212 self.redo_stack.len()
213 }
214
215 #[must_use]
217 pub fn total_snapshots(&self) -> usize {
218 self.undo_stack.len() + self.redo_stack.len()
219 }
220
221 #[must_use]
223 pub fn config(&self) -> &SnapshotConfig {
224 &self.config
225 }
226
227 #[must_use]
229 pub fn is_empty(&self) -> bool {
230 self.undo_stack.is_empty()
231 }
232
233 pub fn clear(&mut self) {
239 self.undo_stack.clear();
240 self.redo_stack.clear();
241 }
242
243 fn enforce_depth(&mut self) {
245 while self.undo_stack.len() > self.config.max_depth {
246 self.undo_stack.pop_front();
247 }
248 }
249}
250
251#[cfg(feature = "hamt")]
274pub mod persistent {
275 pub use im::{HashMap, HashSet, OrdMap, OrdSet, Vector};
276}
277
278#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn new_store_is_empty() {
288 let store = SnapshotStore::<i32>::with_default_config();
289 assert!(store.is_empty());
290 assert!(!store.can_undo());
291 assert!(!store.can_redo());
292 assert_eq!(store.undo_depth(), 0);
293 assert_eq!(store.redo_depth(), 0);
294 assert_eq!(store.total_snapshots(), 0);
295 assert!(store.current().is_none());
296 }
297
298 #[test]
299 fn push_makes_current_available() {
300 let mut store = SnapshotStore::with_default_config();
301 store.push(42);
302 assert!(!store.is_empty());
303 assert_eq!(**store.current().unwrap(), 42);
304 assert_eq!(store.undo_depth(), 1);
305 assert!(!store.can_undo()); }
307
308 #[test]
309 fn push_two_enables_undo() {
310 let mut store = SnapshotStore::with_default_config();
311 store.push(1);
312 store.push(2);
313 assert!(store.can_undo());
314 assert!(!store.can_redo());
315 assert_eq!(store.undo_depth(), 2);
316 }
317
318 #[test]
319 fn undo_restores_previous() {
320 let mut store = SnapshotStore::with_default_config();
321 store.push(1);
322 store.push(2);
323 store.push(3);
324
325 let prev = store.undo().unwrap();
326 assert_eq!(*prev, 2);
327 assert_eq!(**store.current().unwrap(), 2);
328 assert!(store.can_redo());
329 }
330
331 #[test]
332 fn undo_all_stops_at_initial() {
333 let mut store = SnapshotStore::with_default_config();
334 store.push(1);
335 store.push(2);
336 store.push(3);
337
338 assert!(store.undo().is_some()); assert!(store.undo().is_some()); assert!(store.undo().is_none()); assert_eq!(**store.current().unwrap(), 1);
342 }
343
344 #[test]
345 fn redo_restores_undone() {
346 let mut store = SnapshotStore::with_default_config();
347 store.push(1);
348 store.push(2);
349 store.undo();
350
351 let restored = store.redo().unwrap();
352 assert_eq!(*restored, 2);
353 assert_eq!(**store.current().unwrap(), 2);
354 assert!(!store.can_redo());
355 }
356
357 #[test]
358 fn push_clears_redo() {
359 let mut store = SnapshotStore::with_default_config();
360 store.push(1);
361 store.push(2);
362 store.undo();
363 assert!(store.can_redo());
364
365 store.push(3); assert!(!store.can_redo());
367 assert_eq!(store.redo_depth(), 0);
368 assert_eq!(**store.current().unwrap(), 3);
369 }
370
371 #[test]
372 fn redo_on_empty_returns_none() {
373 let mut store = SnapshotStore::<i32>::with_default_config();
374 assert!(store.redo().is_none());
375 }
376
377 #[test]
378 fn undo_on_empty_returns_none() {
379 let mut store = SnapshotStore::<i32>::with_default_config();
380 assert!(store.undo().is_none());
381 }
382
383 #[test]
384 fn undo_on_single_returns_none() {
385 let mut store = SnapshotStore::with_default_config();
386 store.push(42);
387 assert!(store.undo().is_none());
388 }
389
390 #[test]
391 fn depth_limit_evicts_oldest() {
392 let mut store = SnapshotStore::new(SnapshotConfig::new(3));
393 store.push(1);
394 store.push(2);
395 store.push(3);
396 store.push(4);
397
398 assert_eq!(store.undo_depth(), 3);
399 assert_eq!(**store.current().unwrap(), 4);
401 let prev = store.undo().unwrap();
402 assert_eq!(*prev, 3);
403 let prev = store.undo().unwrap();
404 assert_eq!(*prev, 2);
405 assert!(store.undo().is_none());
406 }
407
408 #[test]
409 fn depth_limit_one_keeps_only_latest() {
410 let mut store = SnapshotStore::new(SnapshotConfig::new(1));
411 store.push(1);
412 store.push(2);
413 store.push(3);
414
415 assert_eq!(store.undo_depth(), 1);
416 assert_eq!(**store.current().unwrap(), 3);
417 assert!(!store.can_undo());
418 }
419
420 #[test]
421 fn clear_removes_all() {
422 let mut store = SnapshotStore::with_default_config();
423 store.push(1);
424 store.push(2);
425 store.undo();
426
427 store.clear();
428
429 assert!(store.is_empty());
430 assert!(!store.can_undo());
431 assert!(!store.can_redo());
432 assert_eq!(store.total_snapshots(), 0);
433 }
434
435 #[test]
436 fn multiple_undo_redo_cycle() {
437 let mut store = SnapshotStore::with_default_config();
438 store.push(1);
439 store.push(2);
440 store.push(3);
441
442 store.undo(); store.undo(); assert_eq!(store.undo_depth(), 1);
447 assert_eq!(store.redo_depth(), 2);
448
449 store.redo(); store.redo(); assert_eq!(store.undo_depth(), 3);
454 assert_eq!(store.redo_depth(), 0);
455 assert_eq!(**store.current().unwrap(), 3);
456 }
457
458 #[test]
459 fn push_arc_avoids_double_wrap() {
460 let mut store = SnapshotStore::with_default_config();
461 let arc = Arc::new(42);
462 store.push_arc(arc.clone());
463
464 assert_eq!(**store.current().unwrap(), 42);
465 assert!(Arc::ptr_eq(store.current().unwrap(), &arc));
467 }
468
469 #[test]
470 fn structural_sharing_verified() {
471 let mut store = SnapshotStore::with_default_config();
473 let state = Arc::new(vec![1, 2, 3, 4, 5]);
474 store.push_arc(state.clone());
475
476 store.push_arc(state.clone());
478
479 let s1 = store.undo().unwrap();
481 let s2 = store.current().unwrap();
482 assert!(Arc::ptr_eq(&s1, s2));
485 }
486
487 #[test]
488 fn many_snapshots_within_memory() {
489 let mut store = SnapshotStore::new(SnapshotConfig::new(1000));
491 let data = Arc::new(vec![0u8; 1024]); for _ in 0..1000 {
494 store.push_arc(data.clone());
495 }
496
497 assert_eq!(store.undo_depth(), 1000);
499 assert_eq!(Arc::strong_count(&data), 1001); }
501
502 #[test]
503 fn config_default() {
504 let config = SnapshotConfig::default();
505 assert_eq!(config.max_depth, 100);
506 }
507
508 #[test]
509 fn config_unlimited() {
510 let config = SnapshotConfig::unlimited();
511 assert_eq!(config.max_depth, usize::MAX);
512 }
513
514 #[test]
515 fn config_clone() {
516 let config = SnapshotConfig::new(50);
517 let cloned = config.clone();
518 assert_eq!(cloned.max_depth, 50);
519 }
520
521 #[test]
522 fn config_debug() {
523 let config = SnapshotConfig::new(42);
524 let s = format!("{config:?}");
525 assert!(s.contains("42"));
526 }
527
528 #[test]
529 fn store_debug() {
530 let mut store = SnapshotStore::with_default_config();
531 store.push(1);
532 let s = format!("{store:?}");
533 assert!(s.contains("SnapshotStore"));
534 assert!(s.contains("undo_depth"));
535 }
536
537 #[test]
538 fn config_accessor() {
539 let store = SnapshotStore::<i32>::new(SnapshotConfig::new(42));
540 assert_eq!(store.config().max_depth, 42);
541 }
542
543 #[test]
544 fn total_snapshots_accounts_for_both_stacks() {
545 let mut store = SnapshotStore::with_default_config();
546 store.push(1);
547 store.push(2);
548 store.push(3);
549 assert_eq!(store.total_snapshots(), 3);
550
551 store.undo();
552 assert_eq!(store.total_snapshots(), 3); store.undo();
555 assert_eq!(store.total_snapshots(), 3); }
557
558 #[test]
563 fn im_hashmap_structural_sharing() {
564 use im::HashMap;
565
566 let mut map = HashMap::new();
567 for i in 0..1000 {
568 map.insert(format!("key_{i}"), i);
569 }
570
571 let mut store = SnapshotStore::with_default_config();
572
573 store.push(map.clone());
575
576 let mut map2 = map.clone();
578 map2.insert("new_key".to_string(), 9999);
579 store.push(map2);
580
581 let prev = store.undo().unwrap();
583 assert_eq!(prev.len(), 1000);
584 assert!(!prev.contains_key("new_key"));
585
586 let restored = store.redo().unwrap();
588 assert_eq!(restored.len(), 1001);
589 assert_eq!(restored.get("new_key"), Some(&9999));
590 }
591
592 #[test]
593 fn im_vector_structural_sharing() {
594 use im::Vector;
595
596 let mut vec: Vector<u32> = (0..1000).collect();
597
598 let mut store = SnapshotStore::with_default_config();
599 store.push(vec.clone());
600
601 vec.push_back(9999);
603 store.push(vec);
604
605 let prev = store.undo().unwrap();
607 assert_eq!(prev.len(), 1000);
608
609 let restored = store.redo().unwrap();
611 assert_eq!(restored.len(), 1001);
612 assert_eq!(restored.back(), Some(&9999));
613 }
614
615 #[test]
616 fn im_hashmap_many_snapshots_memory_efficiency() {
617 use im::HashMap;
618
619 let mut state: HashMap<String, Vec<u8>> = HashMap::new();
621 for i in 0..100 {
622 state.insert(format!("entry_{i}"), vec![0u8; 100]);
623 }
624
625 let mut store = SnapshotStore::new(SnapshotConfig::new(1000));
626
627 for i in 0..50 {
629 store.push(state.clone());
630 state.insert(format!("entry_{}", i % 100), vec![i as u8; 100]);
632 }
633
634 assert_eq!(store.undo_depth(), 50);
635
636 for _ in 0..49 {
638 let prev = store.undo().unwrap();
639 assert_eq!(prev.len(), 100);
640 }
641 assert!(store.undo().is_none());
642 }
643
644 #[test]
645 fn depth_limit_zero_evicts_everything() {
646 let mut store = SnapshotStore::new(SnapshotConfig::new(0));
647 store.push(42);
648 assert!(store.is_empty());
649 }
650
651 #[test]
652 fn push_arc_clears_redo() {
653 let mut store = SnapshotStore::with_default_config();
654 store.push(1);
655 store.push(2);
656 store.undo();
657 assert!(store.can_redo());
658
659 store.push_arc(Arc::new(3));
660 assert!(!store.can_redo());
661 }
662
663 #[test]
664 fn undo_redo_returns_correct_arc() {
665 let mut store = SnapshotStore::with_default_config();
666 store.push("a");
667 store.push("b");
668 store.push("c");
669
670 let undone = store.undo().unwrap();
671 assert_eq!(*undone, "b");
672
673 let redone = store.redo().unwrap();
674 assert_eq!(*redone, "c");
675 }
676}