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:
parent
59ccfe5918
commit
35840221bb
@ -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
|
||||
##########################################################################
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -6,7 +6,7 @@ ignore = forms,hooks_gen.py
|
||||
[TYPECHECK]
|
||||
ignored-modules=win32file,pywintypes,socket,win32pipe,winrt,pyaudio
|
||||
ignored-classes=
|
||||
SearchTerm,
|
||||
SearchNode,
|
||||
Config,
|
||||
|
||||
[REPORTS]
|
||||
|
@ -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()
|
||||
|
@ -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)),
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -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],
|
||||
)
|
||||
|
@ -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))
|
||||
|
@ -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']}",
|
||||
|
@ -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 {
|
||||
|
@ -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> {
|
||||
|
@ -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};
|
||||
|
@ -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""#
|
||||
);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user