Skip to main content

escriba_core/
selection.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4use crate::id::CaretId;
5use crate::position::Position;
6use crate::range::Range;
7
8/// A single cursor — anchor + head. Visual selection is the range between
9/// them. When equal, it's just an insertion point.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, JsonSchema)]
11pub struct Cursor {
12    pub id: CaretId,
13    pub anchor: Position,
14    pub head: Position,
15}
16
17impl Cursor {
18    #[must_use]
19    pub const fn new(id: CaretId, anchor: Position, head: Position) -> Self {
20        Self { id, anchor, head }
21    }
22
23    #[must_use]
24    pub fn at(id: CaretId, p: Position) -> Self {
25        Self {
26            id,
27            anchor: p,
28            head: p,
29        }
30    }
31
32    /// The selected range — normalized so start ≤ end.
33    #[must_use]
34    pub fn range(self) -> Range {
35        Range::new(self.anchor, self.head).normalized()
36    }
37
38    #[must_use]
39    pub const fn is_caret(self) -> bool {
40        // Cannot use == on Position in const fn without #![feature(const_trait_impl)],
41        // so inline the equality check.
42        self.anchor.line == self.head.line && self.anchor.column == self.head.column
43    }
44
45    /// Move the head to `p`, leaving the anchor fixed — grows the selection.
46    #[must_use]
47    pub const fn extend_to(self, p: Position) -> Self {
48        Self {
49            id: self.id,
50            anchor: self.anchor,
51            head: p,
52        }
53    }
54
55    /// Collapse to the head — becomes a pure caret.
56    #[must_use]
57    pub const fn collapse(self) -> Self {
58        Self {
59            id: self.id,
60            anchor: self.head,
61            head: self.head,
62        }
63    }
64}
65
66/// A multi-cursor selection — ordered by primary first, then secondaries
67/// in insertion order.
68#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
69pub struct Selection {
70    carets: Vec<Cursor>,
71    primary: usize,
72}
73
74impl Selection {
75    #[must_use]
76    pub fn single(cursor: Cursor) -> Self {
77        Self {
78            carets: vec![cursor],
79            primary: 0,
80        }
81    }
82
83    #[must_use]
84    pub fn carets(&self) -> &[Cursor] {
85        &self.carets
86    }
87
88    #[must_use]
89    pub fn primary(&self) -> &Cursor {
90        &self.carets[self.primary]
91    }
92
93    pub fn add(&mut self, c: Cursor) {
94        self.carets.push(c);
95    }
96
97    pub fn map_primary(&mut self, f: impl FnOnce(Cursor) -> Cursor) {
98        let idx = self.primary;
99        self.carets[idx] = f(self.carets[idx]);
100    }
101
102    pub fn map_all(&mut self, mut f: impl FnMut(Cursor) -> Cursor) {
103        for c in &mut self.carets {
104            *c = f(*c);
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn caret_is_empty_range() {
115        let c = Cursor::at(CaretId(0), Position::new(1, 3));
116        assert!(c.is_caret());
117        assert!(c.range().is_empty());
118    }
119
120    #[test]
121    fn extend_grows_but_anchor_stays() {
122        let c = Cursor::at(CaretId(0), Position::new(0, 0));
123        let grown = c.extend_to(Position::new(0, 5));
124        assert_eq!(grown.anchor, Position::new(0, 0));
125        assert_eq!(grown.head, Position::new(0, 5));
126    }
127
128    #[test]
129    fn collapse_makes_caret() {
130        let c = Cursor::new(CaretId(0), Position::new(0, 0), Position::new(0, 5));
131        let collapsed = c.collapse();
132        assert!(collapsed.is_caret());
133        assert_eq!(collapsed.head, Position::new(0, 5));
134    }
135
136    #[test]
137    fn primary_is_first_by_default() {
138        let s = Selection::single(Cursor::at(CaretId(0), Position::new(3, 2)));
139        assert_eq!(s.primary().head, Position::new(3, 2));
140    }
141}