// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod changes; use std::collections::VecDeque; pub(crate) use changes::UndoableChange; pub use crate::ops::Op; use crate::{ ops::{OpChanges, StateChanges}, prelude::*, }; const UNDO_LIMIT: usize = 30; #[derive(Debug)] pub(crate) struct UndoableOp { pub kind: Op, pub timestamp: TimestampSecs, pub changes: Vec, pub counter: usize, } impl UndoableOp { /// True if changes non-empty, or a custom undo step. fn has_changes(&self) -> bool { !self.changes.is_empty() || matches!(self.kind, Op::Custom(_)) } } #[derive(Debug, PartialEq)] enum UndoMode { NormalOp, Undoing, Redoing, } impl Default for UndoMode { fn default() -> Self { Self::NormalOp } } pub struct UndoStatus { pub undo: Option, pub redo: Option, pub last_step: usize, } pub struct UndoOutput { pub undone_op: Op, pub reverted_to: TimestampSecs, pub new_undo_status: UndoStatus, pub counter: usize, } #[derive(Debug, Default)] pub(crate) struct UndoManager { // undo steps are added to the front of a double-ended queue, so we can // efficiently cap the number of steps we retain in memory undo_steps: VecDeque, // redo steps are added to the end redo_steps: Vec, mode: UndoMode, current_step: Option, counter: usize, } impl UndoManager { fn save(&mut self, item: UndoableChange) { if let Some(step) = self.current_step.as_mut() { step.changes.push(item) } } fn begin_step(&mut self, op: Option) { if op.is_none() { self.undo_steps.clear(); self.redo_steps.clear(); } else if self.mode == UndoMode::NormalOp { // a normal op clears the redo queue self.redo_steps.clear(); } self.current_step = op.map(|op| UndoableOp { kind: op, timestamp: TimestampSecs::now(), changes: vec![], counter: { self.counter += 1; self.counter }, }); } fn end_step(&mut self, skip_undo: bool) { if let Some(step) = self.current_step.take() { if step.has_changes() && !skip_undo { if self.mode == UndoMode::Undoing { self.redo_steps.push(step); } else { self.undo_steps.truncate(UNDO_LIMIT - 1); self.undo_steps.push_front(step); } } } } fn can_undo(&self) -> Option<&Op> { self.undo_steps.front().map(|s| &s.kind) } fn can_redo(&self) -> Option<&Op> { self.redo_steps.last().map(|s| &s.kind) } fn previous_op(&self) -> Option<&UndoableOp> { self.undo_steps.front() } fn current_op(&self) -> Option<&UndoableOp> { self.current_step.as_ref() } fn op_changes(&self) -> OpChanges { let current_op = self .current_step .as_ref() .expect("current_changes() called when no op set"); let changes = StateChanges::from(¤t_op.changes[..]); OpChanges { op: current_op.kind.clone(), changes, } } fn merge_undoable_ops(&mut self, starting_from: usize) -> Result { let target_idx = self .undo_steps .iter() .enumerate() .filter_map(|(idx, op)| { if op.counter == starting_from { Some(idx) } else { None } }) .next() .ok_or_else(|| AnkiError::invalid_input("target undo op not found"))?; let mut removed = vec![]; for _ in 0..target_idx { removed.push(self.undo_steps.pop_front().unwrap()); } let target = self.undo_steps.front_mut().unwrap(); for step in removed.into_iter().rev() { target.changes.extend(step.changes.into_iter()); } Ok(OpChanges { op: target.kind.clone(), changes: StateChanges::from(&target.changes[..]), }) } /// Start a new step with a custom name, and return its associated /// counter value, which can be used with `merge_undoable_ops`. fn add_custom_step(&mut self, name: String) -> usize { self.begin_step(Some(Op::Custom(name))); self.end_step(false); self.counter } } impl Collection { pub fn can_undo(&self) -> Option<&Op> { self.state.undo.can_undo() } pub fn can_redo(&self) -> Option<&Op> { self.state.undo.can_redo() } pub fn undo(&mut self) -> Result> { if let Some(step) = self.state.undo.undo_steps.pop_front() { self.undo_inner(step, UndoMode::Undoing) } else { Err(AnkiError::UndoEmpty) } } pub fn redo(&mut self) -> Result> { if let Some(step) = self.state.undo.redo_steps.pop() { self.undo_inner(step, UndoMode::Redoing) } else { Err(AnkiError::UndoEmpty) } } pub fn undo_status(&self) -> UndoStatus { UndoStatus { undo: self.can_undo().cloned(), redo: self.can_redo().cloned(), last_step: self.state.undo.counter, } } /// Merge multiple undoable operations into one, and return the union of /// their changes. pub fn merge_undoable_ops(&mut self, starting_from: usize) -> Result { self.state.undo.merge_undoable_ops(starting_from) } /// Add an empty custom undo step, which subsequent changes can be merged into. pub fn add_custom_undo_step(&mut self, name: String) -> usize { self.state.undo.add_custom_step(name) } } impl Collection { /// If op is None, clears the undo/redo queues. pub(crate) fn begin_undoable_operation(&mut self, op: Option) { self.state.undo.begin_step(op); } /// Called at the end of a successful transaction. /// In most instances, this will also clear the study queues. pub(crate) fn end_undoable_operation(&mut self, skip_undo: bool) { self.state.undo.end_step(skip_undo); } pub(crate) fn discard_undo_and_study_queues(&mut self) { self.state.undo.begin_step(None); self.clear_study_queues(); } pub(crate) fn update_state_after_dbproxy_modification(&mut self) { self.discard_undo_and_study_queues(); self.state.modified_by_dbproxy = true; } #[inline] pub(crate) fn save_undo(&mut self, item: impl Into) { self.state.undo.save(item.into()); } pub(crate) fn current_undo_op(&self) -> Option<&UndoableOp> { self.state.undo.current_op() } pub(crate) fn previous_undo_op(&self) -> Option<&UndoableOp> { self.state.undo.previous_op() } pub(crate) fn undoing_or_redoing(&self) -> bool { self.state.undo.mode != UndoMode::NormalOp } pub(crate) fn current_undo_step_has_changes(&self) -> bool { self.state .undo .current_op() .map(|op| op.has_changes()) .unwrap_or_default() } /// Used for coalescing successive note updates. pub(crate) fn pop_last_change(&mut self) -> Option { self.state .undo .current_step .as_mut() .expect("no operation active") .changes .pop() } /// Return changes made by the current op. Must only be called in a transaction, /// when an operation was passed to transact(). pub(crate) fn op_changes(&self) -> OpChanges { self.state.undo.op_changes() } fn undo_inner(&mut self, step: UndoableOp, mode: UndoMode) -> Result> { let undone_op = step.kind; let reverted_to = step.timestamp; let changes = step.changes; let counter = step.counter; self.state.undo.mode = mode; let res = self.transact(undone_op.clone(), |col| { for change in changes.into_iter().rev() { change.undo(col)?; } Ok(UndoOutput { undone_op, reverted_to, new_undo_status: col.undo_status(), counter, }) }); self.state.undo.mode = UndoMode::NormalOp; res } } impl From<&[UndoableChange]> for StateChanges { fn from(changes: &[UndoableChange]) -> Self { let mut out = StateChanges::default(); if !changes.is_empty() { out.mtime = true; } for change in changes { match change { UndoableChange::Card(_) => out.card = true, UndoableChange::Note(_) => out.note = true, UndoableChange::Deck(_) => out.deck = true, UndoableChange::Tag(_) => out.tag = true, UndoableChange::Revlog(_) => {} UndoableChange::Queue(_) => {} UndoableChange::Config(_) => out.config = true, UndoableChange::DeckConfig(_) => out.deck_config = true, UndoableChange::Collection(_) => {} UndoableChange::Notetype(_) => out.notetype = true, } } out } } #[cfg(test)] mod test { use super::UndoableChange; use crate::{card::Card, collection::open_test_collection, prelude::*}; #[test] fn undo() -> Result<()> { let mut col = open_test_collection(); let mut card = Card { interval: 1, ..Default::default() }; col.add_card(&mut card).unwrap(); let cid = card.id; assert_eq!(col.can_undo(), None); assert_eq!(col.can_redo(), None); // outside of a transaction, no undo info recorded let card = col .get_and_update_card(cid, |card| { card.interval = 2; Ok(()) }) .unwrap(); assert_eq!(card.interval, 2); assert_eq!(col.can_undo(), None); assert_eq!(col.can_redo(), None); // record a few undo steps for i in 3..=4 { col.transact(Op::UpdateCard, |col| { col.get_and_update_card(cid, |card| { card.interval = i; Ok(()) }) .unwrap(); Ok(()) }) .unwrap(); } assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), None); // undo a step col.undo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), Some(&Op::UpdateCard)); // and again col.undo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 2); assert_eq!(col.can_undo(), None); assert_eq!(col.can_redo(), Some(&Op::UpdateCard)); // redo a step col.redo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), Some(&Op::UpdateCard)); // and another col.redo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), None); // and undo the redo col.undo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), Some(&Op::UpdateCard)); // if any action is performed, it should clear the redo queue col.transact(Op::UpdateCard, |col| { col.get_and_update_card(cid, |card| { card.interval = 5; Ok(()) }) })?; assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 5); assert_eq!(col.can_undo(), Some(&Op::UpdateCard)); assert_eq!(col.can_redo(), None); // and any action that doesn't support undoing will clear both queues col.transact_no_undo(|_col| Ok(())).unwrap(); assert_eq!(col.can_undo(), None); assert_eq!(col.can_redo(), None); // if an object is mutated multiple times in one operation, // the changes should be undone in the correct order col.transact(Op::UpdateCard, |col| { col.get_and_update_card(cid, |card| { card.interval = 10; Ok(()) })?; col.get_and_update_card(cid, |card| { card.interval = 15; Ok(()) }) })?; assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 15); col.undo()?; assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 5); Ok(()) } #[test] fn custom() -> Result<()> { let mut col = open_test_collection(); // perform some actions in separate steps let nt = col.get_notetype_by_name("Basic")?.unwrap(); let mut note = nt.new_note(); col.add_note(&mut note, DeckId(1))?; assert_eq!(col.undo_status().last_step, 1); let card = col.storage.all_cards_of_note(note.id)?.remove(0); col.transact(Op::UpdateCard, |col| { col.get_and_update_card(card.id, |card| { card.due = 10; Ok(()) }) })?; let restore_point = col.add_custom_undo_step("hello".to_string()); col.transact(Op::UpdateCard, |col| { col.get_and_update_card(card.id, |card| { card.due = 20; Ok(()) }) })?; col.transact(Op::UpdateCard, |col| { col.get_and_update_card(card.id, |card| { card.due = 30; Ok(()) }) })?; // dummy op name col.transact(Op::Bury, |col| col.set_current_notetype_id(NotetypeId(123)))?; // merge subsequent changes into our restore point let op = col.merge_undoable_ops(restore_point)?; assert!(op.changes.card); assert!(op.changes.config); // the last undo action should be at the end of the step list, // before the modtime bump assert!(matches!( col.state .undo .previous_op() .unwrap() .changes .iter() .rev() .nth(1) .unwrap(), UndoableChange::Config(_) )); // if we then undo, we'll be back to before step 3 assert_eq!(col.storage.get_card(card.id)?.unwrap().due, 30); col.undo()?; assert_eq!(col.storage.get_card(card.id)?.unwrap().due, 10); Ok(()) } #[test] fn undo_mtime_bump() -> Result<()> { let mut col = open_test_collection(); col.storage.db.execute_batch("update col set mod = 0")?; // a no-op change should not bump mtime let out = col.set_config_bool(BoolKey::AddingDefaultsToCurrentDeck, true, true)?; assert_eq!( col.storage.get_collection_timestamps()?.collection_change.0, 0 ); assert!(!out.changes.had_change()); // if there is an undoable step, mtime should change let out = col.set_config_bool(BoolKey::AddingDefaultsToCurrentDeck, false, true)?; assert_ne!( col.storage.get_collection_timestamps()?.collection_change.0, 0 ); assert!(out.changes.had_change()); // when skipping undo, mtime should still only be bumped on a change col.storage.db.execute_batch("update col set mod = 0")?; let out = col.set_config_bool(BoolKey::AddingDefaultsToCurrentDeck, false, false)?; assert_eq!( col.storage.get_collection_timestamps()?.collection_change.0, 0 ); assert!(!out.changes.had_change()); // op output will reflect changes were made let out = col.set_config_bool(BoolKey::AddingDefaultsToCurrentDeck, true, false)?; assert_ne!( col.storage.get_collection_timestamps()?.collection_change.0, 0 ); assert!(out.changes.had_change()); Ok(()) } }