mirror of
https://github.com/ankitects/anki.git
synced 2025-09-25 01:06:35 -04:00
Enable removal of multiple tags from the sidebar
This commit is contained in:
parent
f4aeb0c097
commit
25d57574c9
7 changed files with 75 additions and 7 deletions
|
@ -349,7 +349,7 @@ class SidebarTreeView(QTreeView):
|
||||||
),
|
),
|
||||||
SidebarItemType.TAG: (
|
SidebarItemType.TAG: (
|
||||||
(tr(TR.ACTIONS_RENAME), self.rename_tag),
|
(tr(TR.ACTIONS_RENAME), self.rename_tag),
|
||||||
(tr(TR.ACTIONS_DELETE), self.remove_tag),
|
(tr(TR.ACTIONS_DELETE), self.remove_tags),
|
||||||
),
|
),
|
||||||
SidebarItemType.SAVED_SEARCH: (
|
SidebarItemType.SAVED_SEARCH: (
|
||||||
(tr(TR.ACTIONS_RENAME), self.rename_saved_search),
|
(tr(TR.ACTIONS_RENAME), self.rename_saved_search),
|
||||||
|
@ -1081,15 +1081,14 @@ class SidebarTreeView(QTreeView):
|
||||||
self.refresh()
|
self.refresh()
|
||||||
self.mw.deckBrowser.refresh()
|
self.mw.deckBrowser.refresh()
|
||||||
|
|
||||||
def remove_tag(self, item: SidebarItem) -> None:
|
def remove_tags(self, item: SidebarItem) -> None:
|
||||||
self.browser.editor.saveNow(lambda: self._remove_tag(item))
|
self.browser.editor.saveNow(lambda: self._remove_tags(item))
|
||||||
|
|
||||||
def _remove_tag(self, item: SidebarItem) -> None:
|
def _remove_tags(self, _item: SidebarItem) -> None:
|
||||||
old_name = item.full_name
|
tags = self._selected_tags()
|
||||||
|
|
||||||
def do_remove() -> None:
|
def do_remove() -> None:
|
||||||
self.mw.col.tags.remove(old_name)
|
self.col._backend.expunge_tags(" ".join(tags))
|
||||||
self.col.tags.rename(old_name, "")
|
|
||||||
|
|
||||||
def on_done(fut: Future) -> None:
|
def on_done(fut: Future) -> None:
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
||||||
|
@ -1246,3 +1245,10 @@ class SidebarTreeView(QTreeView):
|
||||||
for item in self._selected_items()
|
for item in self._selected_items()
|
||||||
if item.item_type == SidebarItemType.SAVED_SEARCH
|
if item.item_type == SidebarItemType.SAVED_SEARCH
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def _selected_tags(self) -> List[str]:
|
||||||
|
return [
|
||||||
|
item.full_name
|
||||||
|
for item in self._selected_items()
|
||||||
|
if item.item_type == SidebarItemType.TAG
|
||||||
|
]
|
||||||
|
|
|
@ -225,6 +225,7 @@ service BackendService {
|
||||||
|
|
||||||
rpc ClearUnusedTags(Empty) returns (Empty);
|
rpc ClearUnusedTags(Empty) returns (Empty);
|
||||||
rpc AllTags(Empty) returns (StringList);
|
rpc AllTags(Empty) returns (StringList);
|
||||||
|
rpc ExpungeTags(String) returns (Empty);
|
||||||
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
||||||
rpc ClearTag(String) returns (Empty);
|
rpc ClearTag(String) returns (Empty);
|
||||||
rpc TagTree(Empty) returns (TagTreeNode);
|
rpc TagTree(Empty) returns (TagTreeNode);
|
||||||
|
|
|
@ -1418,6 +1418,13 @@ impl BackendService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn expunge_tags(&self, tags: pb::String) -> BackendResult<pb::Empty> {
|
||||||
|
self.with_col(|col| {
|
||||||
|
col.expunge_tags(tags.val.as_str())?;
|
||||||
|
Ok(().into())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult<pb::Empty> {
|
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult<pb::Empty> {
|
||||||
self.with_col(|col| {
|
self.with_col(|col| {
|
||||||
col.transact(None, |col| {
|
col.transact(None, |col| {
|
||||||
|
|
|
@ -487,3 +487,11 @@ impl From<ParseIntError> for AnkiError {
|
||||||
AnkiError::ParseNumError
|
AnkiError::ParseNumError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<regex::Error> for AnkiError {
|
||||||
|
fn from(_err: regex::Error) -> Self {
|
||||||
|
AnkiError::InvalidInput {
|
||||||
|
info: "invalid regex".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -152,6 +152,12 @@ impl Note {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remove_tags(&mut self, re: &Regex) -> bool {
|
||||||
|
let old_len = self.tags.len();
|
||||||
|
self.tags.retain(|tag| !re.is_match(tag));
|
||||||
|
old_len > self.tags.len()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn replace_tags<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
|
pub(crate) fn replace_tags<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
|
||||||
let mut changed = false;
|
let mut changed = false;
|
||||||
for tag in &mut self.tags {
|
for tag in &mut self.tags {
|
||||||
|
|
|
@ -73,6 +73,15 @@ impl SqliteStorage {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear all matching tags where tag_group is a regexp group that should not match whitespace.
|
||||||
|
pub(crate) fn clear_tag_group(&self, tag_group: &str) -> Result<()> {
|
||||||
|
self.db
|
||||||
|
.prepare_cached("delete from tags where tag regexp ?")?
|
||||||
|
.execute(&[format!("(?i)^{}($|::)", tag_group)])?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
.prepare_cached("update tags set collapsed = ? where tag = ?")?
|
||||||
|
|
|
@ -285,6 +285,37 @@ impl Collection {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take tags as a whitespace-separated string and remove them from all notes and the storage.
|
||||||
|
pub fn expunge_tags(&mut self, tags: &str) -> Result<usize> {
|
||||||
|
let tag_group = format!("({})", regex::escape(tags.trim()).replace(' ', "|"));
|
||||||
|
let nids = self.nids_for_tags(&tag_group)?;
|
||||||
|
let re = Regex::new(&format!("(?i)^{}(::.*)?$", tag_group))?;
|
||||||
|
self.transact(None, |col| {
|
||||||
|
col.storage.clear_tag_group(&tag_group)?;
|
||||||
|
col.transform_notes(&nids, |note, _nt| {
|
||||||
|
Ok(TransformNoteOutput {
|
||||||
|
changed: note.remove_tags(&re),
|
||||||
|
generate_cards: false,
|
||||||
|
mark_modified: true,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Take tags as a regexp group, i.e. separated with pipes and wrapped in brackets, and return
|
||||||
|
/// the ids of all notes with one of them.
|
||||||
|
fn nids_for_tags(&mut self, tag_group: &str) -> Result<Vec<NoteID>> {
|
||||||
|
let mut stmt = self
|
||||||
|
.storage
|
||||||
|
.db
|
||||||
|
.prepare("select id from notes where tags regexp ?")?;
|
||||||
|
let args = format!("(?i).* {}(::| ).*", tag_group);
|
||||||
|
let nids = stmt
|
||||||
|
.query_map(&[args], |row| row.get(0))?
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
Ok(nids)
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> {
|
pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> {
|
||||||
let mut name = name;
|
let mut name = name;
|
||||||
let tag;
|
let tag;
|
||||||
|
|
Loading…
Reference in a new issue