Merge branch 'more-backend-search' into main

This commit is contained in:
Damien Elmes 2021-01-31 14:21:51 +10:00
commit cb805cf355
23 changed files with 444 additions and 350 deletions

View File

@ -83,6 +83,7 @@ browsing-reposition = Reposition...
browsing-reposition-new-cards = Reposition New Cards
browsing-reschedule = Reschedule
browsing-save-current-filter = Save Current Filter...
browsing-search-bar-hint = Type here and press Enter to search for cards and notes.
browsing-search-in = Search in:
browsing-search-within-formatting-slow = Search within formatting (slow)
browsing-shift-position-of-existing-cards = Shift position of existing cards
@ -101,7 +102,6 @@ browsing-today = Today
browsing-toggle-mark = Toggle Mark
browsing-toggle-suspend = Toggle Suspend
browsing-treat-input-as-regular-expression = Treat input as regular expression
browsing-type-here-to-search = <type here to search; hit enter to show current deck>
browsing-whole-collection = Whole Collection
browsing-you-must-have-at-least-one = You must have at least one column.
browsing-group =

View File

@ -17,6 +17,7 @@ import anki.find
import anki.latex # sets up hook
import anki.template
from anki import hooks
from anki.backend_pb2 import SearchTerm
from anki.cards import Card
from anki.config import ConfigManager
from anki.consts import *
@ -26,18 +27,22 @@ from anki.errors import AnkiError
from anki.media import MediaManager, media_paths_from_col_path
from anki.models import ModelManager
from anki.notes import Note
from anki.rsbackend import (
from anki.rsbackend import ( # pylint: disable=unused-import
TR,
BackendNoteTypeID,
ConcatSeparator,
DBError,
FormatTimeSpanContext,
InvalidInput,
Progress,
RustBackend,
from_json_bytes,
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
ConfigBoolKey = pb.ConfigBool.Key # pylint: disable=no-member
@ -458,8 +463,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,
@ -472,13 +477,76 @@ 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
findReplace = find_and_replace
# Search Strings
##########################################################################
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 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.
"""
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=searches)
if negate:
search_string = self.backend.negate_search(search_string)
return search_string
def replace_search_term(self, search: str, replacement: str) -> str:
return self.backend.replace_search_term(search=search, replacement=replacement)
# Config
##########################################################################

View File

@ -6,7 +6,6 @@ from __future__ import annotations
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,43 +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
if search:
search = "(" + search + ") "
search += '"%s:*"' % fieldName.replace('"', '"')
# 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

@ -50,9 +50,6 @@ TagTreeNode = pb.TagTreeNode
NoteType = pb.NoteType
DeckTreeNode = pb.DeckTreeNode
StockNoteType = pb.StockNoteType
FilterToSearchIn = pb.FilterToSearchIn
NamedFilter = pb.FilterToSearchIn.NamedFilter
DupeIn = pb.FilterToSearchIn.DupeIn
BackendNoteTypeID = pb.NoteTypeID
ConcatSeparator = pb.ConcatenateSearchesIn.Separator
SyncAuth = pb.SyncAuth

View File

@ -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)

View File

@ -5,6 +5,7 @@ ignore = forms,hooks_gen.py
[TYPECHECK]
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
ignored-classes=SearchTerm
[REPORTS]
output-format=colorized

View File

@ -7,6 +7,7 @@ import aqt.deckchooser
import aqt.editor
import aqt.forms
import aqt.modelchooser
from anki.collection import SearchTerm
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("nid:%s" % nid):
if self.mw.col.findNotes(SearchTerm(nid=nid)):
note = self.mw.col.getNote(nid)
fields = note.fields
txt = htmlToTextLine(", ".join(fields))
@ -161,9 +162,7 @@ class AddCards(QDialog):
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid):
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
browser.onSearchActivated()
self.mw.browser_search(SearchTerm(nid=nid))
def addNote(self, note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId()

View File

@ -13,19 +13,11 @@ from typing import List, Optional, Sequence, Tuple, cast
import aqt
import aqt.forms
from anki.cards import Card
from anki.collection import Collection, ConfigBoolKey
from anki.collection import Collection, ConfigBoolKey, InvalidInput, SearchTerm
from anki.consts import *
from anki.lang import without_unicode_isolation
from anki.models import NoteType
from anki.notes import Note
from anki.rsbackend import (
BackendNoteTypeID,
ConcatSeparator,
DupeIn,
FilterToSearchIn,
InvalidInput,
NamedFilter,
)
from anki.stats import CardStats
from anki.utils import htmlToTextLine, ids2str, isMac, isWin
from aqt import AnkiQt, gui_hooks
@ -193,23 +185,18 @@ class DataModel(QAbstractTableModel):
def search(self, txt: str) -> None:
self.beginReset()
self.cards = []
exception: Optional[Exception] = None
try:
ctx = SearchContext(search=txt, browser=self.browser)
gui_hooks.browser_will_search(ctx)
if ctx.card_ids is None:
ctx.search = self.browser.normalize_search(ctx.search)
ctx.card_ids = self.col.find_cards(ctx.search, order=ctx.order)
gui_hooks.browser_did_search(ctx)
self.cards = ctx.card_ids
except Exception as e:
exception = e
except Exception as err:
raise err
finally:
self.endReset()
if exception:
show_invalid_search_error(exception)
def reset(self):
self.beginReset()
self.endReset()
@ -615,16 +602,13 @@ class Browser(QMainWindow):
######################################################################
def setupSearch(self):
qconnect(self.form.searchButton.clicked, self.onSearchActivated)
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
self._searchPrompt = tr(TR.BROWSING_TYPE_HERE_TO_SEARCH)
self.form.searchEdit.addItems(
[self._searchPrompt] + self.mw.pm.profile["searchHistory"]
self.form.searchEdit.lineEdit().setPlaceholderText(
tr(TR.BROWSING_SEARCH_BAR_HINT)
)
self.search_for("is:current", self._searchPrompt)
# then replace text for easily showing the deck
self.form.searchEdit.lineEdit().selectAll()
self.form.searchEdit.addItems(self.mw.pm.profile["searchHistory"])
self.search_for(self.col.build_search_string(SearchTerm(current_deck=True)), "")
self.form.searchEdit.setFocus()
# search triggered by user
@ -632,57 +616,48 @@ class Browser(QMainWindow):
self.editor.saveNow(self._onSearchActivated)
def _onSearchActivated(self):
# grab search text and normalize
prompt = self.form.searchEdit.lineEdit().text()
text = self.form.searchEdit.lineEdit().text()
try:
normed = self.col.build_search_string(text)
except InvalidInput as err:
show_invalid_search_error(err)
else:
self.search_for(normed)
self.update_history()
# convert guide text before we save history
txt = "deck:current " if prompt == self._searchPrompt else prompt
self.update_history(txt)
def search_for(self, search: str, prompt: Optional[str] = None):
"""Keep track of search string so that we reuse identical search when
refreshing, rather than whatever is currently in the search field.
Optionally set the search bar to a different text than the actual search.
"""
# keep track of search string so that we reuse identical search when
# refreshing, rather than whatever is currently in the search field
self.search_for(txt)
self._lastSearchTxt = search
prompt = search if prompt == None else prompt
self.form.searchEdit.lineEdit().setText(prompt)
self.search()
def update_history(self, search: str) -> None:
def search(self):
"""Search triggered programmatically. Caller must have saved note first."""
try:
self.model.search(self._lastSearchTxt)
except Exception as err:
show_invalid_search_error(err)
if not self.model.cards:
# no row change will fire
self._onRowChanged(None, None)
def update_history(self):
sh = self.mw.pm.profile["searchHistory"]
if search in sh:
sh.remove(search)
sh.insert(0, search)
if self._lastSearchTxt in sh:
sh.remove(self._lastSearchTxt)
sh.insert(0, self._lastSearchTxt)
sh = sh[:30]
self.form.searchEdit.clear()
self.form.searchEdit.addItems(sh)
self.mw.pm.profile["searchHistory"] = sh
def search_for(self, search: str, prompt: Optional[str] = None) -> None:
self._lastSearchTxt = search
self.form.searchEdit.lineEdit().setText(prompt or search)
self.search()
# search triggered programmatically. caller must have saved note first.
def search(self) -> None:
if "is:current" in self._lastSearchTxt:
# show current card if there is one
c = self.card = self.mw.reviewer.card
nid = c and c.nid or 0
if nid:
search = "nid:%d" % nid
search = gui_hooks.default_search(search, c)
self.model.search(search)
self.focusCid(c.id)
else:
self.model.search(self._lastSearchTxt)
if not self.model.cards:
# no row change will fire
self._onRowChanged(None, None)
def normalize_search(self, search: str) -> str:
normed = self.col.backend.normalize_search(search)
self._lastSearchTxt = normed
self.form.searchEdit.lineEdit().setText(normed)
return normed
def updateTitle(self):
def updateTitle(self) -> int:
selected = len(self.form.tableView.selectionModel().selectedRows())
cur = len(self.model.cards)
self.setWindowTitle(
@ -692,6 +667,21 @@ class Browser(QMainWindow):
)
return selected
def show_single_card(self, card: Optional[Card]):
"""Try to search for the according note and select the given card."""
nid: Optional[int] = card and card.nid or 0
if nid:
def on_show_single_card():
self.card = card
search = self.col.build_search_string(SearchTerm(nid=nid))
search = gui_hooks.default_search(search, card)
self.search_for(search, "")
self.focusCid(card.id)
self.editor.saveNow(on_show_single_card)
def onReset(self):
self.sidebar.refresh()
self.editor.setNote(None)
@ -972,33 +962,20 @@ QTableView {{ gridline-color: {grid} }}
ml.popupOver(self.form.filter)
def update_search(self, *terms: str):
"Modify the current search string based on modified keys, then refresh."
def update_search(self, *terms: Union[str, SearchTerm]):
"""Modify the current search string based on modified keys, then refresh."""
try:
search = self.col.backend.concatenate_searches(
sep=ConcatSeparator.AND, searches=terms
)
search = self.col.build_search_string(*terms)
mods = self.mw.app.keyboardModifiers()
if mods & Qt.AltModifier:
search = self.col.backend.negate_search(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.backend.replace_search_term(
search=cur, replacement=search
)
elif mods & Qt.ControlModifier:
search = self.col.backend.concatenate_searches(
# pylint: disable=no-member
sep=ConcatSeparator.AND,
searches=[cur, search],
)
elif mods & Qt.ShiftModifier:
search = self.col.backend.concatenate_searches(
# pylint: disable=no-member
sep=ConcatSeparator.OR,
searches=[cur, search],
)
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
search = self.col.replace_search_term(cur, search)
elif mods & Qt.ControlModifier:
search = self.col.build_search_string(cur, search)
elif mods & Qt.ShiftModifier:
search = self.col.build_search_string(cur, search, match_any=True)
except InvalidInput as e:
show_invalid_search_error(e)
else:
@ -1016,7 +993,7 @@ QTableView {{ gridline-color: {grid} }}
ml.addSeparator()
else:
label, filter_name = row
ml.addItem(label, self.sidebar._named_filter(filter_name))
ml.addItem(label, self.sidebar._filter_func(filter_name))
return ml
def _todayFilters(self):
@ -1024,9 +1001,19 @@ 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(rated=SearchTerm.Rated(days=1)),
),
(
tr(TR.BROWSING_AGAIN_TODAY),
SearchTerm(
rated=SearchTerm.Rated(
days=1, rating=SearchTerm.RATING_AGAIN
)
),
),
)
)
)
@ -1037,20 +1024,41 @@ 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(card_state=SearchTerm.CARD_STATE_NEW),
),
(
tr(TR.SCHEDULING_LEARNING),
SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN),
),
(
tr(TR.SCHEDULING_REVIEW),
SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW),
),
(
tr(TR.FILTERING_IS_DUE),
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE),
),
None,
(tr(TR.BROWSING_SUSPENDED), NamedFilter.SUSPENDED),
(tr(TR.BROWSING_BURIED), NamedFilter.BURIED),
(
tr(TR.BROWSING_SUSPENDED),
SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED),
),
(
tr(TR.BROWSING_BURIED),
SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED),
),
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=SearchTerm.FLAG_RED)),
(
tr(TR.ACTIONS_ORANGE_FLAG),
SearchTerm(flag=SearchTerm.FLAG_ORANGE),
),
(tr(TR.ACTIONS_GREEN_FLAG), SearchTerm(flag=SearchTerm.FLAG_GREEN)),
(tr(TR.ACTIONS_BLUE_FLAG), SearchTerm(flag=SearchTerm.FLAG_BLUE)),
(tr(TR.BROWSING_NO_FLAG), SearchTerm(flag=SearchTerm.FLAG_NONE)),
(tr(TR.BROWSING_ANY_FLAG), SearchTerm(flag=SearchTerm.FLAG_ANY)),
)
)
)
@ -1450,7 +1458,9 @@ where id in %s"""
tv = self.form.tableView
tv.selectionModel().clear()
search = "nid:" + ",".join([str(x) for x in nids])
search = self.col.build_search_string(
SearchTerm(nids=SearchTerm.IdList(ids=nids))
)
self.search_for(search)
tv.selectAll()
@ -1593,17 +1603,6 @@ where id in %s"""
# Edit: finding dupes
######################################################################
# filter called by the editor
def search_dupe(self, mid: int, text: str):
self.form.searchEdit.lineEdit().setText(
self.col.backend.filter_to_search(
FilterToSearchIn(
dupe=DupeIn(mid=BackendNoteTypeID(ntid=mid), text=text)
)
)
)
self.onSearchActivated()
def onFindDupes(self):
self.editor.saveNow(self._onFindDupes)
@ -1648,7 +1647,12 @@ where id in %s"""
def duplicatesReport(self, web, fname, search, frm, web_context):
self.mw.progress.start()
res = self.mw.col.findDupes(fname, search)
try:
res = self.mw.col.findDupes(fname, search)
except InvalidInput as e:
self.mw.progress.finish()
show_invalid_search_error(e)
return
if not self._dupesButton:
self._dupesButton = b = frm.buttonBox.addButton(
tr(TR.BROWSING_TAG_DUPLICATES), QDialogButtonBox.ActionRole
@ -1665,7 +1669,11 @@ where id in %s"""
t += (
"""<li><a href=# onclick="pycmd('%s');return false;">%s</a>: %s</a>"""
% (
"nid:" + ",".join(str(id) for id in nids),
html.escape(
self.col.build_search_string(
SearchTerm(nids=SearchTerm.IdList(ids=nids))
)
),
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),
html.escape(val),
)
@ -1761,9 +1769,10 @@ where id in %s"""
def focusCid(self, cid):
try:
row = self.model.cards.index(cid)
except:
row = list(self.model.cards).index(cid)
except ValueError:
return
self.form.tableView.clearSelection()
self.form.tableView.selectRow(row)

View File

@ -2,6 +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 SearchTerm
from anki.consts import *
from aqt.qt import *
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
@ -159,26 +160,42 @@ class CustomStudy(QDialog):
dyn = self.mw.col.decks.get(did)
# and then set various options
if i == RADIO_FORGOT:
dyn["terms"][0] = ["rated:%d:1" % spin, DYN_MAX_SIZE, DYN_RANDOM]
search = self.mw.col.build_search_string(
SearchTerm(
rated=SearchTerm.Rated(days=spin, rating=SearchTerm.RATING_AGAIN)
)
)
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
dyn["resched"] = False
elif i == RADIO_AHEAD:
dyn["terms"][0] = ["prop:due<=%d" % spin, DYN_MAX_SIZE, DYN_DUE]
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:
dyn["terms"][0] = ["is:new added:%s" % spin, DYN_MAX_SIZE, DYN_OLDEST]
search = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
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 = "is:new "
terms = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
)
ord = DYN_ADDED
dyn["resched"] = True
elif type == TYPE_DUE:
terms = "is:due "
terms = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
)
ord = DYN_DUE
dyn["resched"] = True
elif type == TYPE_REVIEW:
terms = "-is:new "
terms = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW), negate=True
)
ord = DYN_RANDOM
dyn["resched"] = True
else:
@ -187,7 +204,9 @@ class CustomStudy(QDialog):
dyn["resched"] = False
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
# add deck limit
dyn["terms"][0][0] = 'deck:"%s" %s ' % (self.deck["name"], 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
self.created_custom_study = True

View File

@ -4,8 +4,8 @@
from typing import List, Optional
import aqt
from anki.collection import InvalidInput, SearchTerm
from anki.lang import without_unicode_isolation
from anki.rsbackend import InvalidInput
from aqt.qt import *
from aqt.utils import (
TR,
@ -47,8 +47,14 @@ class DeckConf(QDialog):
self.initialSetup()
self.loadConf()
if search:
self.form.search.setText(search + " is:due")
self.form.search_2.setText(search + " is:new")
search = self.mw.col.build_search_string(
search, SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
)
self.form.search.setText(search)
search_2 = self.mw.col.build_search_string(
search, SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
)
self.form.search_2.setText(search_2)
self.form.search.selectAll()
if self.mw.col.schedVer() == 1:
@ -119,11 +125,11 @@ class DeckConf(QDialog):
else:
d["delays"] = None
search = self.mw.col.backend.normalize_search(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.backend.normalize_search(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

View File

@ -21,6 +21,7 @@ from bs4 import BeautifulSoup
import aqt
import aqt.sound
from anki.cards import Card
from anki.collection import SearchTerm
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
@ -537,8 +538,13 @@ class Editor:
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
def showDupes(self):
browser = aqt.dialogs.open("Browser", self.mw)
browser.search_dupe(self.note.model()["id"], self.note.fields[0])
self.mw.browser_search(
SearchTerm(
dupe=SearchTerm.Dupe(
notetype_id=self.note.model()["id"], first_field=self.note.fields[0]
)
)
)
def fieldsAreBlank(self, previousNote=None):
if not self.note:

View File

@ -66,9 +66,7 @@ class EmptyCardsDialog(QDialog):
self._delete_button.clicked.connect(self._on_delete)
def _on_note_link_clicked(self, link):
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.lineEdit().setText(link)
browser.onSearchActivated()
self.mw.browser_search(link)
def _on_delete(self):
self.mw.progress.start()

View File

@ -107,13 +107,6 @@
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QPushButton" name="searchButton">
<property name="text">
<string>ACTIONS_SEARCH</string>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QPushButton" name="filter">
<property name="text">
@ -158,12 +151,12 @@
<attribute name="horizontalHeaderCascadingSectionResizes">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>20</number>
</attribute>
<attribute name="horizontalHeaderHighlightSections">
<bool>false</bool>
</attribute>
<attribute name="horizontalHeaderMinimumSectionSize">
<number>20</number>
</attribute>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
@ -223,7 +216,7 @@
<x>0</x>
<y>0</y>
<width>750</width>
<height>22</height>
<height>21</height>
</rect>
</property>
<widget class="QMenu" name="menuEdit">

View File

@ -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
@ -1045,7 +1045,8 @@ title="%s" %s>%s</button>""" % (
aqt.dialogs.open("AddCards", self)
def onBrowse(self) -> None:
aqt.dialogs.open("Browser", self)
browser = aqt.dialogs.open("Browser", self)
browser.show_single_card(self.reviewer.card)
def onEditCurrent(self):
aqt.dialogs.open("EditCurrent", self)
@ -1141,7 +1142,7 @@ title="%s" %s>%s</button>""" % (
deck = self.col.decks.current()
if not search:
if not deck["dyn"]:
search = 'deck:"%s" ' % 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))
):
@ -1617,3 +1618,14 @@ title="%s" %s>%s</button>""" % (
def serverURL(self) -> str:
return "http://127.0.0.1:%d/" % self.mediaServer.getPort()
# Helpers for all windows
##########################################################################
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.build_search_string(*terms)
browser = aqt.dialogs.open("Browser", self)
browser.form.searchEdit.lineEdit().setText(search)
browser.onSearchActivated()

View File

@ -9,6 +9,7 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt
from anki.collection import SearchTerm
from anki.rsbackend import TR, Interrupted, ProgressKind, pb
from aqt.qt import *
from aqt.utils import (
@ -145,9 +146,7 @@ class MediaChecker:
if out is not None:
nid, err = out
browser = aqt.dialogs.open("Browser", self.mw)
browser.form.searchEdit.lineEdit().setText("nid:%d" % nid)
browser.onSearchActivated()
self.mw.browser_search(SearchTerm(nid=nid))
showText(err, type="html")
else:
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))

View File

@ -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
@ -71,8 +72,8 @@ class Overview:
elif url == "opts":
self.mw.onDeckConf()
elif url == "cram":
deck = self.mw.col.decks.current()
self.mw.onCram("'deck:%s'" % deck["name"])
deck = self.mw.col.decks.current()["name"]
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()

View File

@ -9,15 +9,9 @@ from enum import Enum
from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, Sequence, Tuple, cast
import aqt
from anki.collection import ConfigBoolKey
from anki.collection import ConfigBoolKey, InvalidInput, SearchTerm
from anki.errors import DeckRenameError
from anki.rsbackend import (
DeckTreeNode,
FilterToSearchIn,
InvalidInput,
NamedFilter,
TagTreeNode,
)
from anki.rsbackend import DeckTreeNode, TagTreeNode
from aqt import gui_hooks
from aqt.main import ResetReason
from aqt.models import Models
@ -42,6 +36,7 @@ class SidebarItemType(Enum):
COLLECTION = 1
CURRENT_DECK = 2
SAVED_SEARCH = 3
FILTER = 3 # legacy alias for SAVED_SEARCH
DECK = 4
NOTETYPE = 5
TAG = 6
@ -466,14 +461,12 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
tr(TR.BROWSING_WHOLE_COLLECTION),
":/icons/collection.svg",
self._named_filter(NamedFilter.WHOLE_COLLECTION),
item_type=SidebarItemType.COLLECTION,
)
root.addChild(item)
item = SidebarItem(
tr(TR.BROWSING_CURRENT_DECK),
":/icons/deck.svg",
self._named_filter(NamedFilter.CURRENT_DECK),
item_type=SidebarItemType.CURRENT_DECK,
)
root.addChild(item)
@ -499,8 +492,8 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
name,
icon,
self._saved_filter(filt),
item_type=SidebarItemType.SAVED_SEARCH,
self._filter_func(filt),
item_type=SidebarItemType.FILTER,
)
root.addChild(item)
@ -519,7 +512,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
icon,
self._tag_filter(head + node.name),
self._filter_func(SearchTerm(tag=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.TAG,
@ -551,7 +544,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
icon,
self._deck_filter(head + node.name),
self._filter_func(SearchTerm(deck=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.DECK,
@ -584,8 +577,8 @@ class SidebarTreeView(QTreeView):
for nt in sorted(self.col.models.all(), key=lambda nt: nt["name"].lower()):
item = SidebarItem(
nt["name"],
icon=icon,
onClick=self._note_filter(nt["name"]),
icon,
self._filter_func(SearchTerm(note=nt["name"])),
item_type=SidebarItemType.NOTETYPE,
id=nt["id"],
)
@ -594,7 +587,9 @@ class SidebarTreeView(QTreeView):
child = SidebarItem(
tmpl["name"],
icon,
onClick=self._template_filter(nt["name"], c),
self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c)
),
item_type=SidebarItemType.TEMPLATE,
full_name=nt["name"] + "::" + tmpl["name"],
)
@ -602,34 +597,8 @@ class SidebarTreeView(QTreeView):
root.addChild(item)
def _named_filter(self, name: "FilterToSearchIn.NamedFilterValue") -> Callable:
return lambda: self.browser.update_search(
self.col.backend.filter_to_search(FilterToSearchIn(name=name))
)
def _tag_filter(self, tag: str) -> Callable:
return lambda: self.browser.update_search(
self.col.backend.filter_to_search(FilterToSearchIn(tag=tag))
)
def _deck_filter(self, deck: str) -> Callable:
return lambda: self.browser.update_search(
self.col.backend.filter_to_search(FilterToSearchIn(deck=deck))
)
def _note_filter(self, note: str) -> Callable:
return lambda: self.browser.update_search(
self.col.backend.filter_to_search(FilterToSearchIn(note=note))
)
def _template_filter(self, note: str, template: int) -> Callable:
return lambda: self.browser.update_search(
self.col.backend.filter_to_search(FilterToSearchIn(note=note)),
self.col.backend.filter_to_search(FilterToSearchIn(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
###########################
@ -807,7 +776,7 @@ class SidebarTreeView(QTreeView):
def save_current_search(self, _item=None) -> None:
try:
filt = self.col.backend.normalize_search(
filt = self.col.build_search_string(
self.browser.form.searchEdit.lineEdit().text()
)
except InvalidInput as e:

View File

@ -84,7 +84,7 @@ service BackendService {
// searching
rpc FilterToSearch(FilterToSearchIn) returns (String);
rpc FilterToSearch(SearchTerm) returns (String);
rpc NormalizeSearch(String) returns (String);
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
@ -765,37 +765,58 @@ message BuiltinSearchOrder {
bool reverse = 2;
}
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 SearchTerm {
message Dupe {
int64 notetype_id = 1;
string first_field = 2;
}
message DupeIn {
NoteTypeID mid = 1;
string text = 2;
enum Flag {
FLAG_NONE = 0;
FLAG_ANY = 1;
FLAG_RED = 2;
FLAG_ORANGE = 3;
FLAG_GREEN = 4;
FLAG_BLUE = 5;
}
enum Rating {
RATING_ANY = 0;
RATING_AGAIN = 1;
RATING_HARD = 2;
RATING_GOOD = 3;
RATING_EASY = 4;
RATING_BY_RESCHEDULE = 5;
}
message Rated {
uint32 days = 1;
Rating rating = 2;
}
enum CardState {
CARD_STATE_NEW = 0;
CARD_STATE_LEARN = 1;
CARD_STATE_REVIEW = 2;
CARD_STATE_DUE = 3;
CARD_STATE_SUSPENDED = 4;
CARD_STATE_BURIED = 5;
}
message IdList {
repeated int64 ids = 1;
}
oneof filter {
NamedFilter name = 1;
string tag = 2;
string deck = 3;
string note = 4;
uint32 template = 5;
DupeIn dupe = 6;
string tag = 1;
string deck = 2;
string note = 3;
uint32 template = 4;
int64 nid = 5;
Dupe dupe = 6;
string field_name = 7;
Rated rated = 8;
uint32 added_in_days = 9;
int32 due_in_days = 10;
bool whole_collection = 11;
bool current_deck = 12;
Flag flag = 13;
CardState card_state = 14;
IdList nids = 15;
}
}

View File

@ -36,7 +36,8 @@ use crate::{
sched::timespan::{answer_button_time, time_span},
search::{
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
BoolSeparator, EaseKind, Node, SearchNode, SortMode, StateKind, TemplateKind,
BoolSeparator, Node, PropertyKind, RatingKind, SearchNode, SortMode, StateKind,
TemplateKind,
},
stats::studied_today,
sync::{
@ -262,6 +263,16 @@ impl From<pb::NoteId> for NoteID {
}
}
impl pb::search_term::IdList {
fn into_id_string(self) -> String {
self.ids
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(",")
}
}
impl From<pb::NoteTypeId> for NoteTypeID {
fn from(ntid: pb::NoteTypeId) -> Self {
NoteTypeID(ntid.ntid)
@ -280,41 +291,11 @@ impl From<pb::DeckConfigId> for DeckConfID {
}
}
impl From<pb::FilterToSearchIn> 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)))),
}
}
impl From<pb::SearchTerm> for Node<'_> {
fn from(msg: pb::SearchTerm) -> Self {
use pb::search_term::Filter;
use pb::search_term::Flag;
match msg.filter.unwrap_or(Filter::WholeCollection(true)) {
Filter::Tag(s) => Node::Search(SearchNode::Tag(
escape_anki_wildcards(&s).into_owned().into(),
)),
@ -327,10 +308,41 @@ impl From<pb::FilterToSearchIn> for Node<'_> {
Filter::Template(u) => {
Node::Search(SearchNode::CardTemplate(TemplateKind::Ordinal(u as u16)))
}
Filter::Nid(nid) => Node::Search(SearchNode::NoteIDs(nid.to_string().into())),
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(),
note_type_id: dupe.notetype_id.into(),
text: dupe.first_field.into(),
}),
Filter::FieldName(s) => Node::Search(SearchNode::SingleField {
field: escape_anki_wildcards(&s).into_owned().into(),
text: "*".to_string().into(),
is_re: false,
}),
Filter::Rated(rated) => Node::Search(SearchNode::Rated {
days: rated.days,
ease: rated.rating().into(),
}),
Filter::AddedInDays(u) => Node::Search(SearchNode::AddedInDays(u)),
Filter::DueInDays(i) => Node::Search(SearchNode::Property {
operator: "<=".to_string(),
kind: PropertyKind::Due(i),
}),
Filter::WholeCollection(_) => Node::Search(SearchNode::WholeCollection),
Filter::CurrentDeck(_) => Node::Search(SearchNode::Deck("current".into())),
Filter::CardState(state) => Node::Search(SearchNode::State(
pb::search_term::CardState::from_i32(state)
.unwrap_or_default()
.into(),
)),
Filter::Flag(flag) => match Flag::from_i32(flag).unwrap_or(Flag::Any) {
Flag::None => 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)),
},
}
}
}
@ -344,6 +356,32 @@ impl From<BoolSeparatorProto> for BoolSeparator {
}
}
impl From<pb::search_term::Rating> for RatingKind {
fn from(r: pb::search_term::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,
}
}
}
impl From<pb::search_term::CardState> for StateKind {
fn from(k: pb::search_term::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,
}
}
}
impl BackendService for Backend {
fn latest_progress(&self, _input: Empty) -> BackendResult<pb::Progress> {
let progress = self.progress_state.lock().unwrap().last_progress;
@ -466,7 +504,7 @@ impl BackendService for Backend {
// searching
//-----------------------------------------------
fn filter_to_search(&self, input: pb::FilterToSearchIn) -> Result<pb::String> {
fn filter_to_search(&self, input: pb::SearchTerm) -> Result<pb::String> {
Ok(write_nodes(&[input.into()]).into())
}

View File

@ -5,7 +5,7 @@ mod sqlwriter;
mod writer;
pub use cards::SortMode;
pub use parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind};
pub use parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind};
pub use writer::{
concatenate_searches, negate_search, normalize_search, replace_search_term, write_nodes,
BoolSeparator,

View File

@ -58,7 +58,7 @@ pub enum SearchNode<'a> {
NoteType(Cow<'a, str>),
Rated {
days: u32,
ease: EaseKind,
ease: RatingKind,
},
Tag(Cow<'a, str>),
Duplicates {
@ -67,7 +67,7 @@ pub enum SearchNode<'a> {
},
State(StateKind),
Flag(u8),
NoteIDs(&'a str),
NoteIDs(Cow<'a, str>),
CardIDs(&'a str),
Property {
operator: String,
@ -87,7 +87,7 @@ pub enum PropertyKind {
Lapses(u32),
Ease(f32),
Position(u32),
Rated(i32, EaseKind),
Rated(i32, RatingKind),
}
#[derive(Debug, PartialEq, Clone)]
@ -109,7 +109,7 @@ pub enum TemplateKind<'a> {
}
#[derive(Debug, PartialEq, Clone)]
pub enum EaseKind {
pub enum RatingKind {
AnswerButton(u8),
AnyAnswerButton,
ManualReschedule,
@ -318,7 +318,7 @@ fn search_node_for_text_with_argument<'a>(
"is" => parse_state(val)?,
"did" => parse_did(val)?,
"mid" => parse_mid(val)?,
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?),
"nid" => SearchNode::NoteIDs(check_id_list(val, key)?.into()),
"cid" => SearchNode::CardIDs(check_id_list(val, key)?),
"re" => SearchNode::Regex(unescape_quotes(val)),
"nc" => SearchNode::NoCombining(unescape(val)?),
@ -353,7 +353,7 @@ fn parse_flag(s: &str) -> ParseResult<SearchNode> {
fn parse_resched(s: &str) -> ParseResult<SearchNode> {
parse_u32(s, "resched:").map(|days| SearchNode::Rated {
days,
ease: EaseKind::ManualReschedule,
ease: RatingKind::ManualReschedule,
})
}
@ -392,7 +392,7 @@ fn parse_prop(prop_clause: &str) -> ParseResult<SearchNode> {
"rated" => parse_prop_rated(num, prop_clause)?,
"resched" => PropertyKind::Rated(
parse_negative_i32(num, prop_clause)?,
EaseKind::ManualReschedule,
RatingKind::ManualReschedule,
),
"ivl" => PropertyKind::Interval(parse_u32(num, prop_clause)?),
"reps" => PropertyKind::Reps(parse_u32(num, prop_clause)?),
@ -470,9 +470,9 @@ fn parse_i64<'a>(num: &str, context: &'a str) -> ParseResult<'a, i64> {
})
}
fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, EaseKind> {
fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'a, RatingKind> {
Ok(if let Some(num) = num {
EaseKind::AnswerButton(
RatingKind::AnswerButton(
num.parse()
.map_err(|_| ())
.and_then(|n| if matches!(n, 1..=4) { Ok(n) } else { Err(()) })
@ -487,7 +487,7 @@ fn parse_answer_button<'a>(num: Option<&str>, context: &'a str) -> ParseResult<'
})?,
)
} else {
EaseKind::AnyAnswerButton
RatingKind::AnyAnswerButton
})
}
@ -813,7 +813,7 @@ mod test {
assert_eq!(parse("tag:hard")?, vec![Search(Tag("hard".into()))]);
assert_eq!(
parse("nid:1237123712,2,3")?,
vec![Search(NoteIDs("1237123712,2,3"))]
vec![Search(NoteIDs("1237123712,2,3".into()))]
);
assert_eq!(parse("is:due")?, vec![Search(State(StateKind::Due))]);
assert_eq!(parse("flag:3")?, vec![Search(Flag(3))]);

View File

@ -1,7 +1,7 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::parser::{EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind};
use super::parser::{Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind};
use crate::{
card::{CardQueue, CardType},
collection::Collection,
@ -211,7 +211,7 @@ impl SqlWriter<'_> {
Ok(())
}
fn write_rated(&mut self, op: &str, days: i64, ease: &EaseKind) -> Result<()> {
fn write_rated(&mut self, op: &str, days: i64, ease: &RatingKind) -> Result<()> {
let today_cutoff = self.col.timing_today()?.next_day_at;
let target_cutoff_ms = (today_cutoff + 86_400 * days) * 1_000;
let day_before_cutoff_ms = (today_cutoff + 86_400 * (days - 1)) * 1_000;
@ -240,9 +240,9 @@ impl SqlWriter<'_> {
.unwrap();
match ease {
EaseKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
EaseKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
EaseKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
RatingKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
RatingKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
}
.unwrap();

View File

@ -5,7 +5,7 @@ use crate::{
decks::DeckID as DeckIDType,
err::Result,
notetype::NoteTypeID as NoteTypeIDType,
search::parser::{parse, EaseKind, Node, PropertyKind, SearchNode, StateKind, TemplateKind},
search::parser::{parse, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind},
};
use itertools::Itertools;
use std::mem;
@ -154,8 +154,8 @@ fn write_template(template: &TemplateKind) -> String {
}
}
fn write_rated(days: &u32, ease: &EaseKind) -> String {
use EaseKind::*;
fn write_rated(days: &u32, ease: &RatingKind) -> String {
use RatingKind::*;
match ease {
AnswerButton(n) => format!("\"rated:{}:{}\"", days, n),
AnyAnswerButton => format!("\"rated:{}\"", days),
@ -196,9 +196,9 @@ fn write_property(operator: &str, kind: &PropertyKind) -> String {
Ease(f) => format!("\"prop:ease{}{}\"", operator, f),
Position(u) => format!("\"prop:pos{}{}\"", operator, u),
Rated(u, ease) => match ease {
EaseKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
EaseKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
EaseKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
RatingKind::AnswerButton(val) => format!("\"prop:rated{}{}:{}\"", operator, u, val),
RatingKind::AnyAnswerButton => format!("\"prop:rated{}{}\"", operator, u),
RatingKind::ManualReschedule => format!("\"prop:resched{}{}\"", operator, u),
},
}
}