diff --git a/proto/backend.proto b/proto/backend.proto index 7a3fdc7e9..a38b59472 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -56,6 +56,10 @@ message CardID { int64 cid = 1; } +message CardIDs { + repeated int64 cids = 1; +} + message DeckID { int64 did = 1; } @@ -96,6 +100,8 @@ service BackendService { rpc ExtendLimits (ExtendLimitsIn) returns (Empty); rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut); rpc CongratsInfo (Empty) returns (CongratsInfoOut); + rpc RestoreBuriedAndSuspendedCards (CardIDs) returns (Empty); + rpc UnburyCardsInCurrentDeck (UnburyCardsInCurrentDeckIn) returns (Empty); // stats @@ -1027,3 +1033,12 @@ message CongratsInfoOut { bool is_filtered_deck = 7; bool bridge_commands_supported = 8; } + +message UnburyCardsInCurrentDeckIn { + enum Mode { + ALL = 0; + SCHED_ONLY = 1; + USER_ONLY = 2; + } + Mode mode = 1; +} diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 29ae247c9..9f44e9b3f 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -121,32 +121,6 @@ class Scheduler(V2): else: return 3 - def unburyCards(self) -> None: - "Unbury cards." - self.col.log( - self.col.db.list( - f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED}" - ) - ) - self.col.db.execute( - f"update cards set queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED}" - ) - - def unburyCardsForDeck(self) -> None: # type: ignore[override] - sids = self._deckLimit() - self.col.log( - self.col.db.list( - f"select id from cards where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s" - % sids - ) - ) - self.col.db.execute( - f"update cards set mod=?,usn=?,queue=type where queue = {QUEUE_TYPE_SIBLING_BURIED} and did in %s" - % sids, - intTime(), - self.col.usn(), - ) - # Getting the next card ########################################################################## @@ -848,16 +822,6 @@ did = ?, queue = %s, due = ?, usn = ? where id = ?""" self.col.usn(), ) - def unsuspendCards(self, ids: List[int]) -> None: - "Unsuspend cards." - self.col.log(ids) - self.col.db.execute( - "update cards set queue=type,mod=?,usn=? " - f"where queue = {QUEUE_TYPE_SUSPENDED} and id in " + ids2str(ids), - intTime(), - self.col.usn(), - ) - def buryCards(self, cids: List[int], manual: bool = False) -> None: # v1 only supported automatic burying assert not manual diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 6eb23c81d..d7197d57a 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -7,7 +7,18 @@ import pprint import random import time from heapq import * -from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, Union +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + List, + Optional, + Sequence, + Set, + Tuple, + Union, +) import anki # pylint: disable=unused-import import anki.backend_pb2 as pb @@ -25,6 +36,12 @@ from anki.rsbackend import ( ) from anki.utils import ids2str, intTime +UnburyCurrentDeckMode = pb.UnburyCardsInCurrentDeckIn.Mode # pylint: disable=no-member +if TYPE_CHECKING: + UnburyCurrentDeckModeValue = ( + pb.UnburyCardsInCurrentDeckIn.ModeValue # pylint: disable=no-member + ) + # card types: 0=new, 1=lrn, 2=rev, 3=relrn # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, # 4=preview, -1=suspended, -2=sibling buried, -3=manually buried @@ -1265,12 +1282,6 @@ where id = ? self.today = timing.days_elapsed self.dayCutoff = timing.next_day_at - # unbury if the day has rolled over - unburied = self.col.conf.get("lastUnburied", 0) - if unburied < self.today or self.today + 7 < unburied: - self.unburyCards() - self.col.conf["lastUnburied"] = self.today - def _checkDay(self) -> None: # check if the day has rolled over if time.time() > self.dayCutoff: @@ -1364,16 +1375,16 @@ where id = ? # Suspending & burying ########################################################################## - # learning and relearning cards may be seconds-based or day-based; - # other types map directly to queues - _restoreQueueSnippet = f""" -queue = (case when type in ({CARD_TYPE_LRN},{CARD_TYPE_RELEARNING}) then - (case when (case when odue then odue else due end) > 1000000000 then - {QUEUE_TYPE_LRN} else {QUEUE_TYPE_DAY_LEARN_RELEARN} end) -else - type -end) -""" + def unsuspend_cards(self, ids: List[int]) -> None: + self.col.backend.restore_buried_and_suspended_cards(ids) + + def unbury_cards(self, ids: List[int]) -> None: + self.col.backend.restore_buried_and_suspended_cards(ids) + + def unbury_cards_in_current_deck( + self, mode: UnburyCurrentDeckModeValue = UnburyCurrentDeckMode.ALL, + ) -> None: + self.col.backend.unbury_cards_in_current_deck(mode) def suspendCards(self, ids: List[int]) -> None: "Suspend cards." @@ -1385,18 +1396,6 @@ end) self.col.usn(), ) - def unsuspendCards(self, ids: List[int]) -> None: - "Unsuspend cards." - self.col.log(ids) - self.col.db.execute( - ( - f"update cards set %s,mod=?,usn=? where queue = {QUEUE_TYPE_SUSPENDED} and id in %s" - ) - % (self._restoreQueueSnippet, ids2str(ids)), - intTime(), - self.col.usn(), - ) - def buryCards(self, cids: List[int], manual: bool = True) -> None: queue = manual and QUEUE_TYPE_MANUALLY_BURIED or QUEUE_TYPE_SIBLING_BURIED self.col.log(cids) @@ -1416,42 +1415,24 @@ update cards set queue=?,mod=?,usn=? where id in """ ) self.buryCards(cids) + # legacy + def unburyCards(self) -> None: - "Unbury all buried cards in all decks." - self.col.log( - self.col.db.list( - f"select id from cards where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})" - ) - ) - self.col.db.execute( - f"update cards set %s where queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})" - % self._restoreQueueSnippet + print( + "please use unbury_cards() or unbury_cards_in_current_deck instead of unburyCards()" ) + self.unbury_cards_in_current_deck() def unburyCardsForDeck(self, type: str = "all") -> None: if type == "all": - queue = ( - f"queue in ({QUEUE_TYPE_SIBLING_BURIED}, {QUEUE_TYPE_MANUALLY_BURIED})" - ) + mode = UnburyCurrentDeckMode.ALL elif type == "manual": - queue = f"queue = {QUEUE_TYPE_MANUALLY_BURIED}" - elif type == "siblings": - queue = f"queue = {QUEUE_TYPE_SIBLING_BURIED}" - else: - raise Exception("unknown type") + mode = UnburyCurrentDeckMode.USER_ONLY + else: # elif type == "siblings": + mode = UnburyCurrentDeckMode.SCHED_ONLY + self.unbury_cards_in_current_deck(mode) - self.col.log( - self.col.db.list( - "select id from cards where %s and did in %s" - % (queue, self._deckLimit()) - ) - ) - self.col.db.execute( - "update cards set mod=?,usn=?,%s where %s and did in %s" - % (self._restoreQueueSnippet, queue, self._deckLimit()), - intTime(), - self.col.usn(), - ) + unsuspendCards = unsuspend_cards # Sibling spacing ########################################################################## diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index dc33a86ea..4144b2f2d 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -503,7 +503,7 @@ def test_misc(): col.sched.buryNote(c.nid) col.reset() assert not col.sched.getCard() - col.sched.unburyCards() + col.sched.unbury_cards_in_current_deck() col.reset() assert col.sched.getCard() diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 2c4d9c3f2..14296a59f 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -6,6 +6,7 @@ import time from anki import hooks from anki.consts import * from anki.lang import without_unicode_isolation +from anki.schedv2 import UnburyCurrentDeckMode from anki.utils import intTime from tests.shared import getEmptyCol as getEmptyColOrig @@ -609,22 +610,18 @@ def test_bury(): col.reset() assert not col.sched.getCard() - col.sched.unburyCardsForDeck( # pylint: disable=unexpected-keyword-arg - type="manual" - ) + col.sched.unbury_cards_in_current_deck(UnburyCurrentDeckMode.USER_ONLY) c.load() assert c.queue == QUEUE_TYPE_NEW c2.load() assert c2.queue == QUEUE_TYPE_SIBLING_BURIED - col.sched.unburyCardsForDeck( # pylint: disable=unexpected-keyword-arg - type="siblings" - ) + col.sched.unbury_cards_in_current_deck(UnburyCurrentDeckMode.SCHED_ONLY) c2.load() assert c2.queue == QUEUE_TYPE_NEW col.sched.buryCards([c.id, c2.id]) - col.sched.unburyCardsForDeck(type="all") # pylint: disable=unexpected-keyword-arg + col.sched.unbury_cards_in_current_deck() col.reset() @@ -1214,7 +1211,7 @@ def test_moveVersions(): assert c.queue == QUEUE_TYPE_SIBLING_BURIED # and it should be new again when unburied - col.sched.unburyCards() + col.sched.unbury_cards_in_current_deck() c.load() assert c.type == CARD_TYPE_NEW and c.queue == QUEUE_TYPE_NEW diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 89760083b..8a168d59e 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -235,6 +235,12 @@ impl From for CardID { } } +impl pb::CardIDs { + fn into_native(self) -> Vec { + self.cids.into_iter().map(CardID).collect() + } +} + impl From for NoteID { fn from(nid: pb::NoteId) -> Self { NoteID(nid.nid) @@ -442,8 +448,14 @@ impl BackendService for Backend { // scheduling //----------------------------------------------- + /// This behaves like _updateCutoff() in older code - it also unburies at the start of + /// a new day. fn sched_timing_today(&mut self, _input: pb::Empty) -> Result { - self.with_col(|col| col.timing_today().map(Into::into)) + self.with_col(|col| { + let timing = col.timing_today()?; + col.unbury_if_day_rolled_over(timing)?; + Ok(timing.into()) + }) } fn local_minutes_west(&mut self, input: pb::Int64) -> BackendResult { @@ -502,6 +514,23 @@ impl BackendService for Backend { self.with_col(|col| col.congrats_info()) } + fn restore_buried_and_suspended_cards(&mut self, input: pb::CardIDs) -> BackendResult { + self.with_col(|col| { + col.unbury_or_unsuspend_cards(&input.into_native()) + .map(Into::into) + }) + } + + fn unbury_cards_in_current_deck( + &mut self, + input: pb::UnburyCardsInCurrentDeckIn, + ) -> BackendResult { + self.with_col(|col| { + col.unbury_cards_in_current_deck(input.mode()) + .map(Into::into) + }) + } + // statistics //----------------------------------------------- @@ -695,7 +724,7 @@ impl BackendService for Backend { .storage .get_card(card.id)? .ok_or_else(|| AnkiError::invalid_input("missing card"))?; - ctx.update_card(&mut card, &orig) + ctx.update_card(&mut card, &orig, ctx.usn()?) }) }) .map(Into::into) diff --git a/rslib/src/card.rs b/rslib/src/card.rs index 7db495bdb..e7c275371 100644 --- a/rslib/src/card.rs +++ b/rslib/src/card.rs @@ -97,6 +97,11 @@ impl Default for Card { } impl Card { + pub fn set_modified(&mut self, usn: Usn) { + self.mtime = TimestampSecs::now(); + self.usn = usn; + } + pub(crate) fn return_home(&mut self, sched: SchedulerVersion) { if self.odid.0 == 0 { // this should not happen @@ -149,34 +154,17 @@ impl Card { self.ctype = CardType::New; } } - - pub(crate) fn restore_queue_after_bury_or_suspend(&mut self) { - self.queue = match self.ctype { - CardType::Learn | CardType::Relearn => { - let original_due = if self.odue > 0 { self.odue } else { self.due }; - if original_due > 1_000_000_000 { - // previous interval was in seconds - CardQueue::Learn - } else { - // previous interval was in days - CardQueue::DayLearn - } - } - CardType::New => CardQueue::New, - CardType::Review => CardQueue::Review, - } - } } #[derive(Debug)] pub(crate) struct UpdateCardUndo(Card); impl Undoable for UpdateCardUndo { - fn apply(&self, col: &mut crate::collection::Collection) -> Result<()> { + fn apply(&self, col: &mut crate::collection::Collection, usn: Usn) -> Result<()> { let current = col .storage .get_card(self.0.id)? .ok_or_else(|| AnkiError::invalid_input("card disappeared"))?; - col.update_card(&mut self.0.clone(), ¤t) + col.update_card(&mut self.0.clone(), ¤t, usn) } } @@ -202,19 +190,18 @@ impl Collection { .ok_or_else(|| AnkiError::invalid_input("no such card"))?; let mut card = orig.clone(); func(&mut card)?; - self.update_card(&mut card, &orig)?; + self.update_card(&mut card, &orig, self.usn()?)?; Ok(card) } - pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card) -> Result<()> { + pub(crate) fn update_card(&mut self, card: &mut Card, original: &Card, usn: Usn) -> Result<()> { if card.id.0 == 0 { return Err(AnkiError::invalid_input("card id not set")); } self.state .undo .save_undoable(Box::new(UpdateCardUndo(original.clone()))); - card.mtime = TimestampSecs::now(); - card.usn = self.usn()?; + card.set_modified(usn); self.storage.update_card(card) } diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs new file mode 100644 index 000000000..98410b19a --- /dev/null +++ b/rslib/src/sched/bury_and_suspend.rs @@ -0,0 +1,133 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + backend_proto as pb, + card::{Card, CardID, CardQueue, CardType}, + collection::Collection, + err::Result, +}; + +use super::cutoff::SchedTimingToday; +use pb::unbury_cards_in_current_deck_in::Mode as UnburyDeckMode; + +impl Card { + /// True if card was buried/suspended prior to the call. + pub(crate) fn restore_queue_after_bury_or_suspend(&mut self) -> bool { + if !matches!( + self.queue, + CardQueue::Suspended | CardQueue::SchedBuried | CardQueue::UserBuried + ) { + false + } else { + self.queue = match self.ctype { + CardType::Learn | CardType::Relearn => { + let original_due = if self.odue > 0 { self.odue } else { self.due }; + if original_due > 1_000_000_000 { + // previous interval was in seconds + CardQueue::Learn + } else { + // previous interval was in days + CardQueue::DayLearn + } + } + CardType::New => CardQueue::New, + CardType::Review => CardQueue::Review, + }; + true + } + } +} + +impl Collection { + pub(crate) fn unbury_if_day_rolled_over(&mut self, timing: SchedTimingToday) -> Result<()> { + let last_unburied = self.get_last_unburied_day(); + let today = timing.days_elapsed; + if last_unburied < today || (today + 7) < last_unburied { + self.unbury_on_day_rollover()?; + self.set_last_unburied_day(today)?; + } + + Ok(()) + } + + /// Unbury cards from the previous day. + /// Done automatically, and does not mark the cards as modified. + fn unbury_on_day_rollover(&mut self) -> Result<()> { + self.search_cards_into_table("is:buried")?; + self.storage.for_each_card_in_search(|mut card| { + card.restore_queue_after_bury_or_suspend(); + self.storage.update_card(&card) + })?; + self.clear_searched_cards() + } + + /// Unsuspend/unbury cards in search table, and clear it. + /// Marks the cards as modified. + fn unsuspend_or_unbury_searched_cards(&mut self) -> Result<()> { + let usn = self.usn()?; + for original in self.storage.all_searched_cards()? { + let mut card = original.clone(); + if card.restore_queue_after_bury_or_suspend() { + self.update_card(&mut card, &original, usn)?; + } + } + self.clear_searched_cards() + } + + pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { + self.transact(None, |col| { + col.set_search_table_to_card_ids(cids)?; + col.unsuspend_or_unbury_searched_cards() + }) + } + + pub fn unbury_cards_in_current_deck(&mut self, mode: UnburyDeckMode) -> Result<()> { + let search = match mode { + UnburyDeckMode::All => "is:buried", + UnburyDeckMode::UserOnly => "is:buried-manually", + UnburyDeckMode::SchedOnly => "is:buried-sibling", + }; + self.transact(None, |col| { + col.search_cards_into_table(&format!("deck:current {}", search))?; + col.unsuspend_or_unbury_searched_cards() + }) + } +} + +#[cfg(test)] +mod test { + use crate::{ + card::{Card, CardQueue}, + collection::{open_test_collection, Collection}, + search::SortMode, + }; + + #[test] + fn unbury() { + let mut col = open_test_collection(); + let mut card = Card::default(); + card.queue = CardQueue::UserBuried; + col.add_card(&mut card).unwrap(); + let assert_count = |col: &mut Collection, cnt| { + assert_eq!( + col.search_cards("is:buried", SortMode::NoOrder) + .unwrap() + .len(), + cnt + ); + }; + assert_count(&mut col, 1); + // day 0, last unburied 0, so no change + let timing = col.timing_today().unwrap(); + col.unbury_if_day_rolled_over(timing).unwrap(); + assert_count(&mut col, 1); + // move creation time back and it should succeed + let mut stamp = col.storage.creation_stamp().unwrap(); + stamp.0 -= 86_400; + col.storage.set_creation_stamp(stamp).unwrap(); + let timing = col.timing_today().unwrap(); + col.unbury_if_day_rolled_over(timing).unwrap(); + assert_count(&mut col, 0); + } +} diff --git a/rslib/src/sched/mod.rs b/rslib/src/sched/mod.rs index ba7ddf143..f7c03a840 100644 --- a/rslib/src/sched/mod.rs +++ b/rslib/src/sched/mod.rs @@ -5,6 +5,7 @@ use crate::{ collection::Collection, config::SchedulerVersion, err::Result, timestamp::TimestampSecs, }; +pub mod bury_and_suspend; pub(crate) mod congrats; pub mod cutoff; pub mod timespan; @@ -79,61 +80,4 @@ impl Collection { SchedulerVersion::V2 => self.set_v2_rollover(hour as u32), } } - - pub(crate) fn unbury_if_day_rolled_over(&mut self) -> Result<()> { - let last_unburied = self.get_last_unburied_day(); - let today = self.timing_today()?.days_elapsed; - if last_unburied < today || (today + 7) < last_unburied { - self.unbury_on_day_rollover()?; - self.set_last_unburied_day(today)?; - } - - Ok(()) - } - - fn unbury_on_day_rollover(&mut self) -> Result<()> { - self.search_cards_into_table("is:buried")?; - self.storage.for_each_card_in_search(|mut card| { - card.restore_queue_after_bury_or_suspend(); - self.storage.update_card(&card) - })?; - self.clear_searched_cards()?; - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::{ - card::{Card, CardQueue}, - collection::{open_test_collection, Collection}, - search::SortMode, - }; - - #[test] - fn unbury() { - let mut col = open_test_collection(); - let mut card = Card::default(); - card.queue = CardQueue::UserBuried; - col.add_card(&mut card).unwrap(); - let assert_count = |col: &mut Collection, cnt| { - assert_eq!( - col.search_cards("is:buried", SortMode::NoOrder) - .unwrap() - .len(), - cnt - ); - }; - assert_count(&mut col, 1); - // day 0, last unburied 0, so no change - col.unbury_if_day_rolled_over().unwrap(); - assert_count(&mut col, 1); - // move creation time back and it should succeed - let mut stamp = col.storage.creation_stamp().unwrap(); - stamp.0 -= 86_400; - col.storage.set_creation_stamp(stamp).unwrap(); - col.unbury_if_day_rolled_over().unwrap(); - assert_count(&mut col, 0); - } } diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 3ce336944..4be6d3648 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -5,12 +5,10 @@ use super::{ parser::Node, sqlwriter::{RequiredTable, SqlWriter}, }; -use crate::card::CardID; -use crate::card::CardType; -use crate::collection::Collection; -use crate::config::SortKind; -use crate::err::Result; -use crate::search::parser::parse; +use crate::{ + card::CardID, card::CardType, collection::Collection, config::SortKind, err::Result, + search::parser::parse, +}; use rusqlite::NO_PARAMS; #[derive(Debug, PartialEq, Clone)] @@ -95,10 +93,9 @@ impl Collection { let writer = SqlWriter::new(self); let (sql, args) = writer.build_cards_query(&top_node, RequiredTable::Cards)?; - self.storage.db.execute_batch(concat!( - "drop table if exists search_cids;", - "create temporary table search_cids (id integer primary key not null);" - ))?; + self.storage + .db + .execute_batch(include_str!("search_cids_setup.sql"))?; let sql = format!("insert into search_cids {}", sql); self.storage.db.prepare(&sql)?.execute(&args)?; @@ -106,6 +103,24 @@ impl Collection { Ok(()) } + /// Injects the provided card IDs into the search_cids table, for + /// when ids have arrived outside of a search. + /// Clear with clear_searched_cards(). + pub(crate) fn set_search_table_to_card_ids(&mut self, cards: &[CardID]) -> Result<()> { + self.storage + .db + .execute_batch(include_str!("search_cids_setup.sql"))?; + let mut stmt = self + .storage + .db + .prepare_cached("insert into search_cids values (?)")?; + for cid in cards { + stmt.execute(&[cid])?; + } + + Ok(()) + } + pub(crate) fn clear_searched_cards(&self) -> Result<()> { self.storage .db diff --git a/rslib/src/search/search_cids_setup.sql b/rslib/src/search/search_cids_setup.sql new file mode 100644 index 000000000..c49ea0206 --- /dev/null +++ b/rslib/src/search/search_cids_setup.sql @@ -0,0 +1,2 @@ +drop table if exists search_cids; +create temporary table search_cids (id integer primary key not null); \ No newline at end of file diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 8fe9791af..4117244c5 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -279,7 +279,7 @@ impl super::SqliteStorage { Ok(()) } - + pub(crate) fn congrats_info(&self, current: &Deck, today: u32) -> Result { self.update_active_decks(current)?; self.db diff --git a/rslib/src/sync/mod.rs b/rslib/src/sync/mod.rs index d3fe5f2a9..961704600 100644 --- a/rslib/src/sync/mod.rs +++ b/rslib/src/sync/mod.rs @@ -319,7 +319,8 @@ where SyncActionRequired::FullSyncRequired { .. } => Ok(state.into()), SyncActionRequired::NormalSyncRequired => { self.col.storage.begin_trx()?; - self.col.unbury_if_day_rolled_over()?; + self.col + .unbury_if_day_rolled_over(self.col.timing_today()?)?; match self.normal_sync_inner(state).await { Ok(success) => { self.col.storage.commit_trx()?; diff --git a/rslib/src/undo.rs b/rslib/src/undo.rs index 87c1f3818..bc84add78 100644 --- a/rslib/src/undo.rs +++ b/rslib/src/undo.rs @@ -4,11 +4,13 @@ use crate::{ collection::{Collection, CollectionOp}, err::Result, + types::Usn, }; use std::fmt; pub(crate) trait Undoable: fmt::Debug + Send { - fn apply(&self, ctx: &mut Collection) -> Result<()>; + /// Undo the recorded action. + fn apply(&self, ctx: &mut Collection, usn: Usn) -> Result<()>; } #[derive(Debug)] @@ -97,8 +99,9 @@ impl Collection { let changes = step.changes; self.state.undo.mode = UndoMode::Undoing; let res = self.transact(Some(step.kind), |col| { + let usn = col.usn()?; for change in changes.iter().rev() { - change.apply(col)?; + change.apply(col, usn)?; } Ok(()) }); @@ -113,8 +116,9 @@ impl Collection { let changes = step.changes; self.state.undo.mode = UndoMode::Redoing; let res = self.transact(Some(step.kind), |col| { + let usn = col.usn()?; for change in changes.iter().rev() { - change.apply(col)?; + change.apply(col, usn)?; } Ok(()) });