Skip to main content

reovim_kernel/core/
mark.rs

1//! Mark storage for bookmark operations.
2//!
3//! Marks allow jumping to saved positions within buffers.
4//!
5//! # Vim Mark Types
6//!
7//! - `'a` to `'z` - Buffer-local marks
8//! - `'A` to `'Z` - Global marks (across buffers)
9//! - `''` - Last jump position
10//! - `'.` - Last edit position
11//! - `'^` - Last insert position
12
13use std::collections::HashMap;
14
15use crate::mm::{BufferId, Position};
16
17/// A bookmark position with optional buffer association.
18///
19/// For global marks, both `position` and `buffer_id` are significant.
20/// For buffer-local marks, only `position` is used (buffer is implicit).
21///
22/// # Example
23///
24/// ```
25/// use reovim_kernel::api::v1::*;
26///
27/// let mark = Mark {
28///     position: Position::new(10, 5),
29///     buffer_id: BufferId::new(),
30/// };
31/// ```
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Mark {
34    /// Position within the buffer.
35    pub position: Position,
36    /// Buffer this mark belongs to (for global marks).
37    pub buffer_id: BufferId,
38}
39
40impl Mark {
41    /// Create a new mark at the given position and buffer.
42    #[must_use]
43    pub const fn new(position: Position, buffer_id: BufferId) -> Self {
44        Self {
45            position,
46            buffer_id,
47        }
48    }
49}
50
51/// Special mark types for automatic bookmarks.
52///
53/// These marks are automatically maintained by the editor.
54///
55/// # Example
56///
57/// ```
58/// use reovim_kernel::api::v1::*;
59///
60/// // Jump to last position before current jump
61/// let last_jump = SpecialMark::LastJump; // ''
62///
63/// // Jump to last edit location
64/// let last_edit = SpecialMark::LastEdit; // '.
65/// ```
66#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
67pub enum SpecialMark {
68    /// Position before last jump (vim: `''` or backtick-backtick)
69    LastJump,
70    /// Position of last edit (`'.`)
71    LastEdit,
72    /// Position of last insert (`'^`)
73    LastInsert,
74    /// Start of last visual selection (`'<`)
75    VisualStart,
76    /// End of last visual selection (`'>`)
77    VisualEnd,
78    /// Position where last exited insert mode
79    LastExitInsert,
80}
81
82/// Mark storage for all mark types.
83///
84/// Manages buffer-local marks, global marks, and special marks.
85///
86/// # Mark Types
87///
88/// - **Local marks** (`'a`-`'z`): Stored per-buffer, only position is tracked
89/// - **Global marks** (`'A`-`'Z`): Stored globally, includes buffer ID
90/// - **Special marks**: Automatically maintained editor positions
91///
92/// # Example
93///
94/// ```
95/// use reovim_kernel::api::v1::*;
96///
97/// let mut marks = MarkBank::new();
98///
99/// // Set local mark
100/// marks.set_local('a', Position::new(10, 0));
101///
102/// // Set global mark
103/// let buffer_id = BufferId::new();
104/// marks.set_global('A', Mark::new(Position::new(5, 0), buffer_id));
105///
106/// // Set special mark (usually done automatically)
107/// marks.set_special(SpecialMark::LastJump, Mark::new(Position::new(0, 0), buffer_id));
108/// ```
109#[derive(Debug, Clone, Default)]
110pub struct MarkBank {
111    /// Buffer-local marks ('a'-'z).
112    ///
113    /// These are positions within a single buffer.
114    /// The buffer context is provided externally when accessing.
115    local: HashMap<char, Position>,
116
117    /// Global marks ('A'-'Z).
118    ///
119    /// These include buffer ID for cross-buffer jumps.
120    global: HashMap<char, Mark>,
121
122    /// Special marks maintained by the editor.
123    special: HashMap<SpecialMark, Mark>,
124}
125
126impl MarkBank {
127    /// Create a new empty mark bank.
128    #[must_use]
129    pub fn new() -> Self {
130        Self {
131            local: HashMap::new(),
132            global: HashMap::new(),
133            special: HashMap::new(),
134        }
135    }
136
137    // === Local Marks ===
138
139    /// Set a local mark ('a'-'z').
140    ///
141    /// Returns `true` if successful, `false` if the mark name is invalid.
142    pub fn set_local(&mut self, name: char, position: Position) -> bool {
143        if name.is_ascii_lowercase() {
144            self.local.insert(name, position);
145            true
146        } else {
147            false
148        }
149    }
150
151    /// Get a local mark ('a'-'z').
152    ///
153    /// Returns `None` if the mark doesn't exist or name is invalid.
154    #[must_use]
155    pub fn get_local(&self, name: char) -> Option<Position> {
156        if name.is_ascii_lowercase() {
157            self.local.get(&name).copied()
158        } else {
159            None
160        }
161    }
162
163    /// Delete a local mark.
164    ///
165    /// Returns `true` if the mark existed and was deleted.
166    pub fn delete_local(&mut self, name: char) -> bool {
167        if name.is_ascii_lowercase() {
168            self.local.remove(&name).is_some()
169        } else {
170            false
171        }
172    }
173
174    /// List all local marks.
175    #[must_use]
176    pub fn list_local(&self) -> Vec<(char, Position)> {
177        let mut marks: Vec<_> = self.local.iter().map(|(&c, &p)| (c, p)).collect();
178        marks.sort_by_key(|(c, _)| *c);
179        marks
180    }
181
182    // === Global Marks ===
183
184    /// Set a global mark ('A'-'Z').
185    ///
186    /// Returns `true` if successful, `false` if the mark name is invalid.
187    pub fn set_global(&mut self, name: char, mark: Mark) -> bool {
188        if name.is_ascii_uppercase() {
189            self.global.insert(name, mark);
190            true
191        } else {
192            false
193        }
194    }
195
196    /// Get a global mark ('A'-'Z').
197    ///
198    /// Returns `None` if the mark doesn't exist or name is invalid.
199    #[must_use]
200    pub fn get_global(&self, name: char) -> Option<&Mark> {
201        if name.is_ascii_uppercase() {
202            self.global.get(&name)
203        } else {
204            None
205        }
206    }
207
208    /// Delete a global mark.
209    ///
210    /// Returns `true` if the mark existed and was deleted.
211    pub fn delete_global(&mut self, name: char) -> bool {
212        if name.is_ascii_uppercase() {
213            self.global.remove(&name).is_some()
214        } else {
215            false
216        }
217    }
218
219    /// List all global marks.
220    #[must_use]
221    pub fn list_global(&self) -> Vec<(char, &Mark)> {
222        let mut marks: Vec<_> = self.global.iter().map(|(&c, m)| (c, m)).collect();
223        marks.sort_by_key(|(c, _)| *c);
224        marks
225    }
226
227    // === Special Marks ===
228
229    /// Set a special mark.
230    pub fn set_special(&mut self, mark: SpecialMark, value: Mark) {
231        self.special.insert(mark, value);
232    }
233
234    /// Get a special mark.
235    #[must_use]
236    pub fn get_special(&self, mark: SpecialMark) -> Option<&Mark> {
237        self.special.get(&mark)
238    }
239
240    /// Clear a special mark.
241    pub fn clear_special(&mut self, mark: SpecialMark) {
242        self.special.remove(&mark);
243    }
244
245    // === Combined Operations ===
246
247    /// Get mark by character (local, global, or special shorthand).
248    ///
249    /// - `'a'`-`'z'` - Local marks (returns `None` for position, needs buffer context)
250    /// - `'A'`-`'Z'` - Global marks
251    /// - `'\''` or `` '`' `` - Last jump
252    /// - `'.'` - Last edit
253    /// - `'^'` - Last insert
254    /// - `'<'` - Visual start
255    /// - `'>'` - Visual end
256    #[must_use]
257    pub fn get_by_char(&self, c: char) -> Option<MarkResult<'_>> {
258        match c {
259            'a'..='z' => self.local.get(&c).map(|&p| MarkResult::Local(p)),
260            'A'..='Z' => self.global.get(&c).map(MarkResult::Global),
261            '\'' | '`' => self
262                .special
263                .get(&SpecialMark::LastJump)
264                .map(MarkResult::Global),
265            '.' => self
266                .special
267                .get(&SpecialMark::LastEdit)
268                .map(MarkResult::Global),
269            '^' => self
270                .special
271                .get(&SpecialMark::LastInsert)
272                .map(MarkResult::Global),
273            '<' => self
274                .special
275                .get(&SpecialMark::VisualStart)
276                .map(MarkResult::Global),
277            '>' => self
278                .special
279                .get(&SpecialMark::VisualEnd)
280                .map(MarkResult::Global),
281            _ => None,
282        }
283    }
284
285    /// Clear all local marks (for when buffer is closed).
286    pub fn clear_local(&mut self) {
287        self.local.clear();
288    }
289
290    /// Clear all marks.
291    pub fn clear_all(&mut self) {
292        self.local.clear();
293        self.global.clear();
294        self.special.clear();
295    }
296}
297
298/// Result of a mark lookup.
299#[derive(Debug, Clone, Copy)]
300pub enum MarkResult<'a> {
301    /// Local mark - position within current buffer.
302    Local(Position),
303    /// Global mark - includes buffer ID.
304    Global(&'a Mark),
305}
306
307impl MarkResult<'_> {
308    /// Get the position from either mark type.
309    #[must_use]
310    pub const fn position(&self) -> Position {
311        match self {
312            Self::Local(pos) => *pos,
313            Self::Global(mark) => mark.position,
314        }
315    }
316
317    /// Get buffer ID if this is a global mark.
318    #[must_use]
319    pub const fn buffer_id(&self) -> Option<BufferId> {
320        match self {
321            Self::Local(_) => None,
322            Self::Global(mark) => Some(mark.buffer_id),
323        }
324    }
325}