tweak search wording and tidy up API

- SearchTerm -> SearchNode
- Operator -> Joiner; share between messages
- build_search_string() supports specifying AND/OR as a convenience
- group_searches() makes it easier to negate
This commit is contained in:
Damien Elmes 2021-02-11 19:57:19 +10:00
parent 59ccfe5918
commit 35840221bb
15 changed files with 202 additions and 193 deletions

View File

@ -43,7 +43,8 @@ from anki.utils import (
)
# public exports
SearchTerm = _pb.SearchTerm
SearchNode = _pb.SearchNode
SearchJoiner = Literal["AND", "OR"]
Progress = _pb.Progress
Config = _pb.Config
EmptyCardsReport = _pb.EmptyCardsReport
@ -471,7 +472,7 @@ class Collection:
)
return self._backend.search_cards(search=query, order=mode)
def find_notes(self, *terms: Union[str, SearchTerm]) -> Sequence[int]:
def find_notes(self, *terms: Union[str, SearchNode]) -> Sequence[int]:
return self._backend.search_notes(self.build_search_string(*terms))
def find_and_replace(
@ -487,7 +488,7 @@ class Collection:
# returns array of ("dupestr", [nids])
def findDupes(self, fieldName: str, search: str = "") -> List[Tuple[Any, list]]:
nids = self.findNotes(search, SearchTerm(field_name=fieldName))
nids = self.findNotes(search, SearchNode(field_name=fieldName))
# go through notes
vals: Dict[str, List[int]] = {}
dupes = []
@ -526,72 +527,85 @@ class Collection:
# Search Strings
##########################################################################
def group_search_terms(self, *terms: Union[str, SearchTerm]) -> SearchTerm:
"""Join provided search terms and strings into a single SearchTerm.
If multiple terms provided, they will be ANDed together into a group.
If a single term is provided, it is returned as-is.
"""
assert terms
# convert raw text to SearchTerms
search_terms = [
term if isinstance(term, SearchTerm) else SearchTerm(unparsed_search=term)
for term in terms
]
# if there's more than one, wrap it in an implicit AND
if len(search_terms) > 1:
return SearchTerm(group=SearchTerm.Group(terms=search_terms))
else:
return search_terms[0]
def build_search_string(
self,
*terms: Union[str, SearchTerm],
*nodes: Union[str, SearchNode],
joiner: SearchJoiner = "AND",
) -> str:
"""Join provided search terms together, and return a normalized search string.
Terms are joined by an implicit AND. You can make an explict AND or OR
by wrapping in a group:
terms = [... one or more SearchTerms()]
group = SearchTerm.Group(op=SearchTerm.Group.OR, terms=terms)
term = SearchTerm(group=group)
"""Join one or more searches, and return a normalized search string.
To negate, wrap in a negated search term:
term = SearchTerm(negated=term)
term = SearchNode(negated=col.group_searches(...))
Invalid search terms will throw an exception.
Invalid searches will throw an exception.
"""
term = self.group_search_terms(*terms)
return self._backend.filter_to_search(term)
term = self.group_searches(*nodes, joiner=joiner)
return self._backend.build_search_string(term)
def group_searches(
self,
*nodes: Union[str, SearchNode],
joiner: SearchJoiner = "AND",
) -> SearchNode:
"""Join provided search nodes and strings into a single SearchNode.
If a single SearchNode is provided, it is returned as-is.
At least one node must be provided.
"""
assert nodes
# convert raw text to SearchNodes
search_nodes = [
node if isinstance(node, SearchNode) else SearchNode(parsable_text=node)
for node in nodes
]
# if there's more than one, wrap them in a group
if len(search_nodes) > 1:
return SearchNode(
group=SearchNode.Group(
nodes=search_nodes, joiner=self._pb_search_separator(joiner)
)
)
else:
return search_nodes[0]
# pylint: disable=no-member
def join_searches(
self,
existing_term: SearchTerm,
additional_term: SearchTerm,
existing_node: SearchNode,
additional_node: SearchNode,
operator: Literal["AND", "OR"],
) -> str:
"""
AND or OR `additional_term` to `existing_term`, without wrapping `existing_term` in brackets.
If you're building a search query yourself, prefer using SearchTerm(group=SearchTerm.Group(...))
Used by the Browse screen to avoid adding extra brackets when joining.
If you're building a search query yourself, you probably don't need this.
"""
if operator == "AND":
sep = _pb.ConcatenateSearchesIn.AND
else:
sep = _pb.ConcatenateSearchesIn.OR
search_string = self._backend.concatenate_searches(
sep=sep, existing_search=existing_term, additional_search=additional_term
search_string = self._backend.join_search_nodes(
joiner=self._pb_search_separator(operator),
existing_node=existing_node,
additional_node=additional_node,
)
return search_string
def replace_search_term(self, search: SearchTerm, replacement: SearchTerm) -> str:
return self._backend.replace_search_term(search=search, replacement=replacement)
def replace_in_search_node(
self, existing_node: SearchNode, replacement_node: SearchNode
) -> str:
"""If nodes of the same type as `replacement_node` are found in existing_node, replace them.
You can use this to replace any "deck" clauses in a search with a different deck for example.
"""
return self._backend.replace_search_node(
existing_node=existing_node, replacement_node=replacement_node
)
def _pb_search_separator(self, operator: SearchJoiner) -> SearchNode.Group.Joiner.V:
# pylint: disable=no-member
if operator == "AND":
return SearchNode.Group.Joiner.AND
else:
return SearchNode.Group.Joiner.OR
# Config
##########################################################################

View File

@ -90,7 +90,7 @@ class TagManager:
def rename(self, old: str, new: str) -> int:
"Rename provided tag, returning number of changed notes."
nids = self.col.find_notes(anki.collection.SearchTerm(tag=old))
nids = self.col.find_notes(anki.collection.SearchNode(tag=old))
if not nids:
return 0
escaped_name = re.sub(r"[*_\\]", r"\\\g<0>", old)

View File

@ -51,9 +51,9 @@ fn want_release_gil(method: u32) -> bool {
| BackendMethod::LatestProgress
| BackendMethod::SetWantsAbort
| BackendMethod::I18nResources
| BackendMethod::ConcatenateSearches
| BackendMethod::ReplaceSearchTerm
| BackendMethod::FilterToSearch
| BackendMethod::JoinSearchNodes
| BackendMethod::ReplaceSearchNode
| BackendMethod::BuildSearchString
)
} else {
false

View File

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

View File

@ -6,7 +6,7 @@ import aqt.deckchooser
import aqt.editor
import aqt.forms
import aqt.modelchooser
from anki.collection import SearchTerm
from anki.collection import SearchNode
from anki.consts import MODEL_CLOZE
from anki.notes import Note
from anki.utils import htmlToTextLine, isMac
@ -144,7 +144,7 @@ class AddCards(QDialog):
def onHistory(self) -> None:
m = QMenu(self)
for nid in self.history:
if self.mw.col.findNotes(SearchTerm(nid=nid)):
if self.mw.col.findNotes(SearchNode(nid=nid)):
note = self.mw.col.getNote(nid)
fields = note.fields
txt = htmlToTextLine(", ".join(fields))
@ -161,7 +161,7 @@ class AddCards(QDialog):
m.exec_(self.historyButton.mapToGlobal(QPoint(0, 0)))
def editHistory(self, nid: int) -> None:
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
def addNote(self, note: Note) -> Optional[Note]:
note.model()["did"] = self.deckChooser.selectedId()

View File

@ -12,7 +12,7 @@ from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple, Union,
import aqt
import aqt.forms
from anki.cards import Card
from anki.collection import Collection, Config, SearchTerm
from anki.collection import Collection, Config, SearchNode
from anki.consts import *
from anki.errors import InvalidInput
from anki.lang import without_unicode_isolation
@ -442,7 +442,7 @@ class Browser(QMainWindow):
self,
mw: AnkiQt,
card: Optional[Card] = None,
search: Optional[Tuple[Union[str, SearchTerm]]] = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None,
) -> None:
"""
card : try to search for its note and select it
@ -615,7 +615,7 @@ class Browser(QMainWindow):
self,
_mw: AnkiQt,
card: Optional[Card] = None,
search: Optional[Tuple[Union[str, SearchTerm]]] = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None,
) -> None:
if search is not None:
self.search_for_terms(*search)
@ -630,7 +630,7 @@ class Browser(QMainWindow):
def setupSearch(
self,
card: Optional[Card] = None,
search: Optional[Tuple[Union[str, SearchTerm]]] = None,
search: Optional[Tuple[Union[str, SearchNode]]] = None,
) -> None:
qconnect(self.form.searchEdit.lineEdit().returnPressed, self.onSearchActivated)
self.form.searchEdit.setCompleter(None)
@ -644,7 +644,7 @@ class Browser(QMainWindow):
self.show_single_card(card)
else:
self.search_for(
self.col.build_search_string(SearchTerm(deck="current")), ""
self.col.build_search_string(SearchNode(deck="current")), ""
)
self.form.searchEdit.setFocus()
@ -707,7 +707,7 @@ class Browser(QMainWindow):
)
return selected
def search_for_terms(self, *search_terms: Union[str, SearchTerm]) -> None:
def search_for_terms(self, *search_terms: Union[str, SearchNode]) -> None:
search = self.col.build_search_string(*search_terms)
self.form.searchEdit.setEditText(search)
self.onSearchActivated()
@ -717,7 +717,7 @@ class Browser(QMainWindow):
def on_show_single_card() -> None:
self.card = card
search = self.col.build_search_string(SearchTerm(nid=card.nid))
search = self.col.build_search_string(SearchNode(nid=card.nid))
search = gui_hooks.default_search(search, card)
self.search_for(search, "")
self.focusCid(card.id)
@ -1407,7 +1407,7 @@ where id in %s"""
tv.selectionModel().clear()
search = self.col.build_search_string(
SearchTerm(nids=SearchTerm.IdList(ids=nids))
SearchNode(nids=SearchNode.IdList(ids=nids))
)
self.search_for(search)
@ -1626,7 +1626,7 @@ where id in %s"""
% (
html.escape(
self.col.build_search_string(
SearchTerm(nids=SearchTerm.IdList(ids=nids))
SearchNode(nids=SearchNode.IdList(ids=nids))
)
),
tr(TR.BROWSING_NOTE_COUNT, count=len(nids)),

View File

@ -2,7 +2,7 @@
# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import aqt
from anki.collection import SearchTerm
from anki.collection import SearchNode
from anki.consts import *
from aqt.qt import *
from aqt.utils import TR, disable_help_button, showInfo, showWarning, tr
@ -164,20 +164,20 @@ class CustomStudy(QDialog):
# and then set various options
if i == RADIO_FORGOT:
search = self.mw.col.build_search_string(
SearchTerm(
rated=SearchTerm.Rated(days=spin, rating=SearchTerm.RATING_AGAIN)
SearchNode(
rated=SearchNode.Rated(days=spin, rating=SearchNode.RATING_AGAIN)
)
)
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_RANDOM]
dyn["resched"] = False
elif i == RADIO_AHEAD:
search = self.mw.col.build_search_string(SearchTerm(due_in_days=spin))
search = self.mw.col.build_search_string(SearchNode(due_in_days=spin))
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_DUE]
dyn["resched"] = True
elif i == RADIO_PREVIEW:
search = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
SearchTerm(added_in_days=spin),
SearchNode(card_state=SearchNode.CARD_STATE_NEW),
SearchNode(added_in_days=spin),
)
dyn["terms"][0] = [search, DYN_MAX_SIZE, DYN_OLDEST]
dyn["resched"] = False
@ -185,19 +185,19 @@ class CustomStudy(QDialog):
type = f.cardType.currentRow()
if type == TYPE_NEW:
terms = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)
SearchNode(card_state=SearchNode.CARD_STATE_NEW)
)
ord = DYN_ADDED
dyn["resched"] = True
elif type == TYPE_DUE:
terms = self.mw.col.build_search_string(
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE)
SearchNode(card_state=SearchNode.CARD_STATE_DUE)
)
ord = DYN_DUE
dyn["resched"] = True
elif type == TYPE_REVIEW:
terms = self.mw.col.build_search_string(
SearchTerm(negated=SearchTerm(card_state=SearchTerm.CARD_STATE_NEW))
SearchNode(negated=SearchNode(card_state=SearchNode.CARD_STATE_NEW))
)
ord = DYN_RANDOM
dyn["resched"] = True
@ -208,7 +208,7 @@ class CustomStudy(QDialog):
dyn["terms"][0] = [(terms + tags).strip(), spin, ord]
# add deck limit
dyn["terms"][0][0] = self.mw.col.build_search_string(
dyn["terms"][0][0], SearchTerm(deck=self.deck["name"])
dyn["terms"][0][0], SearchNode(deck=self.deck["name"])
)
self.mw.col.decks.save(dyn)
# generate cards

