deck searching

A bit more complicated than it needs to be, as we don't have the
full deck manager infrastructure yet.
This commit is contained in:
Damien Elmes 2020-03-17 17:02:58 +10:00
parent f559ae3ef8
commit 9f3cc0982d
6 changed files with 129 additions and 21 deletions

11
rslib/src/config.rs Normal file
View file

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

29
rslib/src/decks.rs Normal file
View file

@ -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<Item = ObjID> + '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
}

View file

@ -13,6 +13,8 @@ pub mod backend;
pub mod card; pub mod card;
pub mod cloze; pub mod cloze;
pub mod collection; pub mod collection;
pub mod config;
pub mod decks;
pub mod err; pub mod err;
pub mod i18n; pub mod i18n;
pub mod latex; pub mod latex;

View file

@ -3,7 +3,11 @@
use super::parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind}; use super::parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind};
use crate::card::CardQueue; 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::notes::field_checksum;
use crate::text::matches_wildcard;
use crate::{ use crate::{
collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID, collection::RequestContext, text::strip_html_preserving_image_filenames, types::ObjID,
}; };
@ -21,34 +25,35 @@ struct SearchContext<'a> {
fn node_to_sql<'a>( fn node_to_sql<'a>(
ctx: &'a mut RequestContext<'a>, ctx: &'a mut RequestContext<'a>,
node: &'a Node, node: &'a Node,
) -> (String, Vec<ToSqlOutput<'a>>) { ) -> Result<(String, Vec<ToSqlOutput<'a>>)> {
let sql = String::new(); let sql = String::new();
let args = vec![]; let args = vec![];
let mut sctx = SearchContext { ctx, sql, args }; let mut sctx = SearchContext { ctx, sql, args };
write_node_to_sql(&mut sctx, node); write_node_to_sql(&mut sctx, node)?;
(sctx.sql, sctx.args) 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 { match node {
Node::And => write!(ctx.sql, " and ").unwrap(), Node::And => write!(ctx.sql, " and ").unwrap(),
Node::Or => write!(ctx.sql, " or ").unwrap(), Node::Or => write!(ctx.sql, " or ").unwrap(),
Node::Not(node) => { Node::Not(node) => {
write!(ctx.sql, "not ").unwrap(); write!(ctx.sql, "not ").unwrap();
write_node_to_sql(ctx, node); write_node_to_sql(ctx, node)?;
} }
Node::Group(nodes) => { Node::Group(nodes) => {
write!(ctx.sql, "(").unwrap(); write!(ctx.sql, "(").unwrap();
for node in nodes { for node in nodes {
write_node_to_sql(ctx, node); write_node_to_sql(ctx, node)?;
} }
write!(ctx.sql, ")").unwrap(); 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 { match node {
SearchNode::UnqualifiedText(text) => write_unqualified(ctx, text), SearchNode::UnqualifiedText(text) => write_unqualified(ctx, text),
SearchNode::SingleField { field, 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(); write!(ctx.sql, "c.id > {}", days).unwrap();
} }
SearchNode::CardTemplate(template) => write_template(ctx, template), 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) => { SearchNode::NoteTypeID(ntid) => {
write!(ctx.sql, "n.mid = {}", ntid).unwrap(); 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(); write!(ctx.sql, "c.id in ({})", cids).unwrap();
} }
SearchNode::Property { operator, kind } => write_prop(ctx, operator, kind), SearchNode::Property { operator, kind } => write_prop(ctx, operator, kind),
} };
Ok(())
} }
fn write_unqualified(ctx: &mut SearchContext, text: &str) { fn write_unqualified(ctx: &mut SearchContext, text: &str) {
@ -180,19 +186,43 @@ fn write_state(ctx: &mut SearchContext, state: &StateKind) {
.unwrap() .unwrap()
} }
// fixme: need deck manager fn write_deck(ctx: &mut SearchContext, deck: &str) -> Result<()> {
fn write_deck(ctx: &mut SearchContext, deck: &str) {
match deck { match deck {
"*" => write!(ctx.sql, "true").unwrap(), "*" => write!(ctx.sql, "true").unwrap(),
"filtered" => write!(ctx.sql, "c.odid > 0").unwrap(), "filtered" => write!(ctx.sql, "c.odid > 0").unwrap(),
"current" => { deck => {
todo!() // fixme: need current deck and child decks 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, &current.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<String> =
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 Ok(())
todo!()
}
}
} }
// fixme: need note type manager // fixme: need note type manager

View file

@ -2,10 +2,11 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use crate::collection::CollectionOp; use crate::collection::CollectionOp;
use crate::config::Config;
use crate::err::Result; use crate::err::Result;
use crate::err::{AnkiError, DBErrorKind}; use crate::err::{AnkiError, DBErrorKind};
use crate::time::{i64_unix_millis, i64_unix_secs}; 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 rusqlite::{params, Connection, NO_PARAMS};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
@ -210,4 +211,18 @@ impl StorageContext<'_> {
Ok(-1) Ok(-1)
} }
} }
pub(crate) fn all_decks(&self) -> Result<Vec<Deck>> {
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<Config> {
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()?)?)
})
}
} }

View file

@ -219,8 +219,20 @@ pub(crate) fn normalize_to_nfc(s: &str) -> Cow<str> {
} }
} }
/// 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)] #[cfg(test)]
mod test { mod test {
use super::matches_wildcard;
use crate::text::{ use crate::text::{
extract_av_tags, strip_av_tags, strip_html, strip_html_preserving_image_filenames, AVTag, 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);
}
} }