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}