Keep tags in human form and update the tags table structure

See https://github.com/ankitects/anki/pull/900#issuecomment-758284016

- Leave tag names alone and add the collapsed and config columns to the tags table.
- Update The DB check code to preserve the collapse state of used tags.
- Add a simple test for clearing tags and their children
This commit is contained in:
abdo 2021-01-12 23:12:35 +03:00
parent 0b5bb711a1
commit 9a68d84483
11 changed files with 106 additions and 230 deletions

View file

@ -1158,7 +1158,6 @@ QTableView {{ gridline-color: {grid} }}
toggle_expand(), toggle_expand(),
not node.collapsed, not node.collapsed,
item_type=SidebarItemType.TAG, item_type=SidebarItemType.TAG,
id=node.tag_id,
full_name=head + node.name, full_name=head + node.name,
) )
root.addChild(item) root.addChild(item)

View file

@ -797,15 +797,10 @@ message SetTagCollapsedIn {
bool collapsed = 2; bool collapsed = 2;
} }
message TagConfig {
bool browser_collapsed = 1;
}
message Tag { message Tag {
int64 id = 1; string name = 1;
string name = 2; sint32 usn = 2;
sint32 usn = 3; bool collapsed = 3;
TagConfig config = 4;
} }
message GetChangedTagsOut { message GetChangedTagsOut {
@ -813,10 +808,9 @@ message GetChangedTagsOut {
} }
message TagTreeNode { message TagTreeNode {
int64 tag_id = 1; string name = 1;
string name = 2; repeated TagTreeNode children = 2;
repeated TagTreeNode children = 3; uint32 level = 3;
uint32 level = 5;
bool collapsed = 4; bool collapsed = 4;
} }

View file

@ -44,7 +44,6 @@ use crate::{
get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress, get_remote_sync_meta, sync_abort, sync_login, FullSyncProgress, NormalSyncProgress,
SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage, SyncActionRequired, SyncAuth, SyncMeta, SyncOutput, SyncStage,
}, },
tags::Tag,
template::RenderedNode, template::RenderedNode,
text::{extract_av_tags, strip_av_tags, AVTag}, text::{extract_av_tags, strip_av_tags, AVTag},
timestamp::TimestampSecs, timestamp::TimestampSecs,
@ -1295,27 +1294,23 @@ impl BackendService for Backend {
//------------------------------------------------------------------- //-------------------------------------------------------------------
fn all_tags(&self, _input: Empty) -> BackendResult<pb::AllTagsOut> { fn all_tags(&self, _input: Empty) -> BackendResult<pb::AllTagsOut> {
let tags: Vec<pb::Tag> = let tags: Vec<pb::Tag> = self.with_col(|col| {
self.with_col(|col| Ok(col.all_tags()?.into_iter().map(|t| t.into()).collect()))?; Ok(col
.storage
.all_tags()?
.into_iter()
.map(|t| t.into())
.collect())
})?;
Ok(pb::AllTagsOut { tags }) Ok(pb::AllTagsOut { tags })
} }
fn set_tag_collapsed(&self, input: pb::SetTagCollapsedIn) -> BackendResult<pb::Bool> { fn set_tag_collapsed(&self, input: pb::SetTagCollapsedIn) -> BackendResult<pb::Bool> {
self.with_col(|col| { self.with_col(|col| {
let name = &input.name; col.transact(None, |col| {
let mut tag = if let Some(tag) = col.storage.get_tag(name)? { col.set_tag_collapsed(&input.name, input.collapsed)?;
tag Ok(pb::Bool { val: true })
} else { })
// tag is missing, register it
let t = Tag {
name: name.to_owned(),
..Default::default()
};
col.register_tag(t)?.0
};
tag.config.browser_collapsed = input.collapsed;
col.update_tag(&tag)?;
Ok(pb::Bool { val: true })
}) })
} }
@ -1336,7 +1331,7 @@ impl BackendService for Backend {
fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Bool> { fn clear_tag(&self, tag: pb::String) -> BackendResult<pb::Bool> {
self.with_col(|col| { self.with_col(|col| {
col.transact(None, |col| { col.transact(None, |col| {
col.clear_tag(tag.val.as_str())?; col.storage.clear_tag(tag.val.as_str())?;
Ok(pb::Bool { val: true }) Ok(pb::Bool { val: true })
}) })
}) })

View file

@ -243,6 +243,7 @@ impl Collection {
let stamp = TimestampMillis::now(); let stamp = TimestampMillis::now();
// will rebuild tag list below // will rebuild tag list below
let old_tags = self.storage.all_tags_sorted()?;
self.storage.clear_tags()?; self.storage.clear_tags()?;
let total_notes = self.storage.total_notes()?; let total_notes = self.storage.total_notes()?;
@ -294,6 +295,18 @@ impl Collection {
} }
} }
let new_tags = self.storage.all_tags_sorted()?;
for old in old_tags.into_iter() {
for new in new_tags.iter() {
if new.name == old.name {
self.storage.set_tag_collapsed(&new.name, new.collapsed)?;
break;
} else if new.name.starts_with(&old.name) {
self.set_tag_collapsed(&old.name, old.collapsed)?;
}
}
}
// if the collection is empty and the user has deleted all note types, ensure at least // if the collection is empty and the user has deleted all note types, ensure at least
// one note type exists // one note type exists
if self.storage.get_all_notetype_names()?.is_empty() { if self.storage.get_all_notetype_names()?.is_empty() {

View file

@ -10,7 +10,6 @@ use crate::{
notes::field_checksum, notes::field_checksum,
notetype::NoteTypeID, notetype::NoteTypeID,
storage::ids_to_string, storage::ids_to_string,
tags::human_tag_name_to_native,
text::{ text::{
is_glob, matches_glob, normalize_to_nfc, strip_html_preserving_media_filenames, is_glob, matches_glob, normalize_to_nfc, strip_html_preserving_media_filenames,
to_custom_re, to_re, to_sql, to_text, without_combining, to_custom_re, to_re, to_sql, to_text, without_combining,
@ -204,8 +203,7 @@ impl SqlWriter<'_> {
text => { text => {
write!(self.sql, "n.tags regexp ?").unwrap(); write!(self.sql, "n.tags regexp ?").unwrap();
let re = &to_custom_re(text, r"\S"); let re = &to_custom_re(text, r"\S");
let native_name = human_tag_name_to_native(re); self.args.push(format!("(?i).* {}(::| ).*", re));
self.args.push(format!("(?i).* {}(\x1f| ).*", native_name));
} }
} }
} }
@ -681,14 +679,14 @@ mod test {
s(ctx, r"tag:one"), s(ctx, r"tag:one"),
( (
"(n.tags regexp ?)".into(), "(n.tags regexp ?)".into(),
vec!["(?i).* one(\x1f| ).*".into()] vec!["(?i).* one(::| ).*".into()]
) )
); );
assert_eq!( assert_eq!(
s(ctx, r"tag:foo::bar"), s(ctx, r"tag:foo::bar"),
( (
"(n.tags regexp ?)".into(), "(n.tags regexp ?)".into(),
vec!["(?i).* foo\x1fbar(\x1f| ).*".into()] vec!["(?i).* foo::bar(::| ).*".into()]
) )
); );
@ -696,7 +694,7 @@ mod test {
s(ctx, r"tag:o*n\*et%w%oth_re\_e"), s(ctx, r"tag:o*n\*et%w%oth_re\_e"),
( (
"(n.tags regexp ?)".into(), "(n.tags regexp ?)".into(),
vec!["(?i).* o\\S*n\\*et%w%oth\\Sre_e(\u{1f}| ).*".into()] vec![r"(?i).* o\S*n\*et%w%oth\Sre_e(::| ).*".into()]
) )
); );
assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![])); assert_eq!(s(ctx, "tag:none"), ("(n.tags = '')".into(), vec![]));

