diff --git a/rslib/backend.proto b/rslib/backend.proto index a3966e47a..820b0a832 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -84,6 +84,7 @@ service BackendService { // searching + rpc FilterToSearch (FilterToSearchIn) returns (String); rpc NormalizeSearch (String) returns (String); rpc SearchCards (SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); @@ -756,6 +757,35 @@ message BuiltinSearchOrder { bool reverse = 2; } +message FilterToSearchIn { + enum NamedFilter { + WHOLE_COLLECTION = 0; + CURRENT_DECK = 1; + ADDED_TODAY = 2; + STUDIED_TODAY = 3; + AGAIN_TODAY = 4; + NEW = 5; + LEARN = 6; + REVIEW = 7; + DUE = 8; + SUSPENDED = 9; + BURIED = 10; + RED_FLAG = 11; + ORANGE_FLAG = 12; + GREEN_FLAG = 13; + BLUE_FLAG = 14; + NO_FLAG = 15; + ANY_FLAG = 16; + } + oneof filter { + NamedFilter name = 1; + string tag = 2; + string deck = 3; + string note = 4; + uint32 template = 5; + } +} + message ConcatenateSearchesIn { enum Separator { AND = 0; diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 22868773f..18ee036bf 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -36,8 +36,8 @@ use crate::{ sched::new::NewCardSortOrder, sched::timespan::{answer_button_time, time_span}, search::{ - concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, - SortMode, + concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, + BoolSeparator, Node, SearchNode, SortMode, StateKind, TemplateKind, }, stats::studied_today, sync::{ @@ -45,7 +45,7 @@ use crate::{ SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage, }, template::RenderedNode, - text::{extract_av_tags, strip_av_tags, AVTag}, + text::{escape_anki_wildcards, extract_av_tags, strip_av_tags, AVTag}, timestamp::TimestampSecs, types::Usn, }; @@ -277,6 +277,46 @@ impl From for DeckConfID { } } +impl From for Node<'_> { + fn from(msg: pb::FilterToSearchIn) -> Self { + use pb::filter_to_search_in::Filter as F; + use pb::filter_to_search_in::NamedFilter as NF; + use Node as N; + use SearchNode as SN; + match msg.filter.unwrap_or(F::Name(NF::WholeCollection as i32)) { + F::Name(name) => match NF::from_i32(name).unwrap_or(NF::WholeCollection) { + NF::WholeCollection => N::Search(SN::WholeCollection), + NF::CurrentDeck => N::Search(SN::Deck("current".into())), + NF::AddedToday => N::Search(SN::AddedInDays(1)), + NF::StudiedToday => N::Search(SN::Rated { + days: 1, + ease: None, + }), + NF::AgainToday => N::Search(SN::Rated { + days: 1, + ease: Some(1), + }), + NF::New => N::Search(SN::State(StateKind::New)), + NF::Learn => N::Search(SN::State(StateKind::Learning)), + NF::Review => N::Search(SN::State(StateKind::Review)), + NF::Due => N::Search(SN::State(StateKind::Due)), + NF::Suspended => N::Search(SN::State(StateKind::Suspended)), + NF::Buried => N::Search(SN::State(StateKind::Buried)), + NF::RedFlag => N::Search(SN::Flag(1)), + NF::OrangeFlag => N::Search(SN::Flag(2)), + NF::GreenFlag => N::Search(SN::Flag(3)), + NF::BlueFlag => N::Search(SN::Flag(4)), + NF::NoFlag => N::Search(SN::Flag(0)), + NF::AnyFlag => N::Not(Box::new(N::Search(SN::Flag(0)))), + }, + F::Tag(s) => N::Search(SN::Tag(escape_anki_wildcards(&s).into_owned().into())), + F::Deck(s) => N::Search(SN::Deck(escape_anki_wildcards(&s).into_owned().into())), + F::Note(s) => N::Search(SN::NoteType(escape_anki_wildcards(&s).into_owned().into())), + F::Template(u) => N::Search(SN::CardTemplate(TemplateKind::Ordinal(u as u16))), + } + } +} + impl From for BoolSeparator { fn from(sep: BoolSeparatorProto) -> Self { match sep { @@ -408,6 +448,10 @@ impl BackendService for Backend { // searching //----------------------------------------------- + fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result { + Ok(write_nodes(&[input.into()]).into()) + } + fn normalize_search(&self, input: pb::String) -> Result { Ok(normalize_search(&input.val)?.into()) } diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index 607b9d767..5eef7e86e 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -5,6 +5,8 @@ mod sqlwriter; mod writer; pub use cards::SortMode; +pub use parser::{Node, PropertyKind, SearchNode, StateKind, TemplateKind}; pub use writer::{ - concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, + concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes, + BoolSeparator, }; diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 9e03abcc5..7763c965f 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -42,7 +42,7 @@ impl From> for ParseError { type ParseResult = std::result::Result; #[derive(Debug, PartialEq)] -pub(super) enum Node<'a> { +pub enum Node<'a> { And, Or, Not(Box>), @@ -51,7 +51,7 @@ pub(super) enum Node<'a> { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum SearchNode<'a> { +pub enum SearchNode<'a> { // text without a colon UnqualifiedText(Cow<'a, str>), // foo:bar, where foo doesn't match a term below @@ -91,7 +91,7 @@ pub(super) enum SearchNode<'a> { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum PropertyKind { +pub enum PropertyKind { Due(i32), Interval(u32), Reps(u32), @@ -101,7 +101,7 @@ pub(super) enum PropertyKind { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum StateKind { +pub enum StateKind { New, Review, Learning, @@ -113,7 +113,7 @@ pub(super) enum StateKind { } #[derive(Debug, PartialEq, Clone)] -pub(super) enum TemplateKind<'a> { +pub enum TemplateKind<'a> { Ordinal(u16), Name(Cow<'a, str>), } diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index ec7051893..e112ebcf5 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -87,7 +87,7 @@ pub fn replace_search_term(search: &str, replacement: &str) -> Result { Ok(write_nodes(&nodes)) } -fn write_nodes<'a, I>(nodes: I) -> String +pub fn write_nodes<'a, I>(nodes: I) -> String where I: IntoIterator>, { diff --git a/rslib/src/text.rs b/rslib/src/text.rs index 0f892ecd0..10391334e 100644 --- a/rslib/src/text.rs +++ b/rslib/src/text.rs @@ -331,6 +331,14 @@ pub(crate) fn escape_sql(txt: &str) -> Cow { RE.replace_all(&txt, r"\$0") } +/// Escape Anki wildcards and the backslash for escaping them: \*_ +pub(crate) fn escape_anki_wildcards(txt: &str) -> Cow { + lazy_static! { + static ref RE: Regex = Regex::new(r"[\\*_]").unwrap(); + } + RE.replace_all(&txt, r"\$0") +} + /// Compare text with a possible glob, folding case. pub(crate) fn matches_glob(text: &str, search: &str) -> bool { if is_glob(search) {