Skip to main content

reovim_kernel/mm/
position.rs

1//! Position and cursor types for text navigation.
2//!
3//! This module provides the fundamental types for representing locations
4//! within a text buffer and tracking cursor state.
5
6/// A position within a text buffer.
7///
8/// Positions are 0-indexed for both line and column.
9/// Column counts Unicode scalar values (chars), not bytes or graphemes.
10///
11/// # Example
12///
13/// ```
14/// use reovim_kernel::api::v1::*;
15///
16/// let pos = Position::new(5, 10);
17/// assert_eq!(pos.line, 5);
18/// assert_eq!(pos.column, 10);
19///
20/// // Positions are ordered by line first, then column
21/// assert!(Position::new(0, 5) < Position::new(1, 0));
22/// ```
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
24pub struct Position {
25    /// Line number (0-indexed).
26    pub line: usize,
27    /// Column number (0-indexed, counting chars).
28    pub column: usize,
29}
30
31impl Position {
32    /// Create a new position.
33    #[must_use]
34    pub const fn new(line: usize, column: usize) -> Self {
35        Self { line, column }
36    }
37
38    /// Create a position at the start of the buffer (0, 0).
39    #[must_use]
40    pub const fn origin() -> Self {
41        Self { line: 0, column: 0 }
42    }
43
44    /// Create a position at the start of a specific line.
45    #[must_use]
46    pub const fn line_start(line: usize) -> Self {
47        Self { line, column: 0 }
48    }
49}
50
51impl PartialOrd for Position {
52    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
53        Some(self.cmp(other))
54    }
55}
56
57impl Ord for Position {
58    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
59        match self.line.cmp(&other.line) {
60            std::cmp::Ordering::Equal => self.column.cmp(&other.column),
61            ord => ord,
62        }
63    }
64}
65
66impl std::fmt::Display for Position {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        write!(f, "{}:{}", self.line + 1, self.column + 1)
69    }
70}
71
72/// Cursor state within a buffer.
73///
74/// The cursor tracks the current position, an optional selection anchor,
75/// and a preferred column for vertical movement.
76///
77/// # Selection
78///
79/// When `anchor` is `Some`, a selection extends from `anchor` to `position`.
80/// The selection can be in either direction (anchor before or after position).
81///
82/// # Preferred Column
83///
84/// When moving vertically through lines of varying lengths, the cursor
85/// attempts to maintain its horizontal position. The `preferred_column`
86/// stores this target column.
87///
88/// # Example
89///
90/// ```
91/// use reovim_kernel::api::v1::*;
92///
93/// let mut cursor = Cursor::new(Position::new(0, 5));
94///
95/// // Start a selection
96/// cursor.start_selection();
97/// cursor.position = Position::new(0, 10);
98///
99/// // Get normalized bounds (start before end)
100/// let (start, end) = cursor.selection_bounds().unwrap();
101/// assert_eq!(start, Position::new(0, 5));
102/// assert_eq!(end, Position::new(0, 10));
103/// ```
104#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
105pub struct Cursor {
106    /// Current cursor position.
107    pub position: Position,
108    /// Selection anchor (if selection is active).
109    ///
110    /// When `Some`, a selection extends from `anchor` to `position`.
111    pub anchor: Option<Position>,
112    /// Preferred column for vertical movement (j/k).
113    ///
114    /// This preserves horizontal position when navigating through lines
115    /// of varying lengths.
116    pub preferred_column: Option<usize>,
117}
118
119impl Cursor {
120    /// Create a new cursor at the given position.
121    #[must_use]
122    pub const fn new(position: Position) -> Self {
123        Self {
124            position,
125            anchor: None,
126            preferred_column: None,
127        }
128    }
129
130    /// Create a cursor at the origin (0, 0).
131    #[must_use]
132    pub const fn origin() -> Self {
133        Self::new(Position::origin())
134    }
135
136    /// Start a selection at the current position.
137    pub const fn start_selection(&mut self) {
138        self.anchor = Some(self.position);
139    }
140
141    /// Clear the current selection.
142    pub const fn clear_selection(&mut self) {
143        self.anchor = None;
144    }
145
146    /// Check if a selection is active.
147    #[must_use]
148    pub const fn has_selection(&self) -> bool {
149        self.anchor.is_some()
150    }
151
152    /// Get the selection bounds (start, end) in document order.
153    ///
154    /// Returns `None` if no selection is active.
155    /// The returned positions are always ordered (start <= end).
156    #[must_use]
157    pub fn selection_bounds(&self) -> Option<(Position, Position)> {
158        self.anchor.map(|anchor| {
159            if anchor <= self.position {
160                (anchor, self.position)
161            } else {
162                (self.position, anchor)
163            }
164        })
165    }
166
167    /// Set preferred column to current column.
168    ///
169    /// Call this after horizontal movements to update the target column
170    /// for subsequent vertical movements.
171    pub const fn update_preferred_column(&mut self) {
172        self.preferred_column = Some(self.position.column);
173    }
174
175    /// Clear preferred column.
176    ///
177    /// Call this after horizontal movements that should reset
178    /// the vertical movement target.
179    pub const fn clear_preferred_column(&mut self) {
180        self.preferred_column = None;
181    }
182
183    /// Get the effective column for vertical movement.
184    ///
185    /// Returns the preferred column if set, otherwise the current column.
186    #[must_use]
187    pub fn effective_column(&self) -> usize {
188        self.preferred_column.unwrap_or(self.position.column)
189    }
190}