View file

@ -5,7 +5,7 @@ use crate::{
err::Result, err::Result,
notes::{Note, NoteID}, notes::{Note, NoteID},
notetype::NoteTypeID, notetype::NoteTypeID,
tags::{human_tag_name_to_native, join_tags, native_tag_name_to_human, split_tags}, tags::{join_tags, split_tags},
timestamp::TimestampMillis, timestamp::TimestampMillis,
}; };
use rusqlite::{params, Row, NO_PARAMS}; use rusqlite::{params, Row, NO_PARAMS};
@ -18,11 +18,6 @@ pub(crate) fn join_fields(fields: &[String]) -> String {
fields.join("\x1f") fields.join("\x1f")
} }
fn native_tags_str(tags: &[String]) -> String {
let s: Vec<_> = tags.iter().map(|t| human_tag_name_to_native(t)).collect();
join_tags(&s)
}
fn row_to_note(row: &Row) -> Result<Note> { fn row_to_note(row: &Row) -> Result<Note> {
Ok(Note { Ok(Note {
id: row.get(0)?, id: row.get(0)?,
@ -31,7 +26,7 @@ fn row_to_note(row: &Row) -> Result<Note> {
mtime: row.get(3)?, mtime: row.get(3)?,
usn: row.get(4)?, usn: row.get(4)?,
tags: split_tags(row.get_raw(5).as_str()?) tags: split_tags(row.get_raw(5).as_str()?)
.map(|t| native_tag_name_to_human(t)) .map(Into::into)
.collect(), .collect(),
fields: split_fields(row.get_raw(6).as_str()?), fields: split_fields(row.get_raw(6).as_str()?),
sort_field: None, sort_field: None,
@ -57,7 +52,7 @@ impl super::SqliteStorage {
note.notetype_id, note.notetype_id,
note.mtime, note.mtime,
note.usn, note.usn,
native_tags_str(&note.tags), join_tags(&note.tags),
join_fields(&note.fields()), join_fields(&note.fields()),
note.sort_field.as_ref().unwrap(), note.sort_field.as_ref().unwrap(),
note.checksum.unwrap(), note.checksum.unwrap(),
@ -75,7 +70,7 @@ impl super::SqliteStorage {
note.notetype_id, note.notetype_id,
note.mtime, note.mtime,
note.usn, note.usn,
native_tags_str(&note.tags), join_tags(&note.tags),
join_fields(&note.fields()), join_fields(&note.fields()),
note.sort_field.as_ref().unwrap(), note.sort_field.as_ref().unwrap(),
note.checksum.unwrap(), note.checksum.unwrap(),
@ -93,7 +88,7 @@ impl super::SqliteStorage {
note.notetype_id, note.notetype_id,
note.mtime, note.mtime,
note.usn, note.usn,
native_tags_str(&note.tags), join_tags(&note.tags),
join_fields(&note.fields()), join_fields(&note.fields()),
note.sort_field.as_ref().unwrap(), note.sort_field.as_ref().unwrap(),
note.checksum.unwrap(), note.checksum.unwrap(),
@ -162,69 +157,18 @@ impl super::SqliteStorage {
.map_err(Into::into) .map_err(Into::into)
} }
// get distinct note tags in human form // get distinct note tags
pub(crate) fn get_note_tags(&self, nids: Vec<NoteID>) -> Result<Vec<String>> { pub(crate) fn get_note_tags(&self, nids: Vec<NoteID>) -> Result<Vec<String>> {
if nids.is_empty() { if nids.is_empty() {
self.db self.db
.prepare_cached("select distinct tags from notes")? .prepare_cached("select distinct tags from notes")?
.query_and_then(NO_PARAMS, |r| { .query_and_then(NO_PARAMS, |r| Ok(r.get_raw(0).as_str()?.to_owned()))?
let t = r.get_raw(0).as_str()?;
Ok(native_tag_name_to_human(t))
})?
.collect() .collect()
} else { } else {
self.db self.db
.prepare_cached("select distinct tags from notes where id in ?")? .prepare_cached("select distinct tags from notes where id in ?")?
.query_and_then(nids, |r| { .query_and_then(nids, |r| Ok(r.get_raw(0).as_str()?.to_owned()))?
let t = r.get_raw(0).as_str()?;
Ok(native_tag_name_to_human(t))
})?
.collect() .collect()
} }
} }
pub(super) fn upgrade_notes_to_schema17(&self) -> Result<()> {
let notes: Result<Vec<(NoteID, String)>> = self
.db
.prepare_cached("select id, tags from notes")?
.query_and_then(NO_PARAMS, |row| -> Result<(NoteID, String)> {
let id = NoteID(row.get_raw(0).as_i64()?);
let tags: Vec<String> = split_tags(row.get_raw(1).as_str()?)
.map(|t| human_tag_name_to_native(t))
.collect();
let tags = join_tags(&tags);
Ok((id, tags))
})?
.collect();
notes?.into_iter().try_for_each(|(id, tags)| -> Result<_> {
self.db
.prepare_cached("update notes set tags = ? where id = ?")?
.execute(params![tags, id])?;
Ok(())
})?;
Ok(())
}
pub(super) fn downgrade_notes_from_schema17(&self) -> Result<()> {
let notes: Result<Vec<(NoteID, String)>> = self
.db
.prepare_cached("select id, tags from notes")?
.query_and_then(NO_PARAMS, |row| -> Result<(NoteID, String)> {
let id = NoteID(row.get_raw(0).as_i64()?);
let tags: Vec<String> = split_tags(row.get_raw(1).as_str()?)
.map(|t| native_tag_name_to_human(t))
.collect();
let tags = join_tags(&tags);
Ok((id, tags))
})?
.collect();
notes?.into_iter().try_for_each(|(id, tags)| -> Result<_> {
self.db
.prepare_cached("update notes set tags = ? where id = ?")?
.execute(params![tags, id])?;
Ok(())
})?;
Ok(())
}
} }

View file

@ -1,3 +1,3 @@
INSERT INSERT
OR REPLACE INTO tags (id, name, usn, config) OR REPLACE INTO tags (tag, usn, collapsed)
VALUES (?, ?, ?, ?) VALUES (?, ?, ?)

View file

@ -2,30 +2,23 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
use super::SqliteStorage; use super::SqliteStorage;
use crate::{ use crate::{err::Result, tags::Tag, types::Usn};
err::Result,
tags::{human_tag_name_to_native, native_tag_name_to_human, Tag, TagConfig, TagID},
timestamp::TimestampMillis,
types::Usn,
};
use prost::Message;
use rusqlite::{params, Row, NO_PARAMS}; use rusqlite::{params, Row, NO_PARAMS};
use std::collections::HashMap; use std::collections::HashMap;
fn row_to_tag(row: &Row) -> Result<Tag> { fn row_to_tag(row: &Row) -> Result<Tag> {
let config = TagConfig::decode(row.get_raw(3).as_blob()?)?;
Ok(Tag { Ok(Tag {
id: row.get(0)?, name: row.get(0)?,
name: row.get(1)?, usn: row.get(1)?,
usn: row.get(2)?, collapsed: row.get(2)?,
config,
}) })
} }
impl SqliteStorage { impl SqliteStorage {
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 id, name, usn, config from tags")? .prepare_cached("select tag, usn, collapsed from tags")?
.query_and_then(NO_PARAMS, row_to_tag)? .query_and_then(NO_PARAMS, row_to_tag)?
.collect() .collect()
} }
@ -33,54 +26,30 @@ impl SqliteStorage {
/// Get all tags in human form, sorted by name /// Get all tags in human form, sorted by name
pub(crate) fn all_tags_sorted(&self) -> Result<Vec<Tag>> { pub(crate) fn all_tags_sorted(&self) -> Result<Vec<Tag>> {
self.db self.db
.prepare_cached("select id, name, usn, config from tags order by name")? .prepare_cached("select tag, usn, collapsed from tags order by tag")?
.query_and_then(NO_PARAMS, |row| { .query_and_then(NO_PARAMS, row_to_tag)?
let mut tag = row_to_tag(row)?;
tag.name = native_tag_name_to_human(&tag.name);
Ok(tag)
})?
.collect() .collect()
} }
/// Get tag by human name /// Get tag by human name
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 id, name, usn, config from tags where name = ?")? .prepare_cached("select tag, usn, collapsed from tags where tag = ?")?
.query_and_then(&[human_tag_name_to_native(name)], |row| { .query_and_then(&[name], row_to_tag)?
let mut tag = row_to_tag(row)?;
tag.name = native_tag_name_to_human(&tag.name);
Ok(tag)
})?
.next() .next()
.transpose() .transpose()
} }
fn alloc_id(&self) -> rusqlite::Result<TagID> { pub(crate) fn register_tag(&self, tag: &Tag) -> Result<()> {
self.db
.prepare_cached(include_str!("alloc_id.sql"))?
.query_row(&[TimestampMillis::now()], |r| r.get(0))
}
pub(crate) fn register_tag(&self, tag: &mut Tag) -> Result<()> {
let mut config = vec![];
tag.config.encode(&mut config)?;
tag.id = self.alloc_id()?;
self.update_tag(tag)?;
Ok(())
}
pub(crate) fn update_tag(&self, tag: &Tag) -> Result<()> {
let mut config = vec![];
tag.config.encode(&mut config)?;
self.db self.db
.prepare_cached(include_str!("add.sql"))? .prepare_cached(include_str!("add.sql"))?
.execute(params![tag.id, tag.name, tag.usn, config])?; .execute(params![tag.name, tag.usn, tag.collapsed])?;
Ok(()) Ok(())
} }
pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result<Option<String>> { pub(crate) fn preferred_tag_case(&self, tag: &str) -> Result<Option<String>> {
self.db self.db
.prepare_cached("select name from tags where name = ?")? .prepare_cached("select tag from tags where tag = ?")?
.query_and_then(params![tag], |row| row.get(0))? .query_and_then(params![tag], |row| row.get(0))?
.next() .next()
.transpose() .transpose()
@ -89,8 +58,16 @@ impl SqliteStorage {
pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> { pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> {
self.db self.db
.prepare_cached("delete from tags where name regexp ?")? .prepare_cached("delete from tags where tag regexp ?")?
.execute(&[format!("^{}($|\x1f)", regex::escape(tag))])?; .execute(&[format!("^{}($|::)", regex::escape(tag))])?;
Ok(())
}
pub(crate) fn set_tag_collapsed(&self, tag: &str, collapsed: bool) -> Result<()> {
self.db
.prepare_cached("update tags set collapsed = ? where tag = ?")?
.execute(params![collapsed, tag])?;
Ok(()) Ok(())
} }
@ -111,7 +88,7 @@ impl SqliteStorage {
pub(crate) fn tags_pending_sync(&self, usn: Usn) -> Result<Vec<String>> { pub(crate) fn tags_pending_sync(&self, usn: Usn) -> Result<Vec<String>> {
self.db self.db
.prepare_cached(&format!( .prepare_cached(&format!(
"select name from tags where {}", "select tag from tags where {}",
usn.pending_object_clause() usn.pending_object_clause()
))? ))?
.query_and_then(&[usn], |r| r.get(0).map_err(Into::into))? .query_and_then(&[usn], |r| r.get(0).map_err(Into::into))?
@ -121,7 +98,7 @@ impl SqliteStorage {
pub(crate) fn update_tag_usns(&self, tags: &[String], new_usn: Usn) -> Result<()> { pub(crate) fn update_tag_usns(&self, tags: &[String], new_usn: Usn) -> Result<()> {
let mut stmt = self let mut stmt = self
.db .db
.prepare_cached("update tags set usn=? where name=?")?; .prepare_cached("update tags set usn=? where tag=?")?;
for tag in tags { for tag in tags {
stmt.execute(params![new_usn, tag])?; stmt.execute(params![new_usn, tag])?;
} }
@ -173,18 +150,7 @@ impl SqliteStorage {
.collect::<Result<Vec<Tag>>>()?; .collect::<Result<Vec<Tag>>>()?;
self.db self.db
.execute_batch(include_str!["../upgrades/schema17_upgrade.sql"])?; .execute_batch(include_str!["../upgrades/schema17_upgrade.sql"])?;
tags.into_iter().try_for_each(|mut tag| -> Result<()> { tags.into_iter()
tag.name = human_tag_name_to_native(&tag.name); .try_for_each(|tag| -> Result<()> { self.register_tag(&tag) })
self.register_tag(&mut tag)
})
}
pub(super) fn downgrade_tags_from_schema17(&self) -> Result<()> {
let tags = self.all_tags()?;
self.clear_tags()?;
tags.into_iter().try_for_each(|mut tag| -> Result<()> {
tag.name = native_tag_name_to_human(&tag.name);
self.register_tag(&mut tag)
})
} }
} }

View file

@ -33,7 +33,6 @@ impl SqliteStorage {
} }
if ver < 17 { if ver < 17 {
self.upgrade_tags_to_schema17()?; self.upgrade_tags_to_schema17()?;
self.upgrade_notes_to_schema17()?;
self.db.execute_batch("update col set ver = 17")?; self.db.execute_batch("update col set ver = 17")?;
} }
@ -47,9 +46,7 @@ impl SqliteStorage {
self.downgrade_decks_from_schema15()?; self.downgrade_decks_from_schema15()?;
self.downgrade_notetypes_from_schema15()?; self.downgrade_notetypes_from_schema15()?;
self.downgrade_config_from_schema14()?; self.downgrade_config_from_schema14()?;
self.downgrade_tags_from_schema17()?;
self.downgrade_tags_from_schema14()?; self.downgrade_tags_from_schema14()?;
self.downgrade_notes_from_schema17()?;
self.db self.db
.execute_batch(include_str!("schema11_downgrade.sql"))?; .execute_batch(include_str!("schema11_downgrade.sql"))?;

View file

@ -1,7 +1,7 @@
DROP TABLE tags; DROP TABLE tags;
CREATE TABLE tags ( CREATE TABLE tags (
id integer PRIMARY KEY NOT NULL, tag text NOT NULL PRIMARY KEY COLLATE unicase,
name text NOT NULL COLLATE unicase,
usn integer NOT NULL, usn integer NOT NULL,
config blob NOT NULL collapsed boolean NOT NULL,
); config blob NULL
) without rowid;

View file

@ -1,11 +1,9 @@
// Copyright: Ankitects Pty Ltd and contributors // Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
pub use crate::backend_proto::TagConfig;
use crate::{ use crate::{
backend_proto::{Tag as TagProto, TagTreeNode}, backend_proto::{Tag as TagProto, TagTreeNode},
collection::Collection, collection::Collection,
define_newtype,
err::{AnkiError, Result}, err::{AnkiError, Result},
notes::{NoteID, TransformNoteOutput}, notes::{NoteID, TransformNoteOutput},
text::{normalize_to_nfc, to_re}, text::{normalize_to_nfc, to_re},
@ -21,14 +19,11 @@ use std::{
}; };
use unicase::UniCase; use unicase::UniCase;
define_newtype!(TagID, i64);
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Tag { pub struct Tag {
pub id: TagID,
pub name: String, pub name: String,
pub usn: Usn, pub usn: Usn,
pub config: TagConfig, pub collapsed: bool,
} }
impl Ord for Tag { impl Ord for Tag {
@ -54,10 +49,9 @@ impl Eq for Tag {}
impl Default for Tag { impl Default for Tag {
fn default() -> Self { fn default() -> Self {
Tag { Tag {
id: TagID(0),
name: "".to_string(), name: "".to_string(),
usn: Usn(-1), usn: Usn(-1),
config: Default::default(), collapsed: false,
} }
} }
} }
@ -65,10 +59,9 @@ impl Default for Tag {
impl From<Tag> for TagProto { impl From<Tag> for TagProto {
fn from(t: Tag) -> Self { fn from(t: Tag) -> Self {
TagProto { TagProto {
id: t.id.0,
name: t.name, name: t.name,
usn: t.usn.0, usn: t.usn.0,
config: Some(t.config), collapsed: t.collapsed,
} }
} }
} }
@ -76,10 +69,9 @@ impl From<Tag> for TagProto {
impl From<TagProto> for Tag { impl From<TagProto> for Tag {
fn from(t: TagProto) -> Self { fn from(t: TagProto) -> Self {
Tag { Tag {
id: TagID(t.id),
name: t.name, name: t.name,
usn: Usn(t.usn), usn: Usn(t.usn),
config: t.config.unwrap(), collapsed: t.collapsed,
} }
} }
} }
@ -119,17 +111,13 @@ fn normalized_tag_name_component(comp: &str) -> Cow<str> {
} }
} }
pub(crate) fn human_tag_name_to_native(name: &str) -> String { fn normalize_tag_name(name: &str) -> String {
let mut out = String::with_capacity(name.len()); let mut out = String::with_capacity(name.len());
for comp in name.split("::") { for comp in name.split("::") {
out.push_str(&normalized_tag_name_component(comp)); out.push_str(&normalized_tag_name_component(comp));
out.push('\x1f'); out.push_str("::");
} }
out.trim_end_matches('\x1f').into() out.trim_end_matches("::").into()
}
pub(crate) fn native_tag_name_to_human(name: &str) -> String {
name.replace('\x1f', "::")
} }
fn fill_missing_tags(tags: Vec<Tag>) -> Vec<Tag> { fn fill_missing_tags(tags: Vec<Tag>) -> Vec<Tag> {
@ -177,11 +165,10 @@ fn add_child_nodes(tags: &mut Peekable<impl Iterator<Item = Tag>>, parent: &mut
l if l == parent.level + 1 => { l if l == parent.level + 1 => {
// next item is an immediate descendent of parent // next item is an immediate descendent of parent
parent.children.push(TagTreeNode { parent.children.push(TagTreeNode {
tag_id: tag.id.0,
name: (*split_name.last().unwrap()).into(), name: (*split_name.last().unwrap()).into(),
children: vec![], children: vec![],
level: parent.level + 1, level: parent.level + 1,
collapsed: tag.config.browser_collapsed, collapsed: tag.collapsed,
}); });
tags.next(); tags.next();
} }
@ -199,19 +186,6 @@ fn add_child_nodes(tags: &mut Peekable<impl Iterator<Item = Tag>>, parent: &mut
} }
impl Collection { impl Collection {
pub fn all_tags(&self) -> Result<Vec<Tag>> {
self.storage
.all_tags()?
.into_iter()
.map(|t| {
Ok(Tag {
name: native_tag_name_to_human(&t.name),
..t
})
})
.collect()
}
pub fn tag_tree(&mut self) -> Result<TagTreeNode> { pub fn tag_tree(&mut self) -> Result<TagTreeNode> {
let tags = self.storage.all_tags_sorted()?; let tags = self.storage.all_tags_sorted()?;
let tree = tags_to_tree(tags); let tree = tags_to_tree(tags);
@ -253,20 +227,19 @@ impl Collection {
} }
pub(crate) fn register_tag(&self, tag: Tag) -> Result<(Tag, bool)> { pub(crate) fn register_tag(&self, tag: Tag) -> Result<(Tag, bool)> {
let native_name = human_tag_name_to_native(&tag.name); let normalized_name = normalize_tag_name(&tag.name);
let mut t = Tag { let mut t = Tag {
name: native_name.clone(), name: normalized_name.clone(),
..tag ..tag
}; };
if native_name.is_empty() { if normalized_name.is_empty() {
return Ok((t, false)); return Ok((t, false));
} }
if let Some(preferred) = self.storage.preferred_tag_case(&native_name)? { if let Some(preferred) = self.storage.preferred_tag_case(&normalized_name)? {
t.name = native_tag_name_to_human(&preferred); t.name = preferred;
Ok((t, false)) Ok((t, false))
} else { } else {
self.storage.register_tag(&mut t)?; self.storage.register_tag(&t)?;
t.name = native_tag_name_to_human(&t.name);
Ok((t, true)) Ok((t, true))
} }
} }
@ -287,19 +260,16 @@ impl Collection {
Ok(changed) Ok(changed)
} }
pub(crate) fn update_tag(&self, tag: &Tag) -> Result<()> { pub(crate) fn set_tag_collapsed(&self, name: &str, collapsed: bool) -> Result<()> {
let native_name = human_tag_name_to_native(&tag.name); if self.storage.get_tag(name)?.is_none() {
self.storage.update_tag(&Tag { // tag is missing, register it
id: tag.id, let t = Tag {
name: native_name, name: name.to_owned(),
usn: tag.usn, ..Default::default()
config: tag.config.clone(), };
}) self.register_tag(t)?;
} }
self.storage.set_tag_collapsed(name, collapsed)
pub(crate) fn clear_tag(&self, tag: &str) -> Result<()> {
let native_name = human_tag_name_to_native(tag);
self.storage.clear_tag(&native_name)
} }
fn replace_tags_for_notes_inner<R: Replacer>( fn replace_tags_for_notes_inner<R: Replacer>(
@ -425,11 +395,6 @@ mod test {
col.update_note(&mut note)?; col.update_note(&mut note)?;
assert_eq!(&note.tags, &["one", "two"]); assert_eq!(&note.tags, &["one", "two"]);
// note.tags is in human form
note.tags = vec!["foo::bar".into()];
col.update_note(&mut note)?;
assert_eq!(&note.tags, &["foo::bar"]);
Ok(()) Ok(())
} }
@ -483,6 +448,11 @@ mod test {
let note = col.storage.get_note(note.id)?.unwrap(); let note = col.storage.get_note(note.id)?.unwrap();
assert_eq!(&note.tags, &["foo::bar", "foo::bar::bar", "foo::bar::foo",]); assert_eq!(&note.tags, &["foo::bar", "foo::bar::bar", "foo::bar::foo",]);
// tag children are also cleared when clearing their parent
col.register_tags("a a::b a::b::c", Usn(-1), true)?;
col.storage.clear_tag("a")?;
assert_eq!(col.storage.all_tags()?, vec![]);
Ok(()) Ok(())
} }
} }