use crate::model::Pad;
use serde::Serialize;
use std::collections::HashMap;
use std::str::FromStr;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "type", content = "text")]
pub enum MatchSegment {
Plain(String),
Match(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct SearchMatch {
pub line_number: usize, pub segments: Vec<MatchSegment>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(tag = "type", content = "value")]
pub enum DisplayIndex {
Pinned(usize),
Regular(usize),
Archived(usize),
Deleted(usize),
}
impl std::fmt::Display for DisplayIndex {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DisplayIndex::Pinned(i) => write!(f, "p{}", i),
DisplayIndex::Regular(i) => write!(f, "{}", i),
DisplayIndex::Archived(i) => write!(f, "ar{}", i),
DisplayIndex::Deleted(i) => write!(f, "d{}", i),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PadSelector {
Path(Vec<DisplayIndex>),
Range(Vec<DisplayIndex>, Vec<DisplayIndex>), Uuid(Uuid),
ShortUuid(String),
Title(String),
}
impl std::fmt::Display for PadSelector {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PadSelector::Path(path) => {
let s: Vec<String> = path.iter().map(|idx| idx.to_string()).collect();
write!(f, "{}", s.join("."))
}
PadSelector::Range(start, end) => {
let s_start: Vec<String> = start.iter().map(|idx| idx.to_string()).collect();
let s_end: Vec<String> = end.iter().map(|idx| idx.to_string()).collect();
write!(f, "{}-{}", s_start.join("."), s_end.join("."))
}
PadSelector::Uuid(uuid) => write!(f, "{}", uuid),
PadSelector::ShortUuid(hex) => write!(f, "{}", hex),
PadSelector::Title(t) => write!(f, "\"{}\"", t),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct DisplayPad {
pub pad: Pad,
pub index: DisplayIndex,
pub matches: Option<Vec<SearchMatch>>,
pub children: Vec<DisplayPad>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum IndexBucket {
Active,
Archived,
Deleted,
}
#[derive(Debug, Clone)]
struct TaggedPad {
pad: Pad,
bucket: IndexBucket,
}
pub fn index_pads(active: Vec<Pad>, archived: Vec<Pad>, deleted: Vec<Pad>) -> Vec<DisplayPad> {
let mut all_tagged: Vec<TaggedPad> = Vec::new();
for pad in active {
all_tagged.push(TaggedPad {
pad,
bucket: IndexBucket::Active,
});
}
for pad in archived {
all_tagged.push(TaggedPad {
pad,
bucket: IndexBucket::Archived,
});
}
for pad in deleted {
all_tagged.push(TaggedPad {
pad,
bucket: IndexBucket::Deleted,
});
}
let mut parent_map: HashMap<Option<Uuid>, Vec<TaggedPad>> = HashMap::new();
for tagged in all_tagged {
parent_map
.entry(tagged.pad.metadata.parent_id)
.or_default()
.push(tagged);
}
let root_pads = parent_map.remove(&None).unwrap_or_default();
index_level(root_pads, &parent_map)
}
fn index_level(
mut pads: Vec<TaggedPad>,
parent_map: &HashMap<Option<Uuid>, Vec<TaggedPad>>,
) -> Vec<DisplayPad> {
pads.sort_by(|a, b| b.pad.metadata.created_at.cmp(&a.pad.metadata.created_at));
let mut results = Vec::new();
let mut add_pad = |tagged: TaggedPad, index: DisplayIndex| {
let children = parent_map
.get(&Some(tagged.pad.metadata.id))
.cloned()
.unwrap_or_default();
let indexed_children = index_level(children, parent_map);
results.push(DisplayPad {
pad: tagged.pad,
index,
matches: None,
children: indexed_children,
});
};
let mut pinned_idx = 1;
for tagged in &pads {
if tagged.bucket == IndexBucket::Active && tagged.pad.metadata.is_pinned {
add_pad(tagged.clone(), DisplayIndex::Pinned(pinned_idx));
pinned_idx += 1;
}
}
let mut regular_idx = 1;
for tagged in &pads {
if tagged.bucket == IndexBucket::Active {
add_pad(tagged.clone(), DisplayIndex::Regular(regular_idx));
regular_idx += 1;
}
}
let mut archived_idx = 1;
for tagged in &pads {
if tagged.bucket == IndexBucket::Archived {
add_pad(tagged.clone(), DisplayIndex::Archived(archived_idx));
archived_idx += 1;
}
}
let mut deleted_idx = 1;
for tagged in &pads {
if tagged.bucket == IndexBucket::Deleted {
add_pad(tagged.clone(), DisplayIndex::Deleted(deleted_idx));
deleted_idx += 1;
}
}
results
}
impl std::str::FromStr for DisplayIndex {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some(rest) = s.strip_prefix("ar") {
if let Ok(n) = rest.parse() {
return Ok(DisplayIndex::Archived(n));
}
}
if let Some(rest) = s.strip_prefix('p') {
if let Ok(n) = rest.parse() {
return Ok(DisplayIndex::Pinned(n));
}
}
if let Some(rest) = s.strip_prefix('d') {
if let Ok(n) = rest.parse() {
return Ok(DisplayIndex::Deleted(n));
}
}
if let Ok(n) = s.parse() {
return Ok(DisplayIndex::Regular(n));
}
Err(format!("Invalid index format: {}", s))
}
}
pub fn parse_index_or_range(s: &str) -> Result<PadSelector, String> {
if let Ok(uuid) = Uuid::parse_str(s) {
return Ok(PadSelector::Uuid(uuid));
}
if let Some(dash_pos) = s.find('-') {
if dash_pos > 0 {
let start_str = &s[..dash_pos];
let end_str = &s[dash_pos + 1..];
let start_path = parse_path(start_str)?;
let end_path = parse_path(end_str)?;
return Ok(PadSelector::Range(start_path, end_path));
}
}
match parse_path(s) {
Ok(path) => Ok(PadSelector::Path(path)),
Err(_) if is_hex_string(s) => Ok(PadSelector::ShortUuid(s.to_lowercase())),
Err(e) => Err(e),
}
}
fn is_hex_string(s: &str) -> bool {
!s.is_empty() && s.chars().all(|c| c.is_ascii_hexdigit())
}
fn parse_path(s: &str) -> Result<Vec<DisplayIndex>, String> {
s.split('.').map(DisplayIndex::from_str).collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_pad(title: &str, pinned: bool) -> Pad {
let mut p = Pad::new(title.to_string(), "".to_string());
p.metadata.is_pinned = pinned;
p
}
#[test]
fn test_indexing_buckets() {
let p1 = make_pad("Regular 1", false);
let p2 = make_pad("Pinned 1", true);
let p3 = make_pad("Deleted 1", false);
let p4 = make_pad("Regular 2", false);
let active = vec![p1, p2, p4];
let deleted = vec![p3];
let indexed = index_pads(active, vec![], deleted);
let pinned_entries: Vec<_> = indexed
.iter()
.filter(|dp| matches!(dp.index, DisplayIndex::Pinned(_)))
.collect();
assert_eq!(pinned_entries.len(), 1);
assert_eq!(pinned_entries[0].pad.metadata.title, "Pinned 1");
assert_eq!(pinned_entries[0].index, DisplayIndex::Pinned(1));
let regular_entries: Vec<_> = indexed
.iter()
.filter(|dp| matches!(dp.index, DisplayIndex::Regular(_)))
.collect();
assert_eq!(regular_entries.len(), 3);
assert_eq!(regular_entries[0].pad.metadata.title, "Regular 2"); assert_eq!(regular_entries[0].index, DisplayIndex::Regular(1));
assert_eq!(regular_entries[2].pad.metadata.title, "Regular 1"); assert_eq!(regular_entries[2].index, DisplayIndex::Regular(3));
let deleted_entries: Vec<_> = indexed
.iter()
.filter(|dp| matches!(dp.index, DisplayIndex::Deleted(_)))
.collect();
assert_eq!(deleted_entries.len(), 1);
assert_eq!(deleted_entries[0].pad.metadata.title, "Deleted 1");
}
#[test]
fn test_pinned_pad_has_both_indexes() {
let p1 = make_pad("Note A", false);
let p2 = make_pad("Note B", true); let p3 = make_pad("Note C", false);
let indexed = index_pads(vec![p1, p2, p3], vec![], vec![]);
let note_b_entries: Vec<_> = indexed
.iter()
.filter(|dp| dp.pad.metadata.title == "Note B")
.collect();
assert_eq!(note_b_entries.len(), 2);
assert!(note_b_entries
.iter()
.any(|dp| dp.index == DisplayIndex::Pinned(1)));
assert!(note_b_entries
.iter()
.any(|dp| dp.index == DisplayIndex::Regular(2)));
}
#[test]
fn test_parsing() {
use std::str::FromStr;
assert_eq!(DisplayIndex::from_str("1"), Ok(DisplayIndex::Regular(1)));
assert_eq!(DisplayIndex::from_str("42"), Ok(DisplayIndex::Regular(42)));
assert_eq!(DisplayIndex::from_str("p1"), Ok(DisplayIndex::Pinned(1)));
assert_eq!(DisplayIndex::from_str("p99"), Ok(DisplayIndex::Pinned(99)));
assert_eq!(DisplayIndex::from_str("d1"), Ok(DisplayIndex::Deleted(1)));
assert_eq!(DisplayIndex::from_str("d5"), Ok(DisplayIndex::Deleted(5)));
assert_eq!(DisplayIndex::from_str("ar1"), Ok(DisplayIndex::Archived(1)));
assert_eq!(
DisplayIndex::from_str("ar99"),
Ok(DisplayIndex::Archived(99))
);
assert!(DisplayIndex::from_str("").is_err());
assert!(DisplayIndex::from_str("abc").is_err());
assert!(DisplayIndex::from_str("p").is_err());
assert!(DisplayIndex::from_str("d").is_err());
assert!(DisplayIndex::from_str("ar").is_err());
assert!(DisplayIndex::from_str("12a").is_err());
assert!(DisplayIndex::from_str("p1a").is_err());
}
#[test]
fn test_parse_single_index() {
assert_eq!(
parse_index_or_range("3"),
Ok(PadSelector::Path(vec![DisplayIndex::Regular(3)]))
);
assert_eq!(
parse_index_or_range("p2"),
Ok(PadSelector::Path(vec![DisplayIndex::Pinned(2)]))
);
assert_eq!(
parse_index_or_range("d1"),
Ok(PadSelector::Path(vec![DisplayIndex::Deleted(1)]))
);
assert_eq!(
parse_index_or_range("ar3"),
Ok(PadSelector::Path(vec![DisplayIndex::Archived(3)]))
);
}
#[test]
fn test_parse_regular_range() {
assert_eq!(
parse_index_or_range("3-5"),
Ok(PadSelector::Range(
vec![DisplayIndex::Regular(3)],
vec![DisplayIndex::Regular(5)]
))
);
assert_eq!(
parse_index_or_range("3-3"),
Ok(PadSelector::Range(
vec![DisplayIndex::Regular(3)],
vec![DisplayIndex::Regular(3)]
))
);
}
#[test]
fn test_parse_pinned_range() {
assert_eq!(
parse_index_or_range("p1-p3"),
Ok(PadSelector::Range(
vec![DisplayIndex::Pinned(1)],
vec![DisplayIndex::Pinned(3)]
))
);
}
#[test]
fn test_parse_deleted_range() {
assert_eq!(
parse_index_or_range("d2-d4"),
Ok(PadSelector::Range(
vec![DisplayIndex::Deleted(2)],
vec![DisplayIndex::Deleted(4)]
))
);
}
#[test]
fn test_parse_archived_range() {
assert_eq!(
parse_index_or_range("ar1-ar5"),
Ok(PadSelector::Range(
vec![DisplayIndex::Archived(1)],
vec![DisplayIndex::Archived(5)]
))
);
}
#[test]
fn test_parse_range_invalid_format() {
let result = parse_index_or_range("abc-5");
assert!(result.is_err());
let result = parse_index_or_range("3-xyz");
assert!(result.is_err());
let result = parse_index_or_range("-5");
assert!(result.is_err());
let result = parse_index_or_range("3-");
assert!(result.is_err());
}
#[test]
fn test_parse_nested_path() {
assert_eq!(
parse_index_or_range("1.2"),
Ok(PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(2)
]))
);
assert_eq!(
parse_index_or_range("1.2.3"),
Ok(PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(2),
DisplayIndex::Regular(3)
]))
);
}
#[test]
fn test_parse_nested_pinned_path() {
assert_eq!(
parse_index_or_range("1.p1"),
Ok(PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Pinned(1)
]))
);
assert_eq!(
parse_index_or_range("1.2.p1"),
Ok(PadSelector::Path(vec![
DisplayIndex::Regular(1),
DisplayIndex::Regular(2),
DisplayIndex::Pinned(1)
]))
);
}
#[test]
fn test_parse_nested_range() {
assert_eq!(
parse_index_or_range("1.1-1.3"),
Ok(PadSelector::Range(
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(3)]
))
);
assert_eq!(
parse_index_or_range("1.2-2.1"),
Ok(PadSelector::Range(
vec![DisplayIndex::Regular(1), DisplayIndex::Regular(2)],
vec![DisplayIndex::Regular(2), DisplayIndex::Regular(1)]
))
);
}
#[test]
fn test_tree_with_nested_children() {
let mut grandchild = make_pad("Grandchild", false);
let mut child = make_pad("Child", false);
let root = make_pad("Root", false);
child.metadata.parent_id = Some(root.metadata.id);
grandchild.metadata.parent_id = Some(child.metadata.id);
let indexed = index_pads(vec![root, child, grandchild], vec![], vec![]);
assert_eq!(indexed.len(), 1);
assert_eq!(indexed[0].pad.metadata.title, "Root");
assert_eq!(indexed[0].index, DisplayIndex::Regular(1));
assert_eq!(indexed[0].children.len(), 1);
assert_eq!(indexed[0].children[0].pad.metadata.title, "Child");
assert_eq!(indexed[0].children[0].index, DisplayIndex::Regular(1));
assert_eq!(indexed[0].children[0].children.len(), 1);
assert_eq!(
indexed[0].children[0].children[0].pad.metadata.title,
"Grandchild"
);
assert_eq!(
indexed[0].children[0].children[0].index,
DisplayIndex::Regular(1)
);
}
#[test]
fn test_tree_pinned_child_has_dual_index() {
let mut child = make_pad("Pinned Child", true);
let root = make_pad("Root", false);
child.metadata.parent_id = Some(root.metadata.id);
let indexed = index_pads(vec![root, child], vec![], vec![]);
assert_eq!(indexed[0].children.len(), 2);
let pinned_child = indexed[0]
.children
.iter()
.find(|c| matches!(c.index, DisplayIndex::Pinned(_)));
assert!(pinned_child.is_some());
assert_eq!(pinned_child.unwrap().index, DisplayIndex::Pinned(1));
let regular_child = indexed[0]
.children
.iter()
.find(|c| matches!(c.index, DisplayIndex::Regular(_)));
assert!(regular_child.is_some());
assert_eq!(regular_child.unwrap().index, DisplayIndex::Regular(1));
}
#[test]
fn test_tree_deep_nesting_four_levels() {
let mut l4 = make_pad("Level 4", false);
let mut l3 = make_pad("Level 3", false);
let mut l2 = make_pad("Level 2", false);
let l1 = make_pad("Level 1", false);
l2.metadata.parent_id = Some(l1.metadata.id);
l3.metadata.parent_id = Some(l2.metadata.id);
l4.metadata.parent_id = Some(l3.metadata.id);
let indexed = index_pads(vec![l1, l2, l3, l4], vec![], vec![]);
assert_eq!(indexed[0].pad.metadata.title, "Level 1");
assert_eq!(indexed[0].children[0].pad.metadata.title, "Level 2");
assert_eq!(
indexed[0].children[0].children[0].pad.metadata.title,
"Level 3"
);
assert_eq!(
indexed[0].children[0].children[0].children[0]
.pad
.metadata
.title,
"Level 4"
);
assert_eq!(indexed[0].index, DisplayIndex::Regular(1));
assert_eq!(indexed[0].children[0].index, DisplayIndex::Regular(1));
assert_eq!(
indexed[0].children[0].children[0].index,
DisplayIndex::Regular(1)
);
assert_eq!(
indexed[0].children[0].children[0].children[0].index,
DisplayIndex::Regular(1)
);
}
#[test]
fn test_archived_pads_get_archived_index() {
let p1 = make_pad("Active 1", false);
let p2 = make_pad("Archived 1", false);
let p3 = make_pad("Archived 2", false);
let indexed = index_pads(vec![p1], vec![p2, p3], vec![]);
let archived_entries: Vec<_> = indexed
.iter()
.filter(|dp| matches!(dp.index, DisplayIndex::Archived(_)))
.collect();
assert_eq!(archived_entries.len(), 2);
let regular_entries: Vec<_> = indexed
.iter()
.filter(|dp| matches!(dp.index, DisplayIndex::Regular(_)))
.collect();
assert_eq!(regular_entries.len(), 1);
}
#[test]
fn test_parse_uuid() {
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let uuid = Uuid::parse_str(uuid_str).unwrap();
assert_eq!(parse_index_or_range(uuid_str), Ok(PadSelector::Uuid(uuid)));
}
#[test]
fn test_parse_uuid_does_not_interfere_with_indexes() {
assert_eq!(
parse_index_or_range("1"),
Ok(PadSelector::Path(vec![DisplayIndex::Regular(1)]))
);
assert_eq!(
parse_index_or_range("p1"),
Ok(PadSelector::Path(vec![DisplayIndex::Pinned(1)]))
);
assert_eq!(
parse_index_or_range("d1"),
Ok(PadSelector::Path(vec![DisplayIndex::Deleted(1)]))
);
assert_eq!(
parse_index_or_range("ar1"),
Ok(PadSelector::Path(vec![DisplayIndex::Archived(1)]))
);
assert_eq!(
parse_index_or_range("1-3"),
Ok(PadSelector::Range(
vec![DisplayIndex::Regular(1)],
vec![DisplayIndex::Regular(3)]
))
);
assert_eq!(
parse_index_or_range("p1-p3"),
Ok(PadSelector::Range(
vec![DisplayIndex::Pinned(1)],
vec![DisplayIndex::Pinned(3)]
))
);
}
#[test]
fn test_parse_uuid_display() {
let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
let uuid = Uuid::parse_str(uuid_str).unwrap();
let selector = PadSelector::Uuid(uuid);
assert_eq!(format!("{}", selector), uuid_str);
}
#[test]
fn test_parse_short_uuid_hex_prefix() {
assert_eq!(
parse_index_or_range("766d5dab"),
Ok(PadSelector::ShortUuid("766d5dab".to_string()))
);
assert_eq!(
parse_index_or_range("4e704ff3"),
Ok(PadSelector::ShortUuid("4e704ff3".to_string()))
);
}
#[test]
fn test_parse_short_uuid_various_lengths() {
assert_eq!(
parse_index_or_range("abcd"),
Ok(PadSelector::ShortUuid("abcd".to_string()))
);
assert_eq!(
parse_index_or_range("550e8400e29b"),
Ok(PadSelector::ShortUuid("550e8400e29b".to_string()))
);
}
#[test]
fn test_parse_short_uuid_case_insensitive() {
assert_eq!(
parse_index_or_range("ABCDEF01"),
Ok(PadSelector::ShortUuid("abcdef01".to_string()))
);
assert_eq!(
parse_index_or_range("AbCd"),
Ok(PadSelector::ShortUuid("abcd".to_string()))
);
}
#[test]
fn test_di_takes_priority_over_short_uuid() {
assert_eq!(
parse_index_or_range("d3"),
Ok(PadSelector::Path(vec![DisplayIndex::Deleted(3)]))
);
assert_eq!(
parse_index_or_range("123"),
Ok(PadSelector::Path(vec![DisplayIndex::Regular(123)]))
);
assert_eq!(
parse_index_or_range("p1"),
Ok(PadSelector::Path(vec![DisplayIndex::Pinned(1)]))
);
}
#[test]
fn test_hex_with_non_di_chars_becomes_short_uuid() {
assert_eq!(
parse_index_or_range("d3f"),
Ok(PadSelector::ShortUuid("d3f".to_string()))
);
assert_eq!(
parse_index_or_range("1a"),
Ok(PadSelector::ShortUuid("1a".to_string()))
);
}
#[test]
fn test_non_hex_strings_still_error() {
assert!(parse_index_or_range("xyz").is_err());
assert!(parse_index_or_range("meeting").is_err());
assert!(parse_index_or_range("hello").is_err());
}
#[test]
fn test_short_uuid_display() {
let selector = PadSelector::ShortUuid("766d5dab".to_string());
assert_eq!(format!("{}", selector), "766d5dab");
}
#[test]
fn test_all_three_buckets() {
let active = make_pad("Active", false);
let archived = make_pad("Archived", false);
let deleted = make_pad("Deleted", false);
let indexed = index_pads(vec![active], vec![archived], vec![deleted]);
assert_eq!(indexed.len(), 3);
assert!(indexed
.iter()
.any(|dp| matches!(dp.index, DisplayIndex::Regular(_))));
assert!(indexed
.iter()
.any(|dp| matches!(dp.index, DisplayIndex::Archived(_))));
assert!(indexed
.iter()
.any(|dp| matches!(dp.index, DisplayIndex::Deleted(_))));
}
}