From 9f3cc0982d23a936903d37f76e6c1d11349e677f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 17 Mar 2020 17:02:58 +1000 Subject: [PATCH] deck searching A bit more complicated than it needs to be, as we don't have the full deck manager infrastructure yet. --- rslib/src/config.rs | 11 ++++++ rslib/src/decks.rs | 29 +++++++++++++++ rslib/src/lib.rs | 2 ++ rslib/src/search/searcher.rs | 70 +++++++++++++++++++++++++----------- rslib/src/storage/sqlite.rs | 17 ++++++++- rslib/src/text.rs | 21 +++++++++++ 6 files changed, 129 insertions(+), 21 deletions(-) create mode 100644 rslib/src/config.rs create mode 100644 rslib/src/decks.rs diff --git a/rslib/src/config.rs b/rslib/src/config.rs new file mode 100644 index 000000000..2394819c9 --- /dev/null +++ b/rslib/src/config.rs @@ -0,0 +1,11 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::types::ObjID; +use serde_derive::Deserialize; + +#[derive(Deserialize)] +pub struct Config { + #[serde(rename = "curDeck")] + pub(crate) current_deck_id: ObjID, +} diff --git a/rslib/src/decks.rs b/rslib/src/decks.rs new file mode 100644 index 000000000..9bf8e7e54 --- /dev/null +++ b/rslib/src/decks.rs @@ -0,0 +1,29 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +use crate::types::ObjID; +use serde_derive::Deserialize; + +#[derive(Deserialize)] +pub struct Deck { + pub(crate) id: ObjID, + pub(crate) name: String, +} + +pub(crate) fn child_ids<'a>(decks: &'a [Deck], name: &str) -> impl Iterator + 'a { + let prefix = format!("{}::", name); + decks + .iter() + .filter(move |d| d.name.starts_with(&prefix)) + .map(|d| d.id) +} + +pub(crate) fn get_deck(decks: &[Deck], id: ObjID) -> Option<&Deck> { + for d in decks { + if d.id == id { + return Some(d); + } + } + + None +} diff --git a/rslib/src/lib.rs b/rslib/src/lib.rs index b2e410d8c..37a8db7f6 100644 --- a/rslib/src/lib.rs +++ b/rslib/src/lib.rs @@ -13,6 +13,8 @@ pub mod backend; pub mod card; pub mod cloze; pub mod collection; +pub mod config; +pub mod decks; pub mod err; pub mod i18n; pub mod latex; diff --git a/rslib/src/search/searcher.rs b/rslib/src/search/searcher.rs index 65086e5d6..dc9f375a9 100644 --- a/rslib/src/search/searcher.rs +++ b/rslib/src/search/searcher.rs @@ -3,7 +3,11 @@ use super::parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind}; use crate::card::CardQueue; +use crate::decks::child_ids; +use crate::decks::get_deck; +use crate::err::{AnkiError, Result}; use crate::notes::field_checksum; +use crate::text::matches_wildcard; use crate::{ collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID, }; @@ -21,34 +25,35 @@ struct SearchContext<'a> { fn node_to_sql<'a>( ctx: &'a mut RequestContext<'a>, node: &'a Node, -) -> (String, Vec>) { +) -> Result<(String, Vec>)> { let sql = String::new(); let args = vec![]; let mut sctx = SearchContext { ctx, sql, args }; - write_node_to_sql(&mut sctx, node); - (sctx.sql, sctx.args) + write_node_to_sql(&mut sctx, node)?; + Ok((sctx.sql, sctx.args)) } -fn write_node_to_sql(ctx: &mut SearchContext, node: &Node) { +fn write_node_to_sql(ctx: &mut SearchContext, node: &Node) -> Result<()> { match node { Node::And => write!(ctx.sql, " and ").unwrap(), Node::Or => write!(ctx.sql, " or ").unwrap(), Node::Not(node) => { write!(ctx.sql, "not ").unwrap(); - write_node_to_sql(ctx, node); + write_node_to_sql(ctx, node)?; } Node::Group(nodes) => { write!(ctx.sql, "(").unwrap(); for node in nodes { - write_node_to_sql(ctx, node); + write_node_to_sql(ctx, node)?; } write!(ctx.sql, ")").unwrap(); } - Node::Search(search) => write_search_node_to_sql(ctx, search), - } + Node::Search(search) => write_search_node_to_sql(ctx, search)?, + }; + Ok(()) } -fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) { +fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) -> Result<()> { match node { SearchNode::UnqualifiedText(text) => write_unqualified(ctx, text), SearchNode::SingleField { field, text } => { @@ -58,7 +63,7 @@ fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) { write!(ctx.sql, "c.id > {}", days).unwrap(); } SearchNode::CardTemplate(template) => write_template(ctx, template), - SearchNode::Deck(deck) => write_deck(ctx, deck.as_ref()), + SearchNode::Deck(deck) => write_deck(ctx, deck.as_ref())?, SearchNode::NoteTypeID(ntid) => { write!(ctx.sql, "n.mid = {}", ntid).unwrap(); } @@ -77,7 +82,8 @@ fn write_search_node_to_sql(ctx: &mut SearchContext, node: &SearchNode) { write!(ctx.sql, "c.id in ({})", cids).unwrap(); } SearchNode::Property { operator, kind } => write_prop(ctx, operator, kind), - } + }; + Ok(()) } fn write_unqualified(ctx: &mut SearchContext, text: &str) { @@ -180,19 +186,43 @@ fn write_state(ctx: &mut SearchContext, state: &StateKind) { .unwrap() } -// fixme: need deck manager -fn write_deck(ctx: &mut SearchContext, deck: &str) { +fn write_deck(ctx: &mut SearchContext, deck: &str) -> Result<()> { match deck { "*" => write!(ctx.sql, "true").unwrap(), "filtered" => write!(ctx.sql, "c.odid > 0").unwrap(), - "current" => { - todo!() // fixme: need current deck and child decks + deck => { + let all_decks = ctx.ctx.storage.all_decks()?; + let dids_with_children = if deck == "current" { + let config = ctx.ctx.storage.all_config()?; + let mut dids_with_children = vec![config.current_deck_id]; + let current = get_deck(&all_decks, config.current_deck_id) + .ok_or_else(|| AnkiError::invalid_input("invalid current deck"))?; + for child_did in child_ids(&all_decks, ¤t.name) { + dids_with_children.push(child_did); + } + dids_with_children + } else { + let mut dids_with_children = vec![]; + for deck in all_decks.iter().filter(|d| matches_wildcard(&d.name, deck)) { + dids_with_children.push(deck.id); + for child_id in child_ids(&all_decks, &deck.name) { + dids_with_children.push(child_id); + } + } + dids_with_children + }; + + if dids_with_children.is_empty() { + write!(ctx.sql, "false") + } else { + let did_strings: Vec = + dids_with_children.iter().map(ToString::to_string).collect(); + write!(ctx.sql, "c.did in ({})", did_strings.join(",")) + } + .unwrap(); } - _deck => { - // fixme: narrow to dids matching possible wildcard; include children - todo!() - } - } + }; + Ok(()) } // fixme: need note type manager diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs index 967e79727..a689b34c5 100644 --- a/rslib/src/storage/sqlite.rs +++ b/rslib/src/storage/sqlite.rs @@ -2,10 +2,11 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::collection::CollectionOp; +use crate::config::Config; use crate::err::Result; use crate::err::{AnkiError, DBErrorKind}; use crate::time::{i64_unix_millis, i64_unix_secs}; -use crate::types::Usn; +use crate::{decks::Deck, types::Usn}; use rusqlite::{params, Connection, NO_PARAMS}; use std::path::{Path, PathBuf}; @@ -210,4 +211,18 @@ impl StorageContext<'_> { Ok(-1) } } + + pub(crate) fn all_decks(&self) -> Result> { + self.db + .query_row_and_then("select decks from col", NO_PARAMS, |row| -> Result<_> { + Ok(serde_json::from_str(row.get_raw(0).as_str()?)?) + }) + } + + pub(crate) fn all_config(&self) -> Result { + self.db + .query_row_and_then("select conf from col", NO_PARAMS, |row| -> Result<_> { + Ok(serde_json::from_str(row.get_raw(0).as_str()?)?) + }) + } } diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 4d5ff286d..f7a770cc7 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -219,8 +219,20 @@ pub(crate) fn normalize_to_nfc(s: &str) -> Cow { } } +/// True if search is equal to text, folding ascii case. +/// Supports '*' to match 0 or more characters. +pub(crate) fn matches_wildcard(text: &str, search: &str) -> bool { + if search.contains('*') { + let search = format!("^(?i){}$", regex::escape(search).replace(r"\*", ".*")); + Regex::new(&search).unwrap().is_match(text) + } else { + text.eq_ignore_ascii_case(search) + } +} + #[cfg(test)] mod test { + use super::matches_wildcard; use crate::text::{ extract_av_tags, strip_av_tags, strip_html, strip_html_preserving_image_filenames, AVTag, }; @@ -265,4 +277,13 @@ mod test { ] ); } + + #[test] + fn wildcard() { + assert_eq!(matches_wildcard("foo", "bar"), false); + assert_eq!(matches_wildcard("foo", "Foo"), true); + assert_eq!(matches_wildcard("foo", "F*"), true); + assert_eq!(matches_wildcard("foo", "F*oo"), true); + assert_eq!(matches_wildcard("foo", "b*"), false); + } }