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}