diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 1bba877fe..1653bfe88 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -46,6 +46,7 @@ TagUsnTuple = pb.TagUsnTuple NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType +ConcatSeparator = pb.ConcatenateSearchesIn.Separator SyncAuth = pb.SyncAuth SyncOutput = pb.SyncCollectionOut SyncStatus = pb.SyncStatusOut diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index afff8c7ac..f3dedb826 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -53,6 +53,10 @@ fn want_release_gil(method: u32) -> bool { | BackendMethod::LatestProgress | BackendMethod::SetWantsAbort | BackendMethod::I18nResources + | BackendMethod::NormalizeSearch + | BackendMethod::NegateSearch + | BackendMethod::ConcatenateSearches + | BackendMethod::ReplaceSearchTerm ) } else { false diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index ead439982..3c448f89b 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -21,7 +21,7 @@ from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import DeckTreeNode, InvalidInput, pb +from anki.rsbackend import ConcatSeparator, DeckTreeNode, InvalidInput from anki.stats import CardStats from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks @@ -1236,13 +1236,13 @@ QTableView {{ gridline-color: {grid} }} elif mods & Qt.ControlModifier: txt = self.col.backend.concatenate_searches( # pylint: disable=no-member - sep=pb.ConcatenateSearchesIn.Separator.AND, + sep=ConcatSeparator.AND, searches=[cur, txt], ) elif mods & Qt.ShiftModifier: txt = self.col.backend.concatenate_searches( # pylint: disable=no-member - sep=pb.ConcatenateSearchesIn.Separator.OR, + sep=ConcatSeparator.OR, searches=[cur, txt], ) except InvalidInput as e: @@ -1413,14 +1413,20 @@ QTableView {{ gridline-color: {grid} }} return ml def _onSaveFilter(self) -> None: - name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME)) - if not name: - return - filt = self.form.searchEdit.lineEdit().text() - conf = self.col.get_config("savedFilters") - conf[name] = filt - self.col.set_config("savedFilters", conf) - self.maybeRefreshSidebar() + try: + filt = self.col.backend.normalize_search( + self.form.searchEdit.lineEdit().text() + ) + except InvalidInput as e: + showWarning(str(e)) + else: + name = getOnlyText(tr(TR.BROWSING_PLEASE_GIVE_YOUR_FILTER_A_NAME)) + if not name: + return + conf = self.col.get_config("savedFilters") + conf[name] = filt + self.col.set_config("savedFilters", conf) + self.maybeRefreshSidebar() def _onRemoveFilter(self): name = self._currentFilterIsSaved() @@ -1433,7 +1439,15 @@ QTableView {{ gridline-color: {grid} }} # returns name if found def _currentFilterIsSaved(self): filt = self.form.searchEdit.lineEdit().text() + try: + filt = self.col.backend.normalize_search(filt) + except InvalidInput: + pass for k, v in self.col.get_config("savedFilters").items(): + try: + v = self.col.backend.normalize_search(v) + except InvalidInput: + pass if filt == v: return k return None diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 2fe324c77..39aea4675 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -33,7 +33,8 @@ use crate::{ sched::cutoff::local_minutes_west_for_stamp, sched::timespan::{answer_button_time, time_span}, search::{ - concatenate_searches, negate_search, normalize_search, replace_search_term, SortMode, + concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, + SortMode, }, stats::studied_today, sync::{ @@ -273,6 +274,17 @@ impl From for DeckConfID { } } +impl From for BoolSeparator { + fn from(sep: i32) -> Self { + use pb::concatenate_searches_in::Separator; + match Separator::from_i32(sep) { + Some(Separator::And) => BoolSeparator::And, + Some(Separator::Or) => BoolSeparator::Or, + None => BoolSeparator::And, + } + } +} + impl BackendService for Backend { fn latest_progress(&self, _input: Empty) -> BackendResult { let progress = self.progress_state.lock().unwrap().last_progress; @@ -437,7 +449,7 @@ impl BackendService for Backend { } fn concatenate_searches(&self, input: pb::ConcatenateSearchesIn) -> Result { - Ok(concatenate_searches(input.sep, &input.searches)?.into()) + Ok(concatenate_searches(input.sep.into(), &input.searches)?.into()) } fn replace_search_term(&self, input: pb::ReplaceSearchTermIn) -> Result { diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index aed103b4d..607b9d767 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -5,4 +5,6 @@ mod sqlwriter; mod writer; pub use cards::SortMode; -pub use writer::{concatenate_searches, negate_search, normalize_search, replace_search_term}; +pub use writer::{ + concatenate_searches, negate_search, normalize_search, replace_search_term, BoolSeparator, +}; diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index 2867a64b8..ec7051893 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -2,15 +2,20 @@ // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html use crate::{ - backend_proto::concatenate_searches_in::Separator, decks::DeckID as DeckIDType, - err::{AnkiError, Result}, + err::Result, notetype::NoteTypeID as NoteTypeIDType, search::parser::{parse, Node, PropertyKind, SearchNode, StateKind, TemplateKind}, }; use itertools::Itertools; use std::mem; +#[derive(Debug, PartialEq)] +pub enum BoolSeparator { + And, + Or, +} + /// Take an Anki-style search string and convert it into an equivalent /// search string with normalized syntax. pub fn normalize_search(input: &str) -> Result { @@ -38,11 +43,10 @@ pub fn negate_search(input: &str) -> Result { /// Take arbitrary Anki-style search strings and return their concatenation where they /// are separated by the provided boolean operator. /// Empty searches (whole collection) are left out. -pub fn concatenate_searches(sep: i32, searches: &[String]) -> Result { - let bool_node = vec![match Separator::from_i32(sep) { - Some(Separator::Or) => Node::Or, - Some(Separator::And) => Node::And, - None => return Err(AnkiError::SearchError(None)), +pub fn concatenate_searches(sep: BoolSeparator, searches: &[String]) -> Result { + let bool_node = vec![match sep { + BoolSeparator::And => Node::And, + BoolSeparator::Or => Node::Or, }]; Ok(write_nodes( searches @@ -221,25 +225,22 @@ mod test { fn concatenating() -> Result<()> { assert_eq!( r#""foo" AND "bar""#, - concatenate_searches( - Separator::And as i32, - &["foo".to_string(), "bar".to_string()] - ) - .unwrap() + concatenate_searches(BoolSeparator::And, &["foo".to_string(), "bar".to_string()]) + .unwrap() ); assert_eq!( r#""foo" OR "bar""#, concatenate_searches( - Separator::Or as i32, + BoolSeparator::Or, &["foo".to_string(), "".to_string(), "bar".to_string()] ) .unwrap() ); assert_eq!( "", - concatenate_searches(Separator::Or as i32, &["".to_string()]).unwrap() + concatenate_searches(BoolSeparator::Or, &["".to_string()]).unwrap() ); - assert_eq!("", concatenate_searches(Separator::Or as i32, &[]).unwrap()); + assert_eq!("", concatenate_searches(BoolSeparator::Or, &[]).unwrap()); Ok(()) }