undo support for note adding

This commit is contained in:
Damien Elmes 2021-03-05 10:04:42 +10:00
parent 105ce94dd4
commit 40aff4447a
7 changed files with 114 additions and 9 deletions

View File

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

View File

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

View File

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

View File

@ -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<Self>, 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<Self>, 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() {

View File

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

View File

@ -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(&note)?;
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<Self>, 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<Self>, 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(())
}
}

View File

@ -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![