From 76c16180aec1be30d21d4446d80363af9b896707 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 7 Jan 2021 12:50:57 +0100 Subject: [PATCH 1/5] Add native enum for concatenate_search's separator --- rslib/src/backend/mod.rs | 16 ++++++++++++++-- rslib/src/search/mod.rs | 4 +++- rslib/src/search/writer.rs | 31 ++++++++++++++++--------------- 3 files changed, 33 insertions(+), 18 deletions(-) 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(()) } From c2e2a86ec9cdbee3c23cc7c328a2666858d7788a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 7 Jan 2021 13:09:00 +0100 Subject: [PATCH 2/5] Add writer functions to want_release_gil() --- pylib/rsbridge/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) 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 From efd554ea29811d8cdb9cddcb1b136a0108a1edfb Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 7 Jan 2021 17:48:30 +0100 Subject: [PATCH 3/5] Provide ConcatSeparator through rsbackend.py --- pylib/anki/rsbackend.py | 1 + qt/aqt/browser.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) 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/qt/aqt/browser.py b/qt/aqt/browser.py index ead439982..019041bd0 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 DeckTreeNode, InvalidInput, ConcatSeparator 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: From 478a3bd0bc8fb72b839ad1e6456f176e323c6e0a Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 7 Jan 2021 18:20:14 +0100 Subject: [PATCH 4/5] Format --- qt/aqt/browser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 019041bd0..827c22f3d 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, ConcatSeparator +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 From d30062a8a5619ae46c8316641359d1f015f6fadd Mon Sep 17 00:00:00 2001 From: RumovZ Date: Thu, 7 Jan 2021 22:11:21 +0100 Subject: [PATCH 5/5] Compare and save filters normalised When checking whether the current search is a saved filter, compare the searches normalised to allow the detection of equivalent expressions. Invalid searches are accepted unaltered to allow the deletion of invalid saved filters. As for saving new filters, do so normalised, respectively, reject invalid searches. --- qt/aqt/browser.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 827c22f3d..3c448f89b 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -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