Skip to main content

fresh/input/
position_history.rs

1/// Position history for go back/forward navigation like VS Code
2///
3/// This module tracks the user's position history across buffers,
4/// allowing navigation back and forward through editing locations.
5/// Similar to VS Code's Alt+Left/Alt+Right navigation.
6///
7/// ## Architecture
8///
9/// Position history consumes MoveCursor events from the event log and coalesces
10/// consecutive movements into single "jump" entries. This means:
11/// - Many arrow key presses = one jump entry
12/// - Each buffer switch = commits pending movement and adds new entry
13/// - Idle period = commits pending movement
14///
15/// This matches VS Code's behavior where you can navigate back through your
16/// editing trail, not through every single keystroke.
17use crate::model::event::BufferId;
18
19/// A single entry in the position history
20#[derive(Clone, Debug, PartialEq)]
21pub struct PositionEntry {
22    /// The buffer ID
23    pub buffer_id: BufferId,
24
25    /// The cursor position (byte offset)
26    pub position: usize,
27
28    /// Optional selection anchor
29    pub anchor: Option<usize>,
30}
31
32impl PositionEntry {
33    /// Create a new position entry
34    pub fn new(buffer_id: BufferId, position: usize, anchor: Option<usize>) -> Self {
35        Self {
36            buffer_id,
37            position,
38            anchor,
39        }
40    }
41}
42
43/// Pending movement that may be coalesced with subsequent movements
44#[derive(Clone, Debug)]
45struct PendingMovement {
46    /// Starting position of this movement sequence
47    start_entry: PositionEntry,
48}
49
50/// Distance threshold for considering a movement "large" (in bytes)
51/// Movements larger than this will not be coalesced
52const LARGE_JUMP_THRESHOLD: usize = 50;
53
54/// Position history manager
55///
56/// This tracks navigation history across the editor, storing positions
57/// the user has visited. It maintains a stack with a current index,
58/// allowing back/forward navigation.
59///
60/// Movements are coalesced: consecutive MoveCursor events within a short
61/// time period are treated as a single "jump" for navigation purposes.
62pub struct PositionHistory {
63    /// Stack of position entries
64    entries: Vec<PositionEntry>,
65
66    /// Current index in the stack (where we are in history)
67    /// Points to the current position
68    current_index: Option<usize>,
69
70    /// Maximum number of entries to keep
71    max_entries: usize,
72
73    /// Pending movement that hasn't been committed yet
74    /// Gets committed when: buffer switches, timeout expires, or significant event
75    pending_movement: Option<PendingMovement>,
76}
77
78impl PositionHistory {
79    /// Create a new position history with default max entries (100)
80    pub fn new() -> Self {
81        Self::with_capacity(100)
82    }
83
84    /// Create a new position history with specified max entries
85    pub fn with_capacity(max_entries: usize) -> Self {
86        Self {
87            entries: Vec::new(),
88            current_index: None,
89            max_entries,
90            pending_movement: None,
91        }
92    }
93
94    /// Record a cursor movement event
95    ///
96    /// This is called for EVERY MoveCursor event. Consecutive small movements are coalesced
97    /// into a single history entry. The movement is committed to history when:
98    /// - Buffer changes
99    /// - Large jump detected (> 50 bytes distance from pending start position)
100    /// - User triggers back/forward navigation
101    pub fn record_movement(&mut self, buffer_id: BufferId, position: usize, anchor: Option<usize>) {
102        let entry = PositionEntry::new(buffer_id, position, anchor);
103
104        if let Some(pending) = &mut self.pending_movement {
105            // Check if this is a continuation of the current movement
106            if pending.start_entry.buffer_id == buffer_id {
107                // Calculate distance from the pending movement's start position
108                let distance = position.abs_diff(pending.start_entry.position);
109
110                // Check if this is a small movement that should be coalesced
111                if distance <= LARGE_JUMP_THRESHOLD {
112                    // Small movement - keep coalescing, don't commit yet
113                    return;
114                }
115            }
116
117            // Different buffer or large jump - commit the pending movement
118            self.commit_pending_movement();
119        }
120
121        // Start a new pending movement
122        self.pending_movement = Some(PendingMovement { start_entry: entry });
123    }
124
125    /// Commit any pending movement to history
126    ///
127    /// This is called when:
128    /// - Switching buffers
129    /// - Before navigating back/forward
130    pub fn commit_pending_movement(&mut self) {
131        if let Some(pending) = self.pending_movement.take() {
132            // Always call push(), which handles both:
133            // 1. Truncating forward history (if we're not at the end)
134            // 2. Checking for duplicates before adding
135            self.push(pending.start_entry);
136        }
137    }
138
139    /// Push a new position to the history
140    ///
141    /// This is called when the user makes a significant navigation:
142    /// - Switching buffers
143    /// - Large cursor movements (e.g., search, go-to-definition)
144    /// - Opening a file
145    ///
146    /// If we're not at the end of history (user has gone back), this
147    /// truncates the forward history and adds the new position.
148    pub fn push(&mut self, entry: PositionEntry) {
149        // If we're not at the end, truncate forward history FIRST
150        // This ensures forward history is cleared even if the new entry is a duplicate
151        if let Some(current_idx) = self.current_index {
152            self.entries.truncate(current_idx + 1);
153        }
154
155        // Don't add duplicate consecutive entries
156        if let Some(current_idx) = self.current_index {
157            if current_idx < self.entries.len() && self.entries[current_idx] == entry {
158                return;
159            }
160        }
161
162        // Add new entry
163        self.entries.push(entry);
164
165        // Limit size
166        if self.entries.len() > self.max_entries {
167            self.entries.remove(0);
168        }
169
170        // Update current index to point to the new entry
171        self.current_index = Some(self.entries.len() - 1);
172    }
173
174    /// Navigate back in history
175    ///
176    /// Commits any pending movement first, then returns the previous position.
177    /// Returns None if we're at the beginning of history.
178    pub fn back(&mut self) -> Option<&PositionEntry> {
179        // Commit any pending movement before navigating
180        self.commit_pending_movement();
181
182        if self.entries.is_empty() {
183            return None;
184        }
185
186        match self.current_index {
187            None => None,
188            Some(0) => None, // Already at the beginning
189            Some(idx) => {
190                self.current_index = Some(idx - 1);
191                Some(&self.entries[idx - 1])
192            }
193        }
194    }
195
196    /// Navigate forward in history
197    ///
198    /// Returns the next position, or None if we're at the end of history.
199    pub fn forward(&mut self) -> Option<&PositionEntry> {
200        if self.entries.is_empty() {
201            return None;
202        }
203
204        match self.current_index {
205            None => None,
206            Some(idx) if idx >= self.entries.len() - 1 => None, // Already at the end
207            Some(idx) => {
208                self.current_index = Some(idx + 1);
209                Some(&self.entries[idx + 1])
210            }
211        }
212    }
213
214    /// Check if we can go back
215    pub fn can_go_back(&self) -> bool {
216        match self.current_index {
217            Some(idx) => idx > 0,
218            None => false,
219        }
220    }
221
222    /// Check if we can go forward
223    pub fn can_go_forward(&self) -> bool {
224        match self.current_index {
225            Some(idx) => idx < self.entries.len() - 1,
226            None => false,
227        }
228    }
229
230    /// Get the current position entry
231    pub fn current(&self) -> Option<&PositionEntry> {
232        self.current_index.and_then(|idx| self.entries.get(idx))
233    }
234
235    /// Clear all history
236    pub fn clear(&mut self) {
237        self.entries.clear();
238        self.current_index = None;
239    }
240
241    /// Get the number of entries in history
242    pub fn len(&self) -> usize {
243        self.entries.len()
244    }
245
246    /// Check if history is empty
247    pub fn is_empty(&self) -> bool {
248        self.entries.is_empty()
249    }
250
251    /// Get current index (for debugging)
252    pub fn current_index(&self) -> Option<usize> {
253        self.current_index
254    }
255}
256
257impl Default for PositionHistory {
258    fn default() -> Self {
259        Self::new()
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    fn make_entry(buffer_id: usize, position: usize) -> PositionEntry {
268        PositionEntry::new(BufferId(buffer_id), position, None)
269    }
270
271    #[test]
272    fn test_new_history_is_empty() {
273        let history = PositionHistory::new();
274        assert!(history.is_empty());
275        assert_eq!(history.len(), 0);
276        assert!(!history.can_go_back());
277        assert!(!history.can_go_forward());
278    }
279
280    #[test]
281    fn test_push_single_entry() {
282        let mut history = PositionHistory::new();
283        let entry = make_entry(1, 10);
284
285        history.push(entry.clone());
286
287        assert_eq!(history.len(), 1);
288        assert_eq!(history.current(), Some(&entry));
289        assert!(!history.can_go_back());
290        assert!(!history.can_go_forward());
291    }
292
293    #[test]
294    fn test_push_multiple_entries() {
295        let mut history = PositionHistory::new();
296        let entry1 = make_entry(1, 10);
297        let entry2 = make_entry(1, 20);
298        let entry3 = make_entry(2, 5);
299
300        history.push(entry1.clone());
301        history.push(entry2.clone());
302        history.push(entry3.clone());
303
304        assert_eq!(history.len(), 3);
305        assert_eq!(history.current(), Some(&entry3));
306        assert!(history.can_go_back());
307        assert!(!history.can_go_forward());
308    }
309
310    #[test]
311    fn test_back_navigation() {
312        let mut history = PositionHistory::new();
313        let entry1 = make_entry(1, 10);
314        let entry2 = make_entry(1, 20);
315        let entry3 = make_entry(2, 5);
316
317        history.push(entry1.clone());
318        history.push(entry2.clone());
319        history.push(entry3.clone());
320
321        // Go back once
322        let back1 = history.back();
323        assert_eq!(back1, Some(&entry2));
324        assert_eq!(history.current(), Some(&entry2));
325        assert!(history.can_go_back());
326        assert!(history.can_go_forward());
327
328        // Go back again
329        let back2 = history.back();
330        assert_eq!(back2, Some(&entry1));
331        assert_eq!(history.current(), Some(&entry1));
332        assert!(!history.can_go_back());
333        assert!(history.can_go_forward());
334
335        // Try to go back at beginning
336        let back3 = history.back();
337        assert_eq!(back3, None);
338        assert_eq!(history.current(), Some(&entry1));
339    }
340
341    #[test]
342    fn test_forward_navigation() {
343        let mut history = PositionHistory::new();
344        let entry1 = make_entry(1, 10);
345        let entry2 = make_entry(1, 20);
346        let entry3 = make_entry(2, 5);
347
348        history.push(entry1.clone());
349        history.push(entry2.clone());
350        history.push(entry3.clone());
351
352        // Go back twice
353        history.back();
354        history.back();
355        assert_eq!(history.current(), Some(&entry1));
356
357        // Go forward once
358        let fwd1 = history.forward();
359        assert_eq!(fwd1, Some(&entry2));
360        assert_eq!(history.current(), Some(&entry2));
361
362        // Go forward again
363        let fwd2 = history.forward();
364        assert_eq!(fwd2, Some(&entry3));
365        assert_eq!(history.current(), Some(&entry3));
366
367        // Try to go forward at end
368        let fwd3 = history.forward();
369        assert_eq!(fwd3, None);
370        assert_eq!(history.current(), Some(&entry3));
371    }
372
373    #[test]
374    fn test_push_truncates_forward_history() {
375        let mut history = PositionHistory::new();
376        let entry1 = make_entry(1, 10);
377        let entry2 = make_entry(1, 20);
378        let entry3 = make_entry(2, 5);
379        let entry4 = make_entry(2, 15);
380
381        history.push(entry1.clone());
382        history.push(entry2.clone());
383        history.push(entry3.clone());
384
385        // Go back twice
386        history.back();
387        history.back();
388        assert_eq!(history.current(), Some(&entry1));
389
390        // Push new entry - should truncate forward history
391        history.push(entry4.clone());
392
393        assert_eq!(history.len(), 2);
394        assert_eq!(history.current(), Some(&entry4));
395        assert!(history.can_go_back());
396        assert!(!history.can_go_forward());
397
398        // Verify we can go back to entry1
399        let back = history.back();
400        assert_eq!(back, Some(&entry1));
401    }
402
403    #[test]
404    fn test_duplicate_consecutive_entries_not_added() {
405        let mut history = PositionHistory::new();
406        let entry1 = make_entry(1, 10);
407
408        history.push(entry1.clone());
409        history.push(entry1.clone());
410        history.push(entry1.clone());
411
412        assert_eq!(history.len(), 1);
413    }
414
415    #[test]
416    fn test_max_entries_limit() {
417        let mut history = PositionHistory::with_capacity(3);
418
419        for i in 0..5 {
420            history.push(make_entry(1, i * 10));
421        }
422
423        assert_eq!(history.len(), 3);
424        // Should have kept the last 3 entries (20, 30, 40)
425        assert_eq!(history.current(), Some(&make_entry(1, 40)));
426
427        history.back();
428        assert_eq!(history.current(), Some(&make_entry(1, 30)));
429
430        history.back();
431        assert_eq!(history.current(), Some(&make_entry(1, 20)));
432    }
433
434    #[test]
435    fn test_clear() {
436        let mut history = PositionHistory::new();
437
438        history.push(make_entry(1, 10));
439        history.push(make_entry(1, 20));
440
441        assert_eq!(history.len(), 2);
442
443        history.clear();
444
445        assert!(history.is_empty());
446        assert_eq!(history.len(), 0);
447        assert_eq!(history.current(), None);
448        assert!(!history.can_go_back());
449        assert!(!history.can_go_forward());
450    }
451}