mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
make tag deletion undoable, and speed it up
- ~4x faster than before on tag tree with 30k notes - remove the separate clear_tag() backend method
This commit is contained in:
parent
157b74b671
commit
09076da937
13 changed files with 113 additions and 103 deletions
|
@ -65,7 +65,7 @@ class TagManager:
|
||||||
"Set browser expansion state for tag, registering the tag if missing."
|
"Set browser expansion state for tag, registering the tag if missing."
|
||||||
self.col._backend.set_tag_expanded(name=tag, expanded=expanded)
|
self.col._backend.set_tag_expanded(name=tag, expanded=expanded)
|
||||||
|
|
||||||
# Bulk addition/removal from notes
|
# Bulk addition/removal from specific notes
|
||||||
#############################################################
|
#############################################################
|
||||||
|
|
||||||
def bulk_add(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
|
def bulk_add(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
|
||||||
|
@ -84,31 +84,23 @@ class TagManager:
|
||||||
def bulk_remove(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
|
def bulk_remove(self, nids: Sequence[int], tags: str) -> OpChangesWithCount:
|
||||||
return self.bulk_update(nids, tags, "", False)
|
return self.bulk_update(nids, tags, "", False)
|
||||||
|
|
||||||
|
# Bulk addition/removal based on tag
|
||||||
|
#############################################################
|
||||||
|
|
||||||
def rename(self, old: str, new: str) -> OpChangesWithCount:
|
def rename(self, old: str, new: str) -> OpChangesWithCount:
|
||||||
"Rename provided tag and its children, returning number of changed notes."
|
"Rename provided tag and its children, returning number of changed notes."
|
||||||
x = self.col._backend.rename_tags(current_prefix=old, new_prefix=new)
|
x = self.col._backend.rename_tags(current_prefix=old, new_prefix=new)
|
||||||
return x
|
return x
|
||||||
|
|
||||||
def remove(self, tag: str) -> None:
|
def remove(self, space_separated_tags: str) -> OpChangesWithCount:
|
||||||
self.col._backend.clear_tag(tag)
|
"Remove the provided tag(s) and their children from notes and the tag list."
|
||||||
|
return self.col._backend.remove_tags(val=space_separated_tags)
|
||||||
|
|
||||||
def drag_drop(self, source_tags: List[str], target_tag: str) -> None:
|
def drag_drop(self, source_tags: List[str], target_tag: str) -> None:
|
||||||
"""Rename one or more source tags that were dropped on `target_tag`.
|
"""Rename one or more source tags that were dropped on `target_tag`.
|
||||||
If target_tag is "", tags will be placed at the top level."""
|
If target_tag is "", tags will be placed at the top level."""
|
||||||
self.col._backend.drag_drop_tags(source_tags=source_tags, target_tag=target_tag)
|
self.col._backend.drag_drop_tags(source_tags=source_tags, target_tag=target_tag)
|
||||||
|
|
||||||
# legacy routines
|
|
||||||
|
|
||||||
def bulkAdd(self, ids: List[int], tags: str, add: bool = True) -> None:
|
|
||||||
"Add tags in bulk. TAGS is space-separated."
|
|
||||||
if add:
|
|
||||||
self.bulk_add(ids, tags)
|
|
||||||
else:
|
|
||||||
self.bulk_update(ids, tags, "", False)
|
|
||||||
|
|
||||||
def bulkRem(self, ids: List[int], tags: str) -> None:
|
|
||||||
self.bulkAdd(ids, tags, False)
|
|
||||||
|
|
||||||
# String-based utilities
|
# String-based utilities
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
@ -170,3 +162,13 @@ class TagManager:
|
||||||
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
|
self, tags: Collection[str], usn: Optional[int] = None, clear: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
print("tags.register() is deprecated and no longer works")
|
print("tags.register() is deprecated and no longer works")
|
||||||
|
|
||||||
|
def bulkAdd(self, ids: List[int], tags: str, add: bool = True) -> None:
|
||||||
|
"Add tags in bulk. TAGS is space-separated."
|
||||||
|
if add:
|
||||||
|
self.bulk_add(ids, tags)
|
||||||
|
else:
|
||||||
|
self.bulk_update(ids, tags, "", False)
|
||||||
|
|
||||||
|
def bulkRem(self, ids: List[int], tags: str) -> None:
|
||||||
|
self.bulkAdd(ids, tags, False)
|
||||||
|
|
|
@ -31,7 +31,7 @@ from aqt.note_ops import (
|
||||||
clear_unused_tags,
|
clear_unused_tags,
|
||||||
find_and_replace,
|
find_and_replace,
|
||||||
remove_notes,
|
remove_notes,
|
||||||
remove_tags,
|
remove_tags_for_notes,
|
||||||
)
|
)
|
||||||
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
from aqt.previewer import BrowserPreviewer as PreviewDialog
|
||||||
from aqt.previewer import Previewer
|
from aqt.previewer import Previewer
|
||||||
|
@ -1259,7 +1259,7 @@ where id in %s"""
|
||||||
tags := tags or self._prompt_for_tags(tr(TR.BROWSING_ENTER_TAGS_TO_DELETE))
|
tags := tags or self._prompt_for_tags(tr(TR.BROWSING_ENTER_TAGS_TO_DELETE))
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
remove_tags(
|
remove_tags_for_notes(
|
||||||
mw=self.mw, note_ids=self.selected_notes(), space_separated_tags=tags
|
mw=self.mw, note_ids=self.selected_notes(), space_separated_tags=tags
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -43,7 +43,7 @@ def add_tags(*, mw: AnkiQt, note_ids: Sequence[int], space_separated_tags: str)
|
||||||
mw.perform_op(lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags))
|
mw.perform_op(lambda: mw.col.tags.bulk_add(note_ids, space_separated_tags))
|
||||||
|
|
||||||
|
|
||||||
def remove_tags(
|
def remove_tags_for_notes(
|
||||||
*, mw: AnkiQt, note_ids: Sequence[int], space_separated_tags: str
|
*, mw: AnkiQt, note_ids: Sequence[int], space_separated_tags: str
|
||||||
) -> None:
|
) -> None:
|
||||||
mw.perform_op(lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags))
|
mw.perform_op(lambda: mw.col.tags.bulk_remove(note_ids, space_separated_tags))
|
||||||
|
@ -79,6 +79,17 @@ def rename_tag(
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_tags_for_all_notes(
|
||||||
|
*, mw: AnkiQt, parent: QWidget, space_separated_tags: str
|
||||||
|
) -> None:
|
||||||
|
mw.perform_op(
|
||||||
|
lambda: mw.col.tags.remove(space_separated_tags=space_separated_tags),
|
||||||
|
success=lambda out: tooltip(
|
||||||
|
tr(TR.BROWSING_NOTES_UPDATED, count=out.count), parent=parent
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_and_replace(
|
def find_and_replace(
|
||||||
*,
|
*,
|
||||||
mw: AnkiQt,
|
mw: AnkiQt,
|
||||||
|
|
|
@ -19,7 +19,7 @@ from anki.tags import MARKED_TAG
|
||||||
from anki.utils import stripHTML
|
from anki.utils import stripHTML
|
||||||
from aqt import AnkiQt, gui_hooks
|
from aqt import AnkiQt, gui_hooks
|
||||||
from aqt.card_ops import set_card_flag
|
from aqt.card_ops import set_card_flag
|
||||||
from aqt.note_ops import add_tags, remove_notes, remove_tags
|
from aqt.note_ops import add_tags, remove_notes, remove_tags_for_notes
|
||||||
from aqt.profiles import VideoDriver
|
from aqt.profiles import VideoDriver
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.scheduling_ops import (
|
from aqt.scheduling_ops import (
|
||||||
|
@ -847,7 +847,9 @@ time = %(time)d;
|
||||||
def toggle_mark_on_current_note(self) -> None:
|
def toggle_mark_on_current_note(self) -> None:
|
||||||
note = self.card.note()
|
note = self.card.note()
|
||||||
if note.has_tag(MARKED_TAG):
|
if note.has_tag(MARKED_TAG):
|
||||||
remove_tags(mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG)
|
remove_tags_for_notes(
|
||||||
|
mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
add_tags(mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG)
|
add_tags(mw=self.mw, note_ids=[note.id], space_separated_tags=MARKED_TAG)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from aqt.clayout import CardLayout
|
||||||
from aqt.deck_ops import remove_decks
|
from aqt.deck_ops import remove_decks
|
||||||
from aqt.main import ResetReason
|
from aqt.main import ResetReason
|
||||||
from aqt.models import Models
|
from aqt.models import Models
|
||||||
from aqt.note_ops import rename_tag
|
from aqt.note_ops import remove_tags_for_all_notes, rename_tag
|
||||||
from aqt.qt import *
|
from aqt.qt import *
|
||||||
from aqt.theme import ColoredIcon, theme_manager
|
from aqt.theme import ColoredIcon, theme_manager
|
||||||
from aqt.utils import (
|
from aqt.utils import (
|
||||||
|
@ -29,7 +29,6 @@ from aqt.utils import (
|
||||||
getOnlyText,
|
getOnlyText,
|
||||||
show_invalid_search_error,
|
show_invalid_search_error,
|
||||||
showWarning,
|
showWarning,
|
||||||
tooltip,
|
|
||||||
tr,
|
tr,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -1200,21 +1199,13 @@ class SidebarTreeView(QTreeView):
|
||||||
# Tags
|
# Tags
|
||||||
###########################
|
###########################
|
||||||
|
|
||||||
def remove_tags(self, _item: SidebarItem) -> None:
|
def remove_tags(self, item: SidebarItem) -> None:
|
||||||
tags = self._selected_tags()
|
tags = self.mw.col.tags.join(self._selected_tags())
|
||||||
|
item.name = "..."
|
||||||
|
|
||||||
def do_remove() -> int:
|
remove_tags_for_all_notes(
|
||||||
return self.col._backend.expunge_tags(" ".join(tags))
|
mw=self.mw, parent=self.browser, space_separated_tags=tags
|
||||||
|
)
|
||||||
def on_done(fut: Future) -> None:
|
|
||||||
self.mw.requireReset(reason=ResetReason.BrowserRemoveTags, context=self)
|
|
||||||
self.browser.model.endReset()
|
|
||||||
tooltip(tr(TR.BROWSING_NOTES_UPDATED, count=fut.result()), parent=self)
|
|
||||||
self.refresh()
|
|
||||||
|
|
||||||
self.mw.checkpoint(tr(TR.ACTIONS_REMOVE_TAG))
|
|
||||||
self.browser.model.beginReset()
|
|
||||||
self.mw.taskman.with_progress(do_remove, on_done)
|
|
||||||
|
|
||||||
def rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
def rename_tag(self, item: SidebarItem, new_name: str) -> None:
|
||||||
if not new_name or new_name == item.name:
|
if not new_name or new_name == item.name:
|
||||||
|
|
|
@ -219,9 +219,8 @@ service DeckConfigService {
|
||||||
service TagsService {
|
service TagsService {
|
||||||
rpc ClearUnusedTags(Empty) returns (OpChangesWithCount);
|
rpc ClearUnusedTags(Empty) returns (OpChangesWithCount);
|
||||||
rpc AllTags(Empty) returns (StringList);
|
rpc AllTags(Empty) returns (StringList);
|
||||||
rpc ExpungeTags(String) returns (UInt32);
|
rpc RemoveTags(String) returns (OpChangesWithCount);
|
||||||
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
rpc SetTagExpanded(SetTagExpandedIn) returns (Empty);
|
||||||
rpc ClearTag(String) returns (Empty);
|
|
||||||
rpc TagTree(Empty) returns (TagTreeNode);
|
rpc TagTree(Empty) returns (TagTreeNode);
|
||||||
rpc DragDropTags(DragDropTagsIn) returns (Empty);
|
rpc DragDropTags(DragDropTagsIn) returns (Empty);
|
||||||
rpc RenameTags(RenameTagsIn) returns (OpChangesWithCount);
|
rpc RenameTags(RenameTagsIn) returns (OpChangesWithCount);
|
||||||
|
|
|
@ -23,8 +23,8 @@ impl TagsService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn expunge_tags(&self, tags: pb::String) -> Result<pb::UInt32> {
|
fn remove_tags(&self, tags: pb::String) -> Result<pb::OpChangesWithCount> {
|
||||||
self.with_col(|col| col.expunge_tags(tags.val.as_str()).map(Into::into))
|
self.with_col(|col| col.remove_tags(tags.val.as_str()).map(Into::into))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result<pb::Empty> {
|
fn set_tag_expanded(&self, input: pb::SetTagExpandedIn) -> Result<pb::Empty> {
|
||||||
|
@ -36,15 +36,6 @@ impl TagsService for Backend {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_tag(&self, tag: pb::String) -> Result<pb::Empty> {
|
|
||||||
self.with_col(|col| {
|
|
||||||
col.transact_no_undo(|col| {
|
|
||||||
col.storage.clear_tag_and_children(tag.val.as_str())?;
|
|
||||||
Ok(().into())
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tag_tree(&self, _input: pb::Empty) -> Result<pb::TagTreeNode> {
|
fn tag_tree(&self, _input: pb::Empty) -> Result<pb::TagTreeNode> {
|
||||||
self.with_col(|col| col.tag_tree())
|
self.with_col(|col| col.tag_tree())
|
||||||
}
|
}
|
||||||
|
|
|
@ -210,12 +210,6 @@ 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 {
|
||||||
|
|
|
@ -15,6 +15,7 @@ pub enum Op {
|
||||||
RemoveNote,
|
RemoveNote,
|
||||||
RenameDeck,
|
RenameDeck,
|
||||||
RenameTag,
|
RenameTag,
|
||||||
|
RemoveTag,
|
||||||
ScheduleAsNew,
|
ScheduleAsNew,
|
||||||
SetDeck,
|
SetDeck,
|
||||||
SetDueDate,
|
SetDueDate,
|
||||||
|
@ -54,6 +55,7 @@ impl Op {
|
||||||
Op::ClearUnusedTags => TR::BrowsingClearUnusedTags,
|
Op::ClearUnusedTags => TR::BrowsingClearUnusedTags,
|
||||||
Op::SortCards => TR::BrowsingReschedule,
|
Op::SortCards => TR::BrowsingReschedule,
|
||||||
Op::RenameTag => TR::ActionsRenameTag,
|
Op::RenameTag => TR::ActionsRenameTag,
|
||||||
|
Op::RemoveTag => TR::ActionsRemoveTag,
|
||||||
};
|
};
|
||||||
|
|
||||||
i18n.tr(key).to_string()
|
i18n.tr(key).to_string()
|
||||||
|
|
4
rslib/src/storage/tag/get.sql
Normal file
4
rslib/src/storage/tag/get.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
SELECT tag,
|
||||||
|
usn,
|
||||||
|
collapsed
|
||||||
|
FROM tags
|
|
@ -19,7 +19,7 @@ impl SqliteStorage {
|
||||||
/// All tags in the collection, in alphabetical order.
|
/// All tags in the collection, in alphabetical order.
|
||||||
pub(crate) fn all_tags(&self) -> Result<Vec<Tag>> {
|
pub(crate) fn all_tags(&self) -> Result<Vec<Tag>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select tag, usn, collapsed from tags")?
|
.prepare_cached(include_str!("get.sql"))?
|
||||||
.query_and_then(NO_PARAMS, row_to_tag)?
|
.query_and_then(NO_PARAMS, row_to_tag)?
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
@ -43,7 +43,7 @@ impl SqliteStorage {
|
||||||
|
|
||||||
pub(crate) fn get_tag(&self, name: &str) -> Result<Option<Tag>> {
|
pub(crate) fn get_tag(&self, name: &str) -> Result<Option<Tag>> {
|
||||||
self.db
|
self.db
|
||||||
.prepare_cached("select tag, usn, collapsed from tags where tag = ?")?
|
.prepare_cached(&format!("{} where tag = ?", include_str!("get.sql")))?
|
||||||
.query_and_then(&[name], row_to_tag)?
|
.query_and_then(&[name], row_to_tag)?
|
||||||
.next()
|
.next()
|
||||||
.transpose()
|
.transpose()
|
||||||
|
@ -65,11 +65,24 @@ impl SqliteStorage {
|
||||||
.map_err(Into::into)
|
.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_tag_and_children(&self, name: &str) -> Result<Vec<Tag>> {
|
pub(crate) fn get_tags_by_predicate<F>(&self, want: F) -> Result<Vec<Tag>>
|
||||||
self.db
|
where
|
||||||
.prepare_cached("select tag, usn, collapsed from tags where tag regexp ?")?
|
F: Fn(&str) -> bool,
|
||||||
.query_and_then(&[format!("(?i)^{}($|::)", regex::escape(name))], row_to_tag)?
|
{
|
||||||
.collect()
|
let mut query_stmt = self.db.prepare_cached(include_str!("get.sql"))?;
|
||||||
|
let mut rows = query_stmt.query(NO_PARAMS)?;
|
||||||
|
let mut output = vec![];
|
||||||
|
while let Some(row) = rows.next()? {
|
||||||
|
let tag = row.get_raw(0).as_str()?;
|
||||||
|
if want(tag) {
|
||||||
|
output.push(Tag {
|
||||||
|
name: tag.to_owned(),
|
||||||
|
usn: row.get(1)?,
|
||||||
|
expanded: !row.get(2)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn remove_single_tag(&self, tag: &str) -> Result<()> {
|
pub(crate) fn remove_single_tag(&self, tag: &str) -> Result<()> {
|
||||||
|
@ -88,15 +101,6 @@ 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 = ?")?
|
||||||
|
|
|
@ -313,37 +313,6 @@ impl Collection {
|
||||||
Ok(count)
|
Ok(count)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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_no_undo(|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;
|
||||||
|
@ -550,7 +519,7 @@ impl Collection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// remove old prefix from the tag list
|
// remove old prefix from the tag list
|
||||||
for tag in self.storage.get_tag_and_children(old_prefix)? {
|
for tag in self.storage.get_tags_by_predicate(|tag| re.is_match(tag))? {
|
||||||
self.remove_single_tag_undoable(tag)?;
|
self.remove_single_tag_undoable(tag)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -569,6 +538,37 @@ impl Collection {
|
||||||
|
|
||||||
Ok(match_count)
|
Ok(match_count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Take tags as a whitespace-separated string and remove them from all notes and the tag list.
|
||||||
|
pub fn remove_tags(&mut self, tags: &str) -> Result<OpOutput<usize>> {
|
||||||
|
self.transact(Op::RemoveTag, |col| col.remove_tags_inner(tags))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_tags_inner(&mut self, tags: &str) -> Result<usize> {
|
||||||
|
let usn = self.usn()?;
|
||||||
|
|
||||||
|
// gather tags that need removing
|
||||||
|
let mut re = PrefixReplacer::new(tags)?;
|
||||||
|
let matched_notes = self
|
||||||
|
.storage
|
||||||
|
.get_note_tags_by_predicate(|tags| re.is_match(tags))?;
|
||||||
|
let match_count = matched_notes.len();
|
||||||
|
|
||||||
|
// remove from the tag list
|
||||||
|
for tag in self.storage.get_tags_by_predicate(|tag| re.is_match(tag))? {
|
||||||
|
self.remove_single_tag_undoable(tag)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// replace tags
|
||||||
|
for mut note in matched_notes {
|
||||||
|
let original = note.clone();
|
||||||
|
note.tags = re.remove(¬e.tags);
|
||||||
|
note.set_modified(usn);
|
||||||
|
self.update_note_tags_undoable(¬e, original)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(match_count)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// fixme: merge with prefixmatcher
|
// fixme: merge with prefixmatcher
|
||||||
|
|
|
@ -77,6 +77,16 @@ impl PrefixReplacer {
|
||||||
join_tags(tags.as_slice())
|
join_tags(tags.as_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove any matching tags. Does not update seen_tags.
|
||||||
|
pub fn remove(&mut self, space_separated_tags: &str) -> String {
|
||||||
|
let tags: Vec<_> = split_tags(space_separated_tags)
|
||||||
|
.filter(|&tag| !self.is_match(tag))
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
join_tags(tags.as_slice())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn into_seen_tags(self) -> HashSet<String> {
|
pub fn into_seen_tags(self) -> HashSet<String> {
|
||||||
self.seen_tags
|
self.seen_tags
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue