diff --git a/rslib/src/tags/bulkadd.rs b/rslib/src/tags/bulkadd.rs new file mode 100644 index 000000000..dcb9c5f03 --- /dev/null +++ b/rslib/src/tags/bulkadd.rs @@ -0,0 +1,88 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +//! Adding tags to selected notes in the browse screen. + +use std::collections::HashSet; +use unicase::UniCase; + +use super::{join_tags, split_tags}; +use crate::{notes::NoteTags, prelude::*}; + +impl Collection { + pub fn add_tags_to_notes(&mut self, nids: &[NoteID], tags: &str) -> Result> { + self.transact(Op::UpdateTag, |col| col.add_tags_to_notes_inner(nids, tags)) + } +} + +impl Collection { + fn add_tags_to_notes_inner(&mut self, nids: &[NoteID], tags: &str) -> Result { + let usn = self.usn()?; + + // will update tag list for any new tags, and match case + let tags_to_add = self.canonified_tags_as_vec(tags, usn)?; + + // modify notes + let mut match_count = 0; + let notes = self.storage.get_note_tags_by_id_list(nids)?; + for original in notes { + if let Some(updated_tags) = add_missing_tags(&original.tags, &tags_to_add) { + match_count += 1; + let mut note = NoteTags { + tags: updated_tags, + ..original + }; + note.set_modified(usn); + self.update_note_tags_undoable(¬e, original)?; + } + } + + Ok(match_count) + } +} + +/// Returns the sorted new tag string if any tags were added. +fn add_missing_tags(note_tags: &str, desired: &[UniCase]) -> Option { + let mut note_tags: HashSet<_> = split_tags(note_tags) + .map(ToOwned::to_owned) + .map(UniCase::new) + .collect(); + + let mut modified = false; + for tag in desired { + if !note_tags.contains(tag) { + note_tags.insert(tag.clone()); + modified = true; + } + } + if !modified { + return None; + } + + // sort + let mut tags: Vec<_> = note_tags.into_iter().collect::>(); + tags.sort_unstable(); + + // turn back into a string + let tags: Vec<_> = tags.into_iter().map(|s| s.into_inner()).collect(); + Some(join_tags(&tags)) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn add_missing() { + let desired: Vec<_> = ["xyz", "abc", "DEF"] + .iter() + .map(|s| UniCase::new(s.to_string())) + .collect(); + + let add_to = |text| add_missing_tags(text, &desired).unwrap(); + + assert_eq!(&add_to(""), " abc DEF xyz "); + assert_eq!(&add_to("XYZ deF aaa"), " aaa abc deF XYZ "); + assert_eq!(add_missing_tags("def xyz abc", &desired).is_none(), true); + } +} diff --git a/rslib/src/tags/mod.rs b/rslib/src/tags/mod.rs index e243840f3..289d97525 100644 --- a/rslib/src/tags/mod.rs +++ b/rslib/src/tags/mod.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +mod bulkadd; mod dragdrop; mod prefix_replacer; mod register; diff --git a/rslib/src/tags/register.rs b/rslib/src/tags/register.rs index d7c2b356b..7be6f475b 100644 --- a/rslib/src/tags/register.rs +++ b/rslib/src/tags/register.rs @@ -38,6 +38,23 @@ impl Collection { Ok((tags, added)) } + /// Returns true if any cards were added to the tag list. + pub(crate) fn canonified_tags_as_vec( + &mut self, + tags: &str, + usn: Usn, + ) -> Result>> { + let mut out_tags = vec![]; + + for tag in split_tags(tags) { + let mut tag = Tag::new(tag.to_string(), usn); + self.register_tag(&mut tag)?; + out_tags.push(UniCase::new(tag.name)); + } + + Ok(out_tags) + } + /// Adjust tag casing to match any existing parents, and register it if it's not already /// in the tags list. True if the tag was added and not already in tag list. /// In the case the tag is already registered, tag will be mutated to match the existing diff --git a/rslib/src/tags/selectednotes.rs b/rslib/src/tags/selectednotes.rs index 31bf42cde..cc0008458 100644 --- a/rslib/src/tags/selectednotes.rs +++ b/rslib/src/tags/selectednotes.rs @@ -57,42 +57,6 @@ impl Collection { self.replace_tags_for_notes_inner(nids, &tags, repl) } } - - pub fn add_tags_to_notes(&mut self, nids: &[NoteID], tags: &str) -> Result> { - let tags: Vec<_> = split_tags(tags).collect(); - let matcher = regex::RegexSet::new( - tags.iter() - .map(|s| regex::escape(s)) - .map(|s| format!("(?i)^{}$", s)), - ) - .map_err(|_| AnkiError::invalid_input("invalid regex"))?; - - self.transact(Op::UpdateTag, |col| { - col.transform_notes(nids, |note, _nt| { - let mut need_to_add = true; - let mut match_count = 0; - for tag in ¬e.tags { - if matcher.is_match(tag) { - match_count += 1; - } - if match_count == tags.len() { - need_to_add = false; - break; - } - } - - if need_to_add { - note.tags.extend(tags.iter().map(|&s| s.to_string())) - } - - Ok(TransformNoteOutput { - changed: need_to_add, - generate_cards: false, - mark_modified: true, - }) - }) - }) - } } #[cfg(test)]