Refactor search_string() and FilterToSearchIn

See #955.
This commit is contained in:
RumovZ 2021-01-29 18:27:33 +01:00
parent 349bd9d681
commit c299e271e8
16 changed files with 201 additions and 254 deletions

View file

@ -31,19 +31,19 @@ from anki.rsbackend import ( # pylint: disable=unused-import
ConcatSeparator, ConcatSeparator,
DBError, DBError,
DupeIn, DupeIn,
FilterToSearchIn, Flag,
FormatTimeSpanContext, FormatTimeSpanContext,
InvalidInput, InvalidInput,
NamedFilter,
NoteIDs, NoteIDs,
Progress, Progress,
RustBackend, RustBackend,
SearchTerm,
pb, pb,
) )
from anki.sched import Scheduler as V1Scheduler from anki.sched import Scheduler as V1Scheduler
from anki.schedv2 import Scheduler as V2Scheduler from anki.schedv2 import Scheduler as V2Scheduler
from anki.tags import TagManager from anki.tags import TagManager
from anki.utils import devMode, ids2str, intTime from anki.utils import devMode, ids2str, intTime, splitFields, stripHTMLMedia
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.rsbackend import FormatTimeSpanContextValue, TRValue from anki.rsbackend import FormatTimeSpanContextValue, TRValue
@ -460,8 +460,8 @@ class Collection:
) )
return self.backend.search_cards(search=query, order=mode) return self.backend.search_cards(search=query, order=mode)
def find_notes(self, query: str) -> Sequence[int]: def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]:
return self.backend.search_notes(query) return self.backend.search_notes(self.build_search_string(*terms))
def find_and_replace( def find_and_replace(
self, self,
@ -474,8 +474,39 @@ class Collection:
) -> int: ) -> int:
return anki.find.findReplace(self, nids, src, dst, regex, field, fold) 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]]: 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 findCards = find_cards
findNotes = find_notes findNotes = find_notes
@ -484,68 +515,35 @@ class Collection:
# Search Strings # Search Strings
########################################################################## ##########################################################################
def search_string( def build_search_string(
self, self, *terms: Union[str, SearchTerm], negate=False, match_any=False
*,
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,
) -> str: ) -> str:
"""Helper function for the backend's search string operations. """Helper function for the backend's search string operations.
Pass search strings as 'search_strings' to normalize. Pass terms as strings to normalize.
Pass multiple to concatenate (defaults to 'and'). 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. Pass 'negate=True' to negate the end result.
May raise InvalidInput. May raise InvalidInput.
""" """
def append_filter(filter_in): searches = []
filters.append(self.backend.filter_to_search(filter_in)) for term in terms:
if isinstance(term, SearchTerm):
if name: term = self.backend.filter_to_search(term)
append_filter(FilterToSearchIn(name=name)) searches.append(term)
if tag: if match_any:
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:
sep = ConcatSeparator.OR sep = ConcatSeparator.OR
else: else:
sep = ConcatSeparator.AND 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: if negate:
search_string = self.backend.negate_search(search_string) search_string = self.backend.negate_search(search_string)
return search_string return search_string
def replace_search_term(self, search: str, replacement: str) -> str: 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) return self.backend.replace_search_term(search=search, replacement=replacement)
# Config # 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 # legacy name
_Collection = Collection _Collection = Collection

View file

@ -3,10 +3,9 @@
from __future__ import annotations 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.hooks import *
from anki.utils import ids2str, splitFields, stripHTMLMedia
if TYPE_CHECKING: if TYPE_CHECKING:
from anki.collection import Collection from anki.collection import Collection
@ -64,41 +63,3 @@ def fieldNames(col, downcase=True) -> List:
if name not in fields: # slower w/o if name not in fields: # slower w/o
fields.add(name) fields.add(name)
return list(fields) 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

View file

@ -47,8 +47,8 @@ TagTreeNode = pb.TagTreeNode
NoteType = pb.NoteType NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode DeckTreeNode = pb.DeckTreeNode
StockNoteType = pb.StockNoteType StockNoteType = pb.StockNoteType
FilterToSearchIn = pb.FilterToSearchIn SearchTerm = pb.FilterToSearchIn
NamedFilter = pb.FilterToSearchIn.NamedFilter Flag = pb.FilterToSearchIn.Flag
DupeIn = pb.FilterToSearchIn.DupeIn DupeIn = pb.FilterToSearchIn.DupeIn
NoteIDs = pb.NoteIDs NoteIDs = pb.NoteIDs
BackendNoteTypeID = pb.NoteTypeID BackendNoteTypeID = pb.NoteTypeID

View file

@ -16,7 +16,7 @@ import re
from typing import Collection, List, Optional, Sequence, Tuple from typing import Collection, List, Optional, Sequence, Tuple
import anki # pylint: disable=unused-import import anki # pylint: disable=unused-import
from anki.rsbackend import FilterToSearchIn from anki.collection import SearchTerm
from anki.utils import ids2str from anki.utils import ids2str
@ -87,8 +87,7 @@ class TagManager:
def rename_tag(self, old: str, new: str) -> int: def rename_tag(self, old: str, new: str) -> int:
"Rename provided tag, returning number of changed notes." "Rename provided tag, returning number of changed notes."
search = self.col.backend.filter_to_search(FilterToSearchIn(tag=old)) nids = self.col.find_notes(SearchTerm(tag=old))
nids = self.col.find_notes(search)
if not nids: if not nids:
return 0 return 0
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old) escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)

View file

