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>), 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::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 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));
}
}
parse_path(s).map(PadSelector::Path)
}
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_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(_))));
}
}