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}