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}