diff --git a/rslib/backend.proto b/rslib/backend.proto index 056536afc..2cfcd1dd7 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -89,6 +89,7 @@ service BackendService { rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); rpc NegateSearch (String) returns (String); rpc ConcatenateSearches (ConcatenateSearchesIn) returns (String); + rpc ReplaceSearchTerm (ReplaceSearchTermIn) returns (String); rpc FindAndReplace (FindAndReplaceIn) returns (UInt32); // scheduling @@ -755,6 +756,20 @@ message BuiltinSearchOrder { bool reverse = 2; } +message ConcatenateSearchesIn { + enum Separator { + AND = 0; + OR = 1; + } + Separator sep = 1; + repeated string searches = 2; +} + +message ReplaceSearchTermIn { + string search = 1; + string replacement = 2; +} + message CloseCollectionIn { bool downgrade_to_schema11 = 1; } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index b76df4359..485666f88 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -435,10 +435,15 @@ impl BackendService for Backend { fn negate_search(&self, input: pb::String) -> Result { Ok(negate_search(&input.val)?.into()) } + fn concatenate_searches(&self, input: pb::ConcatenateSearchesIn) -> Result { Ok(concatenate_searches(input.sep, &input.searches)?.into()) } + fn replace_search_term(&self, input: pb::ReplaceSearchTermIn) -> Result { + Ok(replace_search_term(&input.search, &input.replacement)?.into()) + } + fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult { let mut search = if input.regex { input.search diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs index 2be9a3d3b..b82f28237 100644 --- a/rslib/src/search/parser.rs +++ b/rslib/src/search/parser.rs @@ -50,7 +50,7 @@ pub(super) enum Node<'a> { Search(SearchNode<'a>), } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(super) enum SearchNode<'a> { // text without a colon UnqualifiedText(Cow<'a, str>), @@ -90,7 +90,7 @@ pub(super) enum SearchNode<'a> { WordBoundary(Cow<'a, str>), } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(super) enum PropertyKind { Due(i32), Interval(u32), @@ -99,7 +99,7 @@ pub(super) enum PropertyKind { Ease(f32), } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(super) enum StateKind { New, Review, @@ -111,7 +111,7 @@ pub(super) enum StateKind { Suspended, } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub(super) enum TemplateKind<'a> { Ordinal(u16), Name(Cow<'a, str>), diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 1a866b9cf..773270e2a 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -9,6 +9,7 @@ use crate::{ search::parser::{parse, Node, PropertyKind, SearchNode, StateKind, TemplateKind}, }; use itertools::Itertools; +use std::mem; /// Take an Anki-style search string and convert it into an equivalent /// search string with normalized syntax. @@ -55,6 +56,31 @@ pub fn concatenate_searches(sep: i32, searches: &[String]) -> Result { )) } +/// Take two Anki-style search strings. If the second one evaluates to a single search +/// node, replace with it all search terms of the same kind in the first search. +/// Then return the possibly modified first search. +pub fn replace_search_term(search: &str, replacement: &str) -> Result { + let mut nodes = parse(search)?; + let new = parse(replacement)?; + if let [Node::Search(search_node)] = &new[..] { + fn update_node_vec<'a>(old_nodes: &mut [Node<'a>], new_node: &SearchNode<'a>) { + fn update_node<'a>(old_node: &mut Node<'a>, new_node: &SearchNode<'a>) { + match old_node { + Node::Not(n) => update_node(n, new_node), + Node::Group(ns) => update_node_vec(ns, new_node), + Node::Search(n) => { + if mem::discriminant(n) == mem::discriminant(new_node) { + *n = new_node.clone(); + } + } + _ => (), + } + } + old_nodes.iter_mut().for_each(|n| update_node(n, new_node)); + } + update_node_vec(&mut nodes, search_node); + } + Ok(write_nodes(&nodes)) } fn write_nodes<'a, I>(nodes: I) -> String