Enable removal of multiple tags from the sidebar

This commit is contained in:
RumovZ 2021-03-02 11:05:16 +01:00
parent f4aeb0c097
commit 25d57574c9
7 changed files with 75 additions and 7 deletions

View file

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

View file

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

View file

@ -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> {
self.with_col(|col| {
col.transact(None, |col| {

View file

@ -487,3 +487,11 @@ impl From<ParseIntError> for AnkiError {
AnkiError::ParseNumError
}
}
impl From<regex::Error> for AnkiError {
fn from(_err: regex::Error) -> Self {
AnkiError::InvalidInput {
info: "invalid regex".into(),
}
}
}

View file

@ -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<T: Replacer>(&mut self, re: &Regex, mut repl: T) -> bool {
let mut changed = false;
for tag in &mut self.tags {

View file

@ -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 = ?")?

View file

@ -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<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<()> {
let mut name = name;
let tag;