From 40aff4447ad1b4e87a7b822c7c42450e1e031cb3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 5 Mar 2021 10:04:42 +1000 Subject: [PATCH] undo support for note adding --- ftl/core/undo.ftl | 1 + pylib/tests/test_undo.py | 3 +- qt/aqt/addcards.py | 1 - rslib/src/card.rs | 39 +++++++++++++++++- rslib/src/collection/op.rs | 2 + rslib/src/notes.rs | 75 +++++++++++++++++++++++++++++++++-- rslib/src/storage/card/mod.rs | 2 +- 7 files changed, 114 insertions(+), 9 deletions(-) diff --git a/ftl/core/undo.ftl b/ftl/core/undo.ftl index 49c64bc49..f0e019547 100644 --- a/ftl/core/undo.ftl +++ b/ftl/core/undo.ftl @@ -8,3 +8,4 @@ undo-redo-action = Redo { $action } undo-action-redone = { $action } Redone undo-answer-card = Answer Card undo-unbury-unsuspend = Unbury/Unsuspend +undo-add-note = Add Note diff --git a/pylib/tests/test_undo.py b/pylib/tests/test_undo.py index fac70af27..fe323c67c 100644 --- a/pylib/tests/test_undo.py +++ b/pylib/tests/test_undo.py @@ -38,7 +38,7 @@ def test_op(): note["Front"] = "one" col.addNote(note) col.reset() - assert col.undoName() == "add" + assert "add" in col.undoName().lower() c = col.sched.getCard() col.sched.answerCard(c, 2) assert col.undoName() == "Review" @@ -54,7 +54,6 @@ def test_review(): note["Front"] = "two" col.addNote(note) col.reset() - assert not col.undoName() # answer assert col.sched.counts() == (2, 0, 0) c = col.sched.getCard() diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 33ccdeaf2..47de845da 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -178,7 +178,6 @@ class AddCards(QDialog): if not askUser(tr(TR.ADDING_YOU_HAVE_A_CLOZE_DELETION_NOTE)): return None self.mw.col.add_note(note, self.deckChooser.selectedId()) - self.mw.col.clear_python_undo() self.addHistory(note) self.previousNote = note self.mw.requireReset(reason=ResetReason.AddCardsAddNote, context=self) diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 21756838c..6d9f70259 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -123,6 +123,25 @@ impl Card { matches!(self.queue, CardQueue::Learn | CardQueue::PreviewRepeat) } } + +#[derive(Debug)] +pub(crate) struct CardAdded(Card); + +impl Undo for CardAdded { + fn undo(self: Box, col: &mut crate::collection::Collection) -> Result<()> { + col.remove_card_for_undo(self.0) + } +} + +#[derive(Debug)] +pub(crate) struct CardRemoved(Card); + +impl Undo for CardRemoved { + fn undo(self: Box, col: &mut crate::collection::Collection) -> Result<()> { + col.add_card_for_undo(self.0) + } +} + #[derive(Debug)] pub(crate) struct CardUpdated(Card); @@ -184,7 +203,16 @@ impl Collection { } card.mtime = TimestampSecs::now(); card.usn = self.usn()?; - self.storage.add_card(card) + self.storage.add_card(card)?; + self.save_undo(Box::new(CardAdded(card.clone()))); + Ok(()) + } + + /// Used for undoing + fn add_card_for_undo(&mut self, card: Card) -> Result<()> { + self.storage.add_or_update_card(&card)?; + self.save_undo(Box::new(CardAdded(card))); + Ok(()) } /// Remove cards and any resulting orphaned notes. @@ -210,13 +238,20 @@ impl Collection { } pub(crate) fn remove_card_only(&mut self, card: Card, usn: Usn) -> Result<()> { - // fixme: undo self.storage.remove_card(card.id)?; self.storage.add_card_grave(card.id, usn)?; + self.save_undo(Box::new(CardRemoved(card))); Ok(()) } + /// Only used when undoing; does not add a grave. + fn remove_card_for_undo(&mut self, card: Card) -> Result<()> { + self.storage.remove_card(card.id)?; + self.save_undo(Box::new(CardRemoved(card))); + Ok(()) + } + pub fn set_deck(&mut self, cards: &[CardID], deck_id: DeckID) -> Result<()> { let deck = self.get_deck(deck_id)?.ok_or(AnkiError::NotFound)?; if deck.is_filtered() { diff --git a/rslib/src/collection/op.rs b/rslib/src/collection/op.rs index c383f52d7..0d7790cce 100644 --- a/rslib/src/collection/op.rs +++ b/rslib/src/collection/op.rs @@ -10,6 +10,7 @@ pub enum CollectionOp { Bury, Suspend, UnburyUnsuspend, + AddNote, } impl Collection { @@ -20,6 +21,7 @@ impl Collection { CollectionOp::Bury => TR::StudyingBury, CollectionOp::Suspend => TR::StudyingSuspend, CollectionOp::UnburyUnsuspend => TR::UndoUnburyUnsuspend, + CollectionOp::AddNote => TR::UndoAddNote, }; self.i18n.tr(key).to_string() diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 8467a52ab..907c11fb4 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -3,7 +3,7 @@ use crate::{ backend_proto as pb, - collection::Collection, + collection::{Collection, CollectionOp}, decks::DeckID, define_newtype, err::{AnkiError, Result}, @@ -298,7 +298,7 @@ impl Collection { } pub fn add_note(&mut self, note: &mut Note, did: DeckID) -> Result<()> { - self.transact(None, |col| { + self.transact(Some(CollectionOp::AddNote), |col| { let nt = col .get_notetype(note.notetype_id)? .ok_or_else(|| AnkiError::invalid_input("missing note type"))?; @@ -319,9 +319,16 @@ impl Collection { note.prepare_for_update(&ctx.notetype, normalize_text)?; note.set_modified(ctx.usn); self.storage.add_note(note)?; + self.save_undo(Box::new(NoteAdded(note.clone()))); self.generate_cards_for_new_note(ctx, note, did) } + 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(()) + } + pub fn update_note(&mut self, note: &mut Note) -> Result<()> { let mut existing_note = self.storage.get_note(note.id)?.ok_or(AnkiError::NotFound)?; if !note_modified(&mut existing_note, note) { @@ -398,6 +405,12 @@ impl Collection { Ok(()) } + fn remove_note_for_undo(&mut self, note: Note) -> Result<()> { + self.storage.remove_note(note.id)?; + self.save_undo(Box::new(NoteRemoved(note))); + Ok(()) + } + /// Remove provided notes, and any cards that use them. pub(crate) fn remove_notes(&mut self, nids: &[NoteID]) -> Result<()> { let usn = self.usn()?; @@ -560,6 +573,24 @@ fn note_modified(existing_note: &mut Note, note: &Note) -> bool { notes_differ } +#[derive(Debug)] +pub(crate) struct NoteAdded(Note); + +impl Undo for NoteAdded { + fn undo(self: Box, col: &mut crate::collection::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 crate::collection::Collection) -> Result<()> { + col.add_note_for_undo(self.0) + } +} + #[derive(Debug)] pub(crate) struct NoteUpdated(Note); @@ -578,7 +609,7 @@ mod test { use super::{anki_base91, field_checksum}; use crate::{ collection::open_test_collection, config::ConfigKey, decks::DeckID, err::Result, - search::SortMode, + prelude::*, search::SortMode, }; #[test] @@ -676,4 +707,42 @@ mod test { Ok(()) } + + #[test] + fn undo() -> Result<()> { + let mut col = open_test_collection(); + let nt = col + .get_notetype_by_name("basic (and reversed card)")? + .unwrap(); + + let assert_initial = |col: &mut Collection| -> Result<()> { + assert_eq!(col.search_notes("")?.len(), 0); + assert_eq!(col.search_cards("", SortMode::NoOrder)?.len(), 0); + Ok(()) + }; + + let assert_after_add = |col: &mut Collection| -> Result<()> { + assert_eq!(col.search_notes("")?.len(), 1); + assert_eq!(col.search_cards("", SortMode::NoOrder)?.len(), 2); + Ok(()) + }; + + assert_initial(&mut col)?; + + let mut note = nt.new_note(); + note.set_field(0, "a")?; + note.set_field(1, "b")?; + + col.add_note(&mut note, DeckID(1)).unwrap(); + + assert_after_add(&mut col)?; + col.undo()?; + assert_initial(&mut col)?; + col.redo()?; + assert_after_add(&mut col)?; + col.undo()?; + assert_initial(&mut col)?; + + Ok(()) + } } diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 0ea6de2b9..40e476ff2 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -128,7 +128,7 @@ impl super::SqliteStorage { Ok(()) } - /// Add or update card, using the provided ID. Used when syncing. + /// Add or update card, using the provided ID. Used for syncing & undoing. pub(crate) fn add_or_update_card(&self, card: &Card) -> Result<()> { let mut stmt = self.db.prepare_cached(include_str!("add_or_update.sql"))?; stmt.execute(params![