@ -7,6 +7,7 @@ import aqt.deckchooser
import aqt.editor import aqt.editor
import aqt.forms import aqt.forms
import aqt.modelchooser import aqt.modelchooser
from anki.collection import nid_search_term
from anki.consts import MODEL_CLOZE from anki.consts import MODEL_CLOZE
from anki.notes import Note from anki.notes import Note
from anki.utils import htmlToTextLine, isMac from anki.utils import htmlToTextLine, isMac
@ -144,7 +145,7 @@ class AddCards(QDialog):
def onHistory(self) -> None: def onHistory(self) -> None:
m = QMenu(self) m = QMenu(self)
for nid in self.history: 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) note = self.mw.col.getNote(nid)
fields = note.fields fields = note.fields
txt = htmlToTextLine(", ".join(fields)) txt = htmlToTextLine(", ".join(fields))
@ -161,7 +162,7 @@ class AddCards(QDialog):
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0))) m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid): def editHistory(self, nid):
self.mw.browser_search(nids=[nid]) self.mw.browser_search(nid_search_term([nid]))
def addNote(self, note) -> Optional[Note]: def addNote(self, note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId() note.model()["did"] = self.deckChooser.selectedId()

View file

@ -13,7 +13,7 @@ from typing import List, Optional, Sequence, Tuple, cast
import aqt import aqt
import aqt.forms import aqt.forms
from anki.cards import Card 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.consts import *
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from anki.models import NoteType from anki.models import NoteType
@ -612,7 +612,9 @@ class Browser(QMainWindow):
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated) qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None) self.form.searchEdit.setCompleter(None)
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH) 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.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"] [self._searchPrompt] + self.mw.pm.profile["searchHistory"]
) )
@ -659,7 +661,7 @@ class Browser(QMainWindow):
c = self.card = self.mw.reviewer.card c = self.card = self.mw.reviewer.card
nid = c and c.nid or 0 nid = c and c.nid or 0
if nid: 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) search = gui_hooks.default_search(search, c)
self.model.search(search) self.model.search(search)
self.focusCid(c.id) self.focusCid(c.id)
@ -671,7 +673,7 @@ class Browser(QMainWindow):
self._onRowChanged(None, None) self._onRowChanged(None, None)
def normalize_search(self, search: str) -> str: 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._lastSearchTxt = normed
self.form.searchEdit.lineEdit().setText(normed) self.form.searchEdit.lineEdit().setText(normed)
return normed return normed
@ -951,23 +953,21 @@ QTableView {{ gridline-color: {grid} }}
ml.popupOver(self.form.filter) 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." "Modify the current search string based on modified keys, then refresh."
try: try:
search = self.col.search_string(searches=list(terms)) search = self.col.build_search_string(*terms)
mods = self.mw.app.keyboardModifiers() mods = self.mw.app.keyboardModifiers()
if mods & Qt.AltModifier: 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()) cur = str(self.form.searchEdit.lineEdit().text())
if cur != self._searchPrompt: if cur != self._searchPrompt:
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier: if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
search = self.col.replace_search_term(cur, search) search = self.col.replace_search_term(cur, search)
elif mods & Qt.ControlModifier: elif mods & Qt.ControlModifier:
search = self.col.search_string(searches=[cur, search]) search = self.col.build_search_string(cur, search)
elif mods & Qt.ShiftModifier: elif mods & Qt.ShiftModifier:
search = self.col.search_string( search = self.col.build_search_string(cur, search, match_any=True)
concat_by_or=True, searches=[cur, search]
)
except InvalidInput as e: except InvalidInput as e:
show_invalid_search_error(e) show_invalid_search_error(e)
else: else:
@ -993,9 +993,9 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild( subm.addChild(
self._simpleFilters( self._simpleFilters(
( (
(tr(TR.BROWSING_ADDED_TODAY), NamedFilter.ADDED_TODAY), (tr(TR.BROWSING_ADDED_TODAY), SearchTerm(added_in_days=1)),
(tr(TR.BROWSING_STUDIED_TODAY), NamedFilter.STUDIED_TODAY), (tr(TR.BROWSING_STUDIED_TODAY), SearchTerm(studied_today=True)),
(tr(TR.BROWSING_AGAIN_TODAY), NamedFilter.AGAIN_TODAY), (tr(TR.BROWSING_AGAIN_TODAY), SearchTerm(forgot_in_days=1)),
) )
) )
) )
@ -1006,20 +1006,20 @@ QTableView {{ gridline-color: {grid} }}
subm.addChild( subm.addChild(
self._simpleFilters( self._simpleFilters(
( (
(tr(TR.ACTIONS_NEW), NamedFilter.NEW), (tr(TR.ACTIONS_NEW), SearchTerm(new=True)),
(tr(TR.SCHEDULING_LEARNING), NamedFilter.LEARN), (tr(TR.SCHEDULING_LEARNING), SearchTerm(learn=True)),
(tr(TR.SCHEDULING_REVIEW), NamedFilter.REVIEW), (tr(TR.SCHEDULING_REVIEW), SearchTerm(review=True)),
(tr(TR.FILTERING_IS_DUE), NamedFilter.DUE), (tr(TR.FILTERING_IS_DUE), SearchTerm(due=True)),
None, None,
(tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED), (tr(TR.BROWSING_SUSPENDED), SearchTerm(suspended=True)),
(tr(TR.BROWSING_BURIED), NamedFilter.BURIED), (tr(TR.BROWSING_BURIED), SearchTerm(buried=True)),
None, None,
(tr(TR.ACTIONS_RED_FLAG), NamedFilter.RED_FLAG), (tr(TR.ACTIONS_RED_FLAG), SearchTerm(flag=Flag.RED)),
(tr(TR.ACTIONS_ORANGE_FLAG), NamedFilter.ORANGE_FLAG), (tr(TR.ACTIONS_ORANGE_FLAG), SearchTerm(flag=Flag.ORANGE)),
(tr(TR.ACTIONS_GREEN_FLAG), NamedFilter.GREEN_FLAG), (tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=Flag.GREEN)),
(tr(TR.ACTIONS_BLUE_FLAG), NamedFilter.BLUE_FLAG), (tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=Flag.BLUE)),
(tr(TR.BROWSING_NO_FLAG), NamedFilter.NO_FLAG), (tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=Flag.WITHOUT)),
(tr(TR.BROWSING_ANY_FLAG), NamedFilter.ANY_FLAG), (tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=Flag.ANY)),
) )
) )
) )
@ -1045,9 +1045,7 @@ QTableView {{ gridline-color: {grid} }}
def _onSaveFilter(self) -> None: def _onSaveFilter(self) -> None:
try: try:
filt = self.col.search_string( filt = self.col.build_search_string(self.form.searchEdit.lineEdit().text())
searches=[self.form.searchEdit.lineEdit().text()]
)
except InvalidInput as e: except InvalidInput as e:
show_invalid_search_error(e) show_invalid_search_error(e)
else: else:
@ -1088,12 +1086,12 @@ QTableView {{ gridline-color: {grid} }}
def _currentFilterIsSaved(self) -> Optional[str]: def _currentFilterIsSaved(self) -> Optional[str]:
filt = self.form.searchEdit.lineEdit().text() filt = self.form.searchEdit.lineEdit().text()
try: try:
filt = self.col.search_string(searches=[filt]) filt = self.col.build_search_string(filt)
except InvalidInput: except InvalidInput:
pass pass
for k, v in self.col.get_config("savedFilters").items(): for k, v in self.col.get_config("savedFilters").items():
try: try:
v = self.col.search_string(searches=[v]) v = self.col.build_search_string(v)
except InvalidInput: except InvalidInput:
pass pass
if filt == v: if filt == v:
@ -1494,7 +1492,7 @@ where id in %s"""
tv = self.form.tableView tv = self.form.tableView
tv.selectionModel().clear() tv.selectionModel().clear()
search = self.col.search_string(nids=nids) search = self.col.build_search_string(nid_search_term(nids))
self.search_for(search) self.search_for(search)
tv.selectAll() tv.selectAll()
@ -1703,7 +1701,7 @@ where id in %s"""
t += ( t += (
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>""" """<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
% ( % (
self.col.search_string(nids=nids).replace('"', "&quot;"), html.escape(self.col.build_search_string(nid_search_term(nids))),
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)), tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
html.escape(val), html.escape(val),
) )

