diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 379f50e1e..639f8cb88 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -31,19 +31,19 @@ from anki.rsbackend import ( # pylint: disable=unused-import ConcatSeparator, DBError, DupeIn, - FilterToSearchIn, + Flag, FormatTimeSpanContext, InvalidInput, - NamedFilter, NoteIDs, Progress, RustBackend, + SearchTerm, pb, ) from anki.sched import Scheduler as V1Scheduler from anki.schedv2 import Scheduler as V2Scheduler from anki.tags import TagManager -from anki.utils import devMode, ids2str, intTime +from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia if TYPE_CHECKING: from anki.rsbackend import FormatTimeSpanContextValue, TRValue @@ -460,8 +460,8 @@ class Collection: ) return self.backend.search_cards(search=query, order=mode) - def find_notes(self, query: str) -> Sequence[int]: - return self.backend.search_notes(query) + def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]: + return self.backend.search_notes(self.build_search_string(*terms)) def find_and_replace( self, @@ -474,8 +474,39 @@ class Collection: ) -> int: return anki.find.findReplace(self, nids, src, dst, regex, field, fold) + # returns array of ("dupestr", [nids]) def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]: - return anki.find.findDupes(self, fieldName, search) + nids = self.findNotes(search, SearchTerm(field_name=fieldName)) + # go through notes + vals: Dict[str, List[int]] = {} + dupes = [] + fields: Dict[int, int] = {} + + def ordForMid(mid): + if mid not in fields: + model = self.models.get(mid) + for c, f in enumerate(model["flds"]): + if f["name"].lower() == fieldName.lower(): + fields[mid] = c + break + return fields[mid] + + for nid, mid, flds in self.db.all( + "select id, mid, flds from notes where id in " + ids2str(nids) + ): + flds = splitFields(flds) + ord = ordForMid(mid) + if ord is None: + continue + val = flds[ord] + val = stripHTMLMedia(val) + # empty does not count as duplicate + if not val: + continue + vals.setdefault(val, []).append(nid) + if len(vals[val]) == 2: + dupes.append((val, vals[val])) + return dupes findCards = find_cards findNotes = find_notes @@ -484,68 +515,35 @@ class Collection: # Search Strings ########################################################################## - def search_string( - self, - *, - negate: bool = False, - concat_by_or: bool = False, - searches: Optional[List[str]] = None, - name: Optional["FilterToSearchIn.NamedFilterValue"] = None, - tag: Optional[str] = None, - deck: Optional[str] = None, - note: Optional[str] = None, - template: Optional[int] = None, - dupe: Optional[Tuple[int, str]] = None, - forgot_in_days: Optional[int] = None, - added_in_days: Optional[int] = None, - due_in_days: Optional[int] = None, - nids: Optional[List[int]] = None, - field_name: Optional[str] = None, + def build_search_string( + self, *terms: Union[str, SearchTerm], negate=False, match_any=False ) -> str: """Helper function for the backend's search string operations. - Pass search strings as 'search_strings' to normalize. - Pass multiple to concatenate (defaults to 'and'). + Pass terms as strings to normalize. + Pass fields of backend.proto/FilterToSearchIn as valid SearchTerms. + Pass multiple terms to concatenate (defaults to 'and', 'or' when 'match_any=True'). Pass 'negate=True' to negate the end result. May raise InvalidInput. """ - def append_filter(filter_in): - filters.append(self.backend.filter_to_search(filter_in)) - - if name: - append_filter(FilterToSearchIn(name=name)) - if tag: - append_filter(FilterToSearchIn(tag=tag)) - if deck: - append_filter(FilterToSearchIn(deck=deck)) - if note: - append_filter(FilterToSearchIn(note=note)) - if template: - append_filter(FilterToSearchIn(template=template)) - if dupe: - dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=dupe[0]), text=dupe[1]) - append_filter(FilterToSearchIn(dupe=dupe_in)) - if forgot_in_days: - append_filter(FilterToSearchIn(forgot_in_days=forgot_in_days)) - if added_in_days: - append_filter(FilterToSearchIn(added_in_days=added_in_days)) - if due_in_days: - append_filter(FilterToSearchIn(due_in_days=due_in_days)) - if nids: - append_filter(FilterToSearchIn(nids=NoteIDs(nids=nids))) - if field_name: - append_filter(FilterToSearchIn(field_name=field_name)) - if concat_by_or: + searches = [] + for term in terms: + if isinstance(term, SearchTerm): + term = self.backend.filter_to_search(term) + searches.append(term) + if match_any: sep = ConcatSeparator.OR else: sep = ConcatSeparator.AND - search_string = self.backend.concatenate_searches(sep=sep, searches=filters) + search_string = self.backend.concatenate_searches(sep=sep, searches=searches) if negate: search_string = self.backend.negate_search(search_string) return search_string def replace_search_term(self, search: str, replacement: str) -> str: + """Wrapper for the according backend function.""" + return self.backend.replace_search_term(search=search, replacement=replacement) # Config @@ -788,5 +786,18 @@ table.review-log {{ {revlog_style} }} ) +def dupe_search_term(mid: int, text: str) -> SearchTerm: + """Helper function for building a DupeIn message.""" + + dupe_in = DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text) + return SearchTerm(dupe=dupe_in) + + +def nid_search_term(nids: List[int]) -> SearchTerm: + """Helper function for building a NoteIDs message.""" + + return SearchTerm(nids=NoteIDs(nids=nids)) + + # legacy name _Collection = Collection diff --git a/pylib/anki/find.py b/pylib/anki/find.py index c7bf14cdb..ff83d5e2e 100644 --- a/pylib/anki/find.py +++ b/pylib/anki/find.py @@ -3,10 +3,9 @@ from __future__ import annotations -from typing import TYPE_CHECKING, Optional, Set, Tuple +from typing import TYPE_CHECKING, Optional, Set from anki.hooks import * -from anki.utils import ids2str, splitFields, stripHTMLMedia if TYPE_CHECKING: from anki.collection import Collection @@ -64,41 +63,3 @@ def fieldNames(col, downcase=True) -> List: if name not in fields: # slower w/o fields.add(name) return list(fields) - - -# returns array of ("dupestr", [nids]) -def findDupes( - col: Collection, fieldName: str, search: str = "" -) -> List[Tuple[Any, List]]: - # limit search to notes with applicable field name - search = col.search_string(searches=[search], field_name=fieldName) - # go through notes - vals: Dict[str, List[int]] = {} - dupes = [] - fields: Dict[int, int] = {} - - def ordForMid(mid): - if mid not in fields: - model = col.models.get(mid) - for c, f in enumerate(model["flds"]): - if f["name"].lower() == fieldName.lower(): - fields[mid] = c - break - return fields[mid] - - for nid, mid, flds in col.db.all( - "select id, mid, flds from notes where id in " + ids2str(col.findNotes(search)) - ): - flds = splitFields(flds) - ord = ordForMid(mid) - if ord is None: - continue - val = flds[ord] - val = stripHTMLMedia(val) - # empty does not count as duplicate - if not val: - continue - vals.setdefault(val, []).append(nid) - if len(vals[val]) == 2: - dupes.append((val, vals[val])) - return dupes diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index d4b16fe71..90ed6a815 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -47,8 +47,8 @@ TagTreeNode = pb.TagTreeNode NoteType = pb.NoteType DeckTreeNode = pb.DeckTreeNode StockNoteType = pb.StockNoteType -FilterToSearchIn = pb.FilterToSearchIn -NamedFilter = pb.FilterToSearchIn.NamedFilter +SearchTerm = pb.FilterToSearchIn +Flag = pb.FilterToSearchIn.Flag DupeIn = pb.FilterToSearchIn.DupeIn NoteIDs = pb.NoteIDs BackendNoteTypeID = pb.NoteTypeID diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index 5ced38aa2..c8e969568 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -16,7 +16,7 @@ import re from typing import Collection, List, Optional, Sequence, Tuple import anki # pylint: disable=unused-import -from anki.rsbackend import FilterToSearchIn +from anki.collection import SearchTerm from anki.utils import ids2str @@ -87,8 +87,7 @@ class TagManager: def rename_tag(self, old: str, new: str) -> int: "Rename provided tag, returning number of changed notes." - search = self.col.backend.filter_to_search(FilterToSearchIn(tag=old)) - nids = self.col.find_notes(search) + nids = self.col.find_notes(SearchTerm(tag=old)) if not nids: return 0 escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) diff --git a/qt/aqt/addcards.py b/qt/aqt/addcards.py index 3cfdde261..7de8b3f24 100644 --- a/qt/aqt/addcards.py +++ b/qt/aqt/addcards.py @@ -7,6 +7,7 @@ import aqt.deckchooser import aqt.editor import aqt.forms import aqt.modelchooser +from anki.collection import nid_search_term from anki.consts import MODEL_CLOZE from anki.notes import Note from anki.utils import htmlToTextLine, isMac @@ -144,7 +145,7 @@ class AddCards(QDialog): def onHistory(self) -> None: m = QMenu(self) for nid in self.history: - if self.mw.col.findNotes(self.mw.col.search_string(nids=[nid])): + if self.mw.col.findNotes(nid_search_term([nid])): note = self.mw.col.getNote(nid) fields = note.fields txt = htmlToTextLine(", ".join(fields)) @@ -161,7 +162,7 @@ class AddCards(QDialog): m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) def editHistory(self, nid): - self.mw.browser_search(nids=[nid]) + self.mw.browser_search(nid_search_term([nid])) def addNote(self, note) -> Optional[Note]: note.model()["did"] = self.deckChooser.selectedId() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 317652c5a..a91834d01 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -13,7 +13,7 @@ from typing import List, Optional, Sequence, Tuple, cast import aqt import aqt.forms from anki.cards import Card -from anki.collection import Collection, InvalidInput, NamedFilter +from anki.collection import Collection, Flag, InvalidInput, SearchTerm, nid_search_term from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType @@ -612,7 +612,9 @@ class Browser(QMainWindow): qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) self.form.searchEdit.setCompleter(None) self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH) - self._searchPromptFilter = self.col.search_string(name=NamedFilter.CURRENT_DECK) + self._searchPromptFilter = self.col.build_search_string( + SearchTerm(current_deck=True) + ) self.form.searchEdit.addItems( [self._searchPrompt] + self.mw.pm.profile["searchHistory"] ) @@ -659,7 +661,7 @@ class Browser(QMainWindow): c = self.card = self.mw.reviewer.card nid = c and c.nid or 0 if nid: - search = self.col.search_string(nids=[nid]) + search = self.col.build_search_string(nid_search_term([nid])) search = gui_hooks.default_search(search, c) self.model.search(search) self.focusCid(c.id) @@ -671,7 +673,7 @@ class Browser(QMainWindow): self._onRowChanged(None, None) def normalize_search(self, search: str) -> str: - normed = self.col.search_string(searches=[search]) + normed = self.col.build_search_string(search) self._lastSearchTxt = normed self.form.searchEdit.lineEdit().setText(normed) return normed @@ -951,23 +953,21 @@ QTableView {{ gridline-color: {grid} }} ml.popupOver(self.form.filter) - def update_search(self, *terms: str): + def update_search(self, *terms: Union[str, SearchTerm]): "Modify the current search string based on modified keys, then refresh." try: - search = self.col.search_string(searches=list(terms)) + search = self.col.build_search_string(*terms) mods = self.mw.app.keyboardModifiers() if mods & Qt.AltModifier: - search = self.col.search_string(negate=True, searches=[search]) + search = self.col.build_search_string(search, negate=True) cur = str(self.form.searchEdit.lineEdit().text()) if cur != self._searchPrompt: if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: search = self.col.replace_search_term(cur, search) elif mods & Qt.ControlModifier: - search = self.col.search_string(searches=[cur, search]) + search = self.col.build_search_string(cur, search) elif mods & Qt.ShiftModifier: - search = self.col.search_string( - concat_by_or=True, searches=[cur, search] - ) + search = self.col.build_search_string(cur, search, match_any=True) except InvalidInput as e: show_invalid_search_error(e) else: @@ -993,9 +993,9 @@ QTableView {{ gridline-color: {grid} }} subm.addChild( self._simpleFilters( ( - (tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY), - (tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY), - (tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY), + (tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)), + (tr(TR.BROWSING_STUDIED_TODAY), SearchTerm(studied_today=True)), + (tr(TR.BROWSING_AGAIN_TODAY), SearchTerm(forgot_in_days=1)), ) ) ) @@ -1006,20 +1006,20 @@ QTableView {{ gridline-color: {grid} }} subm.addChild( self._simpleFilters( ( - (tr(TR.ACTIONS_NEW), NamedFilter.NEW), - (tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN), - (tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW), - (tr(TR.FILTERING_IS_DUE), NamedFilter.DUE), + (tr(TR.ACTIONS_NEW), SearchTerm(new=True)), + (tr(TR.SCHEDULING_LEARNING), SearchTerm(learn=True)), + (tr(TR.SCHEDULING_REVIEW), SearchTerm(review=True)), + (tr(TR.FILTERING_IS_DUE), SearchTerm(due=True)), None, - (tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED), - (tr(TR.BROWSING_BURIED), NamedFilter.BURIED), + (tr(TR.BROWSING_SUSPENDED), SearchTerm(suspended=True)), + (tr(TR.BROWSING_BURIED), SearchTerm(buried=True)), None, - (tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG), - (tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG), - (tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG), - (tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG), - (tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG), - (tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG), + (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=Flag.RED)), + (tr(TR.ACTIONS_ORANGE_FLAG), SearchTerm(flag=Flag.ORANGE)), + (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=Flag.GREEN)), + (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=Flag.BLUE)), + (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=Flag.WITHOUT)), + (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=Flag.ANY)), ) ) ) @@ -1045,9 +1045,7 @@ QTableView {{ gridline-color: {grid} }} def _onSaveFilter(self) -> None: try: - filt = self.col.search_string( - searches=[self.form.searchEdit.lineEdit().text()] - ) + filt = self.col.build_search_string(self.form.searchEdit.lineEdit().text()) except InvalidInput as e: show_invalid_search_error(e) else: @@ -1088,12 +1086,12 @@ QTableView {{ gridline-color: {grid} }} def _currentFilterIsSaved(self) -> Optional[str]: filt = self.form.searchEdit.lineEdit().text() try: - filt = self.col.search_string(searches=[filt]) + filt = self.col.build_search_string(filt) except InvalidInput: pass for k, v in self.col.get_config("savedFilters").items(): try: - v = self.col.search_string(searches=[v]) + v = self.col.build_search_string(v) except InvalidInput: pass if filt == v: @@ -1494,7 +1492,7 @@ where id in %s""" tv = self.form.tableView tv.selectionModel().clear() - search = self.col.search_string(nids=nids) + search = self.col.build_search_string(nid_search_term(nids)) self.search_for(search) tv.selectAll() @@ -1703,7 +1701,7 @@ where id in %s""" t += ( """
  • %s: %s""" % ( - self.col.search_string(nids=nids).replace('"', """), + html.escape(self.col.build_search_string(nid_search_term(nids))), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), html.escape(val), ) diff --git a/qt/aqt/customstudy.py b/qt/aqt/customstudy.py index ca5c5c3c6..e65f4040b 100644 --- a/qt/aqt/customstudy.py +++ b/qt/aqt/customstudy.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import aqt -from anki.collection import NamedFilter +from anki.collection import SearchTerm from anki.consts import * from aqt.qt import * from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr @@ -160,29 +160,33 @@ class CustomStudy(QDialog): dyn = self.mw.col.decks.get(did) # and then set various options if i == RADIO_FORGOT: - search = self.mw.col.search_string(forgot_in_days=spin) + search = self.mw.col.build_search_string(SearchTerm(forgot_in_days=spin)) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM] dyn["resched"] = False elif i == RADIO_AHEAD: - search = self.mw.col.search_string(due_in_days=spin) + search = self.mw.col.build_search_string(SearchTerm(due_in_days=spin)) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE] dyn["resched"] = True elif i == RADIO_PREVIEW: - search = self.mw.col.search_string(name=NamedFilter.NEW, added_in_days=spin) + search = self.mw.col.build_search_string( + SearchTerm(new=True), SearchTerm(added_in_days=spin) + ) dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST] dyn["resched"] = False elif i == RADIO_CRAM: type = f.cardType.currentRow() if type == TYPE_NEW: - terms = self.mw.col.search_string(name=NamedFilter.NEW) + terms = self.mw.col.build_search_string(SearchTerm(new=True)) ord = DYN_ADDED dyn["resched"] = True elif type == TYPE_DUE: - terms = self.mw.col.search_string(name=NamedFilter.DUE) + terms = self.mw.col.build_search_string(SearchTerm(due=True)) ord = DYN_DUE dyn["resched"] = True elif type == TYPE_REVIEW: - terms = self.mw.col.search_string(negate=True, name=NamedFilter.NEW) + terms = self.mw.col.build_search_string( + SearchTerm(new=True), negate=True + ) ord = DYN_RANDOM dyn["resched"] = True else: @@ -191,8 +195,8 @@ class CustomStudy(QDialog): dyn["resched"] = False dyn["terms"][0] = [(terms + tags).strip(), spin, ord] # add deck limit - dyn["terms"][0][0] = self.mw.col.search_string( - deck=self.deck["name"], searches=[dyn["terms"][0][0]] + dyn["terms"][0][0] = self.mw.col.build_search_string( + dyn["terms"][0][0], SearchTerm(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 9ae506431..1a0242835 100644 --- a/qt/aqt/dyndeckconf.py +++ b/qt/aqt/dyndeckconf.py @@ -4,7 +4,7 @@ from typing import List, Optional import aqt -from anki.collection import InvalidInput, NamedFilter +from anki.collection import InvalidInput, SearchTerm from anki.lang import without_unicode_isolation from aqt.qt import * from aqt.utils import ( @@ -47,11 +47,9 @@ class DeckConf(QDialog): self.initialSetup() self.loadConf() if search: - search = self.mw.col.search_string(searches=[search], name=NamedFilter.DUE) + search = self.mw.col.build_search_string(search, SearchTerm(due=True)) self.form.search.setText(search) - search_2 = self.mw.col.search_string( - searches=[search], name=NamedFilter.NEW - ) + search_2 = self.mw.col.build_search_string(search, SearchTerm(new=True)) self.form.search_2.setText(search_2) self.form.search.selectAll() @@ -123,11 +121,11 @@ class DeckConf(QDialog): else: d["delays"] = None - search = self.mw.col.search_string(searches=[f.search.text()]) + search = self.mw.col.build_search_string(f.search.text()) terms = [[search, f.limit.value(), f.order.currentIndex()]] if f.secondFilter.isChecked(): - search_2 = self.mw.col.search_string(searches=[f.search_2.text()]) + search_2 = self.mw.col.build_search_string(f.search_2.text()) terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()]) d["terms"] = terms diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index cff937d44..9899390c5 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -21,6 +21,7 @@ from bs4 import BeautifulSoup import aqt import aqt.sound from anki.cards import Card +from anki.collection import dupe_search_term from anki.hooks import runFilter from anki.httpclient import HttpClient from anki.notes import Note @@ -539,7 +540,9 @@ class Editor: self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) def showDupes(self): - self.mw.browser_search(dupe=(self.note.model()["id"], self.note.fields[0])) + self.mw.browser_search( + dupe_search_term(self.note.model()["id"], self.note.fields[0]) + ) def fieldsAreBlank(self, previousNote=None): if not self.note: diff --git a/qt/aqt/emptycards.py b/qt/aqt/emptycards.py index 49df85598..e399ce1da 100644 --- a/qt/aqt/emptycards.py +++ b/qt/aqt/emptycards.py @@ -66,7 +66,7 @@ class EmptyCardsDialog(QDialog): self._delete_button.clicked.connect(self._on_delete) def _on_note_link_clicked(self, link): - self.mw.browser_search(searches=[link]) + self.mw.browser_search(link) def _on_delete(self): self.mw.progress.start() diff --git a/qt/aqt/main.py b/qt/aqt/main.py index b0e46f6c3..c98973da9 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -26,7 +26,7 @@ import aqt.stats import aqt.toolbar import aqt.webview from anki import hooks -from anki.collection import Collection +from anki.collection import Collection, SearchTerm from anki.decks import Deck from anki.hooks import runHook from anki.lang import without_unicode_isolation @@ -1141,7 +1141,7 @@ title="%s" %s>%s""" % ( deck = self.col.decks.current() if not search: if not deck["dyn"]: - search = self.col.search_string(deck=deck["name"]) + search = self.col.build_search_string(SearchTerm(deck=deck["name"])) while self.col.decks.id_for_name( without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n)) ): @@ -1621,10 +1621,10 @@ title="%s" %s>%s""" % ( # Helpers for all windows ########################################################################## - def browser_search(self, **kwargs) -> None: - """Wrapper for col.search_string() to look up the result in the browser.""" + def browser_search(self, *terms: Union[str, SearchTerm]) -> None: + """Wrapper for col.build_search_string() to look up the result in the browser.""" - search = self.col.search_string(**kwargs) + search = self.col.build_search_string(*terms) browser = aqt.dialogs.open("Browser", self) browser.form.searchEdit.lineEdit().setText(search) browser.onSearchActivated() diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 86616cec1..3b128975d 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -9,6 +9,7 @@ from concurrent.futures import Future from typing import Iterable, List, Optional, Sequence, TypeVar import aqt +from anki.collection import nid_search_term from anki.rsbackend import TR, Interrupted, ProgressKind, pb from aqt.qt import * from aqt.utils import ( @@ -145,7 +146,7 @@ class MediaChecker: if out is not None: nid, err = out - self.mw.browser_search(nids=[nid]) + self.mw.browser_search(nid_search_term([nid])) showText(err, type="html") else: tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 7f0eef43e..088065c95 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -7,6 +7,7 @@ from dataclasses import dataclass from typing import Optional import aqt +from anki.collection import SearchTerm from aqt import gui_hooks from aqt.sound import av_player from aqt.toolbar import BottomBar @@ -72,7 +73,7 @@ class Overview: self.mw.onDeckConf() elif url == "cram": deck = self.mw.col.decks.current()["name"] - self.mw.onCram(self.mw.col.search_string(deck=deck)) + self.mw.onCram(self.mw.col.build_search_string(SearchTerm(deck=deck))) elif url == "refresh": self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected()) self.mw.reset() diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f9732241c..d90c2804d 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -9,10 +9,7 @@ from enum import Enum from typing import Iterable, List, Optional import aqt -from anki.collection import ( # pylint: disable=unused-import - FilterToSearchIn, - NamedFilter, -) +from anki.collection import SearchTerm from anki.errors import DeckRenameError from anki.rsbackend import DeckTreeNode, TagTreeNode from aqt import gui_hooks @@ -294,14 +291,14 @@ class SidebarTreeView(QTreeView): item = SidebarItem( tr(TR.BROWSING_WHOLE_COLLECTION), ":/icons/collection.svg", - self._named_filter(NamedFilter.WHOLE_COLLECTION), + self._filter_func(SearchTerm(whole_collection=True)), item_type=SidebarItemType.COLLECTION, ) root.addChild(item) item = SidebarItem( tr(TR.BROWSING_CURRENT_DECK), ":/icons/deck.svg", - self._named_filter(NamedFilter.CURRENT_DECK), + self._filter_func(SearchTerm(current_deck=True)), item_type=SidebarItemType.CURRENT_DECK, ) root.addChild(item) @@ -313,7 +310,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( name, ":/icons/heart.svg", - self._saved_filter(filt), + self._filter_func(filt), item_type=SidebarItemType.FILTER, ) root.addChild(item) @@ -333,7 +330,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( node.name, ":/icons/tag.svg", - self._tag_filter(head + node.name), + self._filter_func(SearchTerm(tag=head + node.name)), toggle_expand(), not node.collapsed, item_type=SidebarItemType.TAG, @@ -358,7 +355,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( node.name, ":/icons/deck.svg", - self._deck_filter(head + node.name), + self._filter_func(SearchTerm(deck=head + node.name)), toggle_expand(), not node.collapsed, item_type=SidebarItemType.DECK, @@ -377,7 +374,7 @@ class SidebarTreeView(QTreeView): item = SidebarItem( nt["name"], ":/icons/notetype.svg", - self._note_filter(nt["name"]), + self._filter_func(SearchTerm(note=nt["name"])), item_type=SidebarItemType.NOTETYPE, id=nt["id"], ) @@ -386,32 +383,17 @@ class SidebarTreeView(QTreeView): child = SidebarItem( tmpl["name"], ":/icons/notetype.svg", - self._template_filter(nt["name"], c), + self._filter_func( + SearchTerm(note=nt["name"]), SearchTerm(template=c) + ), item_type=SidebarItemType.TEMPLATE, ) item.addChild(child) root.addChild(item) - def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable: - return lambda: self.browser.update_search(self.col.search_string(name=name)) - - def _tag_filter(self, tag: str) -> Callable: - return lambda: self.browser.update_search(self.col.search_string(tag=tag)) - - def _deck_filter(self, deck: str) -> Callable: - return lambda: self.browser.update_search(self.col.search_string(deck=deck)) - - def _note_filter(self, note: str) -> Callable: - return lambda: self.browser.update_search(self.col.search_string(note=note)) - - def _template_filter(self, note: str, template: int) -> Callable: - return lambda: self.browser.update_search( - self.col.search_string(note=note), self.col.search_string(template=template) - ) - - def _saved_filter(self, saved: str) -> Callable: - return lambda: self.browser.update_search(saved) + def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable: + return lambda: self.browser.update_search(self.col.build_search_string(*terms)) # Context menu actions ########################### diff --git a/rslib/backend.proto b/rslib/backend.proto index fed42f957..44b028845 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -765,41 +765,39 @@ message BuiltinSearchOrder { } message FilterToSearchIn { - enum NamedFilter { - WHOLE_COLLECTION = 0; - CURRENT_DECK = 1; - ADDED_TODAY = 2; - STUDIED_TODAY = 3; - AGAIN_TODAY = 4; - NEW = 5; - LEARN = 6; - REVIEW = 7; - DUE = 8; - SUSPENDED = 9; - BURIED = 10; - RED_FLAG = 11; - ORANGE_FLAG = 12; - GREEN_FLAG = 13; - BLUE_FLAG = 14; - NO_FLAG = 15; - ANY_FLAG = 16; - } message DupeIn { NoteTypeID mid = 1; string text = 2; } + enum Flag { + WITHOUT = 0; + ANY = 1; + RED = 2; + ORANGE = 3; + GREEN = 4; + BLUE = 5; + } oneof filter { - NamedFilter name = 1; - string tag = 2; - string deck = 3; - string note = 4; - uint32 template = 5; + string tag = 1; + string deck = 2; + string note = 3; + uint32 template = 4; + NoteIDs nids = 5; DupeIn dupe = 6; - uint32 forgot_in_days = 7; - uint32 added_in_days = 8; - int32 due_in_days = 9; - NoteIDs nids = 10; - string field_name = 11; + string field_name = 7; + uint32 forgot_in_days = 8; + uint32 added_in_days = 9; + int32 due_in_days = 10; + bool whole_collection = 11; + bool current_deck = 12; + bool studied_today = 13; + bool new = 14; + bool learn = 15; + bool review = 16; + bool due = 17; + bool suspended = 18; + bool buried = 19; + Flag flag = 20; } } diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index 5a6f6aba2..8d6b19dc7 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -293,38 +293,8 @@ impl From for DeckConfID { impl From for Node<'_> { fn from(msg: pb::FilterToSearchIn) -> Self { use pb::filter_to_search_in::Filter; - use pb::filter_to_search_in::NamedFilter; - match msg - .filter - .unwrap_or(Filter::Name(NamedFilter::WholeCollection as i32)) - { - Filter::Name(name) => { - match NamedFilter::from_i32(name).unwrap_or(NamedFilter::WholeCollection) { - NamedFilter::WholeCollection => Node::Search(SearchNode::WholeCollection), - NamedFilter::CurrentDeck => Node::Search(SearchNode::Deck("current".into())), - NamedFilter::AddedToday => Node::Search(SearchNode::AddedInDays(1)), - NamedFilter::StudiedToday => Node::Search(SearchNode::Rated { - days: 1, - ease: EaseKind::AnyAnswerButton, - }), - NamedFilter::AgainToday => Node::Search(SearchNode::Rated { - days: 1, - ease: EaseKind::AnswerButton(1), - }), - NamedFilter::New => Node::Search(SearchNode::State(StateKind::New)), - NamedFilter::Learn => Node::Search(SearchNode::State(StateKind::Learning)), - NamedFilter::Review => Node::Search(SearchNode::State(StateKind::Review)), - NamedFilter::Due => Node::Search(SearchNode::State(StateKind::Due)), - NamedFilter::Suspended => Node::Search(SearchNode::State(StateKind::Suspended)), - NamedFilter::Buried => Node::Search(SearchNode::State(StateKind::Buried)), - NamedFilter::RedFlag => Node::Search(SearchNode::Flag(1)), - NamedFilter::OrangeFlag => Node::Search(SearchNode::Flag(2)), - NamedFilter::GreenFlag => Node::Search(SearchNode::Flag(3)), - NamedFilter::BlueFlag => Node::Search(SearchNode::Flag(4)), - NamedFilter::NoFlag => Node::Search(SearchNode::Flag(0)), - NamedFilter::AnyFlag => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))), - } - } + use pb::filter_to_search_in::Flag; + match msg.filter.unwrap_or(Filter::WholeCollection(true)) { Filter::Tag(s) => Node::Search(SearchNode::Tag( escape_anki_wildcards(&s).into_owned().into(), )), @@ -337,10 +307,16 @@ impl From for Node<'_> { Filter::Template(u) => { Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) } + Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())), Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates { note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(), text: dupe.text.into(), }), + Filter::FieldName(s) => Node::Search(SearchNode::SingleField { + field: escape_anki_wildcards(&s).into_owned().into(), + text: "*".to_string().into(), + is_re: false, + }), Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated { days: u, ease: EaseKind::AnswerButton(1), @@ -350,12 +326,26 @@ impl From for Node<'_> { operator: "<=".to_string(), kind: PropertyKind::Due(i), }), - Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())), - Filter::FieldName(s) => Node::Search(SearchNode::SingleField { - field: escape_anki_wildcards(&s).into_owned().into(), - text: "*".to_string().into(), - is_re: false, + Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection), + Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())), + Filter::StudiedToday(_) => Node::Search(SearchNode::Rated { + days: 1, + ease: EaseKind::AnyAnswerButton, }), + Filter::New(_) => Node::Search(SearchNode::State(StateKind::New)), + Filter::Learn(_) => Node::Search(SearchNode::State(StateKind::Learning)), + Filter::Review(_) => Node::Search(SearchNode::State(StateKind::Review)), + Filter::Due(_) => Node::Search(SearchNode::State(StateKind::Due)), + Filter::Suspended(_) => Node::Search(SearchNode::State(StateKind::Suspended)), + Filter::Buried(_) => Node::Search(SearchNode::State(StateKind::Buried)), + Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) { + Flag::Without => Node::Search(SearchNode::Flag(0)), + Flag::Any => Node::Not(Box::new(Node::Search(SearchNode::Flag(0)))), + Flag::Red => Node::Search(SearchNode::Flag(1)), + Flag::Orange => Node::Search(SearchNode::Flag(2)), + Flag::Green => Node::Search(SearchNode::Flag(3)), + Flag::Blue => Node::Search(SearchNode::Flag(4)), + }, } } }