Skip to main content

halley_core/
trail.rs

1use std::collections::{HashMap, HashSet};
2
3use crate::field::NodeId;
4
5/// Stable id for a trail entry (not a NodeId).
6#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
7pub struct TrailId(u64);
8
9#[derive(Clone, Debug)]
10struct Entry {
11    node: NodeId,
12    prev: Option<TrailId>,
13    next: Option<TrailId>,
14}
15
16/// Focus history with browser-like back/forward behavior.
17/// - `record(node)` appends and moves the cursor.
18/// - If cursor isn't at tail, forward history is dropped.
19/// - `forget_node(node)` removes all occurrences and preserves chain.
20pub struct Trail {
21    next_id: u64,
22
23    head: Option<TrailId>,
24    tail: Option<TrailId>,
25    cursor: Option<TrailId>,
26
27    entries: HashMap<TrailId, Entry>,
28
29    // Allows O(k) removal of all occurrences of a node.
30    by_node: HashMap<NodeId, HashSet<TrailId>>,
31}
32
33impl Trail {
34    pub fn new() -> Self {
35        Self {
36            next_id: 1,
37            head: None,
38            tail: None,
39            cursor: None,
40            entries: HashMap::new(),
41            by_node: HashMap::new(),
42        }
43    }
44
45    pub fn cursor(&self) -> Option<NodeId> {
46        self.cursor
47            .and_then(|id| self.entries.get(&id))
48            .map(|e| e.node)
49    }
50
51    pub fn is_empty(&self) -> bool {
52        self.head.is_none()
53    }
54
55    pub fn len(&self) -> usize {
56        self.entries.len()
57    }
58
59    pub fn entries(&self) -> Vec<NodeId> {
60        let mut out = Vec::with_capacity(self.entries.len());
61        let mut it = self.head;
62        while let Some(id) = it {
63            let Some(entry) = self.entries.get(&id) else {
64                break;
65            };
66            out.push(entry.node);
67            it = entry.next;
68        }
69        out
70    }
71
72    pub fn cursor_index(&self) -> Option<usize> {
73        let cursor = self.cursor?;
74        let mut index = 0usize;
75        let mut it = self.head;
76        while let Some(id) = it {
77            if id == cursor {
78                return Some(index);
79            }
80            let Some(entry) = self.entries.get(&id) else {
81                break;
82            };
83            index += 1;
84            it = entry.next;
85        }
86        None
87    }
88
89    pub fn node_at_index(&self, index: usize) -> Option<NodeId> {
90        let mut current = 0usize;
91        let mut it = self.head;
92        while let Some(id) = it {
93            let entry = self.entries.get(&id)?;
94            if current == index {
95                return Some(entry.node);
96            }
97            current += 1;
98            it = entry.next;
99        }
100        None
101    }
102
103    pub fn seek_to_index(&mut self, index: usize) -> Option<NodeId> {
104        let mut current = 0usize;
105        let mut it = self.head;
106        while let Some(id) = it {
107            let entry = self.entries.get(&id)?;
108            if current == index {
109                self.cursor = Some(id);
110                return Some(entry.node);
111            }
112            current += 1;
113            it = entry.next;
114        }
115        None
116    }
117
118    pub fn seek_to_node(&mut self, node: NodeId) -> bool {
119        let mut last_match = None;
120        let mut it = self.head;
121        while let Some(id) = it {
122            let Some(entry) = self.entries.get(&id) else {
123                break;
124            };
125            if entry.node == node {
126                last_match = Some(id);
127            }
128            it = entry.next;
129        }
130        if let Some(id) = last_match {
131            self.cursor = Some(id);
132            true
133        } else {
134            false
135        }
136    }
137
138    /// Record a focus visit.
139    pub fn record(&mut self, node: NodeId) {
140        // If we are not at the tail, drop everything after cursor (browser semantics).
141        self.drop_forward();
142
143        let id = TrailId(self.next_id);
144        self.next_id += 1;
145
146        let entry = Entry {
147            node,
148            prev: self.tail,
149            next: None,
150        };
151
152        if let Some(tail_id) = self.tail {
153            if let Some(tail) = self.entries.get_mut(&tail_id) {
154                tail.next = Some(id);
155            }
156        } else {
157            // first entry
158            self.head = Some(id);
159        }
160
161        self.tail = Some(id);
162        self.cursor = Some(id);
163
164        self.entries.insert(id, entry);
165        self.by_node.entry(node).or_default().insert(id);
166    }
167
168    /// Step back in history.
169    pub fn back(&mut self) -> Option<NodeId> {
170        let cur = self.cursor?;
171        let prev = self.entries.get(&cur)?.prev?;
172        self.cursor = Some(prev);
173        self.cursor()
174    }
175
176    /// Step forward in history.
177    pub fn forward(&mut self) -> Option<NodeId> {
178        let cur = self.cursor?;
179        let next = self.entries.get(&cur)?.next?;
180        self.cursor = Some(next);
181        self.cursor()
182    }
183
184    pub fn back_wrapping(&mut self) -> Option<NodeId> {
185        if let Some(node) = self.back() {
186            return Some(node);
187        }
188        let tail = self.tail?;
189        self.cursor = Some(tail);
190        self.cursor()
191    }
192
193    pub fn forward_wrapping(&mut self) -> Option<NodeId> {
194        if let Some(node) = self.forward() {
195            return Some(node);
196        }
197        let head = self.head?;
198        self.cursor = Some(head);
199        self.cursor()
200    }
201
202    pub fn truncate_to(&mut self, max_len: usize) {
203        if max_len == 0 {
204            self.head = None;
205            self.tail = None;
206            self.cursor = None;
207            self.entries.clear();
208            self.by_node.clear();
209            return;
210        }
211
212        while self.entries.len() > max_len {
213            let Some(head) = self.head else {
214                break;
215            };
216            self.remove_entry(head);
217        }
218    }
219
220    /// Remove all history entries for a given node id.
221    pub fn forget_node(&mut self, node: NodeId) {
222        if !self.by_node.contains_key(&node) {
223            return;
224        }
225
226        // Remove occurrences in trail order so cursor repair stays stable.
227        let mut ids = Vec::new();
228        let mut it = self.head;
229        while let Some(id) = it {
230            let next = self.entries.get(&id).and_then(|e| e.next);
231            if self
232                .entries
233                .get(&id)
234                .is_some_and(|entry| entry.node == node)
235            {
236                ids.push(id);
237            }
238            it = next;
239        }
240
241        for id in ids {
242            self.remove_entry(id);
243        }
244    }
245
246    fn drop_forward(&mut self) {
247        // If no cursor, nothing to drop.
248        let Some(cur) = self.cursor else { return };
249
250        // Find the first forward entry
251        let next = match self.entries.get(&cur) {
252            Some(e) => e.next,
253            None => return,
254        };
255
256        // Detach forward chain from cursor
257        if let Some(e) = self.entries.get_mut(&cur) {
258            e.next = None;
259        }
260        self.tail = Some(cur);
261
262        // Remove every node in the forward chain
263        let mut it = next;
264        while let Some(id) = it {
265            let next_id = self.entries.get(&id).and_then(|e| e.next);
266            self.remove_entry(id);
267            it = next_id;
268        }
269    }
270
271    fn remove_entry(&mut self, id: TrailId) {
272        let Some(entry) = self.entries.remove(&id) else {
273            return;
274        };
275
276        // unlink from by_node index
277        if let Some(set) = self.by_node.get_mut(&entry.node) {
278            set.remove(&id);
279            if set.is_empty() {
280                self.by_node.remove(&entry.node);
281            }
282        }
283
284        // splice pointers
285        match entry.prev {
286            Some(p) => {
287                if let Some(pe) = self.entries.get_mut(&p) {
288                    pe.next = entry.next;
289                }
290            }
291            None => {
292                // removing head
293                self.head = entry.next;
294            }
295        }
296
297        match entry.next {
298            Some(n) => {
299                if let Some(ne) = self.entries.get_mut(&n) {
300                    ne.prev = entry.prev;
301                }
302            }
303            None => {
304                // removing tail
305                self.tail = entry.prev;
306            }
307        }
308
309        // cursor policy: if cursor removed, go to prev else next else none
310        if self.cursor == Some(id) {
311            self.cursor = entry.prev.or(entry.next);
312        }
313
314        // if list now empty, clear cursor too
315        if self.head.is_none() {
316            self.cursor = None;
317            self.tail = None;
318        }
319    }
320}
321
322impl Default for Trail {
323    fn default() -> Self {
324        Self::new()
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    use crate::field::NodeId;
332
333    #[test]
334    fn record_sets_cursor_and_allows_back_forward() {
335        let mut t = Trail::new();
336        let a = NodeId::new(1);
337        let b = NodeId::new(2);
338        let c = NodeId::new(3);
339
340        t.record(a);
341        t.record(b);
342        t.record(c);
343
344        assert_eq!(t.cursor(), Some(c));
345        assert_eq!(t.back(), Some(b));
346        assert_eq!(t.back(), Some(a));
347        assert_eq!(t.back(), None); // can't go past head
348        assert_eq!(t.forward(), Some(b));
349        assert_eq!(t.forward(), Some(c));
350        assert_eq!(t.forward(), None); // can't go past tail
351    }
352
353    #[test]
354    fn record_drops_forward_history() {
355        let mut t = Trail::new();
356        let a = NodeId::new(1);
357        let b = NodeId::new(2);
358        let c = NodeId::new(3);
359        let d = NodeId::new(4);
360
361        t.record(a);
362        t.record(b);
363        t.record(c);
364
365        // go back to b
366        assert_eq!(t.back(), Some(b));
367
368        // record d; this should drop forward (c)
369        t.record(d);
370
371        assert_eq!(t.cursor(), Some(d));
372        assert_eq!(t.forward(), None);
373        assert_eq!(t.back(), Some(b));
374    }
375
376    #[test]
377    fn forget_node_removes_all_occurrences_and_preserves_chain() {
378        let mut t = Trail::new();
379        let a = NodeId::new(1);
380        let b = NodeId::new(2);
381        let c = NodeId::new(3);
382
383        // a -> b -> a -> c
384        t.record(a);
385        t.record(b);
386        t.record(a);
387        t.record(c);
388
389        // remove 'a' entries
390        t.forget_node(a);
391
392        // history should now behave like: b -> c
393        assert_eq!(t.cursor(), Some(c));
394        assert_eq!(t.back(), Some(b));
395        assert_eq!(t.back(), None);
396        assert_eq!(t.forward(), Some(c));
397    }
398
399    #[test]
400    fn forget_node_moves_cursor_safely() {
401        let mut t = Trail::new();
402        let a = NodeId::new(1);
403        let b = NodeId::new(2);
404
405        t.record(a);
406        t.record(b);
407
408        // cursor is b; forget b should move cursor to a
409        t.forget_node(b);
410        assert_eq!(t.cursor(), Some(a));
411
412        // forget a should empty list
413        t.forget_node(a);
414        assert_eq!(t.cursor(), None);
415        assert!(t.is_empty());
416    }
417
418    #[test]
419    fn forget_node_with_multiple_occurrences_keeps_cursor_deterministic() {
420        let mut t = Trail::new();
421        let a = NodeId::new(1);
422        let b = NodeId::new(2);
423        let c = NodeId::new(3);
424
425        t.record(a);
426        t.record(b);
427        t.record(a);
428        t.record(c);
429        t.record(a);
430
431        t.forget_node(a);
432
433        assert_eq!(t.cursor(), Some(c));
434        assert_eq!(t.back(), Some(b));
435        assert_eq!(t.forward(), Some(c));
436    }
437}