View file

@ -2,7 +2,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt import aqt
from anki.collection import NamedFilter from anki.collection import SearchTerm
from anki.consts import * from anki.consts import *
from aqt.qt import * from aqt.qt import *
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr 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) dyn = self.mw.col.decks.get(did)
# and then set various options # and then set various options
if i == RADIO_FORGOT: 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["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
dyn["resched"] = False dyn["resched"] = False
elif i == RADIO_AHEAD: 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["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE]
dyn["resched"] = True dyn["resched"] = True
elif i == RADIO_PREVIEW: 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["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST]
dyn["resched"] = False dyn["resched"] = False
elif i == RADIO_CRAM: elif i == RADIO_CRAM:
type = f.cardType.currentRow() type = f.cardType.currentRow()
if type == TYPE_NEW: 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 ord = DYN_ADDED
dyn["resched"] = True dyn["resched"] = True
elif type == TYPE_DUE: 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 ord = DYN_DUE
dyn["resched"] = True dyn["resched"] = True
elif type == TYPE_REVIEW: 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 ord = DYN_RANDOM
dyn["resched"] = True dyn["resched"] = True
else: else:
@ -191,8 +195,8 @@ class CustomStudy(QDialog):
dyn["resched"] = False dyn["resched"] = False
dyn["terms"][0] = [(terms + tags).strip(), spin, ord] dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
# add deck limit # add deck limit
dyn["terms"][0][0] = self.mw.col.search_string( dyn["terms"][0][0] = self.mw.col.build_search_string(
deck=self.deck["name"], searches=[dyn["terms"][0][0]] dyn["terms"][0][0], SearchTerm(deck=self.deck["name"])
) )
self.mw.col.decks.save(dyn) self.mw.col.decks.save(dyn)
# generate cards # generate cards

View file

@ -4,7 +4,7 @@
from typing import List, Optional from typing import List, Optional
import aqt import aqt
from anki.collection import InvalidInput, NamedFilter from anki.collection import InvalidInput, SearchTerm
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
@ -47,11 +47,9 @@ class DeckConf(QDialog):
self.initialSetup() self.initialSetup()
self.loadConf() self.loadConf()
if search: 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) self.form.search.setText(search)
search_2 = self.mw.col.search_string( search_2 = self.mw.col.build_search_string(search, SearchTerm(new=True))
searches=[search], name=NamedFilter.NEW
)
self.form.search_2.setText(search_2) self.form.search_2.setText(search_2)
self.form.search.selectAll() self.form.search.selectAll()
@ -123,11 +121,11 @@ class DeckConf(QDialog):
else: else:
d["delays"] = None 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()]] terms = [[search, f.limit.value(), f.order.currentIndex()]]
if f.secondFilter.isChecked(): 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()]) terms.append([search_2, f.limit_2.value(), f.order_2.currentIndex()])
d["terms"] = terms d["terms"] = terms

