1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::VecDeque;
6use ucm_core::{BlockId, EdgeType};
7
8#[derive(Debug, Clone)]
10pub struct TraversalCursor {
11 pub position: BlockId,
13 pub neighborhood: CursorNeighborhood,
15 pub breadcrumbs: VecDeque<BlockId>,
17 pub view_mode: ViewMode,
19 max_breadcrumbs: usize,
21}
22
23impl TraversalCursor {
24 pub fn new(position: BlockId, max_breadcrumbs: usize) -> Self {
25 Self {
26 position,
27 neighborhood: CursorNeighborhood::default(),
28 breadcrumbs: VecDeque::new(),
29 view_mode: ViewMode::default(),
30 max_breadcrumbs,
31 }
32 }
33
34 pub fn move_to(&mut self, new_position: BlockId) {
36 self.breadcrumbs.push_back(self.position);
38 if self.breadcrumbs.len() > self.max_breadcrumbs {
39 self.breadcrumbs.pop_front();
40 }
41
42 self.position = new_position;
44 self.neighborhood.stale = true;
45 }
46
47 pub fn go_back(&mut self, steps: usize) -> Option<BlockId> {
49 for _ in 0..steps {
50 if let Some(prev) = self.breadcrumbs.pop_back() {
51 self.position = prev;
52 self.neighborhood.stale = true;
53 } else {
54 return None;
55 }
56 }
57 Some(self.position)
58 }
59
60 pub fn can_go_back(&self) -> bool {
62 !self.breadcrumbs.is_empty()
63 }
64
65 pub fn history_depth(&self) -> usize {
67 self.breadcrumbs.len()
68 }
69
70 pub fn clear_history(&mut self) {
72 self.breadcrumbs.clear();
73 }
74
75 pub fn update_neighborhood(&mut self, neighborhood: CursorNeighborhood) {
77 self.neighborhood = neighborhood;
78 self.neighborhood.stale = false;
79 self.neighborhood.computed_at = Utc::now();
80 }
81
82 pub fn needs_refresh(&self) -> bool {
84 self.neighborhood.stale
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct CursorNeighborhood {
91 pub ancestors: Vec<BlockId>,
93 pub children: Vec<BlockId>,
95 pub siblings: Vec<BlockId>,
97 pub connections: Vec<(BlockId, EdgeType)>,
99 pub computed_at: DateTime<Utc>,
101 pub stale: bool,
103}
104
105impl Default for CursorNeighborhood {
106 fn default() -> Self {
107 Self::new()
108 }
109}
110
111impl CursorNeighborhood {
112 pub fn new() -> Self {
113 Self {
114 ancestors: Vec::new(),
115 children: Vec::new(),
116 siblings: Vec::new(),
117 connections: Vec::new(),
118 computed_at: Utc::now(),
119 stale: true,
120 }
121 }
122
123 pub fn total_blocks(&self) -> usize {
125 self.ancestors.len() + self.children.len() + self.siblings.len() + self.connections.len()
126 }
127}
128
129#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132#[derive(Default)]
133pub enum ViewMode {
134 IdsOnly,
136 Preview { length: usize },
138 #[default]
140 Full,
141 Metadata,
143 Adaptive { interest_threshold: f32 },
145}
146
147impl ViewMode {
148 pub fn preview(length: usize) -> Self {
149 Self::Preview { length }
150 }
151
152 pub fn adaptive(threshold: f32) -> Self {
153 Self::Adaptive {
154 interest_threshold: threshold,
155 }
156 }
157}
158
159impl From<ucl_parser::ast::ViewMode> for ViewMode {
161 fn from(mode: ucl_parser::ast::ViewMode) -> Self {
162 match mode {
163 ucl_parser::ast::ViewMode::Full => ViewMode::Full,
164 ucl_parser::ast::ViewMode::Preview { length } => ViewMode::Preview { length },
165 ucl_parser::ast::ViewMode::Metadata => ViewMode::Metadata,
166 ucl_parser::ast::ViewMode::IdsOnly => ViewMode::IdsOnly,
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 fn block_id(s: &str) -> BlockId {
176 s.parse().unwrap_or_else(|_| {
177 let mut bytes = [0u8; 12];
179 let s_bytes = s.as_bytes();
180 for (i, b) in s_bytes.iter().enumerate() {
181 bytes[i % 12] ^= *b;
182 }
183 BlockId::from_bytes(bytes)
184 })
185 }
186
187 #[test]
188 fn test_cursor_movement() {
189 let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 10);
190
191 cursor.move_to(block_id("blk_000000000002"));
192 assert_eq!(cursor.position, block_id("blk_000000000002"));
193 assert_eq!(cursor.breadcrumbs.len(), 1);
194
195 cursor.move_to(block_id("blk_000000000003"));
196 assert_eq!(cursor.position, block_id("blk_000000000003"));
197 assert_eq!(cursor.breadcrumbs.len(), 2);
198
199 cursor.go_back(1);
201 assert_eq!(cursor.position, block_id("blk_000000000002"));
202 assert_eq!(cursor.breadcrumbs.len(), 1);
203 }
204
205 #[test]
206 fn test_cursor_history_limit() {
207 let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 3);
208
209 for i in 2..=5 {
211 cursor.move_to(block_id(&format!("blk_00000000000{}", i)));
212 }
213
214 assert_eq!(cursor.breadcrumbs.len(), 3);
216 }
217
218 #[test]
219 fn test_neighborhood_staleness() {
220 let mut cursor = TraversalCursor::new(block_id("blk_000000000001"), 10);
221
222 assert!(cursor.needs_refresh());
224
225 cursor.update_neighborhood(CursorNeighborhood::new());
227 assert!(!cursor.needs_refresh());
228
229 cursor.move_to(block_id("blk_000000000002"));
231 assert!(cursor.needs_refresh());
232 }
233}