From 35840221bba077139cdd42046cb58890c2fde445 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 11 Feb 2021 19:57:19 +1000 Subject: [PATCH] tweak search wording and tidy up API - SearchTerm -> SearchNode - Operator -> Joiner; share between messages - build_search_string() supports specifying AND/OR as a convenience - group_searches() makes it easier to negate --- pylib/anki/collection.py | 112 +++++++++++++++++++++---------------- pylib/anki/tags.py | 2 +- pylib/rsbridge/lib.rs | 6 +- qt/.pylintrc | 2 +- qt/aqt/addcards.py | 6 +- qt/aqt/browser.py | 18 +++--- qt/aqt/customstudy.py | 20 +++---- qt/aqt/dyndeckconf.py | 10 ++-- qt/aqt/editor.py | 6 +- qt/aqt/mediacheck.py | 4 +- qt/aqt/sidebar.py | 68 +++++++++++----------- rslib/backend.proto | 36 ++++++------ rslib/src/backend/mod.rs | 91 +++++++++++++++--------------- rslib/src/search/mod.rs | 2 +- rslib/src/search/writer.rs | 12 ++-- 15 files changed, 202 insertions(+), 193 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 4c232084a..e2b5c293b 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -43,7 +43,8 @@ from anki.utils import ( ) # public exports -SearchTerm = _pb.SearchTerm +SearchNode = _pb.SearchNode +SearchJoiner = Literal["AND", "OR"] Progress = _pb.Progress Config = _pb.Config EmptyCardsReport = _pb.EmptyCardsReport @@ -471,7 +472,7 @@ class Collection: ) return self._backend.search_cards(search=query, order=mode) - def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]: + def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[int]: return self._backend.search_notes(self.build_search_string(*terms)) def find_and_replace( @@ -487,7 +488,7 @@ class Collection: # returns array of ("dupestr", [nids]) def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]: - nids = self.findNotes(search, SearchTerm(field_name=fieldName)) + nids = self.findNotes(search, SearchNode(field_name=fieldName)) # go through notes vals: Dict[str, List[int]] = {} dupes = [] @@ -526,72 +527,85 @@ class Collection: # Search Strings ########################################################################## - def group_search_terms(self, *terms: Union[str, SearchTerm]) -> SearchTerm: - """Join provided search terms and strings into a single SearchTerm. - If multiple terms provided, they will be ANDed together into a group. - If a single term is provided, it is returned as-is. - """ - assert terms - - # convert raw text to SearchTerms - search_terms = [ - term if isinstance(term, SearchTerm) else SearchTerm(unparsed_search=term) - for term in terms - ] - - # if there's more than one, wrap it in an implicit AND - if len(search_terms) > 1: - return SearchTerm(group=SearchTerm.Group(terms=search_terms)) - else: - return search_terms[0] - def build_search_string( self, - *terms: Union[str, SearchTerm], + *nodes: Union[str, SearchNode], + joiner: SearchJoiner = "AND", ) -> str: - """Join provided search terms together, and return a normalized search string. - - Terms are joined by an implicit AND. You can make an explict AND or OR - by wrapping in a group: - - terms = [... one or more SearchTerms()] - group = SearchTerm.Group(op=SearchTerm.Group.OR, terms=terms) - term = SearchTerm(group=group) + """Join one or more searches, and return a normalized search string. To negate, wrap in a negated search term: - term = SearchTerm(negated=term) + term = SearchNode(negated=col.group_searches(...)) - Invalid search terms will throw an exception. + Invalid searches will throw an exception. """ - term = self.group_search_terms(*terms) - return self._backend.filter_to_search(term) + term = self.group_searches(*nodes, joiner=joiner) + return self._backend.build_search_string(term) + + def group_searches( + self, + *nodes: Union[str, SearchNode], + joiner: SearchJoiner = "AND", + ) -> SearchNode: + """Join provided search nodes and strings into a single SearchNode. + If a single SearchNode is provided, it is returned as-is. + At least one node must be provided. + """ + assert nodes + + # convert raw text to SearchNodes + search_nodes = [ + node if isinstance(node, SearchNode) else SearchNode(parsable_text=node) + for node in nodes + ] + + # if there's more than one, wrap them in a group + if len(search_nodes) > 1: + return SearchNode( + group=SearchNode.Group( + nodes=search_nodes, joiner=self._pb_search_separator(joiner) + ) + ) + else: + return search_nodes[0] - # pylint: disable=no-member def join_searches( self, - existing_term: SearchTerm, - additional_term: SearchTerm, + existing_node: SearchNode, + additional_node: SearchNode, operator: Literal["AND", "OR"], ) -> str: """ AND or OR `additional_term` to `existing_term`, without wrapping `existing_term` in brackets. - If you're building a search query yourself, prefer using SearchTerm(group=SearchTerm.Group(...)) + Used by the Browse screen to avoid adding extra brackets when joining. + If you're building a search query yourself, you probably don't need this. """ - - if operator == "AND": - sep = _pb.ConcatenateSearchesIn.AND - else: - sep = _pb.ConcatenateSearchesIn.OR - - search_string = self._backend.concatenate_searches( - sep=sep, existing_search=existing_term, additional_search=additional_term + search_string = self._backend.join_search_nodes( + joiner=self._pb_search_separator(operator), + existing_node=existing_node, + additional_node=additional_node, ) return search_string - def replace_search_term(self, search: SearchTerm, replacement: SearchTerm) -> str: - return self._backend.replace_search_term(search=search, replacement=replacement) + def replace_in_search_node( + self, existing_node: SearchNode, replacement_node: SearchNode + ) -> str: + """If nodes of the same type as `replacement_node` are found in existing_node, replace them. + + You can use this to replace any "deck" clauses in a search with a different deck for example. + """ + return self._backend.replace_search_node( + existing_node=existing_node, replacement_node=replacement_node + ) + + def _pb_search_separator(self, operator: SearchJoiner) -> SearchNode.Group.Joiner.V: + # pylint: disable=no-member + if operator == "AND": + return SearchNode.Group.Joiner.AND + else: + return SearchNode.Group.Joiner.OR # Config ########################################################################## diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 686cd6ad7..73715fa7e 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -90,7 +90,7 @@ class TagManager: def rename(self, old: str, new: str) -> int: "Rename provided tag, returning number of changed notes." - nids = self.col.find_notes(anki.collection.SearchTerm(tag=old)) + nids = self.col.find_notes(anki.collection.SearchNode(tag=old)) if not nids: return 0 escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) diff --git a/pylib/rsbridge/lib.rs b/pylib/rsbridge/lib.rs index 80050f3c2..a7c2ffc5b 100644 --- a/pylib/rsbridge/lib.rs +++ b/pylib/rsbridge/lib.rs @@ -51,9 +51,9 @@ fn want_release_gil(method: u32) -> bool { | BackendMethod::LatestProgress | BackendMethod::SetWantsAbort | BackendMethod::I18nResources - | BackendMethod::ConcatenateSearches - | BackendMethod::ReplaceSearchTerm - | BackendMethod::FilterToSearch + | BackendMethod::JoinSearchNodes + | BackendMethod::ReplaceSearchNode + | BackendMethod::BuildSearchString ) } else { false diff --git a/qt/.pylintrc b/qt/.pylintrc index 9084c2f85..3507ed84c 100644 --- a/qt/.pylintrc +++ b/qt/.pylintrc @@ -6,7 +6,7 @@ ignore = forms,hooks_gen.py [TYPECHECK] ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio ignored-classes= - SearchTerm, + SearchNode, Config, [REPORTS] diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 9f695ea20..c719d4bbb 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -6,7 +6,7 @@ import aqt.deckchooser import aqt.editor import aqt.forms import aqt.modelchooser -from anki.collection import SearchTerm +from anki.collection import SearchNode from anki.consts import MODEL_CLOZE from anki.notes import Note from anki.utils import htmlToTextLine, isMac @@ -144,7 +144,7 @@ class AddCards(QDialog): def onHistory(self) -> None: m = QMenu(self) for nid in self.history: - if self.mw.col.findNotes(SearchTerm(nid=nid)): + if self.mw.col.findNotes(SearchNode(nid=nid)): note = self.mw.col.getNote(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) @@ -161,7 +161,7 @@ class AddCards(QDialog): m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) def editHistory(self, nid: int) -> None: - aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),)) + aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) def addNote(self, note: Note) -> Optional[Note]: note.model()["did"] = self.deckChooser.selectedId() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index b293c44f1..23703342b 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union, import aqt import aqt.forms from anki.cards import Card -from anki.collection import Collection, Config, SearchTerm +from anki.collection import Collection, Config, SearchNode from anki.consts import * from anki.errors import InvalidInput from anki.lang import without_unicode_isolation @@ -442,7 +442,7 @@ class Browser(QMainWindow): self, mw: AnkiQt, card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchTerm]]] = None, + search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: """ card : try to search for its note and select it @@ -615,7 +615,7 @@ class Browser(QMainWindow): self, _mw: AnkiQt, card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchTerm]]] = None, + search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: if search is not None: self.search_for_terms(*search) @@ -630,7 +630,7 @@ class Browser(QMainWindow): def setupSearch( self, card: Optional[Card] = None, - search: Optional[Tuple[Union[str, SearchTerm]]] = None, + search: Optional[Tuple[Union[str, SearchNode]]] = None, ) -> None: qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) @@ -644,7 +644,7 @@ class Browser(QMainWindow): self.show_single_card(card) else: self.search_for( - self.col.build_search_string(SearchTerm(deck="current")), "" + self.col.build_search_string(SearchNode(deck="current")), "" ) self.form.searchEdit.setFocus() @@ -707,7 +707,7 @@ class Browser(QMainWindow): ) return selected - def search_for_terms(self, *search_terms: Union[str, SearchTerm]) -> None: + def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None: search = self.col.build_search_string(*search_terms) self.form.searchEdit.setEditText(search) self.onSearchActivated() @@ -717,7 +717,7 @@ class Browser(QMainWindow): def on_show_single_card() -> None: self.card = card - search = self.col.build_search_string(SearchTerm(nid=card.nid)) + search = self.col.build_search_string(SearchNode(nid=card.nid)) search = gui_hooks.default_search(search, card) self.search_for(search, "") self.focusCid(card.id) @@ -1407,7 +1407,7 @@ where id in %s""" tv.selectionModel().clear() search = self.col.build_search_string( - SearchTerm(nids=SearchTerm.IdList(ids=nids)) + SearchNode(nids=SearchNode.IdList(ids=nids)) ) self.search_for(search) @@ -1626,7 +1626,7 @@ where id in %s""" % ( html.escape( self.col.build_search_string( - SearchTerm(nids=SearchTerm.IdList(ids=nids)) + SearchNode(nids=SearchNode.IdList(ids=nids)) ) ), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index 8f85c7ddf..5533b7178 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -2,7 +2,7 @@ # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import aqt -from anki.collection import SearchTerm +from anki.collection import SearchNode from anki.consts import * from aqt.qt import * from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr @@ -164,20 +164,20 @@ class CustomStudy(QDialog): # and then set various options if i == RADIO_FORGOT: search = self.mw.col.build_search_string( - SearchTerm( - rated=SearchTerm.Rated(days=spin, rating=SearchTerm.RATING_AGAIN) + SearchNode( + rated=SearchNode.Rated(days=spin, rating=SearchNode.RATING_AGAIN) ) ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM] dyn["resched"] = False elif i == RADIO_AHEAD: - search = self.mw.col.build_search_string(SearchTerm(due_in_days=spin)) + search = self.mw.col.build_search_string(SearchNode(due_in_days=spin)) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE] dyn["resched"] = True elif i == RADIO_PREVIEW: search = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), - SearchTerm(added_in_days=spin), + SearchNode(card_state=SearchNode.CARD_STATE_NEW), + SearchNode(added_in_days=spin), ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST] dyn["resched"] = False @@ -185,19 +185,19 @@ class CustomStudy(QDialog): type = f.cardType.currentRow() if type == TYPE_NEW: terms = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CARD_STATE_NEW) + SearchNode(card_state=SearchNode.CARD_STATE_NEW) ) ord = DYN_ADDED dyn["resched"] = True elif type == TYPE_DUE: terms = self.mw.col.build_search_string( - SearchTerm(card_state=SearchTerm.CARD_STATE_DUE) + SearchNode(card_state=SearchNode.CARD_STATE_DUE) ) ord = DYN_DUE dyn["resched"] = True elif type == TYPE_REVIEW: terms = self.mw.col.build_search_string( - SearchTerm(negated=SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)) + SearchNode(negated=SearchNode(card_state=SearchNode.CARD_STATE_NEW)) ) ord = DYN_RANDOM dyn["resched"] = True @@ -208,7 +208,7 @@ class CustomStudy(QDialog): dyn["terms"][0] = [(terms + tags).strip(), spin, ord] # add deck limit dyn["terms"][0][0] = self.mw.col.build_search_string( - dyn["terms"][0][0], SearchTerm(deck=self.deck["name"]) + dyn["terms"][0][0], SearchNode(deck=self.deck["name"]) ) self.mw.col.decks.save(dyn) # generate cards diff --git a/qt/aqt/dyndeckconf.py b/qt/aqt/dyndeckconf.py index 0904f58ca..8c88df964 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -3,7 +3,7 @@ from typing import Callable, List, Optional import aqt -from anki.collection import SearchTerm +from anki.collection import SearchNode from anki.decks import Deck, DeckRenameError from anki.errors import InvalidInput from anki.lang import without_unicode_isolation @@ -111,14 +111,14 @@ class DeckConf(QDialog): def set_default_searches(self, deck_name: str) -> None: self.form.search.setText( self.mw.col.build_search_string( - SearchTerm(deck=deck_name), - SearchTerm(card_state=SearchTerm.CARD_STATE_DUE), + SearchNode(deck=deck_name), + SearchNode(card_state=SearchNode.CARD_STATE_DUE), ) ) self.form.search_2.setText( self.mw.col.build_search_string( - SearchTerm(deck=deck_name), - SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), + SearchNode(deck=deck_name), + SearchNode(card_state=SearchNode.CARD_STATE_NEW), ) ) diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index ee0fe2e47..1a5a54bf6 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -20,7 +20,7 @@ from bs4 import BeautifulSoup import aqt import aqt.sound from anki.cards import Card -from anki.collection import SearchTerm +from anki.collection import SearchNode from anki.consts import MODEL_CLOZE from anki.hooks import runFilter from anki.httpclient import HttpClient @@ -546,8 +546,8 @@ class Editor: "Browser", self.mw, search=( - SearchTerm( - dupe=SearchTerm.Dupe( + SearchNode( + dupe=SearchNode.Dupe( notetype_id=self.note.model()["id"], first_field=self.note.fields[0], ) diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 01f049a7c..017eb2eab 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -9,7 +9,7 @@ from concurrent.futures import Future from typing import Iterable, List, Optional, Sequence, TypeVar import aqt -from anki.collection import SearchTerm +from anki.collection import SearchNode from anki.errors import Interrupted from anki.lang import TR from anki.media import CheckMediaOut @@ -154,7 +154,7 @@ class MediaChecker: if out is not None: nid, err = out - aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),)) + aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),)) showText(err, type="html") else: tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index 71b5b6498..60a0ebc69 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -8,7 +8,7 @@ from enum import Enum, auto from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast import aqt -from anki.collection import Config, SearchTerm +from anki.collection import Config, SearchNode from anki.decks import DeckTreeNode from anki.errors import DeckRenameError, InvalidInput from anki.tags import TagTreeNode @@ -391,20 +391,20 @@ class SidebarTreeView(QTreeView): if item.is_expanded(searching): self.setExpanded(idx, True) - def update_search(self, *terms: Union[str, SearchTerm]) -> None: + def update_search(self, *terms: Union[str, SearchNode]) -> None: """Modify the current search string based on modifier keys, then refresh.""" mods = self.mw.app.keyboardModifiers() - previous = SearchTerm(unparsed_search=self.browser.current_search()) - current = self.mw.col.group_search_terms(*terms) + previous = SearchNode(parsable_text=self.browser.current_search()) + current = self.mw.col.group_searches(*terms) # if Alt pressed, invert if mods & Qt.AltModifier: - current = SearchTerm(negated=current) + current = SearchNode(negated=current) try: if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: # If Ctrl+Shift, replace searches nodes of the same type. - search = self.col.replace_search_term(previous, current) + search = self.col.replace_in_search_node(previous, current) elif mods & Qt.ControlModifier: # If Ctrl, AND with previous search = self.col.join_searches(previous, current, "AND") @@ -597,7 +597,7 @@ class SidebarTreeView(QTreeView): return top - def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable: + def _filter_func(self, *terms: Union[str, SearchNode]) -> Callable: return lambda: self.update_search(*terms) # Tree: Saved Searches @@ -648,33 +648,33 @@ class SidebarTreeView(QTreeView): name=TR.BROWSING_SIDEBAR_DUE_TODAY, icon=icon, type=type, - on_click=search(SearchTerm(due_on_day=0)), + on_click=search(SearchNode(due_on_day=0)), ) root.add_simple( name=TR.BROWSING_ADDED_TODAY, icon=icon, type=type, - on_click=search(SearchTerm(added_in_days=1)), + on_click=search(SearchNode(added_in_days=1)), ) root.add_simple( name=TR.BROWSING_EDITED_TODAY, icon=icon, type=type, - on_click=search(SearchTerm(edited_in_days=1)), + on_click=search(SearchNode(edited_in_days=1)), ) root.add_simple( name=TR.BROWSING_STUDIED_TODAY, icon=icon, type=type, - on_click=search(SearchTerm(rated=SearchTerm.Rated(days=1))), + on_click=search(SearchNode(rated=SearchNode.Rated(days=1))), ) root.add_simple( name=TR.BROWSING_AGAIN_TODAY, icon=icon, type=type, on_click=search( - SearchTerm( - rated=SearchTerm.Rated(days=1, rating=SearchTerm.RATING_AGAIN) + SearchNode( + rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN) ) ), ) @@ -683,8 +683,8 @@ class SidebarTreeView(QTreeView): icon=icon, type=type, on_click=search( - SearchTerm(card_state=SearchTerm.CARD_STATE_DUE), - SearchTerm(negated=SearchTerm(due_on_day=0)), + SearchNode(card_state=SearchNode.CARD_STATE_DUE), + SearchNode(negated=SearchNode(due_on_day=0)), ), ) @@ -707,32 +707,32 @@ class SidebarTreeView(QTreeView): TR.ACTIONS_NEW, icon=icon.with_color(colors.NEW_COUNT), type=type, - on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)), + on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_NEW)), ) root.add_simple( name=TR.SCHEDULING_LEARNING, icon=icon.with_color(colors.LEARN_COUNT), type=type, - on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN)), + on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_LEARN)), ) root.add_simple( name=TR.SCHEDULING_REVIEW, icon=icon.with_color(colors.REVIEW_COUNT), type=type, - on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW)), + on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_REVIEW)), ) root.add_simple( name=TR.BROWSING_SUSPENDED, icon=icon.with_color(colors.SUSPENDED_FG), type=type, - on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED)), + on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED)), ) root.add_simple( name=TR.BROWSING_BURIED, icon=icon.with_color(colors.BURIED_FG), type=type, - on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED)), + on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_BURIED)), ) # Tree: Flags @@ -748,38 +748,38 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_FLAGS, type=SidebarItemType.FLAG_ROOT, ) - root.on_click = search(SearchTerm(flag=SearchTerm.FLAG_ANY)) + root.on_click = search(SearchNode(flag=SearchNode.FLAG_ANY)) type = SidebarItemType.FLAG root.add_simple( TR.ACTIONS_RED_FLAG, icon=icon.with_color(colors.FLAG1_FG), type=type, - on_click=search(SearchTerm(flag=SearchTerm.FLAG_RED)), + on_click=search(SearchNode(flag=SearchNode.FLAG_RED)), ) root.add_simple( TR.ACTIONS_ORANGE_FLAG, icon=icon.with_color(colors.FLAG2_FG), type=type, - on_click=search(SearchTerm(flag=SearchTerm.FLAG_ORANGE)), + on_click=search(SearchNode(flag=SearchNode.FLAG_ORANGE)), ) root.add_simple( TR.ACTIONS_GREEN_FLAG, icon=icon.with_color(colors.FLAG3_FG), type=type, - on_click=search(SearchTerm(flag=SearchTerm.FLAG_GREEN)), + on_click=search(SearchNode(flag=SearchNode.FLAG_GREEN)), ) root.add_simple( TR.ACTIONS_BLUE_FLAG, icon=icon.with_color(colors.FLAG4_FG), type=type, - on_click=search(SearchTerm(flag=SearchTerm.FLAG_BLUE)), + on_click=search(SearchNode(flag=SearchNode.FLAG_BLUE)), ) root.add_simple( TR.BROWSING_NO_FLAG, icon=icon.with_color(colors.DISABLED), type=type, - on_click=search(SearchTerm(flag=SearchTerm.FLAG_NONE)), + on_click=search(SearchNode(flag=SearchNode.FLAG_NONE)), ) # Tree: Tags @@ -802,7 +802,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( node.name, icon, - self._filter_func(SearchTerm(tag=head + node.name)), + self._filter_func(SearchNode(tag=head + node.name)), toggle_expand(), node.expanded, item_type=SidebarItemType.TAG, @@ -820,12 +820,12 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_TAGS, type=SidebarItemType.TAG_ROOT, ) - root.on_click = self._filter_func(SearchTerm(negated=SearchTerm(tag="none"))) + root.on_click = self._filter_func(SearchNode(negated=SearchNode(tag="none"))) root.add_simple( name=tr(TR.BROWSING_SIDEBAR_UNTAGGED), icon=icon, type=SidebarItemType.TAG_NONE, - on_click=self._filter_func(SearchTerm(tag="none")), + on_click=self._filter_func(SearchNode(tag="none")), ) render(root, tree.children) @@ -848,7 +848,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( node.name, icon, - self._filter_func(SearchTerm(deck=head + node.name)), + self._filter_func(SearchNode(deck=head + node.name)), toggle_expand(), not node.collapsed, item_type=SidebarItemType.DECK, @@ -867,12 +867,12 @@ class SidebarTreeView(QTreeView): collapse_key=Config.Bool.COLLAPSE_DECKS, type=SidebarItemType.DECK_ROOT, ) - root.on_click = self._filter_func(SearchTerm(deck="*")) + root.on_click = self._filter_func(SearchNode(deck="*")) current = root.add_simple( name=tr(TR.BROWSING_CURRENT_DECK), icon=icon, type=SidebarItemType.DECK, - on_click=self._filter_func(SearchTerm(deck="current")), + on_click=self._filter_func(SearchNode(deck="current")), ) current.id = self.mw.col.decks.selected() @@ -895,7 +895,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( nt["name"], icon, - self._filter_func(SearchTerm(note=nt["name"])), + self._filter_func(SearchNode(note=nt["name"])), item_type=SidebarItemType.NOTETYPE, id=nt["id"], ) @@ -905,7 +905,7 @@ class SidebarTreeView(QTreeView): tmpl["name"], icon, self._filter_func( - SearchTerm(note=nt["name"]), SearchTerm(template=c) + SearchNode(note=nt["name"]), SearchNode(template=c) ), item_type=SidebarItemType.NOTETYPE_TEMPLATE, full_name=f"{nt['name']}::{tmpl['name']}", diff --git a/rslib/backend.proto b/rslib/backend.proto index ae4eeedd9..d5cf19d59 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -90,11 +90,11 @@ service BackendService { // searching - rpc FilterToSearch(SearchTerm) returns (String); + rpc BuildSearchString(SearchNode) returns (String); rpc SearchCards(SearchCardsIn) returns (SearchCardsOut); rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut); - rpc ConcatenateSearches(ConcatenateSearchesIn) returns (String); - rpc ReplaceSearchTerm(ReplaceSearchTermIn) returns (String); + rpc JoinSearchNodes(JoinSearchNodesIn) returns (String); + rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String); rpc FindAndReplace(FindAndReplaceIn) returns (UInt32); // scheduling @@ -771,7 +771,7 @@ message SearchNotesOut { repeated int64 note_ids = 2; } -message SearchTerm { +message SearchNode { message Dupe { int64 notetype_id = 1; string first_field = 2; @@ -808,17 +808,17 @@ message SearchTerm { repeated int64 ids = 1; } message Group { - enum Operator { + enum Joiner { AND = 0; OR = 1; } - repeated SearchTerm terms = 1; - Operator op = 2; + repeated SearchNode nodes = 1; + Joiner joiner = 2; } oneof filter { Group group = 1; - SearchTerm negated = 2; - string unparsed_search = 3; + SearchNode negated = 2; + string parsable_text = 3; uint32 template = 4; int64 nid = 5; Dupe dupe = 6; @@ -837,19 +837,15 @@ message SearchTerm { } } -message ConcatenateSearchesIn { - enum Separator { - AND = 0; - OR = 1; - } - Separator sep = 1; - SearchTerm existing_search = 2; - SearchTerm additional_search = 3; +message JoinSearchNodesIn { + SearchNode.Group.Joiner joiner = 1; + SearchNode existing_node = 2; + SearchNode additional_node = 3; } -message ReplaceSearchTermIn { - SearchTerm search = 1; - SearchTerm replacement = 2; +message ReplaceSearchNodeIn { + SearchNode existing_node = 1; + SearchNode replacement_node = 2; } message CloseCollectionIn { diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index e89926e14..8374daafb 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -6,7 +6,6 @@ use crate::{ backend::dbproxy::db_command_bytes, backend_proto as pb, backend_proto::{ - concatenate_searches_in::Separator as BoolSeparatorProto, sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto, AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement, }, @@ -38,7 +37,7 @@ use crate::{ timespan::{answer_button_time, time_span}, }, search::{ - concatenate_searches, parse_search, replace_search_term, write_nodes, BoolSeparator, Node, + concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind, }, stats::studied_today, @@ -267,7 +266,7 @@ impl From for NoteID { } } -impl pb::search_term::IdList { +impl pb::search_node::IdList { fn into_id_string(self) -> String { self.ids .iter() @@ -295,13 +294,13 @@ impl From for DeckConfID { } } -impl TryFrom for Node { +impl TryFrom for Node { type Error = AnkiError; - fn try_from(msg: pb::SearchTerm) -> std::result::Result { - use pb::search_term::group::Operator; - use pb::search_term::Filter; - use pb::search_term::Flag; + fn try_from(msg: pb::SearchNode) -> std::result::Result { + use pb::search_node::group::Joiner; + use pb::search_node::Filter; + use pb::search_node::Flag; Ok(if let Some(filter) = msg.filter { match filter { Filter::Tag(s) => Node::Search(SearchNode::Tag(escape_anki_wildcards(&s))), @@ -340,7 +339,7 @@ impl TryFrom for Node { }), Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)), Filter::CardState(state) => Node::Search(SearchNode::State( - pb::search_term::CardState::from_i32(state) + pb::search_node::CardState::from_i32(state) .unwrap_or_default() .into(), )), @@ -354,27 +353,27 @@ impl TryFrom for Node { }, Filter::Negated(term) => Node::try_from(*term)?.negated(), Filter::Group(mut group) => { - match group.terms.len() { + match group.nodes.len() { 0 => return Err(AnkiError::invalid_input("empty group")), // a group of 1 doesn't need to be a group - 1 => group.terms.pop().unwrap().try_into()?, + 1 => group.nodes.pop().unwrap().try_into()?, // 2+ nodes _ => { - let operator = match group.op() { - Operator::And => Node::And, - Operator::Or => Node::Or, + let joiner = match group.joiner() { + Joiner::And => Node::And, + Joiner::Or => Node::Or, }; let parsed: Vec<_> = group - .terms + .nodes .into_iter() .map(TryFrom::try_from) .collect::>()?; - let joined = parsed.into_iter().intersperse(operator).collect(); + let joined = parsed.into_iter().intersperse(joiner).collect(); Node::Group(joined) } } } - Filter::UnparsedSearch(text) => { + Filter::ParsableText(text) => { let mut nodes = parse_search(&text)?; if nodes.len() == 1 { nodes.pop().unwrap() @@ -389,37 +388,37 @@ impl TryFrom for Node { } } -impl From for BoolSeparator { - fn from(sep: BoolSeparatorProto) -> Self { +impl From for BoolSeparator { + fn from(sep: pb::search_node::group::Joiner) -> Self { match sep { - BoolSeparatorProto::And => BoolSeparator::And, - BoolSeparatorProto::Or => BoolSeparator::Or, + pb::search_node::group::Joiner::And => BoolSeparator::And, + pb::search_node::group::Joiner::Or => BoolSeparator::Or, } } } -impl From for RatingKind { - fn from(r: pb::search_term::Rating) -> Self { +impl From for RatingKind { + fn from(r: pb::search_node::Rating) -> Self { match r { - pb::search_term::Rating::Again => RatingKind::AnswerButton(1), - pb::search_term::Rating::Hard => RatingKind::AnswerButton(2), - pb::search_term::Rating::Good => RatingKind::AnswerButton(3), - pb::search_term::Rating::Easy => RatingKind::AnswerButton(4), - pb::search_term::Rating::Any => RatingKind::AnyAnswerButton, - pb::search_term::Rating::ByReschedule => RatingKind::ManualReschedule, + pb::search_node::Rating::Again => RatingKind::AnswerButton(1), + pb::search_node::Rating::Hard => RatingKind::AnswerButton(2), + pb::search_node::Rating::Good => RatingKind::AnswerButton(3), + pb::search_node::Rating::Easy => RatingKind::AnswerButton(4), + pb::search_node::Rating::Any => RatingKind::AnyAnswerButton, + pb::search_node::Rating::ByReschedule => RatingKind::ManualReschedule, } } } -impl From for StateKind { - fn from(k: pb::search_term::CardState) -> Self { +impl From for StateKind { + fn from(k: pb::search_node::CardState) -> Self { match k { - pb::search_term::CardState::New => StateKind::New, - pb::search_term::CardState::Learn => StateKind::Learning, - pb::search_term::CardState::Review => StateKind::Review, - pb::search_term::CardState::Due => StateKind::Due, - pb::search_term::CardState::Suspended => StateKind::Suspended, - pb::search_term::CardState::Buried => StateKind::Buried, + pb::search_node::CardState::New => StateKind::New, + pb::search_node::CardState::Learn => StateKind::Learning, + pb::search_node::CardState::Review => StateKind::Review, + pb::search_node::CardState::Due => StateKind::Due, + pb::search_node::CardState::Suspended => StateKind::Suspended, + pb::search_node::CardState::Buried => StateKind::Buried, } } } @@ -548,7 +547,7 @@ impl BackendService for Backend { // searching //----------------------------------------------- - fn filter_to_search(&self, input: pb::SearchTerm) -> Result { + fn build_search_string(&self, input: pb::SearchNode) -> Result { Ok(write_nodes(&[input.try_into()?]).into()) } @@ -571,31 +570,31 @@ impl BackendService for Backend { }) } - fn concatenate_searches(&self, input: pb::ConcatenateSearchesIn) -> Result { - let sep = input.sep().into(); + fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result { + let sep = input.joiner().into(); let existing_nodes = { - let node = input.existing_search.unwrap_or_default().try_into()?; + let node = input.existing_node.unwrap_or_default().try_into()?; if let Node::Group(nodes) = node { nodes } else { vec![node] } }; - let additional_node = input.additional_search.unwrap_or_default().try_into()?; + let additional_node = input.additional_node.unwrap_or_default().try_into()?; Ok(concatenate_searches(sep, existing_nodes, additional_node).into()) } - fn replace_search_term(&self, input: pb::ReplaceSearchTermIn) -> Result { + fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result { let existing = { - let node = input.search.unwrap_or_default().try_into()?; + let node = input.existing_node.unwrap_or_default().try_into()?; if let Node::Group(nodes) = node { nodes } else { vec![node] } }; - let replacement = input.replacement.unwrap_or_default().try_into()?; - Ok(replace_search_term(existing, replacement).into()) + let replacement = input.replacement_node.unwrap_or_default().try_into()?; + Ok(replace_search_node(existing, replacement).into()) } fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult { diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs index bb4bc4ff6..b469542df 100644 --- a/rslib/src/search/mod.rs +++ b/rslib/src/search/mod.rs @@ -11,4 +11,4 @@ pub use cards::SortMode; pub use parser::{ parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind, }; -pub use writer::{concatenate_searches, replace_search_term, write_nodes, BoolSeparator}; +pub use writer::{concatenate_searches, replace_search_node, write_nodes, BoolSeparator}; diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs index acd6a6411..2cb2d3a6b 100644 --- a/rslib/src/search/writer.rs +++ b/rslib/src/search/writer.rs @@ -37,7 +37,7 @@ pub fn concatenate_searches( /// Given an existing parsed search, if the provided `replacement` is a single search node such /// as a deck:xxx search, replace any instances of that search in `existing` with the new value. /// Then return the possibly modified first search as a string. -pub fn replace_search_term(mut existing: Vec, replacement: Node) -> String { +pub fn replace_search_node(mut existing: Vec, replacement: Node) -> String { if let Node::Search(search_node) = replacement { fn update_node_vec(old_nodes: &mut [Node], new_node: &SearchNode) { fn update_node(old_node: &mut Node, new_node: &SearchNode) { @@ -242,29 +242,29 @@ mod test { #[test] fn replacing() -> Result<()> { assert_eq!( - replace_search_term(parse("deck:baz bar")?, parse("deck:foo")?.pop().unwrap()), + replace_search_node(parse("deck:baz bar")?, parse("deck:foo")?.pop().unwrap()), r#""deck:foo" AND "bar""#, ); assert_eq!( - replace_search_term( + replace_search_node( parse("tag:foo Or tag:bar")?, parse("tag:baz")?.pop().unwrap() ), r#""tag:baz" OR "tag:baz""#, ); assert_eq!( - replace_search_term( + replace_search_node( parse("foo or (-foo tag:baz)")?, parse("bar")?.pop().unwrap() ), r#""bar" OR (-"bar" AND "tag:baz")"#, ); assert_eq!( - replace_search_term(parse("is:due")?, parse("-is:new")?.pop().unwrap()), + replace_search_node(parse("is:due")?, parse("-is:new")?.pop().unwrap()), r#""is:due""# ); assert_eq!( - replace_search_term(parse("added:1")?, parse("is:due")?.pop().unwrap()), + replace_search_node(parse("added:1")?, parse("is:due")?.pop().unwrap()), r#""added:1""# );