dispatch undo operations via enum instead of trait

To coalesce successive note edits into a single undo op we'll need to
be able to get the original Undoable type, which is awkward to do with
a trait object.
This commit is contained in:
Damien Elmes 2021-03-06 20:34:26 +10:00
parent d70e35e0a2
commit ecea2161e3
20 changed files with 325 additions and 306 deletions

View File

@ -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)

View File

@ -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};

View File

@ -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<Self>, 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<Self>, 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<Self>, 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<Self>, 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<Self>, 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(), &current)
}
pub(crate) enum UndoableCardChange {
Added(Box<Card>),
Updated(Box<Card>),
Removed(Box<Card>),
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, &current)
}
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)
}
}

View File

@ -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<F, R>(&mut self, op: Option<UndoableOp>, func: F) -> Result<R>
pub(crate) fn transact<F, R>(&mut self, op: Option<UndoableOpKind>, func: F) -> Result<R>
where
F: FnOnce(&mut Collection) -> Result<R>,
{

View File

@ -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,

View File

@ -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<Self>, 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, &current)
}
pub(crate) enum UndoableDeckChange {
Updated(Box<Deck>),
}
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, &current)
}
}
}
/// 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)
}
}

View File

@ -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<UndoableOp>,
op: Option<UndoableOpKind>,
) -> 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)? {

View File

@ -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<Self>, 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<Self>, 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<Self>, 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<Self>, 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<Self>, 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(), &current)
}
pub(crate) enum UndoableNoteChange {
Added(Box<Note>),
Updated(Box<Note>),
Removed(Box<Note>),
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, &current)
}
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(&note)?;
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(&note)?;
self.save_undo(Box::new(NoteAdded(note)));
Ok(())
}
}

View File

@ -13,6 +13,6 @@ pub use crate::{
revlog::RevlogID,
timestamp::{TimestampMillis, TimestampSecs},
types::Usn,
undo::UndoableOp,
undo::UndoableOpKind,
};
pub use slog::{debug, Logger};

View File

@ -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::*};

View File

@ -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<Self>, 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<Self>, 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<RevlogEntry>),
Removed(Box<RevlogEntry>),
}
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<RevlogID> {
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)
}
}

View File

@ -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)
})
}

View File

@ -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)?;

View File

@ -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<QueueUpdateAfterAnsweringCard> {
) -> Result<Box<QueueUpdate>> {
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

View File

@ -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<QueueUpdate>),
CardAnswerUndone(Box<QueueUpdate>),
}
#[derive(Debug)]
pub(crate) struct QueueUpdate {
pub entry: QueueEntry,
pub learning_requeue: Option<LearningQueueEntry>,
}
impl Undo for QueueUpdateAfterAnsweringCard {
fn undo(self: Box<Self>, 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<LearningQueueEntry>,
}
impl Undo for QueueUpdateAfterUndoingAnswer {
fn undo(self: Box<Self>, 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<QueueUpdate>) {
self.save_undo(UndoableQueueChange::CardAnswered(change))
}
}

View File

@ -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<usize> {
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;

View File

@ -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<Self>, col: &mut Collection) -> Result<()> {
col.remove_single_tag_undoable(&self.0)
}
}
impl Undo for RemovedTag {
fn undo(self: Box<Self>, col: &mut Collection) -> Result<()> {
col.register_tag_undoable(&self.0)
}
pub(crate) enum UndoableTagChange {
Added(Box<Tag>),
Removed(Box<Tag>),
}
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)
}
}

67
rslib/src/undo/changes.rs Normal file
View File

@ -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<UndoableCardChange> for UndoableChange {
fn from(c: UndoableCardChange) -> Self {
UndoableChange::Card(c)
}
}
impl From<UndoableNoteChange> for UndoableChange {
fn from(c: UndoableNoteChange) -> Self {
UndoableChange::Note(c)
}
}
impl From<UndoableDeckChange> for UndoableChange {
fn from(c: UndoableDeckChange) -> Self {
UndoableChange::Deck(c)
}
}
impl From<UndoableTagChange> for UndoableChange {
fn from(c: UndoableTagChange) -> Self {
UndoableChange::Tag(c)
}
}
impl From<UndoableRevlogChange> for UndoableChange {
fn from(c: UndoableRevlogChange) -> Self {
UndoableChange::Revlog(c)
}
}
impl From<UndoableQueueChange> for UndoableChange {
fn from(c: UndoableQueueChange) -> Self {
UndoableChange::Queue(c)
}
}

View File

@ -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<Self>, col: &mut Collection) -> Result<()>;
}
#[derive(Debug)]
struct UndoStep {
kind: UndoableOp,
changes: Vec<Box<dyn Undo>>,
struct UndoableOp {
kind: UndoableOpKind,
changes: Vec<UndoableChange>,
}
#[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<UndoStep>,
undo_steps: VecDeque<UndoableOp>,
// redo steps are added to the end
redo_steps: Vec<UndoStep>,
redo_steps: Vec<UndoableOp>,
mode: UndoMode,
current_step: Option<UndoStep>,
current_step: Option<UndoableOp>,
}
impl UndoManager {
fn save(&mut self, item: Box<dyn Undo>) {
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<UndoableOp>) {
fn begin_step(&mut self, op: Option<UndoableOpKind>) {
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<UndoableOp> {
fn can_undo(&self) -> Option<UndoableOpKind> {
self.undo_steps.front().map(|s| s.kind)
}
fn can_redo(&self) -> Option<UndoableOp> {
fn can_redo(&self) -> Option<UndoableOpKind> {
self.redo_steps.last().map(|s| s.kind)
}
}
impl Collection {
pub fn can_undo(&self) -> Option<UndoableOp> {
pub fn can_undo(&self) -> Option<UndoableOpKind> {
self.state.undo.can_undo()
}
pub fn can_redo(&self) -> Option<UndoableOp> {
pub fn can_redo(&self) -> Option<UndoableOpKind> {
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<UndoableOp>) {
pub(crate) fn begin_undoable_operation(&mut self, op: Option<UndoableOpKind>) {
self.state.undo.begin_step(op);
}
@ -202,8 +167,8 @@ impl Collection {
}
#[inline]
pub(crate) fn save_undo(&mut self, item: Box<dyn Undo>) {
self.state.undo.save(item)
pub(crate) fn save_undo(&mut self, item: impl Into<UndoableChange>) {
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

41
rslib/src/undo/ops.rs Normal file
View File

@ -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()
}
}