View File

@ -3,7 +3,7 @@
from typing import Callable, List, Optional
import aqt
from anki.collection import SearchTerm
from anki.collection import SearchNode
from anki.decks import Deck, DeckRenameError
from anki.errors import InvalidInput
from anki.lang import without_unicode_isolation
@ -111,14 +111,14 @@ class DeckConf(QDialog):
def set_default_searches(self, deck_name: str) -> None:
self.form.search.setText(
self.mw.col.build_search_string(
SearchTerm(deck=deck_name),
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE),
SearchNode(deck=deck_name),
SearchNode(card_state=SearchNode.CARD_STATE_DUE),
)
)
self.form.search_2.setText(
self.mw.col.build_search_string(
SearchTerm(deck=deck_name),
SearchTerm(card_state=SearchTerm.CARD_STATE_NEW),
SearchNode(deck=deck_name),
SearchNode(card_state=SearchNode.CARD_STATE_NEW),
)
)

View File

@ -20,7 +20,7 @@ from bs4 import BeautifulSoup
import aqt
import aqt.sound
from anki.cards import Card
from anki.collection import SearchTerm
from anki.collection import SearchNode
from anki.consts import MODEL_CLOZE
from anki.hooks import runFilter
from anki.httpclient import HttpClient
@ -546,8 +546,8 @@ class Editor:
"Browser",
self.mw,
search=(
SearchTerm(
dupe=SearchTerm.Dupe(
SearchNode(
dupe=SearchNode.Dupe(
notetype_id=self.note.model()["id"],
first_field=self.note.fields[0],
)

View File

@ -9,7 +9,7 @@ from concurrent.futures import Future
from typing import Iterable, List, Optional, Sequence, TypeVar
import aqt
from anki.collection import SearchTerm
from anki.collection import SearchNode
from anki.errors import Interrupted
from anki.lang import TR
from anki.media import CheckMediaOut
@ -154,7 +154,7 @@ class MediaChecker:
if out is not None:
nid, err = out
aqt.dialogs.open("Browser", self.mw, search=(SearchTerm(nid=nid),))
aqt.dialogs.open("Browser", self.mw, search=(SearchNode(nid=nid),))
showText(err, type="html")
else:
tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED))

View File

@ -8,7 +8,7 @@ from enum import Enum, auto
from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, cast
import aqt
from anki.collection import Config, SearchTerm
from anki.collection import Config, SearchNode
from anki.decks import DeckTreeNode
from anki.errors import DeckRenameError, InvalidInput
from anki.tags import TagTreeNode
@ -391,20 +391,20 @@ class SidebarTreeView(QTreeView):
if item.is_expanded(searching):
self.setExpanded(idx, True)
def update_search(self, *terms: Union[str, SearchTerm]) -> None:
def update_search(self, *terms: Union[str, SearchNode]) -> None:
"""Modify the current search string based on modifier keys, then refresh."""
mods = self.mw.app.keyboardModifiers()
previous = SearchTerm(unparsed_search=self.browser.current_search())
current = self.mw.col.group_search_terms(*terms)
previous = SearchNode(parsable_text=self.browser.current_search())
current = self.mw.col.group_searches(*terms)
# if Alt pressed, invert
if mods & Qt.AltModifier:
current = SearchTerm(negated=current)
current = SearchNode(negated=current)
try:
if mods & Qt.ControlModifier and mods & Qt.ShiftModifier:
# If Ctrl+Shift, replace searches nodes of the same type.
search = self.col.replace_search_term(previous, current)
search = self.col.replace_in_search_node(previous, current)
elif mods & Qt.ControlModifier:
# If Ctrl, AND with previous
search = self.col.join_searches(previous, current, "AND")
@ -597,7 +597,7 @@ class SidebarTreeView(QTreeView):
return top
def _filter_func(self, *terms: Union[str, SearchTerm]) -> Callable:
def _filter_func(self, *terms: Union[str, SearchNode]) -> Callable:
return lambda: self.update_search(*terms)
# Tree: Saved Searches
@ -648,33 +648,33 @@ class SidebarTreeView(QTreeView):
name=TR.BROWSING_SIDEBAR_DUE_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(due_on_day=0)),
on_click=search(SearchNode(due_on_day=0)),
)
root.add_simple(
name=TR.BROWSING_ADDED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(added_in_days=1)),
on_click=search(SearchNode(added_in_days=1)),
)
root.add_simple(
name=TR.BROWSING_EDITED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(edited_in_days=1)),
on_click=search(SearchNode(edited_in_days=1)),
)
root.add_simple(
name=TR.BROWSING_STUDIED_TODAY,
icon=icon,
type=type,
on_click=search(SearchTerm(rated=SearchTerm.Rated(days=1))),
on_click=search(SearchNode(rated=SearchNode.Rated(days=1))),
)
root.add_simple(
name=TR.BROWSING_AGAIN_TODAY,
icon=icon,
type=type,
on_click=search(
SearchTerm(
rated=SearchTerm.Rated(days=1, rating=SearchTerm.RATING_AGAIN)
SearchNode(
rated=SearchNode.Rated(days=1, rating=SearchNode.RATING_AGAIN)
)
),
)
@ -683,8 +683,8 @@ class SidebarTreeView(QTreeView):
icon=icon,
type=type,
on_click=search(
SearchTerm(card_state=SearchTerm.CARD_STATE_DUE),
SearchTerm(negated=SearchTerm(due_on_day=0)),
SearchNode(card_state=SearchNode.CARD_STATE_DUE),
SearchNode(negated=SearchNode(due_on_day=0)),
),
)
@ -707,32 +707,32 @@ class SidebarTreeView(QTreeView):
TR.ACTIONS_NEW,
icon=icon.with_color(colors.NEW_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_NEW)),
on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_NEW)),
)
root.add_simple(
name=TR.SCHEDULING_LEARNING,
icon=icon.with_color(colors.LEARN_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_LEARN)),
on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_LEARN)),
)
root.add_simple(
name=TR.SCHEDULING_REVIEW,
icon=icon.with_color(colors.REVIEW_COUNT),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_REVIEW)),
on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_REVIEW)),
)
root.add_simple(
name=TR.BROWSING_SUSPENDED,
icon=icon.with_color(colors.SUSPENDED_FG),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_SUSPENDED)),
on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_SUSPENDED)),
)
root.add_simple(
name=TR.BROWSING_BURIED,
icon=icon.with_color(colors.BURIED_FG),
type=type,
on_click=search(SearchTerm(card_state=SearchTerm.CARD_STATE_BURIED)),
on_click=search(SearchNode(card_state=SearchNode.CARD_STATE_BURIED)),
)
# Tree: Flags
@ -748,38 +748,38 @@ class SidebarTreeView(QTreeView):
collapse_key=Config.Bool.COLLAPSE_FLAGS,
type=SidebarItemType.FLAG_ROOT,
)
root.on_click = search(SearchTerm(flag=SearchTerm.FLAG_ANY))
root.on_click = search(SearchNode(flag=SearchNode.FLAG_ANY))
type = SidebarItemType.FLAG
root.add_simple(
TR.ACTIONS_RED_FLAG,
icon=icon.with_color(colors.FLAG1_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_RED)),
on_click=search(SearchNode(flag=SearchNode.FLAG_RED)),
)
root.add_simple(
TR.ACTIONS_ORANGE_FLAG,
icon=icon.with_color(colors.FLAG2_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_ORANGE)),
on_click=search(SearchNode(flag=SearchNode.FLAG_ORANGE)),
)
root.add_simple(
TR.ACTIONS_GREEN_FLAG,
icon=icon.with_color(colors.FLAG3_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_GREEN)),
on_click=search(SearchNode(flag=SearchNode.FLAG_GREEN)),
)
root.add_simple(
TR.ACTIONS_BLUE_FLAG,
icon=icon.with_color(colors.FLAG4_FG),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_BLUE)),
on_click=search(SearchNode(flag=SearchNode.FLAG_BLUE)),
)
root.add_simple(
TR.BROWSING_NO_FLAG,
icon=icon.with_color(colors.DISABLED),
type=type,
on_click=search(SearchTerm(flag=SearchTerm.FLAG_NONE)),
on_click=search(SearchNode(flag=SearchNode.FLAG_NONE)),
)
# Tree: Tags
@ -802,7 +802,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
icon,
self._filter_func(SearchTerm(tag=head + node.name)),
self._filter_func(SearchNode(tag=head + node.name)),
toggle_expand(),
node.expanded,
item_type=SidebarItemType.TAG,
@ -820,12 +820,12 @@ class SidebarTreeView(QTreeView):
collapse_key=Config.Bool.COLLAPSE_TAGS,
type=SidebarItemType.TAG_ROOT,
)
root.on_click = self._filter_func(SearchTerm(negated=SearchTerm(tag="none")))
root.on_click = self._filter_func(SearchNode(negated=SearchNode(tag="none")))
root.add_simple(
name=tr(TR.BROWSING_SIDEBAR_UNTAGGED),
icon=icon,
type=SidebarItemType.TAG_NONE,
on_click=self._filter_func(SearchTerm(tag="none")),
on_click=self._filter_func(SearchNode(tag="none")),
)
render(root, tree.children)
@ -848,7 +848,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
node.name,
icon,
self._filter_func(SearchTerm(deck=head + node.name)),
self._filter_func(SearchNode(deck=head + node.name)),
toggle_expand(),
not node.collapsed,
item_type=SidebarItemType.DECK,
@ -867,12 +867,12 @@ class SidebarTreeView(QTreeView):
collapse_key=Config.Bool.COLLAPSE_DECKS,
type=SidebarItemType.DECK_ROOT,
)
root.on_click = self._filter_func(SearchTerm(deck="*"))
root.on_click = self._filter_func(SearchNode(deck="*"))
current = root.add_simple(
name=tr(TR.BROWSING_CURRENT_DECK),
icon=icon,
type=SidebarItemType.DECK,
on_click=self._filter_func(SearchTerm(deck="current")),
on_click=self._filter_func(SearchNode(deck="current")),
)
current.id = self.mw.col.decks.selected()
@ -895,7 +895,7 @@ class SidebarTreeView(QTreeView):
item = SidebarItem(
nt["name"],
icon,
self._filter_func(SearchTerm(note=nt["name"])),
self._filter_func(SearchNode(note=nt["name"])),
item_type=SidebarItemType.NOTETYPE,
id=nt["id"],
)
@ -905,7 +905,7 @@ class SidebarTreeView(QTreeView):
tmpl["name"],
icon,
self._filter_func(
SearchTerm(note=nt["name"]), SearchTerm(template=c)
SearchNode(note=nt["name"]), SearchNode(template=c)
),
item_type=SidebarItemType.NOTETYPE_TEMPLATE,
full_name=f"{nt['name']}::{tmpl['name']}",

