From 25d57574c9b27edddeb6cf74048fc7ae68e0f302 Mon Sep 17 00:00:00 2001 From: RumovZ Date: Tue, 2 Mar 2021 11:05:16 +0100 Subject: [PATCH] Enable removal of multiple tags from the sidebar --- qt/aqt/sidebar.py | 20 +++++++++++++------- rslib/backend.proto | 1 + rslib/src/backend/mod.rs | 7 +++++++ rslib/src/err.rs | 8 ++++++++ rslib/src/notes.rs | 6 ++++++ rslib/src/storage/tag/mod.rs | 9 +++++++++ rslib/src/tags.rs | 31 +++++++++++++++++++++++++++++++ 7 files changed, 75 insertions(+), 7 deletions(-) diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f56723953..3a8b26ad3 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -349,7 +349,7 @@ class SidebarTreeView(QTreeView): ), SidebarItemType.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: ( (tr(TR.ACTIONS_RENAME), self.rename_saved_search), @@ -1081,15 +1081,14 @@ class SidebarTreeView(QTreeView): self.refresh() self.mw.deckBrowser.refresh() - def remove_tag(self, item: SidebarItem) -> None: - self.browser.editor.saveNow(lambda: self._remove_tag(item)) + def remove_tags(self, item: SidebarItem) -> None: + self.browser.editor.saveNow(lambda: self._remove_tags(item)) - def _remove_tag(self, item: SidebarItem) -> None: - old_name = item.full_name + def _remove_tags(self, _item: SidebarItem) -> None: + tags = self._selected_tags() def do_remove() -> None: - self.mw.col.tags.remove(old_name) - self.col.tags.rename(old_name, "") + self.col._backend.expunge_tags(" ".join(tags)) def on_done(fut: Future) -> None: self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self) @@ -1246,3 +1245,10 @@ class SidebarTreeView(QTreeView): for item in self._selected_items() 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 + ] diff --git a/rslib/backend.proto b/rslib/backend.proto index 2300ceb7f..8840cdfde 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -225,6 +225,7 @@ service BackendService { rpc ClearUnusedTags(Empty) returns (Empty); rpc AllTags(Empty) returns (StringList); + rpc ExpungeTags(String) returns (Empty); rpc SetTagExpanded(SetTagExpandedIn) returns (Empty); rpc ClearTag(String) returns (Empty); rpc TagTree(Empty) returns (TagTreeNode); diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index ba09fc9a4..72ca2b7ff 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -1418,6 +1418,13 @@ impl BackendService for Backend { }) } + fn expunge_tags(&self, tags: pb::String) -> BackendResult { + self.with_col(|col| { + col.expunge_tags(tags.val.as_str())?; + Ok(().into()) + }) + } + fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> BackendResult { self.with_col(|col| { col.transact(None, |col| { diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 6a93df75b..015750197 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -487,3 +487,11 @@ impl From for AnkiError { AnkiError::ParseNumError } } + +impl From for AnkiError { + fn from(_err: regex::Error) -> Self { + AnkiError::InvalidInput { + info: "invalid regex".into(), + } + } +} diff --git a/rslib/src/notes.rs b/rslib/src/notes.rs index 6be535d98..f60e61d55 100644 --- a/rslib/src/notes.rs +++ b/rslib/src/notes.rs @@ -152,6 +152,12 @@ impl Note { .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(&mut self, re: &Regex, mut repl: T) -> bool { let mut changed = false; for tag in &mut self.tags { diff --git a/rslib/src/storage/tag/mod.rs b/rslib/src/storage/tag/mod.rs index d3c56c2a2..b70fd99f7 100644 --- a/rslib/src/storage/tag/mod.rs +++ b/rslib/src/storage/tag/mod.rs @@ -73,6 +73,15 @@ impl SqliteStorage { 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<()> { self.db .prepare_cached("update tags set collapsed = ? where tag = ?")? diff --git a/rslib/src/tags.rs b/rslib/src/tags.rs index 7513fecac..15e6e7310 100644 --- a/rslib/src/tags.rs +++ b/rslib/src/tags.rs @@ -285,6 +285,37 @@ impl Collection { 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 { + 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> { + 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::>()?; + Ok(nids) + } + pub(crate) fn set_tag_expanded(&self, name: &str, expanded: bool) -> Result<()> { let mut name = name; let tag;