//! Undo/redo history (MJB-HLR-011). //! //! Each committed edit stores the pair (forward transaction, inverse //! transaction). `cursor` is the number of entries currently *applied*, so //! entries at or beyond it have been reverted and are available to redo. //! //! Undo at the start and redo at the end are no-ops, not errors — reaching //! either end is ordinary use, not a fault (MJB-LLR-052, MJB-LLR-053). use super::transaction::Transaction; #[derive(Debug, Clone)] struct Entry { forward: Transaction, inverse: Transaction, /// Identifies the buffer state this entry produces. Never reused. id: usize, } #[derive(Debug, Clone, Default)] pub struct History { entries: Vec, /// Number of entries applied; also the index of the next redo. cursor: usize, /// Monotonic source of entry ids. /// /// Deliberately *not* the same thing as `cursor`. Using stack depth to /// identify a state is wrong: saving at depth 3, undoing, then making a /// different edit returns to depth 3 while the content differs, so a /// depth comparison would report the buffer clean when it is not. next_id: usize, } impl History { pub fn new() -> Self { Self::default() } /// MJB-LLR-051: record an applied edit. /// /// Anything previously undone is discarded — committing a new edit after an /// undo abandons the branch that was reverted. pub fn commit(&mut self, forward: Transaction, inverse: Transaction) { self.entries.truncate(self.cursor); self.next_id += 1; self.entries.push(Entry { forward, inverse, id: self.next_id, }); self.cursor = self.entries.len(); } /// MJB-LLR-052: the transaction that reverts the most recent edit, or /// `None` when nothing remains to undo. pub fn undo(&mut self) -> Option<&Transaction> { if self.cursor == 0 { return None; } self.cursor -= 1; Some(&self.entries[self.cursor].inverse) } /// MJB-LLR-053: the transaction that reapplies the most recently undone /// edit, or `None` when nothing remains to redo. pub fn redo(&mut self) -> Option<&Transaction> { if self.cursor >= self.entries.len() { return None; } let t = &self.entries[self.cursor].forward; self.cursor += 1; Some(t) } /// Identifies the current buffer state. /// /// Zero means pristine — no edit applied. Otherwise it is the id of the /// most recently applied entry. Two calls return the same value exactly /// when the buffer content is the same, which is what lets "modified" be /// *derived* rather than latched; see /// [`super::document::Document::is_modified`]. pub fn revision(&self) -> usize { match self.cursor { 0 => 0, n => self.entries[n - 1].id, } } pub fn can_undo(&self) -> bool { self.cursor > 0 } pub fn can_redo(&self) -> bool { self.cursor < self.entries.len() } pub fn len(&self) -> usize { self.entries.len() } pub fn is_empty(&self) -> bool { self.entries.is_empty() } } #[cfg(test)] mod tests { use ropey::Rope; use super::*; use crate::buffer::transaction::Transaction; fn edit(text: &str, from: usize, to: usize, ins: Option<&str>) -> (Transaction, Transaction) { let r = Rope::from_str(text); let t = Transaction::change(&r, [(from, to, ins.map(str::to_owned))]); let inv = Transaction::new(t.changes.invert(&r)); (t, inv) } #[test] fn mjb_llr_052_undo_past_start_is_a_noop() { let mut h = History::new(); assert!(h.undo().is_none()); assert!(!h.can_undo()); // Repeated attempts must stay safe, not underflow the cursor. assert!(h.undo().is_none()); assert!(h.undo().is_none()); } #[test] fn mjb_llr_053_redo_past_end_is_a_noop() { let mut h = History::new(); let (f, i) = edit("abc", 0, 1, None); h.commit(f, i); assert!(h.redo().is_none(), "nothing has been undone yet"); assert!(!h.can_redo()); } #[test] fn mjb_llr_051_commit_then_undo_then_redo() { let mut h = History::new(); let (f, i) = edit("abc", 0, 1, None); h.commit(f, i); assert!(h.can_undo()); assert!(h.undo().is_some()); assert!(!h.can_undo()); assert!(h.can_redo()); assert!(h.redo().is_some()); assert!(!h.can_redo()); } #[test] fn mjb_llr_051_commit_after_undo_discards_the_redo_branch() { let mut h = History::new(); let (f1, i1) = edit("abc", 0, 1, None); let (f2, i2) = edit("bc", 0, 1, None); h.commit(f1, i1); h.commit(f2, i2); h.undo(); assert!(h.can_redo()); let (f3, i3) = edit("bc", 1, 2, None); h.commit(f3, i3); assert!(!h.can_redo(), "the undone branch must be discarded"); assert_eq!(h.len(), 2); } /// MJB-LLR-116: a revision identifies *content*, not stack depth. /// /// Regression guard for the trap this replaced: save at depth 2, undo, then /// make a different edit. The cursor returns to 2, but the buffer no longer /// matches what was saved, so the revision must differ. #[test] fn mjb_llr_116_revision_is_not_stack_depth() { let mut h = History::new(); let (f1, i1) = edit("abcdef", 0, 1, None); let (f2, i2) = edit("bcdef", 0, 1, None); h.commit(f1, i1); h.commit(f2, i2); let saved = h.revision(); h.undo(); assert_ne!(h.revision(), saved, "undo leaves a different state"); // A different second edit, landing at the same stack depth. let (f3, i3) = edit("bcdef", 1, 2, None); h.commit(f3, i3); assert_eq!(h.len(), 2, "same depth as when we saved"); assert_ne!( h.revision(), saved, "same depth, different content: must not look saved" ); } #[test] fn mjb_llr_116_revision_is_zero_when_pristine_and_returns_on_undo() { let mut h = History::new(); assert_eq!(h.revision(), 0); let (f, i) = edit("abc", 0, 1, None); h.commit(f, i); let after = h.revision(); assert_ne!(after, 0); h.undo(); assert_eq!(h.revision(), 0, "undoing to pristine returns to revision 0"); h.redo(); assert_eq!(h.revision(), after, "redo restores the same revision"); } #[test] fn mjb_llr_052_undo_walks_back_in_order() { let mut h = History::new(); for n in 0..3 { let (f, i) = edit("abcdef", n, n + 1, None); h.commit(f, i); } assert_eq!(h.len(), 3); for _ in 0..3 { assert!(h.undo().is_some()); } assert!(h.undo().is_none(), "exhausted"); } }