1use alloc::{collections::VecDeque, vec::Vec};
30
31use azul_core::{
32 dom::{DomId, NodeId},
33 selection::{OptionSelectionRange, OptionTextCursor},
34 task::Instant,
35};
36use azul_css::{impl_option, impl_option_inner, AzString};
37
38use super::changeset::TextChangeset;
39
40pub const MAX_UNDO_HISTORY: usize = 10;
42
43pub const MAX_REDO_HISTORY: usize = 10;
45
46#[derive(Debug, Clone)]
50#[repr(C)]
51pub struct NodeStateSnapshot {
52 pub node_id: NodeId,
54 pub text_content: AzString,
56 pub cursor_position: OptionTextCursor,
59 pub selection_range: OptionSelectionRange,
61 pub timestamp: Instant,
63}
64
65#[derive(Debug, Clone)]
69#[repr(C)]
70pub struct UndoableOperation {
71 pub changeset: TextChangeset,
73 pub pre_state: NodeStateSnapshot,
75}
76
77impl_option!(
78 UndoableOperation,
79 OptionUndoableOperation,
80 copy = false,
81 [Debug, Clone]
82);
83
84#[derive(Debug, Clone)]
86pub struct NodeUndoRedoStack {
87 pub node_id: NodeId,
89 pub undo_stack: VecDeque<UndoableOperation>,
91 pub redo_stack: VecDeque<UndoableOperation>,
93}
94
95impl NodeUndoRedoStack {
96 #[must_use] pub fn new(node_id: NodeId) -> Self {
97 Self {
98 node_id,
99 undo_stack: VecDeque::with_capacity(MAX_UNDO_HISTORY),
100 redo_stack: VecDeque::with_capacity(MAX_REDO_HISTORY),
101 }
102 }
103
104 pub fn push_undo(&mut self, operation: UndoableOperation) {
106 self.redo_stack.clear();
108
109 self.undo_stack.push_back(operation);
111
112 if self.undo_stack.len() > MAX_UNDO_HISTORY {
114 self.undo_stack.pop_front();
115 }
116 }
117
118 pub fn push_undo_preserving_redo(&mut self, operation: UndoableOperation) {
123 self.undo_stack.push_back(operation);
124 if self.undo_stack.len() > MAX_UNDO_HISTORY {
125 self.undo_stack.pop_front();
126 }
127 }
128
129 pub fn pop_undo(&mut self) -> Option<UndoableOperation> {
131 self.undo_stack.pop_back()
132 }
133
134 pub fn push_redo(&mut self, operation: UndoableOperation) {
136 self.redo_stack.push_back(operation);
137
138 if self.redo_stack.len() > MAX_REDO_HISTORY {
140 self.redo_stack.pop_front();
141 }
142 }
143
144 pub fn pop_redo(&mut self) -> Option<UndoableOperation> {
146 self.redo_stack.pop_back()
147 }
148
149 #[must_use] pub fn can_undo(&self) -> bool {
151 !self.undo_stack.is_empty()
152 }
153
154 #[must_use] pub fn can_redo(&self) -> bool {
156 !self.redo_stack.is_empty()
157 }
158
159 #[must_use] pub fn peek_undo(&self) -> Option<&UndoableOperation> {
161 self.undo_stack.back()
162 }
163
164 #[must_use] pub fn peek_redo(&self) -> Option<&UndoableOperation> {
166 self.redo_stack.back()
167 }
168}
169
170#[derive(Debug, Clone)]
178pub struct ContentSnapshot {
179 pub pre: Vec<crate::text3::cache::InlineContent>,
181 pub post: Vec<crate::text3::cache::InlineContent>,
183}
184
185const MAX_CONTENT_SNAPSHOTS: usize = 64;
189
190#[derive(Debug, Clone, Default)]
192pub struct UndoRedoManager {
193 pub node_stacks: Vec<NodeUndoRedoStack>,
196 pub content_snapshots: Vec<(super::changeset::ChangesetId, ContentSnapshot)>,
199}
200
201impl UndoRedoManager {
202 #[must_use] pub const fn new() -> Self {
204 Self {
205 node_stacks: Vec::new(),
206 content_snapshots: Vec::new(),
207 }
208 }
209
210 pub fn store_content_snapshot(
212 &mut self,
213 id: super::changeset::ChangesetId,
214 pre: Vec<crate::text3::cache::InlineContent>,
215 post: Vec<crate::text3::cache::InlineContent>,
216 ) {
217 self.content_snapshots.retain(|(existing, _)| *existing != id);
218 self.content_snapshots.push((id, ContentSnapshot { pre, post }));
219 if self.content_snapshots.len() > MAX_CONTENT_SNAPSHOTS {
220 self.content_snapshots.remove(0);
221 }
222 }
223
224 #[must_use] pub fn get_content_snapshot(
226 &self,
227 id: super::changeset::ChangesetId,
228 ) -> Option<&ContentSnapshot> {
229 self.content_snapshots
230 .iter()
231 .find(|(existing, _)| *existing == id)
232 .map(|(_, snap)| snap)
233 }
234
235 pub fn reinstate_undo(&mut self, operation: UndoableOperation) {
241 let node_id = operation
242 .changeset
243 .target
244 .node
245 .into_crate_internal()
246 .expect("TextChangeset target node should not be None");
247 let stack = self.get_or_create_stack_mut(node_id);
248 stack.push_undo_preserving_redo(operation);
249 }
250
251 pub fn get_or_create_stack_mut(&mut self, node_id: NodeId) -> &mut NodeUndoRedoStack {
256 if let Some(pos) = self.node_stacks.iter().position(|s| s.node_id == node_id) {
257 &mut self.node_stacks[pos]
258 } else {
259 self.node_stacks.push(NodeUndoRedoStack::new(node_id));
260 self.node_stacks.last_mut().unwrap()
261 }
262 }
263
264 #[must_use] pub fn get_stack(&self, node_id: NodeId) -> Option<&NodeUndoRedoStack> {
266 self.node_stacks.iter().find(|s| s.node_id == node_id)
267 }
268
269 fn get_stack_mut(&mut self, node_id: NodeId) -> Option<&mut NodeUndoRedoStack> {
271 self.node_stacks.iter_mut().find(|s| s.node_id == node_id)
272 }
273
274 pub fn record_operation(&mut self, changeset: TextChangeset, pre_state: NodeStateSnapshot) {
286 let node_id = changeset
289 .target
290 .node
291 .into_crate_internal()
292 .expect("TextChangeset target node should not be None");
293 let stack = self.get_or_create_stack_mut(node_id);
294
295 let operation = UndoableOperation {
296 changeset,
297 pre_state,
298 };
299
300 stack.push_undo(operation);
301 }
302
303 #[must_use] pub fn can_undo(&self, node_id: NodeId) -> bool {
305 self.get_stack(node_id)
306 .is_some_and(NodeUndoRedoStack::can_undo)
307 }
308
309 #[must_use] pub fn can_redo(&self, node_id: NodeId) -> bool {
311 self.get_stack(node_id)
312 .is_some_and(NodeUndoRedoStack::can_redo)
313 }
314
315 #[must_use] pub fn peek_undo(&self, node_id: NodeId) -> Option<&UndoableOperation> {
319 self.get_stack(node_id).and_then(|s| s.peek_undo())
320 }
321
322 #[must_use] pub fn peek_redo(&self, node_id: NodeId) -> Option<&UndoableOperation> {
326 self.get_stack(node_id).and_then(|s| s.peek_redo())
327 }
328
329 pub fn pop_undo(&mut self, node_id: NodeId) -> Option<UndoableOperation> {
338 self.get_stack_mut(node_id)?.pop_undo()
339 }
340
341 pub fn pop_redo(&mut self, node_id: NodeId) -> Option<UndoableOperation> {
350 self.get_stack_mut(node_id)?.pop_redo()
351 }
352
353 pub fn push_redo(&mut self, operation: UndoableOperation) {
360 let node_id = operation
361 .changeset
362 .target
363 .node
364 .into_crate_internal()
365 .expect("TextChangeset target node should not be None");
366 let stack = self.get_or_create_stack_mut(node_id);
367 stack.push_redo(operation);
368 }
369
370 pub fn push_undo(&mut self, operation: UndoableOperation) {
377 let node_id = operation
378 .changeset
379 .target
380 .node
381 .into_crate_internal()
382 .expect("TextChangeset target node should not be None");
383 let stack = self.get_or_create_stack_mut(node_id);
384 stack.push_undo(operation);
385 }
386
387}
388
389impl crate::managers::NodeIdRemap for UndoRedoManager {
390 fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
403 let old_stacks = core::mem::take(&mut self.node_stacks);
404
405 for mut stack in old_stacks {
406 let stack_dom = stack
408 .undo_stack
409 .front()
410 .or_else(|| stack.redo_stack.front())
411 .map(|op| op.changeset.target.dom);
412
413 if stack_dom.is_some_and(|d| d != dom) {
414 self.node_stacks.push(stack);
416 continue;
417 }
418
419 let Some(new_node_id) = map.resolve(stack.node_id) else {
420 continue;
423 };
424
425 stack.node_id = new_node_id;
426 for op in stack.undo_stack.iter_mut().chain(stack.redo_stack.iter_mut()) {
427 remap_operation(op, dom, map, new_node_id);
428 }
429 self.node_stacks.push(stack);
430 }
431
432 let live: alloc::collections::BTreeSet<_> = self
434 .node_stacks
435 .iter()
436 .flat_map(|s| s.undo_stack.iter().chain(s.redo_stack.iter()))
437 .map(|op| op.changeset.id)
438 .collect();
439 self.content_snapshots.retain(|(id, _)| live.contains(id));
440 }
441}
442
443fn remap_operation(
445 op: &mut UndoableOperation,
446 dom: DomId,
447 map: &crate::managers::NodeIdMap,
448 new_node_id: NodeId,
449) {
450 use azul_core::styled_dom::NodeHierarchyItemId;
451 if op.changeset.target.dom == dom {
452 op.changeset.target.node = NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
453 }
454 if let Some(remapped) = map.resolve(op.pre_state.node_id) {
455 op.pre_state.node_id = remapped;
456 } else {
457 op.pre_state.node_id = new_node_id;
460 }
461}
462
463
464#[cfg(test)]
465mod undo_redo_tests {
466 use super::*;
467 use crate::managers::changeset::{TextChangeset, TextOpInsertText, TextOperation};
468 use azul_core::dom::{DomId, DomNodeId};
469 use azul_core::styled_dom::NodeHierarchyItemId;
470 use azul_core::task::SystemTick;
471 use azul_core::window::CursorPosition;
472
473 fn ts() -> Instant {
474 Instant::Tick(SystemTick { tick_counter: 0 })
475 }
476
477 fn op(id: usize, node: usize) -> UndoableOperation {
478 UndoableOperation {
479 changeset: TextChangeset {
480 id,
481 target: DomNodeId {
482 dom: DomId { inner: 0 },
483 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
484 },
485 operation: TextOperation::InsertText(TextOpInsertText {
486 text: "x".into(),
487 position: CursorPosition::Uninitialized,
488 new_cursor: CursorPosition::Uninitialized,
489 }),
490 timestamp: ts(),
491 },
492 pre_state: NodeStateSnapshot {
493 node_id: NodeId::new(node),
494 text_content: "".into(),
495 cursor_position: None.into(),
496 selection_range: None.into(),
497 timestamp: ts(),
498 },
499 }
500 }
501
502 #[test]
503 fn push_undo_clears_redo_but_reinstate_preserves_it() {
504 let mut stack = NodeUndoRedoStack::new(NodeId::new(1));
505 stack.push_redo(op(1, 1));
506 stack.push_redo(op(2, 1));
507 assert_eq!(stack.redo_stack.len(), 2);
508
509 stack.push_undo(op(3, 1));
511 assert_eq!(stack.redo_stack.len(), 0);
512
513 stack.push_redo(op(4, 1));
515 stack.push_redo(op(5, 1));
516 stack.push_undo_preserving_redo(op(6, 1));
517 assert_eq!(stack.redo_stack.len(), 2);
518 assert!(stack.can_undo());
519 }
520
521 #[test]
522 fn content_snapshots_replace_lookup_and_evict() {
523 let mut mgr = UndoRedoManager::new();
524 mgr.store_content_snapshot(7, Vec::new(), Vec::new());
525 assert!(mgr.get_content_snapshot(7).is_some());
526 assert!(mgr.get_content_snapshot(8).is_none());
527
528 mgr.store_content_snapshot(7, Vec::new(), Vec::new());
530 assert_eq!(mgr.content_snapshots.len(), 1);
531
532 for id in 100..(100 + MAX_CONTENT_SNAPSHOTS) {
534 mgr.store_content_snapshot(id, Vec::new(), Vec::new());
535 }
536 assert!(mgr.content_snapshots.len() <= MAX_CONTENT_SNAPSHOTS);
537 assert!(mgr.get_content_snapshot(7).is_none(), "oldest entry evicted");
538 assert!(mgr
539 .get_content_snapshot(100 + MAX_CONTENT_SNAPSHOTS - 1)
540 .is_some());
541 }
542
543 #[test]
544 fn reinstate_undo_keeps_manager_redo_stack() {
545 let mut mgr = UndoRedoManager::new();
546 mgr.record_operation(op(1, 3).changeset, op(1, 3).pre_state);
547 let popped = mgr.pop_undo(NodeId::new(3)).unwrap();
548 mgr.push_redo(popped);
549 let redone = mgr.pop_redo(NodeId::new(3)).unwrap();
550 mgr.push_redo(op(2, 3));
552 mgr.reinstate_undo(redone);
553 assert!(mgr.can_undo(NodeId::new(3)));
554 assert!(mgr.can_redo(NodeId::new(3)), "redo stack preserved");
555 }
556}
557
558#[cfg(test)]
559mod autotest_generated {
560 use azul_core::{
561 dom::DomNodeId,
562 geom::LogicalPosition,
563 selection::{CursorAffinity, GraphemeClusterId, TextCursor},
564 styled_dom::NodeHierarchyItemId,
565 task::SystemTick,
566 window::CursorPosition,
567 };
568
569 use super::*;
570 use crate::{
571 managers::{
572 changeset::{TextOpInsertText, TextOperation},
573 NodeIdMap, NodeIdRemap,
574 },
575 text3::cache::{InlineContent, InlineSpace},
576 };
577
578 fn tick(t: u64) -> Instant {
583 Instant::Tick(SystemTick { tick_counter: t })
584 }
585
586 fn cursor(run: u32, byte: u32) -> TextCursor {
587 TextCursor {
588 cluster_id: GraphemeClusterId {
589 source_run: run,
590 start_byte_in_run: byte,
591 },
592 affinity: CursorAffinity::Leading,
593 }
594 }
595
596 fn op_full(id: usize, dom: usize, node: usize, text: &str) -> UndoableOperation {
598 UndoableOperation {
599 changeset: TextChangeset {
600 id,
601 target: DomNodeId {
602 dom: DomId { inner: dom },
603 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node))),
604 },
605 operation: TextOperation::InsertText(TextOpInsertText {
606 text: text.into(),
607 position: CursorPosition::Uninitialized,
608 new_cursor: CursorPosition::Uninitialized,
609 }),
610 timestamp: tick(u64::from(id as u32)),
611 },
612 pre_state: NodeStateSnapshot {
613 node_id: NodeId::new(node),
614 text_content: text.into(),
615 cursor_position: Some(cursor(0, 0)).into(),
616 selection_range: None.into(),
617 timestamp: tick(u64::from(id as u32)),
618 },
619 }
620 }
621
622 fn op(id: usize, node: usize) -> UndoableOperation {
624 op_full(id, 0, node, "x")
625 }
626
627 fn op_with_none_target(id: usize) -> UndoableOperation {
630 let mut o = op(id, 0);
631 o.changeset.target.node = NodeHierarchyItemId::from_crate_internal(None);
632 o
633 }
634
635 fn text_of(o: &UndoableOperation) -> &str {
636 match &o.changeset.operation {
637 TextOperation::InsertText(i) => i.text.as_str(),
638 _ => panic!("helper only builds InsertText operations"),
639 }
640 }
641
642 fn target_node(o: &UndoableOperation) -> Option<NodeId> {
643 o.changeset.target.node.into_crate_internal()
644 }
645
646 fn space(width: f32) -> InlineContent {
647 InlineContent::Space(InlineSpace {
648 width,
649 is_breaking: false,
650 is_stretchy: false,
651 })
652 }
653
654 fn space_width(c: &InlineContent) -> f32 {
655 match c {
656 InlineContent::Space(s) => s.width,
657 _ => panic!("expected an InlineContent::Space"),
658 }
659 }
660
661 fn one(c: InlineContent) -> Vec<InlineContent> {
662 vec![c]
663 }
664
665 #[test]
670 fn stack_new_holds_invariants_for_extreme_node_ids() {
671 for node in [0usize, 7, usize::MAX - 1] {
674 let s = NodeUndoRedoStack::new(NodeId::new(node));
675 assert_eq!(s.node_id.index(), node);
676 assert!(s.undo_stack.is_empty());
677 assert!(s.redo_stack.is_empty());
678 assert!(!s.can_undo());
679 assert!(!s.can_redo());
680 assert!(s.peek_undo().is_none());
681 assert!(s.peek_redo().is_none());
682 assert!(s.undo_stack.capacity() >= MAX_UNDO_HISTORY);
683 assert!(s.redo_stack.capacity() >= MAX_REDO_HISTORY);
684 }
685 }
686
687 #[test]
688 fn stack_draining_an_empty_stack_never_panics() {
689 let mut s = NodeUndoRedoStack::new(NodeId::new(0));
690 for _ in 0..100 {
691 assert!(s.pop_undo().is_none());
692 assert!(s.pop_redo().is_none());
693 assert!(!s.can_undo());
694 assert!(!s.can_redo());
695 }
696 }
697
698 #[test]
703 fn undo_stack_saturates_at_max_history_and_evicts_oldest() {
704 let mut s = NodeUndoRedoStack::new(NodeId::new(1));
705 let total = MAX_UNDO_HISTORY * 3 + 7;
706 for id in 0..total {
707 s.push_undo(op(id, 1));
708 }
709 assert_eq!(s.undo_stack.len(), MAX_UNDO_HISTORY, "history is bounded");
710 assert_eq!(s.undo_stack.front().unwrap().changeset.id, total - MAX_UNDO_HISTORY);
712 assert_eq!(s.peek_undo().unwrap().changeset.id, total - 1);
713 }
714
715 #[test]
716 fn redo_stack_saturates_at_max_history_and_evicts_oldest() {
717 let mut s = NodeUndoRedoStack::new(NodeId::new(1));
718 let total = MAX_REDO_HISTORY * 2 + 3;
719 for id in 0..total {
720 s.push_redo(op(id, 1));
721 }
722 assert_eq!(s.redo_stack.len(), MAX_REDO_HISTORY);
723 assert_eq!(s.redo_stack.front().unwrap().changeset.id, total - MAX_REDO_HISTORY);
724 assert_eq!(s.peek_redo().unwrap().changeset.id, total - 1);
725 }
726
727 #[test]
728 fn push_undo_preserving_redo_is_bounded_too_and_leaves_redo_alone() {
729 let mut s = NodeUndoRedoStack::new(NodeId::new(1));
730 for id in 0..MAX_REDO_HISTORY {
731 s.push_redo(op(id, 1));
732 }
733 for id in 1000..(1000 + MAX_UNDO_HISTORY * 4) {
734 s.push_undo_preserving_redo(op(id, 1));
735 }
736 assert_eq!(s.undo_stack.len(), MAX_UNDO_HISTORY);
737 assert_eq!(
738 s.redo_stack.len(),
739 MAX_REDO_HISTORY,
740 "preserving variant must not touch the redo stack"
741 );
742 assert_eq!(s.peek_redo().unwrap().changeset.id, MAX_REDO_HISTORY - 1);
743 }
744
745 #[test]
746 fn push_undo_clears_a_full_redo_stack() {
747 let mut s = NodeUndoRedoStack::new(NodeId::new(1));
748 for id in 0..MAX_REDO_HISTORY {
749 s.push_redo(op(id, 1));
750 }
751 assert!(s.can_redo());
752 s.push_undo(op(999, 1));
753 assert!(!s.can_redo(), "a fresh edit invalidates the whole redo branch");
754 assert!(s.redo_stack.is_empty());
755 assert!(s.peek_redo().is_none());
756 assert!(s.can_undo());
757 }
758
759 #[test]
764 fn pop_undo_and_pop_redo_are_lifo_then_drain_to_none() {
765 let mut s = NodeUndoRedoStack::new(NodeId::new(2));
766 for id in 0..5 {
767 s.push_undo(op(id, 2));
768 }
769 for expected in (0..5).rev() {
770 assert_eq!(s.pop_undo().unwrap().changeset.id, expected);
771 }
772 assert!(s.pop_undo().is_none());
773
774 for id in 0..5 {
775 s.push_redo(op(id, 2));
776 }
777 for expected in (0..5).rev() {
778 assert_eq!(s.pop_redo().unwrap().changeset.id, expected);
779 }
780 assert!(s.pop_redo().is_none());
781 }
782
783 #[test]
784 fn peek_is_non_destructive_and_agrees_with_pop() {
785 let mut s = NodeUndoRedoStack::new(NodeId::new(2));
786 s.push_undo(op(1, 2));
787 s.push_undo(op(2, 2));
788 s.push_redo(op(3, 2));
789
790 for _ in 0..10 {
791 assert_eq!(s.peek_undo().unwrap().changeset.id, 2);
792 assert_eq!(s.peek_redo().unwrap().changeset.id, 3);
793 }
794 assert_eq!(s.undo_stack.len(), 2, "peek must not consume");
795 assert_eq!(s.redo_stack.len(), 1);
796 assert_eq!(s.pop_undo().unwrap().changeset.id, 2, "peek == next pop");
797 assert_eq!(s.pop_redo().unwrap().changeset.id, 3);
798 }
799
800 #[test]
801 fn can_undo_can_redo_track_emptiness_exactly() {
802 let mut s = NodeUndoRedoStack::new(NodeId::new(2));
803 assert!(!s.can_undo() && !s.can_redo());
804
805 s.push_undo(op(1, 2));
806 assert!(s.can_undo() && !s.can_redo());
807 assert_eq!(s.can_undo(), !s.undo_stack.is_empty());
808
809 s.push_redo(op(2, 2));
810 assert!(s.can_undo() && s.can_redo());
811
812 assert!(s.pop_undo().is_some());
813 assert!(!s.can_undo() && s.can_redo());
814 assert!(s.pop_redo().is_some());
815 assert!(!s.can_undo() && !s.can_redo());
816 }
817
818 #[test]
823 fn undo_redo_round_trip_preserves_unicode_payload_exactly() {
824 let nasty = "a\u{0301}👩\u{200D}👩\u{200D}👧\u{202E}rtl\u{0000}end\u{FFFD}";
827 let mut s = NodeUndoRedoStack::new(NodeId::new(4));
828 s.push_undo(op_full(usize::MAX, 0, 4, nasty));
829
830 let undone = s.pop_undo().unwrap();
831 assert_eq!(text_of(&undone), nasty);
832 assert_eq!(undone.pre_state.text_content.as_str(), nasty);
833 assert_eq!(undone.changeset.id, usize::MAX, "ChangesetId::MAX survives");
834
835 s.push_redo(undone);
836 let redone = s.pop_redo().unwrap();
837 assert_eq!(text_of(&redone), nasty, "encode == decode across undo→redo");
838 assert_eq!(redone.pre_state.text_content.as_str(), nasty);
839 assert_eq!(target_node(&redone), Some(NodeId::new(4)));
840 assert_eq!(redone.pre_state.node_id, NodeId::new(4));
841 }
842
843 #[test]
844 fn round_trip_preserves_huge_text_and_extreme_timestamps() {
845 let huge: AzString = "é".repeat(50_000).into();
846 assert_eq!(huge.as_str().len(), 100_000, "2 bytes per 'é'");
847
848 let mut o = op(1, 6);
849 o.pre_state.text_content = huge.clone();
850 o.pre_state.timestamp = tick(u64::MAX);
851 o.changeset.timestamp = tick(u64::MAX);
852
853 let mut s = NodeUndoRedoStack::new(NodeId::new(6));
854 s.push_undo(o);
855 let back = s.pop_undo().unwrap();
856 assert_eq!(back.pre_state.text_content.as_str().len(), 100_000);
857 assert_eq!(back.pre_state.text_content.as_str(), huge.as_str());
858 match back.pre_state.timestamp {
859 Instant::Tick(t) => assert_eq!(t.tick_counter, u64::MAX, "u64::MAX tick unclamped"),
860 Instant::System(_) => panic!("helper builds Tick instants"),
861 }
862 }
863
864 #[test]
865 fn round_trip_preserves_nan_and_infinite_cursor_coordinates() {
866 let mut o = op(1, 6);
867 o.changeset.operation = TextOperation::InsertText(TextOpInsertText {
868 text: "q".into(),
869 position: CursorPosition::InWindow(LogicalPosition {
870 x: f32::NAN,
871 y: f32::NEG_INFINITY,
872 }),
873 new_cursor: CursorPosition::OutOfWindow(LogicalPosition {
874 x: f32::INFINITY,
875 y: -0.0,
876 }),
877 });
878
879 let mut s = NodeUndoRedoStack::new(NodeId::new(6));
880 s.push_undo(o);
881 let back = s.pop_undo().unwrap();
882 match back.changeset.operation {
883 TextOperation::InsertText(i) => {
884 match i.position {
885 CursorPosition::InWindow(p) => {
886 assert!(p.x.is_nan(), "NaN must not be normalized away");
887 assert_eq!(p.y, f32::NEG_INFINITY);
888 }
889 _ => panic!("position variant changed across the stack"),
890 }
891 match i.new_cursor {
892 CursorPosition::OutOfWindow(p) => {
893 assert_eq!(p.x, f32::INFINITY);
894 assert!(
895 p.y.is_sign_negative(),
896 "-0.0 must keep its sign bit through the stack"
897 );
898 }
899 _ => panic!("new_cursor variant changed across the stack"),
900 }
901 }
902 _ => panic!("operation variant changed across the stack"),
903 }
904 }
905
906 #[test]
911 fn manager_new_and_default_are_empty_and_agree() {
912 let a = UndoRedoManager::new();
913 let b = UndoRedoManager::default();
914 for mgr in [&a, &b] {
915 assert!(mgr.node_stacks.is_empty());
916 assert!(mgr.content_snapshots.is_empty());
917 }
918 for node in [0usize, 1, usize::MAX - 1] {
919 assert!(!a.can_undo(NodeId::new(node)));
920 assert!(!a.can_redo(NodeId::new(node)));
921 assert!(a.get_stack(NodeId::new(node)).is_none());
922 assert!(a.peek_undo(NodeId::new(node)).is_none());
923 assert!(a.peek_redo(NodeId::new(node)).is_none());
924 }
925 assert!(a.get_content_snapshot(0).is_none());
926 assert!(a.get_content_snapshot(usize::MAX).is_none());
927 }
928
929 #[test]
930 fn manager_queries_on_unknown_node_return_none_without_allocating_a_stack() {
931 let mut mgr = UndoRedoManager::new();
932 for node in [0usize, 42, usize::MAX - 1] {
933 assert!(mgr.pop_undo(NodeId::new(node)).is_none());
934 assert!(mgr.pop_redo(NodeId::new(node)).is_none());
935 }
936 assert!(
937 mgr.node_stacks.is_empty(),
938 "read/pop paths must not silently create per-node stacks"
939 );
940 }
941
942 #[test]
943 fn get_or_create_stack_mut_is_idempotent_and_mutations_persist() {
944 let mut mgr = UndoRedoManager::new();
945 let boundary = NodeId::new(usize::MAX - 1);
946
947 mgr.get_or_create_stack_mut(NodeId::new(3)).push_undo(op(1, 3));
948 mgr.get_or_create_stack_mut(NodeId::new(3)).push_undo(op(2, 3));
950 assert_eq!(mgr.node_stacks.len(), 1);
951 assert_eq!(mgr.get_stack(NodeId::new(3)).unwrap().undo_stack.len(), 2);
952
953 mgr.get_or_create_stack_mut(boundary).push_undo(op(3, 0));
954 assert_eq!(mgr.node_stacks.len(), 2);
955 assert_eq!(mgr.get_or_create_stack_mut(boundary).node_id, boundary);
956 assert_eq!(mgr.node_stacks.len(), 2, "no duplicate for the boundary id");
957 assert!(mgr.can_undo(boundary));
958 }
959
960 #[test]
965 fn record_operation_is_bounded_and_isolated_per_node() {
966 let mut mgr = UndoRedoManager::new();
967 for id in 0..(MAX_UNDO_HISTORY * 2 + 5) {
968 let o = op(id, 1);
969 mgr.record_operation(o.changeset, o.pre_state);
970 }
971 for id in 500..503 {
972 let o = op(id, 2);
973 mgr.record_operation(o.changeset, o.pre_state);
974 }
975
976 assert_eq!(mgr.node_stacks.len(), 2);
977 assert_eq!(
978 mgr.get_stack(NodeId::new(1)).unwrap().undo_stack.len(),
979 MAX_UNDO_HISTORY
980 );
981 assert_eq!(mgr.get_stack(NodeId::new(2)).unwrap().undo_stack.len(), 3);
982
983 while mgr.pop_undo(NodeId::new(2)).is_some() {}
985 assert!(!mgr.can_undo(NodeId::new(2)));
986 assert!(mgr.can_undo(NodeId::new(1)));
987 assert_eq!(mgr.peek_undo(NodeId::new(1)).unwrap().changeset.id, MAX_UNDO_HISTORY * 2 + 4);
988 }
989
990 #[test]
991 fn manager_push_undo_clears_only_the_target_nodes_redo_stack() {
992 let mut mgr = UndoRedoManager::new();
993 mgr.push_redo(op(1, 1));
994 mgr.push_redo(op(2, 2));
995 assert!(mgr.can_redo(NodeId::new(1)) && mgr.can_redo(NodeId::new(2)));
996
997 mgr.push_undo(op(3, 1));
998 assert!(!mgr.can_redo(NodeId::new(1)), "fresh edit drops node 1's redo branch");
999 assert!(mgr.can_redo(NodeId::new(2)), "node 2 is an independent history");
1000 }
1001
1002 #[test]
1003 fn full_undo_then_redo_cycle_returns_the_same_operation() {
1004 let mut mgr = UndoRedoManager::new();
1005 let original = op_full(11, 0, 5, "hello");
1006 mgr.record_operation(original.changeset.clone(), original.pre_state.clone());
1007
1008 let undone = mgr.pop_undo(NodeId::new(5)).unwrap();
1010 assert_eq!(undone.changeset.id, 11);
1011 mgr.push_redo(undone);
1012 assert!(!mgr.can_undo(NodeId::new(5)));
1013 assert!(mgr.can_redo(NodeId::new(5)));
1014
1015 let redone = mgr.pop_redo(NodeId::new(5)).unwrap();
1017 assert_eq!(text_of(&redone), "hello", "payload survives undo→redo");
1018 mgr.reinstate_undo(redone);
1019 assert!(mgr.can_undo(NodeId::new(5)));
1020 assert!(!mgr.can_redo(NodeId::new(5)));
1021 assert_eq!(mgr.peek_undo(NodeId::new(5)).unwrap().changeset.id, 11);
1022 }
1023
1024 #[test]
1025 fn reinstate_undo_preserves_a_full_redo_stack() {
1026 let mut mgr = UndoRedoManager::new();
1027 for id in 0..MAX_REDO_HISTORY {
1028 mgr.push_redo(op(id, 9));
1029 }
1030 let redone = mgr.pop_redo(NodeId::new(9)).unwrap();
1031 mgr.reinstate_undo(redone);
1032 assert_eq!(
1033 mgr.get_stack(NodeId::new(9)).unwrap().redo_stack.len(),
1034 MAX_REDO_HISTORY - 1,
1035 "reinstating one redo must not wipe the remaining redo branch"
1036 );
1037 assert!(mgr.can_undo(NodeId::new(9)));
1038 }
1039
1040 #[test]
1045 #[should_panic(expected = "TextChangeset target node should not be None")]
1046 fn record_operation_panics_on_none_target() {
1047 let mut mgr = UndoRedoManager::new();
1048 let o = op_with_none_target(1);
1049 mgr.record_operation(o.changeset, o.pre_state);
1050 }
1051
1052 #[test]
1053 #[should_panic(expected = "TextChangeset target node should not be None")]
1054 fn manager_push_undo_panics_on_none_target() {
1055 UndoRedoManager::new().push_undo(op_with_none_target(1));
1056 }
1057
1058 #[test]
1059 #[should_panic(expected = "TextChangeset target node should not be None")]
1060 fn manager_push_redo_panics_on_none_target() {
1061 UndoRedoManager::new().push_redo(op_with_none_target(1));
1062 }
1063
1064 #[test]
1065 #[should_panic(expected = "TextChangeset target node should not be None")]
1066 fn reinstate_undo_panics_on_none_target() {
1067 UndoRedoManager::new().reinstate_undo(op_with_none_target(1));
1068 }
1069
1070 #[test]
1075 fn content_snapshot_does_not_swap_pre_and_post() {
1076 let mut mgr = UndoRedoManager::new();
1077 mgr.store_content_snapshot(3, one(space(1.0)), one(space(2.0)));
1078 let snap = mgr.get_content_snapshot(3).expect("stored id must be found");
1079 assert_eq!(space_width(&snap.pre[0]), 1.0, "pre must stay pre");
1080 assert_eq!(space_width(&snap.post[0]), 2.0, "post must stay post");
1081 }
1082
1083 #[test]
1084 fn content_snapshot_preserves_nan_and_infinite_widths() {
1085 let mut mgr = UndoRedoManager::new();
1086 mgr.store_content_snapshot(1, one(space(f32::NAN)), one(space(f32::INFINITY)));
1087 let snap = mgr.get_content_snapshot(1).unwrap();
1088 assert!(space_width(&snap.pre[0]).is_nan(), "NaN width round-trips");
1089 assert_eq!(space_width(&snap.post[0]), f32::INFINITY);
1090 }
1091
1092 #[test]
1093 fn content_snapshot_boundary_ids_and_misses() {
1094 let mut mgr = UndoRedoManager::new();
1095 mgr.store_content_snapshot(0, Vec::new(), Vec::new());
1096 mgr.store_content_snapshot(usize::MAX, one(space(0.0)), Vec::new());
1097
1098 assert!(mgr.get_content_snapshot(0).is_some(), "id 0 is a real key, not a sentinel");
1099 assert!(mgr.get_content_snapshot(usize::MAX).is_some());
1100 assert!(mgr.get_content_snapshot(1).is_none());
1101 assert!(mgr.get_content_snapshot(usize::MAX - 1).is_none());
1102 assert_eq!(mgr.content_snapshots.len(), 2);
1103 }
1104
1105 #[test]
1106 fn storing_the_same_id_replaces_the_entry_and_refreshes_its_recency() {
1107 let mut mgr = UndoRedoManager::new();
1108 for id in 0..MAX_CONTENT_SNAPSHOTS {
1109 mgr.store_content_snapshot(id, one(space(0.0)), Vec::new());
1110 }
1111 assert_eq!(mgr.content_snapshots.len(), MAX_CONTENT_SNAPSHOTS);
1112
1113 mgr.store_content_snapshot(0, one(space(42.0)), Vec::new());
1116 assert_eq!(mgr.content_snapshots.len(), MAX_CONTENT_SNAPSHOTS, "no duplicate key");
1117 assert_eq!(space_width(&mgr.get_content_snapshot(0).unwrap().pre[0]), 42.0);
1118
1119 mgr.store_content_snapshot(9_999, Vec::new(), Vec::new());
1121 assert_eq!(mgr.content_snapshots.len(), MAX_CONTENT_SNAPSHOTS, "cap holds");
1122 assert!(mgr.get_content_snapshot(0).is_some(), "refreshed entry survived");
1123 assert!(mgr.get_content_snapshot(1).is_none(), "second-oldest evicted");
1124 assert!(mgr.get_content_snapshot(9_999).is_some());
1125 }
1126
1127 #[test]
1128 fn content_snapshot_table_never_exceeds_its_cap_under_heavy_churn() {
1129 let mut mgr = UndoRedoManager::new();
1130 for id in 0..(MAX_CONTENT_SNAPSHOTS * 10) {
1131 mgr.store_content_snapshot(id, one(space(id as f32)), one(space(0.0)));
1132 assert!(mgr.content_snapshots.len() <= MAX_CONTENT_SNAPSHOTS);
1133 }
1134 assert_eq!(mgr.content_snapshots.len(), MAX_CONTENT_SNAPSHOTS);
1135 let newest = MAX_CONTENT_SNAPSHOTS * 10 - 1;
1137 assert!(mgr.get_content_snapshot(newest).is_some());
1138 assert!(mgr.get_content_snapshot(newest - MAX_CONTENT_SNAPSHOTS).is_none());
1139 }
1140
1141 #[test]
1146 fn remap_shifts_stack_and_every_embedded_node_id() {
1147 let mut mgr = UndoRedoManager::new();
1148 let o = op_full(1, 0, 3, "a");
1149 mgr.record_operation(o.changeset, o.pre_state);
1150 mgr.push_redo(op_full(2, 0, 3, "b"));
1151 mgr.store_content_snapshot(1, one(space(1.0)), Vec::new());
1152
1153 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(2))]);
1155 mgr.remap_node_ids(DomId { inner: 0 }, &map);
1156
1157 assert!(mgr.get_stack(NodeId::new(3)).is_none(), "old key must be gone");
1158 let stack = mgr.get_stack(NodeId::new(2)).expect("stack re-keyed to the new id");
1159 assert_eq!(stack.node_id, NodeId::new(2));
1160
1161 let undo = stack.peek_undo().unwrap();
1162 assert_eq!(target_node(undo), Some(NodeId::new(2)), "changeset target remapped");
1163 assert_eq!(undo.pre_state.node_id, NodeId::new(2), "snapshot node remapped");
1164 let redo = stack.peek_redo().unwrap();
1165 assert_eq!(target_node(redo), Some(NodeId::new(2)), "redo stack remapped too");
1166 assert_eq!(redo.pre_state.node_id, NodeId::new(2));
1167
1168 assert!(
1169 mgr.get_content_snapshot(1).is_some(),
1170 "snapshot of a still-live changeset must survive the GC"
1171 );
1172 }
1173
1174 #[test]
1175 fn remap_drops_unmounted_history_and_gcs_its_snapshots() {
1176 let mut mgr = UndoRedoManager::new();
1177 let o = op_full(1, 0, 3, "a");
1178 mgr.record_operation(o.changeset, o.pre_state);
1179 mgr.store_content_snapshot(1, one(space(1.0)), Vec::new());
1180
1181 let map = NodeIdMap::from_pairs([(NodeId::new(0), NodeId::new(0))]);
1183 mgr.remap_node_ids(DomId { inner: 0 }, &map);
1184
1185 assert!(mgr.node_stacks.is_empty(), "history of an unmounted node is dropped");
1186 assert!(!mgr.can_undo(NodeId::new(3)));
1187 assert!(!mgr.can_undo(NodeId::new(2)), "history must NOT re-attach to a neighbour");
1188 assert!(
1189 mgr.content_snapshots.is_empty(),
1190 "snapshots of dropped changesets are GC'd"
1191 );
1192 }
1193
1194 #[test]
1195 fn remap_leaves_stacks_belonging_to_another_dom_untouched() {
1196 let mut mgr = UndoRedoManager::new();
1197 let o = op_full(1, 1, 3, "a");
1199 mgr.record_operation(o.changeset, o.pre_state);
1200 mgr.store_content_snapshot(1, one(space(1.0)), Vec::new());
1201
1202 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(2))]);
1203 mgr.remap_node_ids(DomId { inner: 0 }, &map);
1204
1205 let stack = mgr
1206 .get_stack(NodeId::new(3))
1207 .expect("a foreign DOM's stack keeps its key");
1208 assert_eq!(stack.node_id, NodeId::new(3));
1209 let undo = stack.peek_undo().unwrap();
1210 assert_eq!(target_node(undo), Some(NodeId::new(3)), "foreign target untouched");
1211 assert_eq!(undo.changeset.target.dom, DomId { inner: 1 });
1212 assert_eq!(undo.pre_state.node_id, NodeId::new(3));
1213 assert!(mgr.get_content_snapshot(1).is_some());
1214 }
1215
1216 #[test]
1217 fn remap_of_an_empty_stack_follows_the_map_or_drops_it() {
1218 let mut mapped = UndoRedoManager::new();
1221 mapped.get_or_create_stack_mut(NodeId::new(7));
1222 mapped.remap_node_ids(
1223 DomId { inner: 0 },
1224 &NodeIdMap::from_pairs([(NodeId::new(7), NodeId::new(9))]),
1225 );
1226 assert!(mapped.get_stack(NodeId::new(9)).is_some());
1227 assert!(mapped.get_stack(NodeId::new(7)).is_none());
1228
1229 let mut dropped = UndoRedoManager::new();
1230 dropped.get_or_create_stack_mut(NodeId::new(7));
1231 dropped.remap_node_ids(DomId { inner: 0 }, &NodeIdMap::from_pairs([]));
1232 assert!(dropped.node_stacks.is_empty());
1233 }
1234
1235 #[test]
1236 fn remap_with_an_empty_map_drops_every_stack_of_that_dom() {
1237 let mut mgr = UndoRedoManager::new();
1238 for node in 0..5 {
1239 let o = op_full(node, 0, node, "a");
1240 mgr.record_operation(o.changeset, o.pre_state);
1241 }
1242 assert_eq!(mgr.node_stacks.len(), 5);
1243
1244 mgr.remap_node_ids(DomId { inner: 0 }, &NodeIdMap::from_pairs([]));
1245 assert!(mgr.node_stacks.is_empty(), "nothing survived the rebuild");
1246 assert!(mgr.content_snapshots.is_empty());
1247 }
1248
1249 #[test]
1250 fn remap_is_idempotent_when_the_map_is_the_identity() {
1251 let mut mgr = UndoRedoManager::new();
1252 let o = op_full(1, 0, 3, "a");
1253 mgr.record_operation(o.changeset, o.pre_state);
1254
1255 let identity = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(3))]);
1256 for _ in 0..3 {
1257 mgr.remap_node_ids(DomId { inner: 0 }, &identity);
1258 }
1259 assert_eq!(mgr.node_stacks.len(), 1);
1260 let stack = mgr.get_stack(NodeId::new(3)).unwrap();
1261 assert_eq!(stack.undo_stack.len(), 1, "repeated remaps must not duplicate ops");
1262 assert_eq!(target_node(stack.peek_undo().unwrap()), Some(NodeId::new(3)));
1263 }
1264
1265 #[test]
1266 fn remap_gcs_a_snapshot_whose_changeset_was_never_recorded() {
1267 let mut mgr = UndoRedoManager::new();
1271 mgr.store_content_snapshot(77, one(space(1.0)), Vec::new());
1272 assert!(mgr.get_content_snapshot(77).is_some());
1273
1274 mgr.remap_node_ids(DomId { inner: 0 }, &NodeIdMap::from_pairs([]));
1275 assert!(mgr.get_content_snapshot(77).is_none());
1276 }
1277
1278 #[test]
1283 fn remap_operation_rewrites_target_and_snapshot_for_the_matching_dom() {
1284 let mut o = op_full(1, 0, 3, "a");
1285 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(2))]);
1286 remap_operation(&mut o, DomId { inner: 0 }, &map, NodeId::new(2));
1287
1288 assert_eq!(target_node(&o), Some(NodeId::new(2)));
1289 assert_eq!(o.pre_state.node_id, NodeId::new(2));
1290 }
1291
1292 #[test]
1293 fn remap_operation_leaves_a_foreign_doms_target_alone() {
1294 let mut o = op_full(1, 1, 3, "a");
1295 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(2))]);
1296 remap_operation(&mut o, DomId { inner: 0 }, &map, NodeId::new(2));
1297
1298 assert_eq!(
1299 target_node(&o),
1300 Some(NodeId::new(3)),
1301 "target in DOM 1 must not be rewritten by a DOM 0 reconciliation"
1302 );
1303 assert_eq!(o.changeset.target.dom, DomId { inner: 1 });
1304 assert_eq!(o.pre_state.node_id, NodeId::new(2));
1306 }
1307
1308 #[test]
1309 fn remap_operation_falls_back_to_the_stack_node_for_a_vanished_snapshot() {
1310 let mut o = op_full(1, 0, 3, "a");
1311 o.pre_state.node_id = NodeId::new(88); let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(2))]);
1313 remap_operation(&mut o, DomId { inner: 0 }, &map, NodeId::new(2));
1314
1315 assert_eq!(target_node(&o), Some(NodeId::new(2)));
1316 assert_eq!(
1317 o.pre_state.node_id,
1318 NodeId::new(2),
1319 "unresolvable snapshot node is pinned to the surviving stack node"
1320 );
1321 }
1322
1323 #[test]
1324 fn remap_operation_handles_the_largest_encodable_node_id() {
1325 let boundary = NodeId::new(usize::MAX - 1);
1328 let mut o = op_full(1, 0, 3, "a");
1329 let map = NodeIdMap::from_pairs([(NodeId::new(3), boundary)]);
1330 remap_operation(&mut o, DomId { inner: 0 }, &map, boundary);
1331
1332 assert_eq!(target_node(&o), Some(boundary), "encode/decode is lossless at the boundary");
1333 assert_eq!(o.pre_state.node_id, boundary);
1334 assert_eq!(target_node(&o).unwrap().index(), usize::MAX - 1);
1335 }
1336
1337 #[test]
1338 fn remap_operation_is_idempotent() {
1339 let mut o = op_full(1, 0, 3, "a");
1340 let map = NodeIdMap::from_pairs([
1341 (NodeId::new(3), NodeId::new(2)),
1342 (NodeId::new(2), NodeId::new(2)),
1343 ]);
1344 remap_operation(&mut o, DomId { inner: 0 }, &map, NodeId::new(2));
1345 remap_operation(&mut o, DomId { inner: 0 }, &map, NodeId::new(2));
1346
1347 assert_eq!(target_node(&o), Some(NodeId::new(2)), "no double-shift");
1348 assert_eq!(o.pre_state.node_id, NodeId::new(2));
1349 }
1350}