diff --git a/proto/backend.proto b/proto/backend.proto index 95af6b8f1..483f69fc1 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -107,6 +107,9 @@ service BackendService { rpc EmptyFilteredDeck (DeckID) returns (Empty); rpc RebuildFilteredDeck (DeckID) returns (UInt32); rpc ScheduleCardsAsReviews (ScheduleCardsAsReviewsIn) returns (Empty); + rpc ScheduleCardsAsNew (CardIDs) returns (Empty); + rpc SortCards (SortCardsIn) returns (Empty); + rpc SortDeck (SortDeckIn) returns (Empty); // stats @@ -1065,3 +1068,16 @@ message ScheduleCardsAsReviewsIn { uint32 min_interval = 2; uint32 max_interval = 3; } + +message SortCardsIn { + repeated int64 card_ids = 1; + uint32 starting_from = 2; + uint32 step_size = 3; + bool randomize = 4; + bool shift_existing = 5; +} + +message SortDeckIn { + int64 deck_id = 1; + bool randomize = 2; +} diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 87b2e373d..b91aed151 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -15,7 +15,6 @@ from typing import ( List, Optional, Sequence, - Set, Tuple, Union, ) @@ -1401,26 +1400,16 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", # Resetting ########################################################################## - def forgetCards(self, ids: List[int]) -> None: + def schedule_cards_as_new(self, card_ids: List[int]) -> None: "Put cards at the end of the new queue." - self.remFromDyn(ids) - self.col.db.execute( - f"update cards set type={CARD_TYPE_NEW},queue={QUEUE_TYPE_NEW},ivl=0,due=0,odue=0,factor=?" - " where id in " + ids2str(ids), - STARTING_FACTOR, - ) - pmax = ( - self.col.db.scalar(f"select max(due) from cards where type={CARD_TYPE_NEW}") - or 0 - ) - # takes care of mod + usn - self.sortCards(ids, start=pmax + 1) - self.col.log(ids) + self.col.backend.schedule_cards_as_new(card_ids) - def reschedCards(self, ids: List[int], imin: int, imax: int) -> None: - "Put cards in review queue with a new interval in days (min, max)." + def schedule_cards_as_reviews( + self, card_ids: List[int], min_interval: int, max_interval: int + ) -> None: + "Make cards review cards, with a new interval randomly selected from range." self.col.backend.schedule_cards_as_reviews( - card_ids=ids, min_interval=imin, max_interval=imax + card_ids=card_ids, min_interval=min_interval, max_interval=max_interval ) def resetCards(self, ids: List[int]) -> None: @@ -1440,6 +1429,11 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", self.forgetCards(nonNew) self.col.log(ids) + # legacy + + forgetCards = schedule_cards_as_new + reschedCards = schedule_cards_as_reviews + # Repositioning new cards ########################################################################## @@ -1451,60 +1445,19 @@ and (queue={QUEUE_TYPE_NEW} or (queue={QUEUE_TYPE_REV} and due<=?))""", shuffle: bool = False, shift: bool = False, ) -> None: - scids = ids2str(cids) - now = intTime() - nids = [] - nidsSet: Set[int] = set() - for id in cids: - nid = self.col.db.scalar("select nid from cards where id = ?", id) - if nid not in nidsSet: - nids.append(nid) - nidsSet.add(nid) - if not nids: - # no new cards - return - # determine nid ordering - due = {} - if shuffle: - random.shuffle(nids) - for c, nid in enumerate(nids): - due[nid] = start + c * step - # pylint: disable=undefined-loop-variable - high = start + c * step - # shift? - if shift: - low = self.col.db.scalar( - f"select min(due) from cards where due >= ? and type = {CARD_TYPE_NEW} " - "and id not in %s" % scids, - start, - ) - if low is not None: - shiftby = high - low + 1 - self.col.db.execute( - f""" -update cards set mod=?, usn=?, due=due+? where id not in %s -and due >= ? and queue = {QUEUE_TYPE_NEW}""" - % scids, - now, - self.col.usn(), - shiftby, - low, - ) - # reorder cards - d = [] - for id, nid in self.col.db.execute( - f"select id, nid from cards where type = {CARD_TYPE_NEW} and id in " + scids - ): - d.append((due[nid], now, self.col.usn(), id)) - self.col.db.executemany("update cards set due=?,mod=?,usn=? where id = ?", d) + self.col.backend.sort_cards( + card_ids=cids, + starting_from=start, + step_size=step, + randomize=shuffle, + shift_existing=shift, + ) def randomizeCards(self, did: int) -> None: - cids = self.col.db.list("select id from cards where did = ?", did) - self.sortCards(cids, shuffle=True) + self.col.backend.sort_deck(deck_id=did, randomize=True) def orderCards(self, did: int) -> None: - cids = self.col.db.list("select id from cards where did = ? order by nid", did) - self.sortCards(cids) + self.col.backend.sort_deck(deck_id=did, randomize=False) def resortConf(self, conf) -> None: for did in self.col.decks.didsForConf(conf): diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index e157ba0fc..281f37bbe 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -569,6 +569,34 @@ impl BackendService for Backend { }) } + fn schedule_cards_as_new(&mut self, input: pb::CardIDs) -> BackendResult { + self.with_col(|col| { + col.reschedule_cards_as_new(&input.into_native()) + .map(Into::into) + }) + } + + fn sort_cards(&mut self, input: pb::SortCardsIn) -> BackendResult { + let cids: Vec<_> = input.card_ids.into_iter().map(CardID).collect(); + let (start, step, random, shift) = ( + input.starting_from, + input.step_size, + input.randomize, + input.shift_existing, + ); + self.with_col(|col| { + col.sort_cards(&cids, start, step, random, shift) + .map(Into::into) + }) + } + + fn sort_deck(&mut self, input: pb::SortDeckIn) -> BackendResult { + self.with_col(|col| { + col.sort_deck(input.deck_id.into(), input.randomize) + .map(Into::into) + }) + } + // statistics //----------------------------------------------- diff --git a/rslib/src/config.rs b/rslib/src/config.rs index b3c2e845e..70294ef1f 100644 --- a/rslib/src/config.rs +++ b/rslib/src/config.rs @@ -179,6 +179,10 @@ impl Collection { self.set_config(ConfigKey::CurrentNoteTypeID, &id) } + pub(crate) fn get_next_card_position(&self) -> u32 { + self.get_config_default(ConfigKey::NextNewCardPosition) + } + pub(crate) fn get_and_update_next_card_position(&self) -> Result { let pos: u32 = self .get_config_optional(ConfigKey::NextNewCardPosition) diff --git a/rslib/src/sched/bury_and_suspend.rs b/rslib/src/sched/bury_and_suspend.rs index e14979328..a51c3e416 100644 --- a/rslib/src/sched/bury_and_suspend.rs +++ b/rslib/src/sched/bury_and_suspend.rs @@ -65,7 +65,7 @@ impl Collection { card.restore_queue_after_bury_or_suspend(); self.storage.update_card(&card) })?; - self.clear_searched_cards() + self.storage.clear_searched_cards_table() } /// Unsuspend/unbury cards in search table, and clear it. @@ -78,7 +78,7 @@ impl Collection { self.update_card(&mut card, &original, usn)?; } } - self.clear_searched_cards() + self.storage.clear_searched_cards_table() } pub fn unbury_or_unsuspend_cards(&mut self, cids: &[CardID]) -> Result<()> { @@ -134,7 +134,7 @@ impl Collection { } } - self.clear_searched_cards() + self.storage.clear_searched_cards_table() } pub fn bury_or_suspend_cards( diff --git a/rslib/src/sched/mod.rs b/rslib/src/sched/mod.rs index 8cd709e81..1a90ac43e 100644 --- a/rslib/src/sched/mod.rs +++ b/rslib/src/sched/mod.rs @@ -9,6 +9,7 @@ pub mod bury_and_suspend; pub(crate) mod congrats; pub mod cutoff; mod learning; +pub mod new; mod reviews; pub mod timespan; diff --git a/rslib/src/sched/new.rs b/rslib/src/sched/new.rs new file mode 100644 index 000000000..7ae5ac197 --- /dev/null +++ b/rslib/src/sched/new.rs @@ -0,0 +1,143 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::{ + card::{Card, CardID, CardQueue, CardType}, + collection::Collection, + deckconf::INITIAL_EASE_FACTOR, + decks::DeckID, + err::Result, + notes::NoteID, + search::SortMode, + types::Usn, +}; +use rand::seq::SliceRandom; +use std::collections::{HashMap, HashSet}; + +impl Card { + fn schedule_as_new(&mut self, position: u32) { + self.remove_from_filtered_deck_before_reschedule(); + self.due = position as i32; + self.ctype = CardType::New; + self.queue = CardQueue::New; + self.interval = 0; + if self.ease_factor == 0 { + // unlike the old Python code, we leave the ease factor alone + // if it's already set + self.ease_factor = INITIAL_EASE_FACTOR; + } + } + + /// If the card is new, change its position. + fn set_new_position(&mut self, position: u32) { + if self.queue != CardQueue::New || self.ctype != CardType::New { + return; + } + self.due = position as i32; + } +} +pub(crate) struct NewCardSorter { + position: HashMap, +} + +impl NewCardSorter { + pub(crate) fn new(cards: &[Card], starting_from: u32, step: u32, random: bool) -> Self { + let nids: HashSet<_> = cards.iter().map(|c| c.note_id).collect(); + let mut nids: Vec<_> = nids.into_iter().collect(); + if random { + nids.shuffle(&mut rand::thread_rng()); + } else { + nids.sort_unstable(); + } + + NewCardSorter { + position: nids + .into_iter() + .enumerate() + .map(|(i, nid)| (nid, ((i as u32) * step) + starting_from)) + .collect(), + } + } + + pub(crate) fn position(&self, card: &Card) -> u32 { + self.position + .get(&card.note_id) + .cloned() + .unwrap_or_default() + } +} + +impl Collection { + pub fn reschedule_cards_as_new(&mut self, cids: &[CardID]) -> Result<()> { + let usn = self.usn()?; + let mut position = self.get_next_card_position(); + self.transact(None, |col| { + col.set_search_table_to_card_ids(cids)?; + let cards = col.storage.all_searched_cards()?; + for mut card in cards { + let original = card.clone(); + col.log_manually_scheduled_review(&card, usn, 0)?; + card.schedule_as_new(position); + col.update_card(&mut card, &original, usn)?; + position += 1; + } + col.set_next_card_position(position)?; + col.storage.clear_searched_cards_table()?; + Ok(()) + }) + } + + pub fn sort_cards( + &mut self, + cids: &[CardID], + starting_from: u32, + step: u32, + random: bool, + shift: bool, + ) -> Result<()> { + let usn = self.usn()?; + self.transact(None, |col| { + col.sort_cards_inner(cids, starting_from, step, random, shift, usn) + }) + } + + fn sort_cards_inner( + &mut self, + cids: &[CardID], + starting_from: u32, + step: u32, + random: bool, + shift: bool, + usn: Usn, + ) -> Result<()> { + if shift { + self.shift_existing_cards(starting_from, step * cids.len() as u32, usn)?; + } + self.set_search_table_to_card_ids(cids)?; + let cards = self.storage.all_searched_cards()?; + let sorter = NewCardSorter::new(&cards, starting_from, step, random); + for mut card in cards { + let original = card.clone(); + card.set_new_position(sorter.position(&card)); + self.update_card(&mut card, &original, usn)?; + } + self.storage.clear_searched_cards_table() + } + + /// This creates a transaction - we probably want to split it out + /// in the future if calling it as part of a deck options update. + pub fn sort_deck(&mut self, deck: DeckID, random: bool) -> Result<()> { + let cids = self.search_cards(&format!("did:{}", deck), SortMode::NoOrder)?; + self.sort_cards(&cids, 1, 1, random, false) + } + + fn shift_existing_cards(&mut self, start: u32, by: u32, usn: Usn) -> Result<()> { + self.storage.search_cards_at_or_above_position(start)?; + for mut card in self.storage.all_searched_cards()? { + let original = card.clone(); + card.set_new_position(card.due as u32 + by); + self.update_card(&mut card, &original, usn)?; + } + Ok(()) + } +} diff --git a/rslib/src/sched/reviews.rs b/rslib/src/sched/reviews.rs index e39b22585..1d5b4c945 100644 --- a/rslib/src/sched/reviews.rs +++ b/rslib/src/sched/reviews.rs @@ -44,7 +44,7 @@ impl Collection { card.schedule_as_review(interval, today); col.update_card(&mut card, &original, usn)?; } - col.clear_searched_cards()?; + col.storage.clear_searched_cards_table()?; Ok(()) }) } diff --git a/rslib/src/search/cards.rs b/rslib/src/search/cards.rs index 351abc89b..5f9f5fbd4 100644 --- a/rslib/src/search/cards.rs +++ b/rslib/src/search/cards.rs @@ -9,7 +9,6 @@ use crate::{ card::CardID, card::CardType, collection::Collection, config::SortKind, err::Result, search::parser::parse, }; -use rusqlite::NO_PARAMS; #[derive(Debug, PartialEq, Clone)] pub enum SortMode { @@ -99,9 +98,7 @@ impl Collection { let (mut sql, args) = writer.build_cards_query(&top_node, mode.required_table())?; self.add_order(&mut sql, mode)?; - self.storage - .db - .execute_batch(include_str!("search_cids_setup.sql"))?; + self.storage.setup_searched_cards_table()?; let sql = format!("insert into search_cids {}", sql); self.storage.db.prepare(&sql)?.execute(&args)?; @@ -113,9 +110,7 @@ impl Collection { /// 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"))?; + self.storage.setup_searched_cards_table()?; let mut stmt = self .storage .db @@ -127,13 +122,6 @@ impl Collection { Ok(()) } - pub(crate) fn clear_searched_cards(&self) -> Result<()> { - self.storage - .db - .execute("drop table if exists search_cids", NO_PARAMS)?; - Ok(()) - } - /// If the sort mode is based on a config setting, look it up. fn resolve_config_sort(&self, mode: &mut SortMode) { if mode == &SortMode::FromConfig { diff --git a/rslib/src/stats/graphs.rs b/rslib/src/stats/graphs.rs index 8a2432315..d2afcbee0 100644 --- a/rslib/src/stats/graphs.rs +++ b/rslib/src/stats/graphs.rs @@ -33,7 +33,7 @@ impl Collection { .get_revlog_entries_for_searched_cards(revlog_start)? }; - self.clear_searched_cards()?; + self.storage.clear_searched_cards_table()?; Ok(pb::GraphsOut { cards: cards.into_iter().map(Into::into).collect(), diff --git a/rslib/src/storage/card/at_or_above_position.sql b/rslib/src/storage/card/at_or_above_position.sql new file mode 100644 index 000000000..2621aa55a --- /dev/null +++ b/rslib/src/storage/card/at_or_above_position.sql @@ -0,0 +1,5 @@ +insert into search_cids +select id +from cards +where due >= ? + and type = ? \ No newline at end of file diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 521b5ed81..de7640678 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -315,6 +315,26 @@ impl super::SqliteStorage { .next() .unwrap() } + + pub(crate) fn search_cards_at_or_above_position(&self, start: u32) -> Result<()> { + self.setup_searched_cards_table()?; + self.db + .prepare(include_str!("at_or_above_position.sql"))? + .execute(&[start, CardType::New as u32])?; + Ok(()) + } + + pub(crate) fn setup_searched_cards_table(&self) -> Result<()> { + self.db + .execute_batch(include_str!("search_cids_setup.sql"))?; + Ok(()) + } + + pub(crate) fn clear_searched_cards_table(&self) -> Result<()> { + self.db + .execute("drop table if exists search_cids", NO_PARAMS)?; + Ok(()) + } } #[cfg(test)] diff --git a/rslib/src/search/search_cids_setup.sql b/rslib/src/storage/card/search_cids_setup.sql similarity index 100% rename from rslib/src/search/search_cids_setup.sql rename to rslib/src/storage/card/search_cids_setup.sql