View File

@ -90,11 +90,11 @@ service BackendService {
// searching
rpc FilterToSearch(SearchTerm) returns (String);
rpc BuildSearchString(SearchNode) returns (String);
rpc SearchCards(SearchCardsIn) returns (SearchCardsOut);
rpc SearchNotes(SearchNotesIn) returns (SearchNotesOut);
rpc ConcatenateSearches(ConcatenateSearchesIn) returns (String);
rpc ReplaceSearchTerm(ReplaceSearchTermIn) returns (String);
rpc JoinSearchNodes(JoinSearchNodesIn) returns (String);
rpc ReplaceSearchNode(ReplaceSearchNodeIn) returns (String);
rpc FindAndReplace(FindAndReplaceIn) returns (UInt32);
// scheduling
@ -771,7 +771,7 @@ message SearchNotesOut {
repeated int64 note_ids = 2;
}
message SearchTerm {
message SearchNode {
message Dupe {
int64 notetype_id = 1;
string first_field = 2;
@ -808,17 +808,17 @@ message SearchTerm {
repeated int64 ids = 1;
}
message Group {
enum Operator {
enum Joiner {
AND = 0;
OR = 1;
}
repeated SearchTerm terms = 1;
Operator op = 2;
repeated SearchNode nodes = 1;
Joiner joiner = 2;
}
oneof filter {
Group group = 1;
SearchTerm negated = 2;
string unparsed_search = 3;
SearchNode negated = 2;
string parsable_text = 3;
uint32 template = 4;
int64 nid = 5;
Dupe dupe = 6;
@ -837,19 +837,15 @@ message SearchTerm {
}
}
message ConcatenateSearchesIn {
enum Separator {
AND = 0;
OR = 1;
}
Separator sep = 1;
SearchTerm existing_search = 2;
SearchTerm additional_search = 3;
message JoinSearchNodesIn {
SearchNode.Group.Joiner joiner = 1;
SearchNode existing_node = 2;
SearchNode additional_node = 3;
}
message ReplaceSearchTermIn {
SearchTerm search = 1;
SearchTerm replacement = 2;
message ReplaceSearchNodeIn {
SearchNode existing_node = 1;
SearchNode replacement_node = 2;
}
message CloseCollectionIn {

View File

@ -6,7 +6,6 @@ use crate::{
backend::dbproxy::db_command_bytes,
backend_proto as pb,
backend_proto::{
concatenate_searches_in::Separator as BoolSeparatorProto,
sort_order::builtin::Kind as SortKindProto, sort_order::Value as SortOrderProto,
AddOrUpdateDeckConfigLegacyIn, BackendResult, Empty, RenderedTemplateReplacement,
},
@ -38,7 +37,7 @@ use crate::{
timespan::{answer_button_time, time_span},
},
search::{
concatenate_searches, parse_search, replace_search_term, write_nodes, BoolSeparator, Node,
concatenate_searches, parse_search, replace_search_node, write_nodes, BoolSeparator, Node,
PropertyKind, RatingKind, SearchNode, SortMode, StateKind, TemplateKind,
},
stats::studied_today,
@ -267,7 +266,7 @@ impl From<pb::NoteId> for NoteID {
}
}
impl pb::search_term::IdList {
impl pb::search_node::IdList {
fn into_id_string(self) -> String {
self.ids
.iter()
@ -295,13 +294,13 @@ impl From<pb::DeckConfigId> for DeckConfID {
}
}
impl TryFrom<pb::SearchTerm> for Node {
impl TryFrom<pb::SearchNode> for Node {
type Error = AnkiError;
fn try_from(msg: pb::SearchTerm) -> std::result::Result<Self, Self::Error> {
use pb::search_term::group::Operator;
use pb::search_term::Filter;
use pb::search_term::Flag;
fn try_from(msg: pb::SearchNode) -> std::result::Result<Self, Self::Error> {
use pb::search_node::group::Joiner;
use pb::search_node::Filter;
use pb::search_node::Flag;
Ok(if let Some(filter) = msg.filter {
match filter {
Filter::Tag(s) => Node::Search(SearchNode::Tag(escape_anki_wildcards(&s))),
@ -340,7 +339,7 @@ impl TryFrom<pb::SearchTerm> for Node {
}),
Filter::EditedInDays(u) => Node::Search(SearchNode::EditedInDays(u)),
Filter::CardState(state) => Node::Search(SearchNode::State(
pb::search_term::CardState::from_i32(state)
pb::search_node::CardState::from_i32(state)
.unwrap_or_default()
.into(),
)),
@ -354,27 +353,27 @@ impl TryFrom<pb::SearchTerm> for Node {
},
Filter::Negated(term) => Node::try_from(*term)?.negated(),
Filter::Group(mut group) => {
match group.terms.len() {
match group.nodes.len() {
0 => return Err(AnkiError::invalid_input("empty group")),
// a group of 1 doesn't need to be a group
1 => group.terms.pop().unwrap().try_into()?,
1 => group.nodes.pop().unwrap().try_into()?,
// 2+ nodes
_ => {
let operator = match group.op() {
Operator::And => Node::And,
Operator::Or => Node::Or,
let joiner = match group.joiner() {
Joiner::And => Node::And,
Joiner::Or => Node::Or,
};
let parsed: Vec<_> = group
.terms
.nodes
.into_iter()
.map(TryFrom::try_from)
.collect::<Result<_>>()?;
let joined = parsed.into_iter().intersperse(operator).collect();
let joined = parsed.into_iter().intersperse(joiner).collect();
Node::Group(joined)
}
}
}
Filter::UnparsedSearch(text) => {
Filter::ParsableText(text) => {
let mut nodes = parse_search(&text)?;
if nodes.len() == 1 {
nodes.pop().unwrap()
@ -389,37 +388,37 @@ impl TryFrom<pb::SearchTerm> for Node {
}
}
impl From<BoolSeparatorProto> for BoolSeparator {
fn from(sep: BoolSeparatorProto) -> Self {
impl From<pb::search_node::group::Joiner> for BoolSeparator {
fn from(sep: pb::search_node::group::Joiner) -> Self {
match sep {
BoolSeparatorProto::And => BoolSeparator::And,
BoolSeparatorProto::Or => BoolSeparator::Or,
pb::search_node::group::Joiner::And => BoolSeparator::And,
pb::search_node::group::Joiner::Or => BoolSeparator::Or,
}
}
}
impl From<pb::search_term::Rating> for RatingKind {
fn from(r: pb::search_term::Rating) -> Self {
impl From<pb::search_node::Rating> for RatingKind {
fn from(r: pb::search_node::Rating) -> Self {
match r {
pb::search_term::Rating::Again => RatingKind::AnswerButton(1),
pb::search_term::Rating::Hard => RatingKind::AnswerButton(2),
pb::search_term::Rating::Good => RatingKind::AnswerButton(3),
pb::search_term::Rating::Easy => RatingKind::AnswerButton(4),
pb::search_term::Rating::Any => RatingKind::AnyAnswerButton,
pb::search_term::Rating::ByReschedule => RatingKind::ManualReschedule,
pb::search_node::Rating::Again => RatingKind::AnswerButton(1),
pb::search_node::Rating::Hard => RatingKind::AnswerButton(2),
pb::search_node::Rating::Good => RatingKind::AnswerButton(3),
pb::search_node::Rating::Easy => RatingKind::AnswerButton(4),
pb::search_node::Rating::Any => RatingKind::AnyAnswerButton,
pb::search_node::Rating::ByReschedule => RatingKind::ManualReschedule,
}
}
}
impl From<pb::search_term::CardState> for StateKind {
fn from(k: pb::search_term::CardState) -> Self {
impl From<pb::search_node::CardState> for StateKind {
fn from(k: pb::search_node::CardState) -> Self {
match k {
pb::search_term::CardState::New => StateKind::New,
pb::search_term::CardState::Learn => StateKind::Learning,
pb::search_term::CardState::Review => StateKind::Review,
pb::search_term::CardState::Due => StateKind::Due,
pb::search_term::CardState::Suspended => StateKind::Suspended,
pb::search_term::CardState::Buried => StateKind::Buried,
pb::search_node::CardState::New => StateKind::New,
pb::search_node::CardState::Learn => StateKind::Learning,
pb::search_node::CardState::Review => StateKind::Review,
pb::search_node::CardState::Due => StateKind::Due,
pb::search_node::CardState::Suspended => StateKind::Suspended,
pb::search_node::CardState::Buried => StateKind::Buried,
}
}
}
@ -548,7 +547,7 @@ impl BackendService for Backend {
// searching
//-----------------------------------------------
fn filter_to_search(&self, input: pb::SearchTerm) -> Result<pb::String> {
fn build_search_string(&self, input: pb::SearchNode) -> Result<pb::String> {
Ok(write_nodes(&[input.try_into()?]).into())
}
@ -571,31 +570,31 @@ impl BackendService for Backend {
})
}
fn concatenate_searches(&self, input: pb::ConcatenateSearchesIn) -> Result<pb::String> {
let sep = input.sep().into();
fn join_search_nodes(&self, input: pb::JoinSearchNodesIn) -> Result<pb::String> {
let sep = input.joiner().into();
let existing_nodes = {
let node = input.existing_search.unwrap_or_default().try_into()?;
let node = input.existing_node.unwrap_or_default().try_into()?;
if let Node::Group(nodes) = node {
nodes
} else {
vec![node]
}
};
let additional_node = input.additional_search.unwrap_or_default().try_into()?;
let additional_node = input.additional_node.unwrap_or_default().try_into()?;
Ok(concatenate_searches(sep, existing_nodes, additional_node).into())
}
fn replace_search_term(&self, input: pb::ReplaceSearchTermIn) -> Result<pb::String> {
fn replace_search_node(&self, input: pb::ReplaceSearchNodeIn) -> Result<pb::String> {
let existing = {
let node = input.search.unwrap_or_default().try_into()?;
let node = input.existing_node.unwrap_or_default().try_into()?;
if let Node::Group(nodes) = node {
nodes
} else {
vec![node]
}
};
let replacement = input.replacement.unwrap_or_default().try_into()?;
Ok(replace_search_term(existing, replacement).into())
let replacement = input.replacement_node.unwrap_or_default().try_into()?;
Ok(replace_search_node(existing, replacement).into())
}
fn find_and_replace(&self, input: pb::FindAndReplaceIn) -> BackendResult<pb::UInt32> {

View File

@ -11,4 +11,4 @@ pub use cards::SortMode;
pub use parser::{
parse as parse_search, Node, PropertyKind, RatingKind, SearchNode, StateKind, TemplateKind,
};
pub use writer::{concatenate_searches, replace_search_term, write_nodes, BoolSeparator};
pub use writer::{concatenate_searches, replace_search_node, write_nodes, BoolSeparator};

View File

@ -37,7 +37,7 @@ pub fn concatenate_searches(
/// Given an existing parsed search, if the provided `replacement` is a single search node such
/// as a deck:xxx search, replace any instances of that search in `existing` with the new value.
/// Then return the possibly modified first search as a string.
pub fn replace_search_term(mut existing: Vec<Node>, replacement: Node) -> String {
pub fn replace_search_node(mut existing: Vec<Node>, replacement: Node) -> String {
if let Node::Search(search_node) = replacement {
fn update_node_vec(old_nodes: &mut [Node], new_node: &SearchNode) {
fn update_node(old_node: &mut Node, new_node: &SearchNode) {
@ -242,29 +242,29 @@ mod test {
#[test]
fn replacing() -> Result<()> {
assert_eq!(
replace_search_term(parse("deck:baz bar")?, parse("deck:foo")?.pop().unwrap()),
replace_search_node(parse("deck:baz bar")?, parse("deck:foo")?.pop().unwrap()),
r#""deck:foo" AND "bar""#,
);
assert_eq!(
replace_search_term(
replace_search_node(
parse("tag:foo Or tag:bar")?,
parse("tag:baz")?.pop().unwrap()
),
r#""tag:baz" OR "tag:baz""#,
);
assert_eq!(
replace_search_term(
replace_search_node(
parse("foo or (-foo tag:baz)")?,
parse("bar")?.pop().unwrap()
),
r#""bar" OR (-"bar" AND "tag:baz")"#,
);
assert_eq!(
replace_search_term(parse("is:due")?, parse("-is:new")?.pop().unwrap()),
replace_search_node(parse("is:due")?, parse("-is:new")?.pop().unwrap()),
r#""is:due""#
);
assert_eq!(
replace_search_term(parse("added:1")?, parse("is:due")?.pop().unwrap()),
replace_search_node(parse("added:1")?, parse("is:due")?.pop().unwrap()),
r#""added:1""#
);