View file

@ -21,6 +21,7 @@ from bs4 import BeautifulSoup
import aqt import aqt
import aqt.sound import aqt.sound
from anki.cards import Card from anki.cards import Card
from anki.collection import dupe_search_term
from anki.hooks import runFilter from anki.hooks import runFilter
from anki.httpclient import HttpClient from anki.httpclient import HttpClient
from anki.notes import Note from anki.notes import Note
@ -539,7 +540,9 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols)) self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self): 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): def fieldsAreBlank(self, previousNote=None):
if not self.note: if not self.note:

View file

@ -66,7 +66,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button.clicked.connect(self._on_delete) self._delete_button.clicked.connect(self._on_delete)
def _on_note_link_clicked(self, link): def _on_note_link_clicked(self, link):
self.mw.browser_search(searches=[link]) self.mw.browser_search(link)
def _on_delete(self): def _on_delete(self):
self.mw.progress.start() self.mw.progress.start()

View file

@ -26,7 +26,7 @@ import aqt.stats
import aqt.toolbar import aqt.toolbar
import aqt.webview import aqt.webview
from anki import hooks from anki import hooks
from anki.collection import Collection from anki.collection import Collection, SearchTerm
from anki.decks import Deck from anki.decks import Deck
from anki.hooks import runHook from anki.hooks import runHook
from anki.lang import without_unicode_isolation from anki.lang import without_unicode_isolation
@ -1141,7 +1141,7 @@ title="%s" %s>%s</button>""" % (
deck = self.col.decks.current() deck = self.col.decks.current()
if not search: if not search:
if not deck["dyn"]: 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( while self.col.decks.id_for_name(
without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n)) without_unicode_isolation(tr(TR.QT_MISC_FILTERED_DECK, val=n))
): ):
@ -1621,10 +1621,10 @@ title="%s" %s>%s</button>""" % (
# Helpers for all windows # Helpers for all windows
########################################################################## ##########################################################################
def browser_search(self, **kwargs) -> None: def browser_search(self, *terms: Union[str, SearchTerm]) -> None:
"""Wrapper for col.search_string() to look up the result in the browser.""" """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 = aqt.dialogs.open("Browser", self)
browser.form.searchEdit.lineEdit().setText(search) browser.form.searchEdit.lineEdit().setText(search)
browser.onSearchActivated() browser.onSearchActivated()

View file

@ -9,6 +9,7 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt import aqt
from anki.collection import nid_search_term
from anki.rsbackend import TR, Interrupted, ProgressKind, pb from anki.rsbackend import TR, Interrupted, ProgressKind, pb
from aqt.qt import * from aqt.qt import *
from aqt.utils import ( from aqt.utils import (
@ -145,7 +146,7 @@ class MediaChecker:
if out is not None: if out is not None:
nid, err = out nid, err = out
self.mw.browser_search(nids=[nid]) self.mw.browser_search(nid_search_term([nid]))
showText(err, type="html") showText(err, type="html")
else: else:
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))

View file

@ -7,6 +7,7 @@ from dataclasses import dataclass
from typing import Optional from typing import Optional
import aqt import aqt
from anki.collection import SearchTerm
from aqt import gui_hooks from aqt import gui_hooks
from aqt.sound import av_player from aqt.sound import av_player
from aqt.toolbar import BottomBar from aqt.toolbar import BottomBar
@ -72,7 +73,7 @@ class Overview:
self.mw.onDeckConf() self.mw.onDeckConf()
elif url == "cram": elif url == "cram":
deck = self.mw.col.decks.current()["name"] 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": elif url == "refresh":
self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected()) self.mw.col.sched.rebuild_filtered_deck(self.mw.col.decks.selected())
self.mw.reset() self.mw.reset()

View file

@ -9,10 +9,7 @@ from enum import Enum
from typing import Iterable, List, Optional from typing import Iterable, List, Optional
import aqt import aqt
from anki.collection import ( # pylint: disable=unused-import from anki.collection import SearchTerm
FilterToSearchIn,
NamedFilter,
)
from anki.errors import DeckRenameError from anki.errors import DeckRenameError
from anki.rsbackend import DeckTreeNode, TagTreeNode from anki.rsbackend import DeckTreeNode, TagTreeNode
from aqt import gui_hooks from aqt import gui_hooks
@ -294,14 +291,14 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION), tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg", ":/icons/collection.svg",
self._named_filter(NamedFilter.WHOLE_COLLECTION), self._filter_func(SearchTerm(whole_collection=True)),
item_type=SidebarItemType.COLLECTION, item_type=SidebarItemType.COLLECTION,
) )
root.addChild(item) root.addChild(item)
item = SidebarItem( item = SidebarItem(
tr(TR.BROWSING_CURRENT_DECK), tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg", ":/icons/deck.svg",
self._named_filter(NamedFilter.CURRENT_DECK), self._filter_func(SearchTerm(current_deck=True)),
item_type=SidebarItemType.CURRENT_DECK, item_type=SidebarItemType.CURRENT_DECK,
) )
root.addChild(item) root.addChild(item)
@ -313,7 +310,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
name, name,
":/icons/heart.svg", ":/icons/heart.svg",
self._saved_filter(filt), self._filter_func(filt),
item_type=SidebarItemType.FILTER, item_type=SidebarItemType.FILTER,
) )
root.addChild(item) root.addChild(item)
@ -333,7 +330,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
node.name, node.name,
":/icons/tag.svg", ":/icons/tag.svg",
self._tag_filter(head + node.name), self._filter_func(SearchTerm(tag=head + node.name)),
toggle_expand(), toggle_expand(),
not node.collapsed, not node.collapsed,
item_type=SidebarItemType.TAG, item_type=SidebarItemType.TAG,
@ -358,7 +355,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
node.name, node.name,
":/icons/deck.svg", ":/icons/deck.svg",
self._deck_filter(head + node.name), self._filter_func(SearchTerm(deck=head + node.name)),
toggle_expand(), toggle_expand(),
not node.collapsed, not node.collapsed,
item_type=SidebarItemType.DECK, item_type=SidebarItemType.DECK,
@ -377,7 +374,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem( item = SidebarItem(
nt["name"], nt["name"],
":/icons/notetype.svg", ":/icons/notetype.svg",
self._note_filter(nt["name"]), self._filter_func(SearchTerm(note=nt["name"])),
item_type=SidebarItemType.NOTETYPE, item_type=SidebarItemType.NOTETYPE,
id=nt["id"], id=nt["id"],
) )
@ -386,32 +383,17 @@ class SidebarTreeView(QTreeView):
child = SidebarItem( child = SidebarItem(
tmpl["name"], tmpl["name"],
":/icons/notetype.svg", ":/icons/notetype.svg",
self._template_filter(nt["name"], c), self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c)
),
item_type=SidebarItemType.TEMPLATE, item_type=SidebarItemType.TEMPLATE,
) )
item.addChild(child) item.addChild(child)
root.addChild(item) root.addChild(item)
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable: def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
return lambda: self.browser.update_search(self.col.search_string(name=name)) return lambda: self.browser.update_search(self.col.build_search_string(*terms))
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)
# Context menu actions # Context menu actions
########################### ###########################

View file

@ -765,41 +765,39 @@ message BuiltinSearchOrder {
} }
message FilterToSearchIn { 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 { message DupeIn {
NoteTypeID mid = 1; NoteTypeID mid = 1;
string text = 2; string text = 2;
} }
enum Flag {
WITHOUT = 0;
ANY = 1;
RED = 2;
ORANGE = 3;
GREEN = 4;
BLUE = 5;
}
oneof filter { oneof filter {
NamedFilter name = 1; string tag = 1;
string tag = 2; string deck = 2;
string deck = 3; string note = 3;
string note = 4; uint32 template = 4;
uint32 template = 5; NoteIDs nids = 5;
DupeIn dupe = 6; DupeIn dupe = 6;
uint32 forgot_in_days = 7; string field_name = 7;
uint32 added_in_days = 8; uint32 forgot_in_days = 8;
int32 due_in_days = 9; uint32 added_in_days = 9;
NoteIDs nids = 10; int32 due_in_days = 10;
string field_name = 11; 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;
} }
} }

View file

@ -293,38 +293,8 @@ impl From<pb::DeckConfigId> for DeckConfID {
impl From<pb::FilterToSearchIn> for Node<'_> { impl From<pb::FilterToSearchIn> for Node<'_> {
fn from(msg: pb::FilterToSearchIn) -> Self { fn from(msg: pb::FilterToSearchIn) -> Self {
use pb::filter_to_search_in::Filter; use pb::filter_to_search_in::Filter;
use pb::filter_to_search_in::NamedFilter; use pb::filter_to_search_in::Flag;
match msg match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
.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)))),
}
}
Filter::Tag(s) => Node::Search(SearchNode::Tag( Filter::Tag(s) => Node::Search(SearchNode::Tag(
escape_anki_wildcards(&s).into_owned().into(), escape_anki_wildcards(&s).into_owned().into(),
)), )),
@ -337,10 +307,16 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
Filter::Template(u) => { Filter::Template(u) => {
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16))) 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 { Filter::Dupe(dupe) => Node::Search(SearchNode::Duplicates {
note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(), note_type_id: dupe.mid.unwrap_or(pb::NoteTypeId { ntid: 0 }).into(),
text: dupe.text.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 { Filter::ForgotInDays(u) => Node::Search(SearchNode::Rated {
days: u, days: u,
ease: EaseKind::AnswerButton(1), ease: EaseKind::AnswerButton(1),
@ -350,12 +326,26 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
operator: "<=".to_string(), operator: "<=".to_string(),
kind: PropertyKind::Due(i), kind: PropertyKind::Due(i),
}), }),
Filter::Nids(nids) => Node::Search(SearchNode::NoteIDs(nids.into_id_string().into())), Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection),
Filter::FieldName(s) => Node::Search(SearchNode::SingleField { Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())),
field: escape_anki_wildcards(&s).into_owned().into(), Filter::StudiedToday(_) => Node::Search(SearchNode::Rated {
text: "*".to_string().into(), days: 1,
is_re: false, 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)),
},
} }
} }
} }