Skip to main content

reovim_kernel/mm/
selection.rs

1//! Selection state for visual mode and text operations.
2//!
3//! This module provides types for tracking text selection within a buffer.
4//! It supports three selection modes matching vim's visual mode variants:
5//! character-wise, line-wise, and block (rectangular).
6//!
7//! # Design Philosophy
8//!
9//! Following the kernel "mechanism, not policy" principle:
10//! - `Selection` tracks state (anchor, mode) - mechanism
11//! - Visual mode behavior is defined by modules/drivers - policy
12//! - No keybinding or mode transition logic here
13//!
14//! # Example
15//!
16//! ```
17//! use reovim_kernel::api::v1::*;
18//!
19//! let mut selection = Selection::default();
20//!
21//! // Start a character-wise selection at line 5, column 10
22//! selection.start(Position::new(5, 10), SelectionMode::Character);
23//! assert!(selection.is_active());
24//!
25//! // Get bounds with cursor at line 5, column 20
26//! let cursor = Position::new(5, 20);
27//! let (start, end) = selection.bounds(cursor).unwrap();
28//! assert_eq!(start, Position::new(5, 10));
29//! assert_eq!(end, Position::new(5, 20));
30//! ```
31
32use super::Position;
33
34/// Selection mode determining how text is selected.
35///
36/// These modes correspond to vim's visual mode variants:
37/// - `v` for character-wise
38/// - `V` for line-wise
39/// - `Ctrl-V` for block (rectangular)
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
41pub enum SelectionMode {
42    /// Character-wise selection (vim's `v` mode).
43    ///
44    /// Selection extends character by character from anchor to cursor,
45    /// spanning multiple lines if necessary.
46    #[default]
47    Character,
48
49    /// Block (rectangular) selection (vim's `Ctrl-V` mode).
50    ///
51    /// Selection forms a rectangle defined by the anchor and cursor
52    /// positions, regardless of line lengths.
53    Block,
54
55    /// Line-wise selection (vim's `V` mode).
56    ///
57    /// Selection extends by whole lines from the anchor line to the
58    /// cursor line, inclusive.
59    Line,
60}
61
62impl SelectionMode {
63    /// Check if this is character-wise mode.
64    #[inline]
65    #[must_use]
66    pub const fn is_character(&self) -> bool {
67        matches!(self, Self::Character)
68    }
69
70    /// Check if this is block (rectangular) mode.
71    #[inline]
72    #[must_use]
73    pub const fn is_block(&self) -> bool {
74        matches!(self, Self::Block)
75    }
76
77    /// Check if this is line-wise mode.
78    #[inline]
79    #[must_use]
80    pub const fn is_line(&self) -> bool {
81        matches!(self, Self::Line)
82    }
83}
84
85/// Selection state within a buffer.
86///
87/// Tracks whether a selection is active, where it started (anchor),
88/// and what mode it's in. The selection extends from the anchor to
89/// the current cursor position.
90///
91/// # Selection Direction
92///
93/// The anchor is where selection started. The selection can extend
94/// in either direction from the anchor:
95/// - Forward: cursor is after anchor
96/// - Backward: cursor is before anchor
97///
98/// Use [`bounds()`](Self::bounds) to get normalized (start, end) positions.
99#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
100pub struct Selection {
101    /// The position where selection started.
102    pub anchor: Position,
103
104    /// Whether selection is currently active.
105    pub active: bool,
106
107    /// The selection mode (character/block/line).
108    pub mode: SelectionMode,
109}
110
111impl Selection {
112    /// Create a new inactive selection.
113    #[must_use]
114    pub const fn new() -> Self {
115        Self {
116            anchor: Position::origin(),
117            active: false,
118            mode: SelectionMode::Character,
119        }
120    }
121
122    /// Start a selection at the given position with the specified mode.
123    ///
124    /// This sets the anchor and activates the selection.
125    ///
126    /// # Arguments
127    ///
128    /// * `pos` - The anchor position (where selection starts)
129    /// * `mode` - The selection mode
130    pub const fn start(&mut self, pos: Position, mode: SelectionMode) {
131        self.anchor = pos;
132        self.active = true;
133        self.mode = mode;
134    }
135
136    /// Start a character-wise selection at the given position.
137    ///
138    /// Convenience method equivalent to `start(pos, SelectionMode::Character)`.
139    pub const fn start_char(&mut self, pos: Position) {
140        self.start(pos, SelectionMode::Character);
141    }
142
143    /// Start a line-wise selection at the given position.
144    ///
145    /// Convenience method equivalent to `start(pos, SelectionMode::Line)`.
146    pub const fn start_line(&mut self, pos: Position) {
147        self.start(pos, SelectionMode::Line);
148    }
149
150    /// Start a block selection at the given position.
151    ///
152    /// Convenience method equivalent to `start(pos, SelectionMode::Block)`.
153    pub const fn start_block(&mut self, pos: Position) {
154        self.start(pos, SelectionMode::Block);
155    }
156
157    /// Clear (deactivate) the selection.
158    ///
159    /// The anchor position is preserved but the selection becomes inactive.
160    pub const fn clear(&mut self) {
161        self.active = false;
162    }
163
164    /// Check if selection is currently active.
165    #[inline]
166    #[must_use]
167    pub const fn is_active(&self) -> bool {
168        self.active
169    }
170
171    /// Get the selection mode.
172    #[inline]
173    #[must_use]
174    pub const fn mode(&self) -> SelectionMode {
175        self.mode
176    }
177
178    /// Change the selection mode without changing anchor or active state.
179    pub const fn set_mode(&mut self, mode: SelectionMode) {
180        self.mode = mode;
181    }
182
183    /// Get selection bounds in document order (start <= end).
184    ///
185    /// Returns `None` if selection is not active.
186    /// The returned positions are always ordered so that start comes
187    /// before or equals end in document order.
188    ///
189    /// # Arguments
190    ///
191    /// * `cursor_pos` - The current cursor position (selection endpoint)
192    #[must_use]
193    pub fn bounds(&self, cursor_pos: Position) -> Option<(Position, Position)> {
194        if !self.active {
195            return None;
196        }
197
198        if self.anchor <= cursor_pos {
199            Some((self.anchor, cursor_pos))
200        } else {
201            Some((cursor_pos, self.anchor))
202        }
203    }
204
205    /// Get block selection bounds (top-left, bottom-right).
206    ///
207    /// For block selections, this returns the rectangle corners:
208    /// - Top-left: (`min_line`, `min_column`)
209    /// - Bottom-right: (`max_line`, `max_column`)
210    ///
211    /// Returns `None` if selection is not active or not in block mode.
212    ///
213    /// # Arguments
214    ///
215    /// * `cursor_pos` - The current cursor position
216    #[must_use]
217    #[cfg_attr(coverage_nightly, coverage(off))]
218    pub fn block_bounds(&self, cursor_pos: Position) -> Option<(Position, Position)> {
219        if !self.active || self.mode != SelectionMode::Block {
220            return None;
221        }
222
223        let min_line = self.anchor.line.min(cursor_pos.line);
224        let max_line = self.anchor.line.max(cursor_pos.line);
225        let min_col = self.anchor.column.min(cursor_pos.column);
226        let max_col = self.anchor.column.max(cursor_pos.column);
227
228        Some((Position::new(min_line, min_col), Position::new(max_line, max_col)))
229    }
230
231    /// Get line-wise selection bounds (`start_line`, `end_line`).
232    ///
233    /// For line selections, this returns the range of selected lines.
234    /// Returns `None` if selection is not active.
235    ///
236    /// # Arguments
237    ///
238    /// * `cursor_pos` - The current cursor position
239    #[must_use]
240    pub fn line_bounds(&self, cursor_pos: Position) -> Option<(usize, usize)> {
241        if !self.active {
242            return None;
243        }
244
245        let start_line = self.anchor.line.min(cursor_pos.line);
246        let end_line = self.anchor.line.max(cursor_pos.line);
247
248        Some((start_line, end_line))
249    }
250
251    /// Check if a position is within the selection.
252    ///
253    /// The behavior depends on the selection mode:
254    /// - Character: position is between start and end
255    /// - Block: position is within the rectangle
256    /// - Line: position's line is within the line range
257    ///
258    /// # Arguments
259    ///
260    /// * `pos` - The position to check
261    /// * `cursor_pos` - The current cursor position (selection endpoint)
262    #[must_use]
263    #[cfg_attr(coverage_nightly, coverage(off))]
264    pub fn contains(&self, pos: Position, cursor_pos: Position) -> bool {
265        if !self.active {
266            return false;
267        }
268
269        match self.mode {
270            SelectionMode::Character => {
271                if let Some((start, end)) = self.bounds(cursor_pos) {
272                    pos >= start && pos <= end
273                } else {
274                    false
275                }
276            }
277            SelectionMode::Block => {
278                if let Some((top_left, bottom_right)) = self.block_bounds(cursor_pos) {
279                    pos.line >= top_left.line
280                        && pos.line <= bottom_right.line
281                        && pos.column >= top_left.column
282                        && pos.column <= bottom_right.column
283                } else {
284                    false
285                }
286            }
287            SelectionMode::Line => {
288                if let Some((start_line, end_line)) = self.line_bounds(cursor_pos) {
289                    pos.line >= start_line && pos.line <= end_line
290                } else {
291                    false
292                }
293            }
294        }
295    }
296
297    /// Get the number of lines in the selection.
298    ///
299    /// Returns 0 if selection is not active.
300    ///
301    /// # Arguments
302    ///
303    /// * `cursor_pos` - The current cursor position
304    #[must_use]
305    pub fn line_count(&self, cursor_pos: Position) -> usize {
306        if !self.active {
307            return 0;
308        }
309
310        let start_line = self.anchor.line.min(cursor_pos.line);
311        let end_line = self.anchor.line.max(cursor_pos.line);
312        end_line - start_line + 1
313    }
314}