From b33267f7543c376260eaa2a2396c8ae32bd20508 Mon Sep 17 00:00:00 2001 From: abdo Date: Sat, 9 Jan 2021 04:49:10 +0300 Subject: [PATCH] Do not check for missing tag parents at registration time --- pylib/anki/tags.py | 5 ++ qt/aqt/browser.py | 25 ++++++++-- qt/aqt/sidebar.py | 4 +- rslib/backend.proto | 2 +- rslib/src/backend/mod.rs | 5 +- rslib/src/storage/tag/mod.rs | 19 ++------ rslib/src/tags.rs | 89 ++++++++++++++++++++++-------------- 7 files changed, 88 insertions(+), 61 deletions(-) diff --git a/pylib/anki/tags.py b/pylib/anki/tags.py index ffd288e15..78adba0eb 100644 --- a/pylib/anki/tags.py +++ b/pylib/anki/tags.py @@ -80,6 +80,11 @@ class TagManager: res = self.col.db.list(query) return list(set(self.split(" ".join(res)))) + def toggle_browser_collapse(self, name: str): + tag = self.col.backend.get_tag(name) + tag.config.browser_collapsed = not tag.config.browser_collapsed + self.col.backend.update_tag(tag) + # Bulk addition/removal from notes ############################################################# diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 63ffdf002..3d7161a4c 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -21,7 +21,13 @@ from anki.consts import * from anki.lang import without_unicode_isolation from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import ConcatSeparator, DeckTreeNode, InvalidInput, TagTreeNode +from anki.rsbackend import ( + ConcatSeparator, + DeckTreeNode, + InvalidInput, + NotFoundError, + TagTreeNode, +) from anki.stats import CardStats from anki.utils import htmlToTextLine, ids2str, isMac, isWin from aqt import AnkiQt, gui_hooks @@ -451,8 +457,12 @@ class SidebarItem: expanded: bool = False, item_type: SidebarItemType = SidebarItemType.CUSTOM, id: int = 0, + full_name: str = None, ) -> None: self.name = name + if not full_name: + full_name = name + self.full_name = full_name self.icon = icon self.item_type = item_type self.id = id @@ -1142,12 +1152,16 @@ QTableView {{ gridline-color: {grid} }} return lambda: self.setFilter("tag", full_name) def toggle_expand(): - tid = node.tag_id # pylint: disable=cell-var-from-loop + + full_name = head + node.name # pylint: disable=cell-var-from-loop def toggle(_): - tag = self.mw.col.backend.get_tag(tid) - tag.config.browser_collapsed = not tag.config.browser_collapsed - self.mw.col.backend.update_tag(tag) + try: + self.mw.col.tags.toggle_browser_collapse(full_name) + except NotFoundError: + # tag is missing, register it first + self.mw.col.tags.register([full_name]) + self.mw.col.tags.toggle_browser_collapse(full_name) return toggle @@ -1159,6 +1173,7 @@ QTableView {{ gridline-color: {grid} }} not node.collapsed, item_type=SidebarItemType.TAG, id=node.tag_id, + full_name=head + node.name, ) root.addChild(item) newhead = head + node.name + "::" diff --git a/qt/aqt/sidebar.py b/qt/aqt/sidebar.py index f616bf7f1..d53b63563 100644 --- a/qt/aqt/sidebar.py +++ b/qt/aqt/sidebar.py @@ -118,7 +118,7 @@ class NewSidebarTreeView(SidebarTreeViewBase): self.browser.editor.saveNow(lambda: self._remove_tag(item)) def _remove_tag(self, item: "aqt.browser.SidebarItem") -> None: - old_name = self.mw.col.backend.get_tag(item.id).name + old_name = item.full_name def do_remove(): self.mw.col.backend.clear_tag(old_name) @@ -138,7 +138,7 @@ class NewSidebarTreeView(SidebarTreeViewBase): self.browser.editor.saveNow(lambda: self._rename_tag(item)) def _rename_tag(self, item: "aqt.browser.SidebarItem") -> None: - old_name = self.mw.col.backend.get_tag(item.id).name + old_name = item.full_name new_name = getOnlyText(tr(TR.ACTIONS_NEW_NAME), default=old_name) if new_name == old_name or not new_name: return diff --git a/rslib/backend.proto b/rslib/backend.proto index 10a3d0a95..80aafe3ad 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -212,7 +212,7 @@ service BackendService { rpc RegisterTags (RegisterTagsIn) returns (Bool); rpc AllTags (Empty) returns (AllTagsOut); - rpc GetTag (TagID) returns (Tag); + rpc GetTag (String) returns (Tag); rpc UpdateTag (Tag) returns (Bool); rpc ClearTag (String) returns (Bool); rpc TagTree (Empty) returns (TagTreeNode); diff --git a/rslib/src/backend/mod.rs b/rslib/src/backend/mod.rs index ab78d8f0d..caa6e5318 100644 --- a/rslib/src/backend/mod.rs +++ b/rslib/src/backend/mod.rs @@ -44,7 +44,6 @@ use crate::{ get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress, SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage, }, - tags::TagID, template::RenderedNode, text::{extract_av_tags, strip_av_tags, AVTag}, timestamp::TimestampSecs, @@ -1300,9 +1299,9 @@ impl BackendService for Backend { Ok(pb::AllTagsOut { tags }) } - fn get_tag(&self, input: pb::TagId) -> BackendResult { + fn get_tag(&self, name: pb::String) -> BackendResult { self.with_col(|col| { - if let Some(tag) = col.storage.get_tag(TagID(input.tid))? { + if let Some(tag) = col.storage.get_tag(name.val.as_str())? { Ok(tag.into()) } else { Err(AnkiError::NotFound) diff --git a/rslib/src/storage/tag/mod.rs b/rslib/src/storage/tag/mod.rs index 0aa958cce..51e497835 100644 --- a/rslib/src/storage/tag/mod.rs +++ b/rslib/src/storage/tag/mod.rs @@ -30,22 +30,11 @@ impl SqliteStorage { .collect() } - /// Get all tags in human form, sorted by name - pub(crate) fn all_tags_sorted(&self) -> Result> { + /// Get tag by human name + pub(crate) fn get_tag(&self, name: &str) -> Result> { self.db - .prepare_cached("select id, name, usn, config from tags order by name")? - .query_and_then(NO_PARAMS, |row| { - let mut tag = row_to_tag(row)?; - tag.name = native_tag_name_to_human(&tag.name); - Ok(tag) - })? - .collect() - } - - pub(crate) fn get_tag(&self, id: TagID) -> Result> { - self.db - .prepare_cached("select id, name, usn, config from tags where id = ?")? - .query_and_then(&[id], |row| { + .prepare_cached("select id, name, usn, config from tags where name = ?")? + .query_and_then(&[human_tag_name_to_native(name)], |row| { let mut tag = row_to_tag(row)?; tag.name = native_tag_name_to_human(&tag.name); Ok(tag) diff --git a/rslib/src/tags.rs b/rslib/src/tags.rs index 5c18d6879..8468bc151 100644 --- a/rslib/src/tags.rs +++ b/rslib/src/tags.rs @@ -13,12 +13,17 @@ use crate::{ }; use regex::{NoExpand, Regex, Replacer}; -use std::{borrow::Cow, collections::HashSet, iter::Peekable}; +use std::cmp::Ordering; +use std::{ + borrow::Cow, + collections::{HashMap, HashSet}, + iter::Peekable, +}; use unicase::UniCase; define_newtype!(TagID, i64); -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone)] pub struct Tag { pub id: TagID, pub name: String, @@ -26,6 +31,26 @@ pub struct Tag { pub config: TagConfig, } +impl Ord for Tag { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for Tag { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Tag { + fn eq(&self, other: &Self) -> bool { + self.name == other.name + } +} + +impl Eq for Tag {} + impl Default for Tag { fn default() -> Self { Tag { @@ -107,11 +132,33 @@ pub(crate) fn native_tag_name_to_human(name: &str) -> String { name.replace('\x1f', "::") } -fn immediate_parent_name(native_name: &str) -> Option<&str> { - native_name.rsplitn(2, '\x1f').nth(1) +fn fill_missing_tags(tags: Vec) -> Vec { + let mut filled_tags: HashMap = HashMap::new(); + for tag in tags.into_iter() { + let name = tag.name.to_owned(); + let split: Vec<&str> = (&tag.name).split("::").collect(); + for i in 0..split.len() - 1 { + let comp = split[0..i + 1].join("::"); + let t = Tag { + name: comp.to_owned(), + ..Default::default() + }; + if filled_tags.get(&comp).is_none() { + filled_tags.insert(comp, t); + } + } + if filled_tags.get(&name).is_none() { + filled_tags.insert(name, tag); + } + } + let mut tags: Vec = filled_tags.values().map(|t| (*t).clone()).collect(); + tags.sort_unstable(); + + tags } fn tags_to_tree(tags: Vec) -> TagTreeNode { + let tags = fill_missing_tags(tags); let mut top = TagTreeNode::default(); let mut it = tags.into_iter().peekable(); add_child_nodes(&mut it, &mut top); @@ -166,7 +213,7 @@ impl Collection { } pub fn tag_tree(&mut self) -> Result { - let tags = self.storage.all_tags_sorted()?; + let tags = self.all_tags()?; let tree = tags_to_tree(tags); Ok(tree) @@ -208,37 +255,17 @@ impl Collection { Ok((tags, added)) } - fn create_missing_tag_parents(&self, mut native_name: &str, usn: Usn) -> Result { - let mut added = false; - while let Some(parent_name) = immediate_parent_name(native_name) { - if self.storage.preferred_tag_case(&parent_name)?.is_none() { - let mut t = Tag { - name: parent_name.to_string(), - usn, - ..Default::default() - }; - self.storage.register_tag(&mut t)?; - added = true; - } - native_name = parent_name; - } - Ok(added) - } - pub(crate) fn register_tag<'a>(&self, tag: Tag) -> Result<(Cow<'a, str>, bool)> { let native_name = human_tag_name_to_native(&tag.name); if native_name.is_empty() { return Ok(("".into(), false)); } - let added_parents = self.create_missing_tag_parents(&native_name, tag.usn)?; if let Some(preferred) = self.storage.preferred_tag_case(&native_name)? { - Ok((native_tag_name_to_human(&preferred).into(), added_parents)) + Ok((native_tag_name_to_human(&preferred).into(), false)) } else { let mut t = Tag { name: native_name.clone(), - usn: tag.usn, - config: tag.config, - ..Default::default() + ..tag }; self.storage.register_tag(&mut t)?; Ok((native_tag_name_to_human(&native_name).into(), true)) @@ -457,14 +484,6 @@ mod test { let note = col.storage.get_note(note.id)?.unwrap(); assert_eq!(¬e.tags, &["foo::bar", "foo::bar::bar", "foo::bar::foo",]); - // missing tag parents are registered too when registering their children - col.storage.clear_tags()?; - let mut note = col.storage.get_note(note.id)?.unwrap(); - note.tags = vec!["animal::mammal::cat".into()]; - col.update_note(&mut note)?; - let tags: Vec = col.all_tags()?.into_iter().map(|t| t.name).collect(); - assert_eq!(&tags, &["animal::mammal", "animal", "animal::mammal::cat"]); - Ok(()) } }