Anki/rslib/src/decks/mod.rs
Damien Elmes fa625d7ad8
Minor Rust cleanups (#2272)
* Run cargo +nightly fmt

* Latest prost-build includes clippy workaround

* Tweak Rust protobuf imports

- Avoid use of stringify!(), as JetBrains editors get confused by it
- Stop merging all protobuf symbols into a single namespace

* Remove some unnecessary qualifications

Found via IntelliJ lint

* Migrate some asserts to assert_eq/ne

* Remove mention of node_modules exclusion

This no longer seems to be necessary after migrating away from Bazel,
and excluding it means TS/Svelte files can't be edited properly.
2022-12-16 21:40:27 +10:00

322 lines
9.7 KiB
Rust

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
mod addupdate;
mod counts;
mod current;
mod filtered;
pub(crate) mod limits;
mod name;
mod remove;
mod reparent;
mod schema11;
mod stats;
mod tree;
pub(crate) mod undo;
use std::sync::Arc;
pub(crate) use counts::DueCounts;
pub(crate) use name::immediate_parent_name;
pub use name::NativeDeckName;
pub use schema11::DeckSchema11;
pub use crate::pb::decks::{
deck::{
filtered::{search_term::Order as FilteredSearchOrder, SearchTerm as FilteredSearchTerm},
kind_container::Kind as DeckKind,
Common as DeckCommon, Filtered as FilteredDeck, KindContainer as DeckKindContainer,
Normal as NormalDeck,
},
Deck as DeckProto,
};
use crate::{
define_newtype, error::FilteredDeckError, markdown::render_markdown, prelude::*,
text::sanitize_html_no_images,
};
define_newtype!(DeckId, i64);
impl DeckId {
pub(crate) fn or(self, other: DeckId) -> Self {
if self.0 == 0 {
other
} else {
self
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Deck {
pub id: DeckId,
pub name: NativeDeckName,
pub mtime_secs: TimestampSecs,
pub usn: Usn,
pub common: DeckCommon,
pub kind: DeckKind,
}
impl Deck {
pub fn new_normal() -> Deck {
Deck {
id: DeckId(0),
name: NativeDeckName::from_native_str(""),
mtime_secs: TimestampSecs(0),
usn: Usn(0),
common: DeckCommon {
study_collapsed: true,
browser_collapsed: true,
..Default::default()
},
kind: DeckKind::Normal(NormalDeck {
config_id: 1,
// enable in the future
// markdown_description = true,
..Default::default()
}),
}
}
/// Returns deck config ID if deck is a normal deck.
pub(crate) fn config_id(&self) -> Option<DeckConfigId> {
if let DeckKind::Normal(ref norm) = self.kind {
Some(DeckConfigId(norm.config_id))
} else {
None
}
}
// used by tests at the moment
#[allow(dead_code)]
pub(crate) fn normal(&self) -> Result<&NormalDeck> {
match &self.kind {
DeckKind::Normal(normal) => Ok(normal),
_ => invalid_input!("deck not normal"),
}
}
#[allow(dead_code)]
pub(crate) fn normal_mut(&mut self) -> Result<&mut NormalDeck> {
match &mut self.kind {
DeckKind::Normal(normal) => Ok(normal),
_ => invalid_input!("deck not normal"),
}
}
pub(crate) fn filtered(&self) -> Result<&FilteredDeck> {
if let DeckKind::Filtered(filtered) = &self.kind {
Ok(filtered)
} else {
Err(FilteredDeckError::FilteredDeckRequired.into())
}
}
#[allow(dead_code)]
pub(crate) fn filtered_mut(&mut self) -> Result<&mut FilteredDeck> {
if let DeckKind::Filtered(filtered) = &mut self.kind {
Ok(filtered)
} else {
Err(FilteredDeckError::FilteredDeckRequired.into())
}
}
pub(crate) fn set_modified(&mut self, usn: Usn) {
self.mtime_secs = TimestampSecs::now();
self.usn = usn;
}
pub fn rendered_description(&self) -> String {
if let DeckKind::Normal(normal) = &self.kind {
if normal.markdown_description {
let description = render_markdown(&normal.description);
// before allowing images, we'll need to handle relative image
// links on the various platforms
sanitize_html_no_images(&description)
} else {
String::new()
}
} else {
String::new()
}
}
}
impl Collection {
pub fn get_or_create_normal_deck(&mut self, human_name: &str) -> Result<Deck> {
let name = NativeDeckName::from_human_name(human_name);
if let Some(did) = self.storage.get_deck_id(name.as_native_str())? {
self.storage.get_deck(did).map(|opt| opt.unwrap())
} else {
let mut deck = Deck::new_normal();
deck.name = name;
self.add_or_update_deck(&mut deck)?;
Ok(deck)
}
}
}
impl Collection {
pub(crate) fn get_deck(&mut self, did: DeckId) -> Result<Option<Arc<Deck>>> {
if let Some(deck) = self.state.deck_cache.get(&did) {
return Ok(Some(deck.clone()));
}
if let Some(deck) = self.storage.get_deck(did)? {
let deck = Arc::new(deck);
self.state.deck_cache.insert(did, deck.clone());
Ok(Some(deck))
} else {
Ok(None)
}
}
pub(crate) fn default_deck_is_empty(&self) -> Result<bool> {
self.storage.deck_is_empty(DeckId(1))
}
/// Get a deck based on its human name. If you have a machine name,
/// use the method in storage instead.
pub(crate) fn get_deck_id(&self, human_name: &str) -> Result<Option<DeckId>> {
self.storage
.get_deck_id(NativeDeckName::from_human_name(human_name).as_native_str())
}
}
#[cfg(test)]
mod test {
use crate::{collection::open_test_collection, prelude::*, search::SortMode};
fn sorted_names(col: &Collection) -> Vec<String> {
col.storage
.get_all_deck_names()
.unwrap()
.into_iter()
.map(|d| d.1)
.collect()
}
#[test]
fn adding_updating() -> Result<()> {
let mut col = open_test_collection();
let deck1 = col.get_or_create_normal_deck("foo")?;
let deck2 = col.get_or_create_normal_deck("FOO")?;
assert_eq!(deck1.id, deck2.id);
assert_eq!(sorted_names(&col), vec!["Default", "foo"]);
// missing parents should be automatically created, and case should match
// existing parents
let _deck3 = col.get_or_create_normal_deck("FOO::BAR::BAZ")?;
assert_eq!(
sorted_names(&col),
vec!["Default", "foo", "foo::BAR", "foo::BAR::BAZ"]
);
Ok(())
}
#[test]
fn renaming() -> Result<()> {
let mut col = open_test_collection();
let _ = col.get_or_create_normal_deck("foo::bar::baz")?;
let mut top_deck = col.get_or_create_normal_deck("foo")?;
top_deck.name = NativeDeckName::from_native_str("other");
col.add_or_update_deck(&mut top_deck)?;
assert_eq!(
sorted_names(&col),
vec!["Default", "other", "other::bar", "other::bar::baz"]
);
// should do the right thing in the middle of the tree as well
let mut middle = col.get_or_create_normal_deck("other::bar")?;
middle.name = NativeDeckName::from_native_str("quux\x1ffoo");
col.add_or_update_deck(&mut middle)?;
assert_eq!(
sorted_names(&col),
vec!["Default", "other", "quux", "quux::foo", "quux::foo::baz"]
);
// add another child
let _ = col.get_or_create_normal_deck("quux::foo::baz2");
// quux::foo -> quux::foo::baz::four
// means quux::foo::baz2 should be quux::foo::baz::four::baz2
// and a new quux::foo should have been created
middle.name = NativeDeckName::from_native_str("quux\x1ffoo\x1fbaz\x1ffour");
col.add_or_update_deck(&mut middle)?;
assert_eq!(
sorted_names(&col),
vec![
"Default",
"other",
"quux",
"quux::foo",
"quux::foo::baz",
"quux::foo::baz::four",
"quux::foo::baz::four::baz",
"quux::foo::baz::four::baz2"
]
);
// should handle name conflicts
middle.name = NativeDeckName::from_native_str("other");
col.add_or_update_deck(&mut middle)?;
assert_eq!(middle.name.as_native_str(), "other+");
// public function takes human name
col.rename_deck(middle.id, "one::two")?;
assert_eq!(
sorted_names(&col),
vec![
"Default",
"one",
"one::two",
"one::two::baz",
"one::two::baz2",
"other",
"quux",
"quux::foo",
"quux::foo::baz",
]
);
Ok(())
}
#[test]
fn default() -> Result<()> {
// deleting the default deck will remove cards, but bring the deck back
// as a top level deck
let mut col = open_test_collection();
let mut default = col.get_or_create_normal_deck("default")?;
default.name = NativeDeckName::from_native_str("one\x1ftwo");
col.add_or_update_deck(&mut default)?;
// create a non-default deck confusingly named "default"
let _fake_default = col.get_or_create_normal_deck("default")?;
// add a card to the real default
let nt = col.get_notetype_by_name("Basic")?.unwrap();
let mut note = nt.new_note();
col.add_note(&mut note, default.id)?;
assert_ne!(col.search_cards("", SortMode::NoOrder)?, vec![]);
// add a subdeck
let _ = col.get_or_create_normal_deck("one::two::three")?;
// delete top level
let top = col.get_or_create_normal_deck("one")?;
col.remove_decks_and_child_decks(&[top.id])?;
// should have come back as "Default+" due to conflict
assert_eq!(sorted_names(&col), vec!["default", "Default+"]);
// and the cards it contained should have been removed
assert_eq!(col.search_cards("", SortMode::NoOrder)?, vec![]);
Ok(())
}
}