diff --git a/rslib/backend.proto b/rslib/backend.proto index 396ec4485..b006b3441 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -87,6 +87,7 @@ service BackendService { rpc NormalizeSearch (String) returns (String); rpc SearchCards (SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); + rpc NegateSearch (String) returns (String); rpc FindAndReplace (FindAndReplaceIn) returns (UInt32); // scheduling diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 0efee24f1..6abaa45ae 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -32,7 +32,9 @@ use crate::{ }, sched::cutoff::local_minutes_west_for_stamp, sched::timespan::{answer_button_time, time_span}, - search::{normalize_search, SortMode}, + search::{ + concatenate_searches, negate_search, normalize_search, replace_search_term, SortMode, + }, stats::studied_today, sync::{ get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress, @@ -430,6 +432,9 @@ impl BackendService for Backend { }) } + fn negate_search(&self, input: pb::String) -> Result { + Ok(negate_search(&input.val)?.into()) + } fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult { let mut search = if input.regex { input.search diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index d409c05eb..aed103b4d 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -5,4 +5,4 @@ mod sqlwriter; mod writer; pub use cards::SortMode; -pub use writer::normalize_search; +pub use writer::{concatenate_searches, negate_search, normalize_search, replace_search_term}; diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 8944a00a5..9497decd2 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -14,8 +14,32 @@ pub fn normalize_search(input: &str) -> Result { Ok(write_nodes(&parse(input)?)) } -fn write_nodes(nodes: &[Node]) -> String { - nodes.iter().map(|node| write_node(node)).collect() +/// Take an Anki-style search string and return the negated counterpart. +/// Empty searches (whole collection) remain unchanged. +pub fn negate_search(input: &str) -> Result { + let mut nodes = parse(input)?; + use Node::*; + Ok(if nodes.len() == 1 { + let node = nodes.remove(0); + match node { + Not(n) => write_node(&n), + Search(SearchNode::WholeCollection) => "".to_string(), + Group(_) | Search(_) => write_node(&Not(Box::new(node))), + _ => unreachable!(), + } + } else { + write_node(&Not(Box::new(Group(nodes)))) + }) +} +} + +} + +fn write_nodes<'a, I>(nodes: I) -> String +where + I: IntoIterator>, +{ + nodes.into_iter().map(|node| write_node(node)).collect() } fn write_node(node: &Node) -> String {