diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 068a3fbe0..b4efbc3c3 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -50,7 +50,7 @@ use crate::{ text::{escape_anki_wildcards, extract_av_tags, sanitize_html_no_images, strip_av_tags, AVTag}, timestamp::TimestampSecs, types::Usn, - undo::UndoableOp, + undo::UndoableOpKind, }; use fluent::FluentValue; use futures::future::{AbortHandle, AbortRegistration, Abortable}; @@ -1049,7 +1049,7 @@ impl BackendService for Backend { let op = if input.skip_undo_entry { None } else { - Some(UndoableOp::UpdateNote) + Some(UndoableOpKind::UpdateNote) }; let mut note: Note = input.note.ok_or(AnkiError::NotFound)?.into(); col.update_note_with_op(&mut note, op) diff --git a/rslib/src/card/mod.rs b/rslib/src/card/mod.rs index bb1781f7a..977461767 100644 --- a/rslib/src/card/mod.rs +++ b/rslib/src/card/mod.rs @@ -1,14 +1,13 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -mod undo; +pub(crate) mod undo; use crate::define_newtype; use crate::err::{AnkiError, Result}; use crate::notes::NoteID; use crate::{ collection::Collection, config::SchedulerVersion, timestamp::TimestampSecs, types::Usn, - undo::Undo, }; use crate::{deckconf::DeckConf, decks::DeckID}; diff --git a/rslib/src/card/undo.rs b/rslib/src/card/undo.rs index 5852c079a..b6375c53a 100644 --- a/rslib/src/card/undo.rs +++ b/rslib/src/card/undo.rs @@ -1,62 +1,37 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use super::Undo; use crate::prelude::*; #[derive(Debug)] -pub(crate) struct CardAdded(Card); - -impl Undo for CardAdded { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.remove_card_only(self.0) - } -} - -#[derive(Debug)] -pub(crate) struct CardRemoved(Card); - -impl Undo for CardRemoved { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.readd_deleted_card(self.0) - } -} - -#[derive(Debug)] -pub(crate) struct CardGraveAdded(CardID, Usn); - -impl Undo for CardGraveAdded { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.remove_card_grave(self.0, self.1) - } -} - -#[derive(Debug)] -pub(crate) struct CardGraveRemoved(CardID, Usn); - -impl Undo for CardGraveRemoved { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.add_card_grave_undoable(self.0, self.1) - } -} - -#[derive(Debug)] -pub(crate) struct CardUpdated(Card); - -impl Undo for CardUpdated { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - let current = col - .storage - .get_card(self.0.id)? - .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; - col.update_card_undoable(&mut self.0.clone(), ¤t) - } +pub(crate) enum UndoableCardChange { + Added(Box), + Updated(Box), + Removed(Box), + GraveAdded(Box<(CardID, Usn)>), + GraveRemoved(Box<(CardID, Usn)>), } impl Collection { + pub(crate) fn undo_card_change(&mut self, change: UndoableCardChange) -> Result<()> { + match change { + UndoableCardChange::Added(card) => self.remove_card_only(*card), + UndoableCardChange::Updated(mut card) => { + let current = self + .storage + .get_card(card.id)? + .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; + self.update_card_undoable(&mut *card, ¤t) + } + UndoableCardChange::Removed(card) => self.restore_deleted_card(*card), + UndoableCardChange::GraveAdded(e) => self.remove_card_grave(e.0, e.1), + UndoableCardChange::GraveRemoved(e) => self.add_card_grave_undoable(e.0, e.1), + } + } + pub(super) fn add_card_undoable(&mut self, card: &mut Card) -> Result<(), AnkiError> { self.storage.add_card(card)?; - self.save_undo(Box::new(CardAdded(card.clone()))); + self.save_undo(UndoableCardChange::Added(Box::new(card.clone()))); Ok(()) } @@ -64,7 +39,7 @@ impl Collection { if card.id.0 == 0 { return Err(AnkiError::invalid_input("card id not set")); } - self.save_undo(Box::new(CardUpdated(original.clone()))); + self.save_undo(UndoableCardChange::Updated(Box::new(original.clone()))); self.storage.update_card(card) } @@ -75,30 +50,29 @@ impl Collection { ) -> Result<()> { self.add_card_grave_undoable(card.id, usn)?; self.storage.remove_card(card.id)?; - self.save_undo(Box::new(CardRemoved(card))); - + self.save_undo(UndoableCardChange::Removed(Box::new(card))); Ok(()) } - fn add_card_grave_undoable(&mut self, cid: CardID, usn: Usn) -> Result<()> { - self.save_undo(Box::new(CardGraveAdded(cid, usn))); - self.storage.add_card_grave(cid, usn) - } - - fn readd_deleted_card(&mut self, card: Card) -> Result<()> { + fn restore_deleted_card(&mut self, card: Card) -> Result<()> { self.storage.add_or_update_card(&card)?; - self.save_undo(Box::new(CardAdded(card))); + self.save_undo(UndoableCardChange::Added(Box::new(card))); Ok(()) } fn remove_card_only(&mut self, card: Card) -> Result<()> { self.storage.remove_card(card.id)?; - self.save_undo(Box::new(CardRemoved(card))); + self.save_undo(UndoableCardChange::Removed(Box::new(card))); Ok(()) } + fn add_card_grave_undoable(&mut self, cid: CardID, usn: Usn) -> Result<()> { + self.save_undo(UndoableCardChange::GraveAdded(Box::new((cid, usn)))); + self.storage.add_card_grave(cid, usn) + } + fn remove_card_grave(&mut self, cid: CardID, usn: Usn) -> Result<()> { - self.save_undo(Box::new(CardGraveRemoved(cid, usn))); + self.save_undo(UndoableCardChange::GraveRemoved(Box::new((cid, usn)))); self.storage.remove_card_grave(cid) } } diff --git a/rslib/src/collection.rs b/rslib/src/collection.rs index 408ab7bb5..90db7fd8a 100644 --- a/rslib/src/collection.rs +++ b/rslib/src/collection.rs @@ -82,7 +82,7 @@ pub struct Collection { impl Collection { /// Execute the provided closure in a transaction, rolling back if /// an error is returned. - pub(crate) fn transact(&mut self, op: Option, func: F) -> Result + pub(crate) fn transact(&mut self, op: Option, func: F) -> Result where F: FnOnce(&mut Collection) -> Result, { diff --git a/rslib/src/decks/mod.rs b/rslib/src/decks/mod.rs index fe0290e50..1ba819837 100644 --- a/rslib/src/decks/mod.rs +++ b/rslib/src/decks/mod.rs @@ -4,7 +4,7 @@ mod counts; mod schema11; mod tree; -mod undo; +pub(crate) mod undo; pub use crate::backend_proto::{ deck_kind::Kind as DeckKind, filtered_search_term::FilteredSearchOrder, Deck as DeckProto, diff --git a/rslib/src/decks/undo.rs b/rslib/src/decks/undo.rs index ba41b981a..3251a0a91 100644 --- a/rslib/src/decks/undo.rs +++ b/rslib/src/decks/undo.rs @@ -2,22 +2,26 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::prelude::*; -use crate::undo::Undo; #[derive(Debug)] -pub(crate) struct DeckUpdated(Deck); -impl Undo for DeckUpdated { - fn undo(mut self: Box, col: &mut crate::collection::Collection) -> Result<()> { - let current = col - .storage - .get_deck(self.0.id)? - .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; - col.update_single_deck_undoable(&mut self.0, ¤t) - } +pub(crate) enum UndoableDeckChange { + Updated(Box), } impl Collection { + pub(crate) fn undo_deck_change(&mut self, change: UndoableDeckChange) -> Result<()> { + match change { + UndoableDeckChange::Updated(mut deck) => { + let current = self + .storage + .get_deck(deck.id)? + .ok_or_else(|| AnkiError::invalid_input("deck disappeared"))?; + self.update_single_deck_undoable(&mut *deck, ¤t) + } + } + } + /// Update an individual, existing deck. Caller is responsible for ensuring deck /// is normalized, matches parents, is not a duplicate name, and bumping mtime. /// Clears deck cache. @@ -27,7 +31,7 @@ impl Collection { original: &Deck, ) -> Result<()> { self.state.deck_cache.clear(); - self.save_undo(Box::new(DeckUpdated(original.clone()))); + self.save_undo(UndoableDeckChange::Updated(Box::new(original.clone()))); self.storage.update_deck(deck) } } diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs index bfb268d15..5c5469e90 100644 --- a/rslib/src/notes/mod.rs +++ b/rslib/src/notes/mod.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -mod undo; +pub(crate) mod undo; use crate::backend_proto::note_is_duplicate_or_empty_out::State as DuplicateState; use crate::{ @@ -300,7 +300,7 @@ impl Collection { } pub fn add_note(&mut self, note: &mut Note, did: DeckID) -> Result<()> { - self.transact(Some(UndoableOp::AddNote), |col| { + self.transact(Some(UndoableOpKind::AddNote), |col| { let nt = col .get_notetype(note.notetype_id)? .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; @@ -325,13 +325,13 @@ impl Collection { } pub fn update_note(&mut self, note: &mut Note) -> Result<()> { - self.update_note_with_op(note, Some(UndoableOp::UpdateNote)) + self.update_note_with_op(note, Some(UndoableOpKind::UpdateNote)) } pub(crate) fn update_note_with_op( &mut self, note: &mut Note, - op: Option, + op: Option, ) -> Result<()> { let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?; if !note_modified(&mut existing_note, note) { @@ -388,7 +388,7 @@ impl Collection { /// Remove provided notes, and any cards that use them. pub(crate) fn remove_notes(&mut self, nids: &[NoteID]) -> Result<()> { let usn = self.usn()?; - self.transact(Some(UndoableOp::RemoveNote), |col| { + self.transact(Some(UndoableOpKind::RemoveNote), |col| { for nid in nids { let nid = *nid; if let Some(_existing_note) = col.storage.get_note(nid)? { diff --git a/rslib/src/notes/undo.rs b/rslib/src/notes/undo.rs index ee33ee2f5..a3b0b898b 100644 --- a/rslib/src/notes/undo.rs +++ b/rslib/src/notes/undo.rs @@ -2,62 +2,37 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::prelude::*; -use crate::undo::Undo; #[derive(Debug)] -pub(crate) struct NoteAdded(Note); - -impl Undo for NoteAdded { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.remove_note_for_undo(self.0) - } -} - -#[derive(Debug)] -pub(crate) struct NoteRemoved(Note); - -impl Undo for NoteRemoved { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.add_note_for_undo(self.0) - } -} - -#[derive(Debug)] -pub(crate) struct NoteGraveAdded(NoteID, Usn); - -impl Undo for NoteGraveAdded { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.remove_note_grave_for_undo(self.0, self.1) - } -} - -#[derive(Debug)] -pub(crate) struct NoteGraveRemoved(NoteID, Usn); - -impl Undo for NoteGraveRemoved { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.add_note_grave(self.0, self.1) - } -} - -#[derive(Debug)] -pub(crate) struct NoteUpdated(Note); - -impl Undo for NoteUpdated { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - let current = col - .storage - .get_note(self.0.id)? - .ok_or_else(|| AnkiError::invalid_input("note disappeared"))?; - col.update_note_undoable(&mut self.0.clone(), ¤t) - } +pub(crate) enum UndoableNoteChange { + Added(Box), + Updated(Box), + Removed(Box), + GraveAdded(Box<(NoteID, Usn)>), + GraveRemoved(Box<(NoteID, Usn)>), } impl Collection { + pub(crate) fn undo_note_change(&mut self, change: UndoableNoteChange) -> Result<()> { + match change { + UndoableNoteChange::Added(note) => self.remove_note_without_grave(*note), + UndoableNoteChange::Updated(mut note) => { + let current = self + .storage + .get_note(note.id)? + .ok_or_else(|| AnkiError::invalid_input("note disappeared"))?; + self.update_note_undoable(&mut *note, ¤t) + } + UndoableNoteChange::Removed(note) => self.restore_deleted_note(*note), + UndoableNoteChange::GraveAdded(e) => self.remove_note_grave(e.0, e.1), + UndoableNoteChange::GraveRemoved(e) => self.add_note_grave(e.0, e.1), + } + } + /// Saves in the undo queue, and commits to DB. /// No validation, card generation or normalization is done. pub(super) fn update_note_undoable(&mut self, note: &mut Note, original: &Note) -> Result<()> { - self.save_undo(Box::new(NoteUpdated(original.clone()))); + self.save_undo(UndoableNoteChange::Updated(Box::new(original.clone()))); self.storage.update_note(note)?; Ok(()) @@ -66,7 +41,7 @@ impl Collection { /// Remove a note. Cards must already have been deleted. pub(crate) fn remove_note_only_undoable(&mut self, nid: NoteID, usn: Usn) -> Result<()> { if let Some(note) = self.storage.get_note(nid)? { - self.save_undo(Box::new(NoteRemoved(note))); + self.save_undo(UndoableNoteChange::Removed(Box::new(note))); self.storage.remove_note(nid)?; self.add_note_grave(nid, usn)?; } @@ -76,29 +51,30 @@ impl Collection { /// Add a note, not adding any cards. pub(super) fn add_note_only_undoable(&mut self, note: &mut Note) -> Result<(), AnkiError> { self.storage.add_note(note)?; - self.save_undo(Box::new(NoteAdded(note.clone()))); + self.save_undo(UndoableNoteChange::Added(Box::new(note.clone()))); + + Ok(()) + } + + fn remove_note_without_grave(&mut self, note: Note) -> Result<()> { + self.storage.remove_note(note.id)?; + self.save_undo(UndoableNoteChange::Removed(Box::new(note))); + Ok(()) + } + + fn restore_deleted_note(&mut self, note: Note) -> Result<()> { + self.storage.add_or_update_note(¬e)?; + self.save_undo(UndoableNoteChange::Added(Box::new(note))); Ok(()) } fn add_note_grave(&mut self, nid: NoteID, usn: Usn) -> Result<()> { - self.save_undo(Box::new(NoteGraveAdded(nid, usn))); + self.save_undo(UndoableNoteChange::GraveAdded(Box::new((nid, usn)))); self.storage.add_note_grave(nid, usn) } - fn remove_note_grave_for_undo(&mut self, nid: NoteID, usn: Usn) -> Result<()> { - self.save_undo(Box::new(NoteGraveRemoved(nid, usn))); + fn remove_note_grave(&mut self, nid: NoteID, usn: Usn) -> Result<()> { + self.save_undo(UndoableNoteChange::GraveRemoved(Box::new((nid, usn)))); self.storage.remove_note_grave(nid) } - - fn remove_note_for_undo(&mut self, note: Note) -> Result<()> { - self.storage.remove_note(note.id)?; - self.save_undo(Box::new(NoteRemoved(note))); - Ok(()) - } - - fn add_note_for_undo(&mut self, note: Note) -> Result<()> { - self.storage.add_or_update_note(¬e)?; - self.save_undo(Box::new(NoteAdded(note))); - Ok(()) - } } diff --git a/rslib/src/prelude.rs b/rslib/src/prelude.rs index 9db2e6159..cc7669bd2 100644 --- a/rslib/src/prelude.rs +++ b/rslib/src/prelude.rs @@ -13,6 +13,6 @@ pub use crate::{ revlog::RevlogID, timestamp::{TimestampMillis, TimestampSecs}, types::Usn, - undo::UndoableOp, + undo::UndoableOpKind, }; pub use slog::{debug, Logger}; diff --git a/rslib/src/revlog/mod.rs b/rslib/src/revlog/mod.rs index d81810c11..79f73f99f 100644 --- a/rslib/src/revlog/mod.rs +++ b/rslib/src/revlog/mod.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -mod undo; +pub(crate) mod undo; use crate::serde::{default_on_invalid, deserialize_int_from_number}; use crate::{define_newtype, prelude::*}; diff --git a/rslib/src/revlog/undo.rs b/rslib/src/revlog/undo.rs index 01d7e1110..daa3ce50f 100644 --- a/rslib/src/revlog/undo.rs +++ b/rslib/src/revlog/undo.rs @@ -2,35 +2,35 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::RevlogEntry; -use crate::{prelude::*, undo::Undo}; +use crate::prelude::*; #[derive(Debug)] -pub(crate) struct RevlogAdded(RevlogEntry); -#[derive(Debug)] -pub(crate) struct RevlogRemoved(RevlogEntry); - -impl Undo for RevlogAdded { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.storage.remove_revlog_entry(self.0.id)?; - col.save_undo(Box::new(RevlogRemoved(self.0))); - Ok(()) - } -} - -impl Undo for RevlogRemoved { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.storage.add_revlog_entry(&self.0, false)?; - col.save_undo(Box::new(RevlogAdded(self.0))); - Ok(()) - } +pub(crate) enum UndoableRevlogChange { + Added(Box), + Removed(Box), } impl Collection { + pub(crate) fn undo_revlog_change(&mut self, change: UndoableRevlogChange) -> Result<()> { + match change { + UndoableRevlogChange::Added(revlog) => { + self.storage.remove_revlog_entry(revlog.id)?; + self.save_undo(UndoableRevlogChange::Removed(revlog)); + Ok(()) + } + UndoableRevlogChange::Removed(revlog) => { + self.storage.add_revlog_entry(&revlog, false)?; + self.save_undo(UndoableRevlogChange::Added(revlog)); + Ok(()) + } + } + } + /// Add the provided revlog entry, modifying the ID if it is not unique. pub(crate) fn add_revlog_entry_undoable(&mut self, mut entry: RevlogEntry) -> Result { entry.id = self.storage.add_revlog_entry(&entry, true)?; let id = entry.id; - self.save_undo(Box::new(RevlogAdded(entry))); + self.save_undo(UndoableRevlogChange::Added(Box::new(entry))); Ok(id) } } diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs index 07e1cad2b..307d01556 100644 --- a/rslib/src/scheduler/answering/mod.rs +++ b/rslib/src/scheduler/answering/mod.rs @@ -241,7 +241,7 @@ impl Collection { /// Answer card, writing its new state to the database. pub fn answer_card(&mut self, answer: &CardAnswer) -> Result<()> { - self.transact(Some(UndoableOp::AnswerCard), |col| { + self.transact(Some(UndoableOpKind::AnswerCard), |col| { col.answer_card_inner(answer) }) } diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs index 3bfc055bf..75dd6eb45 100644 --- a/rslib/src/scheduler/bury_and_suspend.rs +++ b/rslib/src/scheduler/bury_and_suspend.rs @@ -69,7 +69,7 @@ impl Collection { } pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { - self.transact(Some(UndoableOp::UnburyUnsuspend), |col| { + self.transact(Some(UndoableOpKind::UnburyUnsuspend), |col| { col.storage.set_search_table_to_card_ids(cids, false)?; col.unsuspend_or_unbury_searched_cards() }) @@ -126,8 +126,8 @@ impl Collection { mode: BuryOrSuspendMode, ) -> Result<()> { let op = match mode { - BuryOrSuspendMode::Suspend => UndoableOp::Suspend, - BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => UndoableOp::Bury, + BuryOrSuspendMode::Suspend => UndoableOpKind::Suspend, + BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => UndoableOpKind::Bury, }; self.transact(Some(op), |col| { col.storage.set_search_table_to_card_ids(cids, false)?; diff --git a/rslib/src/scheduler/queue/mod.rs b/rslib/src/scheduler/queue/mod.rs index 4dacae12c..64d38d1e0 100644 --- a/rslib/src/scheduler/queue/mod.rs +++ b/rslib/src/scheduler/queue/mod.rs @@ -6,7 +6,7 @@ mod entry; mod learning; mod limits; mod main; -mod undo; +pub(crate) mod undo; use std::{ cmp::Reverse, @@ -21,7 +21,7 @@ pub(crate) use { main::{MainQueueEntry, MainQueueEntryKind}, }; -use self::undo::QueueUpdateAfterAnsweringCard; +use self::undo::QueueUpdate; use super::{states::NextCardStates, timing::SchedTimingToday}; @@ -100,14 +100,14 @@ impl CardQueues { &mut self, card: &Card, timing: SchedTimingToday, - ) -> Result { + ) -> Result> { let entry = self.pop_answered(card.id)?; let requeued_learning = self.maybe_requeue_learning_card(card, timing); - Ok(QueueUpdateAfterAnsweringCard { + Ok(Box::new(QueueUpdate { entry, learning_requeue: requeued_learning, - }) + })) } } @@ -146,7 +146,7 @@ impl Collection { ) -> Result<()> { if let Some(queues) = &mut self.state.card_queues { let mutation = queues.update_after_answering_card(card, timing)?; - self.save_undo(Box::new(mutation)); + self.save_queue_update_undo(mutation); Ok(()) } else { // we currenly allow the queues to be empty for unit tests diff --git a/rslib/src/scheduler/queue/undo.rs b/rslib/src/scheduler/queue/undo.rs index 02847da4f..865a30d7c 100644 --- a/rslib/src/scheduler/queue/undo.rs +++ b/rslib/src/scheduler/queue/undo.rs @@ -2,48 +2,47 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::{CardQueues, LearningQueueEntry, QueueEntry, QueueEntryKind}; -use crate::{prelude::*, undo::Undo}; +use crate::prelude::*; #[derive(Debug)] -pub(super) struct QueueUpdateAfterAnsweringCard { +pub(crate) enum UndoableQueueChange { + CardAnswered(Box), + CardAnswerUndone(Box), +} + +#[derive(Debug)] +pub(crate) struct QueueUpdate { pub entry: QueueEntry, pub learning_requeue: Option, } -impl Undo for QueueUpdateAfterAnsweringCard { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - let queues = col.get_queues()?; - if let Some(learning) = self.learning_requeue { - queues.remove_requeued_learning_card_after_undo(learning.id); +impl Collection { + pub(crate) fn undo_queue_change(&mut self, change: UndoableQueueChange) -> Result<()> { + match change { + UndoableQueueChange::CardAnswered(update) => { + let queues = self.get_queues()?; + if let Some(learning) = &update.learning_requeue { + queues.remove_requeued_learning_card_after_undo(learning.id); + } + queues.push_undo_entry(update.entry); + self.save_undo(UndoableQueueChange::CardAnswerUndone(update)); + + Ok(()) + } + UndoableQueueChange::CardAnswerUndone(update) => { + // don't try to update existing queue when redoing; just + // rebuild it instead + self.clear_study_queues(); + // but preserve undo state for a subsequent undo + self.save_undo(UndoableQueueChange::CardAnswered(update)); + + Ok(()) + } } - queues.push_undo_entry(self.entry); - col.save_undo(Box::new(QueueUpdateAfterUndoingAnswer { - entry: self.entry, - learning_requeue: self.learning_requeue, - })); - - Ok(()) } -} -#[derive(Debug)] -pub(super) struct QueueUpdateAfterUndoingAnswer { - pub entry: QueueEntry, - pub learning_requeue: Option, -} - -impl Undo for QueueUpdateAfterUndoingAnswer { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - // don't try to update existing queue when redoing; just - // rebuild it instead - col.clear_study_queues(); - // but preserve undo state for a subsequent undo - col.save_undo(Box::new(QueueUpdateAfterAnsweringCard { - entry: self.entry, - learning_requeue: self.learning_requeue, - })); - - Ok(()) + pub(super) fn save_queue_update_undo(&mut self, change: Box) { + self.save_undo(UndoableQueueChange::CardAnswered(change)) } } diff --git a/rslib/src/tags/mod.rs b/rslib/src/tags/mod.rs index 7ddc7751a..6f0fc6860 100644 --- a/rslib/src/tags/mod.rs +++ b/rslib/src/tags/mod.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -mod undo; +pub(crate) mod undo; use crate::{ backend_proto::TagTreeNode, @@ -310,7 +310,7 @@ impl Collection { tags: &[Regex], mut repl: R, ) -> Result { - self.transact(Some(UndoableOp::UpdateTag), |col| { + self.transact(Some(UndoableOpKind::UpdateTag), |col| { col.transform_notes(nids, |note, _nt| { let mut changed = false; for re in tags { @@ -361,7 +361,7 @@ impl Collection { ) .map_err(|_| AnkiError::invalid_input("invalid regex"))?; - self.transact(Some(UndoableOp::UpdateTag), |col| { + self.transact(Some(UndoableOpKind::UpdateTag), |col| { col.transform_notes(nids, |note, _nt| { let mut need_to_add = true; let mut match_count = 0; diff --git a/rslib/src/tags/undo.rs b/rslib/src/tags/undo.rs index 00297503f..907a7ea86 100644 --- a/rslib/src/tags/undo.rs +++ b/rslib/src/tags/undo.rs @@ -2,36 +2,30 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use super::Tag; -use crate::{prelude::*, undo::Undo}; +use crate::prelude::*; #[derive(Debug)] -struct AddedTag(Tag); - -#[derive(Debug)] -struct RemovedTag(Tag); - -impl Undo for AddedTag { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.remove_single_tag_undoable(&self.0) - } -} - -impl Undo for RemovedTag { - fn undo(self: Box, col: &mut Collection) -> Result<()> { - col.register_tag_undoable(&self.0) - } +pub(crate) enum UndoableTagChange { + Added(Box), + Removed(Box), } impl Collection { + pub(crate) fn undo_tag_change(&mut self, change: UndoableTagChange) -> Result<()> { + match change { + UndoableTagChange::Added(tag) => self.remove_single_tag_undoable(&tag), + UndoableTagChange::Removed(tag) => self.register_tag_undoable(&tag), + } + } /// Adds an already-validated tag to the DB and undo list. /// Caller is responsible for setting usn. pub(super) fn register_tag_undoable(&mut self, tag: &Tag) -> Result<()> { - self.save_undo(Box::new(AddedTag(tag.clone()))); + self.save_undo(UndoableTagChange::Added(Box::new(tag.clone()))); self.storage.register_tag(&tag) } fn remove_single_tag_undoable(&mut self, tag: &Tag) -> Result<()> { - self.save_undo(Box::new(RemovedTag(tag.clone()))); + self.save_undo(UndoableTagChange::Removed(Box::new(tag.clone()))); self.storage.remove_single_tag(&tag.name) } } diff --git a/rslib/src/undo/changes.rs b/rslib/src/undo/changes.rs new file mode 100644 index 000000000..bad7efe28 --- /dev/null +++ b/rslib/src/undo/changes.rs @@ -0,0 +1,67 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + card::undo::UndoableCardChange, decks::undo::UndoableDeckChange, + notes::undo::UndoableNoteChange, prelude::*, revlog::undo::UndoableRevlogChange, + scheduler::queue::undo::UndoableQueueChange, tags::undo::UndoableTagChange, +}; + +#[derive(Debug)] +pub(crate) enum UndoableChange { + Card(UndoableCardChange), + Note(UndoableNoteChange), + Deck(UndoableDeckChange), + Tag(UndoableTagChange), + Revlog(UndoableRevlogChange), + Queue(UndoableQueueChange), +} + +impl UndoableChange { + pub(super) fn undo(self, col: &mut Collection) -> Result<()> { + match self { + UndoableChange::Card(c) => col.undo_card_change(c), + UndoableChange::Note(c) => col.undo_note_change(c), + UndoableChange::Deck(c) => col.undo_deck_change(c), + UndoableChange::Tag(c) => col.undo_tag_change(c), + UndoableChange::Revlog(c) => col.undo_revlog_change(c), + UndoableChange::Queue(c) => col.undo_queue_change(c), + } + } +} + +impl From for UndoableChange { + fn from(c: UndoableCardChange) -> Self { + UndoableChange::Card(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableNoteChange) -> Self { + UndoableChange::Note(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableDeckChange) -> Self { + UndoableChange::Deck(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableTagChange) -> Self { + UndoableChange::Tag(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableRevlogChange) -> Self { + UndoableChange::Revlog(c) + } +} + +impl From for UndoableChange { + fn from(c: UndoableQueueChange) -> Self { + UndoableChange::Queue(c) + } +} diff --git a/rslib/src/undo/mod.rs b/rslib/src/undo/mod.rs index 7f8b64c12..ae3cbb83b 100644 --- a/rslib/src/undo/mod.rs +++ b/rslib/src/undo/mod.rs @@ -1,57 +1,22 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod changes; +mod ops; + +pub(crate) use changes::UndoableChange; +pub use ops::UndoableOpKind; + use crate::backend_proto as pb; -use crate::{collection::Collection, err::Result, prelude::*}; -use std::{collections::VecDeque, fmt}; +use crate::prelude::*; +use std::collections::VecDeque; const UNDO_LIMIT: usize = 30; -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum UndoableOp { - UpdateCard, - AnswerCard, - Bury, - Suspend, - UnburyUnsuspend, - AddNote, - RemoveNote, - UpdateTag, - UpdateNote, -} - -impl UndoableOp { - pub(crate) fn needs_study_queue_reset(self) -> bool { - self != UndoableOp::AnswerCard - } -} - -impl Collection { - pub fn describe_collection_op(&self, op: UndoableOp) -> String { - let key = match op { - UndoableOp::UpdateCard => todo!(), - UndoableOp::AnswerCard => TR::UndoAnswerCard, - UndoableOp::Bury => TR::StudyingBury, - UndoableOp::Suspend => TR::StudyingSuspend, - UndoableOp::UnburyUnsuspend => TR::UndoUnburyUnsuspend, - UndoableOp::AddNote => TR::UndoAddNote, - UndoableOp::RemoveNote => TR::StudyingDeleteNote, - UndoableOp::UpdateTag => TR::UndoUpdateTag, - UndoableOp::UpdateNote => TR::UndoUpdateNote, - }; - - self.i18n.tr(key).to_string() - } -} - -pub(crate) trait Undo: fmt::Debug + Send { - /// Undo the recorded action. - fn undo(self: Box, col: &mut Collection) -> Result<()>; -} #[derive(Debug)] -struct UndoStep { - kind: UndoableOp, - changes: Vec>, +struct UndoableOp { + kind: UndoableOpKind, + changes: Vec, } #[derive(Debug, PartialEq)] @@ -71,21 +36,21 @@ impl Default for UndoMode { 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, + undo_steps: VecDeque, // redo steps are added to the end - redo_steps: Vec, + redo_steps: Vec, mode: UndoMode, - current_step: Option, + current_step: Option, } impl UndoManager { - fn save(&mut self, item: Box) { + 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) { + fn begin_step(&mut self, op: Option) { println!("begin: {:?}", op); if op.is_none() { self.undo_steps.clear(); @@ -94,7 +59,7 @@ impl UndoManager { // a normal op clears the redo queue self.redo_steps.clear(); } - self.current_step = op.map(|op| UndoStep { + self.current_step = op.map(|op| UndoableOp { kind: op, changes: vec![], }); @@ -119,21 +84,21 @@ impl UndoManager { .unwrap_or(true) } - fn can_undo(&self) -> Option { + fn can_undo(&self) -> Option { self.undo_steps.front().map(|s| s.kind) } - fn can_redo(&self) -> Option { + fn can_redo(&self) -> Option { self.redo_steps.last().map(|s| s.kind) } } impl Collection { - pub fn can_undo(&self) -> Option { + pub fn can_undo(&self) -> Option { self.state.undo.can_undo() } - pub fn can_redo(&self) -> Option { + pub fn can_redo(&self) -> Option { self.state.undo.can_redo() } @@ -173,17 +138,17 @@ impl Collection { pb::UndoStatus { undo: self .can_undo() - .map(|op| self.describe_collection_op(op)) + .map(|op| self.describe_op_kind(op)) .unwrap_or_default(), redo: self .can_redo() - .map(|op| self.describe_collection_op(op)) + .map(|op| self.describe_op_kind(op)) .unwrap_or_default(), } } /// If op is None, clears the undo/redo queues. - pub(crate) fn begin_undoable_operation(&mut self, op: Option) { + pub(crate) fn begin_undoable_operation(&mut self, op: Option) { self.state.undo.begin_step(op); } @@ -202,8 +167,8 @@ impl Collection { } #[inline] - pub(crate) fn save_undo(&mut self, item: Box) { - self.state.undo.save(item) + pub(crate) fn save_undo(&mut self, item: impl Into) { + self.state.undo.save(item.into()); } } @@ -237,7 +202,7 @@ mod test { // record a few undo steps for i in 3..=4 { - col.transact(Some(UndoableOp::UpdateCard), |col| { + col.transact(Some(UndoableOpKind::UpdateCard), |col| { col.get_and_update_card(cid, |card| { card.interval = i; Ok(()) @@ -249,41 +214,41 @@ mod test { } assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); - assert_eq!(col.can_undo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::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(UndoableOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::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(UndoableOp::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); // redo a step col.redo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 3); - assert_eq!(col.can_undo(), Some(UndoableOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); // and another col.redo().unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 4); - assert_eq!(col.can_undo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::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(UndoableOp::UpdateCard)); - assert_eq!(col.can_redo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); + assert_eq!(col.can_redo(), Some(UndoableOpKind::UpdateCard)); // if any action is performed, it should clear the redo queue - col.transact(Some(UndoableOp::UpdateCard), |col| { + col.transact(Some(UndoableOpKind::UpdateCard), |col| { col.get_and_update_card(cid, |card| { card.interval = 5; Ok(()) @@ -293,7 +258,7 @@ mod test { }) .unwrap(); assert_eq!(col.storage.get_card(cid).unwrap().unwrap().interval, 5); - assert_eq!(col.can_undo(), Some(UndoableOp::UpdateCard)); + assert_eq!(col.can_undo(), Some(UndoableOpKind::UpdateCard)); assert_eq!(col.can_redo(), None); // and any action that doesn't support undoing will clear both queues diff --git a/rslib/src/undo/ops.rs b/rslib/src/undo/ops.rs new file mode 100644 index 000000000..931cc5a7c --- /dev/null +++ b/rslib/src/undo/ops.rs @@ -0,0 +1,41 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::prelude::*; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum UndoableOpKind { + UpdateCard, + AnswerCard, + Bury, + Suspend, + UnburyUnsuspend, + AddNote, + RemoveNote, + UpdateTag, + UpdateNote, +} + +impl UndoableOpKind { + pub(crate) fn needs_study_queue_reset(self) -> bool { + self != UndoableOpKind::AnswerCard + } +} + +impl Collection { + pub fn describe_op_kind(&self, op: UndoableOpKind) -> String { + let key = match op { + UndoableOpKind::UpdateCard => todo!(), + UndoableOpKind::AnswerCard => TR::UndoAnswerCard, + UndoableOpKind::Bury => TR::StudyingBury, + UndoableOpKind::Suspend => TR::StudyingSuspend, + UndoableOpKind::UnburyUnsuspend => TR::UndoUnburyUnsuspend, + UndoableOpKind::AddNote => TR::UndoAddNote, + UndoableOpKind::RemoveNote => TR::StudyingDeleteNote, + UndoableOpKind::UpdateTag => TR::UndoUpdateTag, + UndoableOpKind::UpdateNote => TR::UndoUpdateNote, + }; + + self.i18n.tr(key).to_string() + } +}