Skip to main content

reovim_kernel/core/
jumplist.rs

1//! Jump list for cursor position history.
2//!
3//! Linux equivalent: Navigation history like shell directory stack
4//!
5//! This module provides the [`Jumplist`] data structure for tracking
6//! "major" cursor position changes, enabling Ctrl-O (jump older) and
7//! Ctrl-I (jump newer) navigation.
8//!
9//! # Design Philosophy
10//!
11//! The jumplist is a **mechanism** for tracking positions. Policy decisions
12//! (what constitutes a "jump") are made by modules/commands that call `push()`.
13//!
14//! # Example
15//!
16//! ```
17//! use reovim_kernel::api::v1::*;
18//!
19//! let buffer_id = BufferId::new();
20//! let mut jumplist = Jumplist::new();
21//!
22//! // Record positions as user navigates
23//! jumplist.push(JumpEntry::new(buffer_id, Position::new(0, 0)));
24//! jumplist.push(JumpEntry::new(buffer_id, Position::new(10, 5)));
25//! jumplist.push(JumpEntry::new(buffer_id, Position::new(50, 0)));
26//!
27//! // Navigate backward (Ctrl-O)
28//! if let Some(entry) = jumplist.backward() {
29//!     println!("Jump to: {:?}", entry.position);
30//! }
31//!
32//! // Navigate forward (Ctrl-I)
33//! if let Some(entry) = jumplist.forward() {
34//!     println!("Jump to: {:?}", entry.position);
35//! }
36//! ```
37
38use crate::mm::{BufferId, Position};
39
40/// Maximum number of entries in the jump list.
41pub const MAX_JUMPLIST_SIZE: usize = 100;
42
43/// A single entry in the jump list.
44///
45/// Each entry records a buffer ID and position within that buffer.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct JumpEntry {
48    /// The buffer this jump entry refers to.
49    pub buffer: BufferId,
50    /// Position within the buffer.
51    pub position: Position,
52}
53
54impl JumpEntry {
55    /// Create a new jump entry.
56    #[must_use]
57    pub const fn new(buffer: BufferId, position: Position) -> Self {
58        Self { buffer, position }
59    }
60}
61
62/// Jump list for tracking cursor position history.
63///
64/// Tracks "major" cursor position changes for Ctrl-O / Ctrl-I navigation.
65/// The list maintains a current index that moves as the user navigates
66/// backward and forward through history.
67///
68/// # Behavior
69///
70/// - `push()`: Adds a new entry, truncating any "future" entries after
71///   the current position (like git commit after detached HEAD)
72/// - `push_current()`: Adds without truncating, used when recording
73///   current position (e.g., on INSERT mode leave)
74/// - `backward()`: Move to older position (Ctrl-O)
75/// - `forward()`: Move to newer position (Ctrl-I)
76///
77/// # Duplicate Suppression
78///
79/// Consecutive duplicate entries are automatically suppressed to prevent
80/// noise from repeated jumps to the same location.
81#[derive(Debug, Default, Clone)]
82pub struct Jumplist {
83    /// Stored entries (oldest first).
84    entries: Vec<JumpEntry>,
85    /// Current position in the list.
86    ///
87    /// When at the end (newest), this equals `entries.len()`.
88    /// After `backward()`, points to the entry we just jumped to.
89    current: usize,
90}
91
92impl Jumplist {
93    /// Create a new empty jump list.
94    #[must_use]
95    pub fn new() -> Self {
96        Self::default()
97    }
98
99    /// Create a jump list with custom maximum size.
100    ///
101    /// Note: The size limit is enforced during push operations.
102    #[must_use]
103    pub fn with_capacity(capacity: usize) -> Self {
104        Self {
105            entries: Vec::with_capacity(capacity.min(MAX_JUMPLIST_SIZE)),
106            current: 0,
107        }
108    }
109
110    /// Record a new position in the jump list.
111    ///
112    /// This truncates any "future" entries if we're not at the end
113    /// (similar to git commit after checkout to an older commit).
114    ///
115    /// # Returns
116    ///
117    /// `true` if the entry was added, `false` if it was a duplicate
118    /// of the most recent entry.
119    ///
120    /// # Example
121    ///
122    /// ```
123    /// use reovim_kernel::api::v1::*;
124    ///
125    /// let buf = BufferId::new();
126    /// let mut list = Jumplist::new();
127    ///
128    /// assert!(list.push(JumpEntry::new(buf, Position::new(0, 0))));
129    /// assert!(!list.push(JumpEntry::new(buf, Position::new(0, 0)))); // Duplicate
130    /// ```
131    pub fn push(&mut self, entry: JumpEntry) -> bool {
132        // Don't add duplicate of last entry
133        if self.entries.last() == Some(&entry) {
134            self.current = self.entries.len();
135            return false;
136        }
137
138        // Truncate any entries after current position
139        self.entries.truncate(self.current);
140
141        // Add new entry
142        self.entries.push(entry);
143
144        // Enforce max size (remove oldest)
145        if self.entries.len() > MAX_JUMPLIST_SIZE {
146            self.entries.remove(0);
147        }
148
149        // Update index to point past the end
150        self.current = self.entries.len();
151        true
152    }
153
154    /// Push a jump entry without truncating history.
155    ///
156    /// Used when recording the current position without intention to
157    /// navigate (e.g., recording position on INSERT mode leave).
158    /// Unlike `push()`, this preserves future entries.
159    ///
160    /// # Returns
161    ///
162    /// `true` if the entry was added, `false` if duplicate.
163    #[cfg_attr(coverage_nightly, coverage(off))]
164    pub fn push_current(&mut self, entry: JumpEntry) -> bool {
165        // Don't add duplicate of last entry
166        if self.entries.last() == Some(&entry) {
167            if !self.entries.is_empty() {
168                self.current = self.entries.len() - 1;
169            }
170            return false;
171        }
172
173        // DON'T truncate - preserve history
174        self.entries.push(entry);
175
176        // Enforce max size
177        if self.entries.len() > MAX_JUMPLIST_SIZE {
178            self.entries.remove(0);
179        }
180
181        // Point past current entry so first Ctrl+O goes to previous
182        self.current = self.entries.len();
183        true
184    }
185
186    /// Jump to older position (Ctrl-O).
187    ///
188    /// Moves the current index backward and returns the entry at that
189    /// position. Returns `None` if already at the beginning.
190    ///
191    /// # Example
192    ///
193    /// ```
194    /// use reovim_kernel::api::v1::*;
195    ///
196    /// let buf = BufferId::new();
197    /// let mut list = Jumplist::new();
198    /// list.push(JumpEntry::new(buf, Position::new(0, 0)));
199    /// list.push(JumpEntry::new(buf, Position::new(10, 5)));
200    ///
201    /// let entry = list.backward().unwrap();
202    /// assert_eq!(entry.position, Position::new(10, 5));
203    /// ```
204    pub fn backward(&mut self) -> Option<&JumpEntry> {
205        if self.current > 0 {
206            self.current -= 1;
207            self.entries.get(self.current)
208        } else {
209            None
210        }
211    }
212
213    /// Jump to newer position (Ctrl-I).
214    ///
215    /// Moves the current index forward and returns the entry at that
216    /// position. Returns `None` if already at the end.
217    pub fn forward(&mut self) -> Option<&JumpEntry> {
218        if self.current < self.entries.len() {
219            let entry = self.entries.get(self.current);
220            self.current += 1;
221            entry
222        } else {
223            None
224        }
225    }
226
227    /// Get the current entry without moving.
228    ///
229    /// Returns `None` if the list is empty or current is past the end.
230    #[must_use]
231    #[cfg_attr(coverage_nightly, coverage(off))]
232    pub fn current(&self) -> Option<&JumpEntry> {
233        if self.current > 0 && self.current <= self.entries.len() {
234            self.entries.get(self.current - 1)
235        } else {
236            None
237        }
238    }
239
240    /// Get the current index for debugging/display.
241    #[must_use]
242    pub const fn current_index(&self) -> usize {
243        self.current
244    }
245
246    /// Get all entries (oldest first).
247    #[must_use]
248    pub fn entries(&self) -> &[JumpEntry] {
249        &self.entries
250    }
251
252    /// Get the number of entries.
253    #[must_use]
254    #[allow(clippy::missing_const_for_fn)] // Vec::len() is not const in stable Rust
255    pub fn len(&self) -> usize {
256        self.entries.len()
257    }
258
259    /// Check if the jump list is empty.
260    #[must_use]
261    #[allow(clippy::missing_const_for_fn)] // Vec::is_empty() is not const in stable Rust
262    pub fn is_empty(&self) -> bool {
263        self.entries.is_empty()
264    }
265
266    /// Clear all entries.
267    pub fn clear(&mut self) {
268        self.entries.clear();
269        self.current = 0;
270    }
271}