longcipher_leptos_components/components/editor/
cursor.rs

1//! Cursor management for the editor
2//!
3//! Handles cursor positioning, movement, and multi-cursor support.
4
5use serde::{Deserialize, Serialize};
6
7/// A position in the document (line and column, both 0-indexed).
8#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
9pub struct CursorPosition {
10    /// Line number (0-indexed)
11    pub line: usize,
12    /// Column number (0-indexed, in characters)
13    pub column: usize,
14}
15
16impl CursorPosition {
17    /// Create a new cursor position.
18    #[must_use]
19    pub const fn new(line: usize, column: usize) -> Self {
20        Self { line, column }
21    }
22
23    /// Create a position at the start of the document.
24    #[must_use]
25    pub const fn zero() -> Self {
26        Self { line: 0, column: 0 }
27    }
28
29    /// Check if this position is before another position.
30    #[must_use]
31    pub fn is_before(&self, other: &Self) -> bool {
32        self.line < other.line || (self.line == other.line && self.column < other.column)
33    }
34
35    /// Get the minimum (earlier) of two positions.
36    #[must_use]
37    pub fn min(&self, other: &Self) -> Self {
38        if self.is_before(other) { *self } else { *other }
39    }
40
41    /// Get the maximum (later) of two positions.
42    #[must_use]
43    pub fn max(&self, other: &Self) -> Self {
44        if self.is_before(other) { *other } else { *self }
45    }
46}
47
48impl PartialOrd for CursorPosition {
49    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
50        Some(self.cmp(other))
51    }
52}
53
54impl Ord for CursorPosition {
55    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
56        match self.line.cmp(&other.line) {
57            std::cmp::Ordering::Equal => self.column.cmp(&other.column),
58            ord => ord,
59        }
60    }
61}
62
63/// A cursor in the editor, with head and anchor for selection.
64#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
65pub struct Cursor {
66    /// The head (active end) of the cursor/selection
67    pub head: CursorPosition,
68    /// The anchor (fixed end) of the cursor/selection
69    pub anchor: CursorPosition,
70    /// Preferred column for vertical movement (remembers column when moving through short lines)
71    pub preferred_column: Option<usize>,
72}
73
74impl Cursor {
75    /// Create a new cursor at a position (no selection).
76    #[must_use]
77    pub const fn new(position: CursorPosition) -> Self {
78        Self {
79            head: position,
80            anchor: position,
81            preferred_column: None,
82        }
83    }
84
85    /// Create a cursor at the start of the document.
86    #[must_use]
87    pub const fn zero() -> Self {
88        Self::new(CursorPosition::zero())
89    }
90
91    /// Create a cursor with a selection range.
92    #[must_use]
93    pub const fn with_selection(head: CursorPosition, anchor: CursorPosition) -> Self {
94        Self {
95            head,
96            anchor,
97            preferred_column: None,
98        }
99    }
100
101    /// Check if the cursor has an active selection.
102    #[must_use]
103    pub fn has_selection(&self) -> bool {
104        self.head != self.anchor
105    }
106
107    /// Get the selection start (minimum position).
108    #[must_use]
109    pub fn selection_start(&self) -> CursorPosition {
110        self.head.min(self.anchor)
111    }
112
113    /// Get the selection end (maximum position).
114    #[must_use]
115    pub fn selection_end(&self) -> CursorPosition {
116        self.head.max(self.anchor)
117    }
118
119    /// Collapse selection by moving anchor to head.
120    pub fn collapse(&mut self) {
121        self.anchor = self.head;
122    }
123
124    /// Move the cursor to a new position, optionally extending selection.
125    pub fn move_to(&mut self, position: CursorPosition, extend_selection: bool) {
126        self.head = position;
127        if !extend_selection {
128            self.anchor = position;
129        }
130    }
131
132    /// Set the preferred column for vertical movement.
133    pub fn set_preferred_column(&mut self, column: usize) {
134        self.preferred_column = Some(column);
135    }
136
137    /// Clear the preferred column.
138    pub fn clear_preferred_column(&mut self) {
139        self.preferred_column = None;
140    }
141}
142
143/// A set of cursors for multi-cursor support.
144#[derive(Debug, Clone, Default, Serialize, Deserialize)]
145pub struct CursorSet {
146    /// All active cursors (primary cursor is first)
147    cursors: Vec<Cursor>,
148}
149
150impl CursorSet {
151    /// Create a new cursor set with a single cursor.
152    #[must_use]
153    pub fn new(cursor: Cursor) -> Self {
154        Self {
155            cursors: vec![cursor],
156        }
157    }
158
159    /// Get the primary (first) cursor.
160    ///
161    /// # Panics
162    ///
163    /// Panics if the cursor set is empty.
164    #[must_use]
165    pub fn primary(&self) -> &Cursor {
166        self.cursors
167            .first()
168            .expect("CursorSet must have at least one cursor")
169    }
170
171    /// Get mutable reference to the primary cursor.
172    ///
173    /// # Panics
174    ///
175    /// Panics if the cursor set is empty.
176    pub fn primary_mut(&mut self) -> &mut Cursor {
177        self.cursors
178            .first_mut()
179            .expect("CursorSet must have at least one cursor")
180    }
181
182    /// Get all cursors.
183    #[must_use]
184    pub fn all(&self) -> &[Cursor] {
185        &self.cursors
186    }
187
188    /// Check if there are multiple cursors.
189    #[must_use]
190    pub fn is_multi(&self) -> bool {
191        self.cursors.len() > 1
192    }
193
194    /// Add a new cursor.
195    pub fn add(&mut self, cursor: Cursor) {
196        self.cursors.push(cursor);
197        self.merge_overlapping();
198    }
199
200    /// Remove all cursors except the primary.
201    pub fn collapse_to_primary(&mut self) {
202        if self.cursors.len() > 1 {
203            let primary = self.cursors[0];
204            self.cursors.clear();
205            self.cursors.push(primary);
206        }
207    }
208
209    /// Merge overlapping cursors/selections.
210    fn merge_overlapping(&mut self) {
211        if self.cursors.len() <= 1 {
212            return;
213        }
214
215        // Sort by selection start
216        self.cursors.sort_by(|a, b| {
217            let a_start = a.selection_start();
218            let b_start = b.selection_start();
219            a_start.cmp(&b_start)
220        });
221
222        let mut merged = Vec::with_capacity(self.cursors.len());
223        merged.push(self.cursors[0]);
224
225        for cursor in &self.cursors[1..] {
226            let last = merged.last_mut().expect("merged is not empty");
227            let last_end = last.selection_end();
228            let cursor_start = cursor.selection_start();
229
230            if cursor_start <= last_end {
231                // Overlapping - merge by extending the last cursor
232                let cursor_end = cursor.selection_end();
233                if cursor_end > last_end {
234                    if cursor.head > cursor.anchor {
235                        last.head = cursor.head;
236                    } else {
237                        last.anchor = cursor_end;
238                    }
239                }
240            } else {
241                // Not overlapping - add as new cursor
242                merged.push(*cursor);
243            }
244        }
245
246        self.cursors = merged;
247    }
248}
249
250#[cfg(test)]
251mod tests {
252    use super::*;
253
254    #[test]
255    fn test_cursor_position_ordering() {
256        let a = CursorPosition::new(0, 5);
257        let b = CursorPosition::new(1, 0);
258        let c = CursorPosition::new(1, 3);
259
260        assert!(a.is_before(&b));
261        assert!(b.is_before(&c));
262        assert!(!c.is_before(&a));
263    }
264
265    #[test]
266    fn test_cursor_selection() {
267        let cursor = Cursor::with_selection(CursorPosition::new(1, 5), CursorPosition::new(0, 3));
268
269        assert!(cursor.has_selection());
270        assert_eq!(cursor.selection_start(), CursorPosition::new(0, 3));
271        assert_eq!(cursor.selection_end(), CursorPosition::new(1, 5));
272    }
273
274    #[test]
275    fn test_cursor_set_merge() {
276        let mut set = CursorSet::new(Cursor::with_selection(
277            CursorPosition::new(0, 0),
278            CursorPosition::new(0, 2),
279        ));
280        set.add(Cursor::new(CursorPosition::new(0, 1)));
281        set.add(Cursor::new(CursorPosition::new(2, 0)));
282
283        // First two should merge since they overlap
284        assert_eq!(set.all().len(), 2);
285    }
286}