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/// The editor's cursor state — the single typed home for "where the
67/// caret(s) are".
68///
69/// **Phase 1 (today):** holds exactly one primary [`Position`]. The
70/// mutation surface (`primary` / `set_primary` / `map_primary`) is the same
71/// shape a single bare `Position` had, so callers route through it without
72/// behavioral change — but now there is ONE typed place that owns cursor
73/// state instead of a loose `Position` field beside an unused multi-caret
74/// [`Selection`]. That removes the dual-source-of-truth that would desync
75/// the moment multi-cursor lands.
76///
77/// **Phase 2 (later):** the inner representation grows to
78/// `{ primary: Position, secondaries: Vec<Position> }` (or wraps
79/// [`Selection`] directly). Because every caller already goes through this
80/// newtype's surface, that growth is a localized change here — not a
81/// fleet-wide rewrite of `self.cursor.line` reads.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
83pub struct Cursors {
84    primary: Position,
85}
86
87impl Cursors {
88    /// One cursor at `p`.
89    #[must_use]
90    pub const fn single(p: Position) -> Self {
91        Self { primary: p }
92    }
93
94    /// The primary caret position — the single read accessor every renderer
95    /// and motion path uses.
96    #[must_use]
97    pub const fn primary(&self) -> Position {
98        self.primary
99    }
100
101    /// Move the primary caret to `p`. The single write path — `set_cursor`
102    /// in the runtime routes here, keeping clamp + viewport-follow as the
103    /// only way the cursor changes.
104    pub const fn set_primary(&mut self, p: Position) {
105        self.primary = p;
106    }
107
108    /// Transform the primary caret in place.
109    pub fn map_primary(&mut self, f: impl FnOnce(Position) -> Position) {
110        self.primary = f(self.primary);
111    }
112
113    /// Number of carets — always 1 in phase 1; the seam multi-cursor grows
114    /// at.
115    #[must_use]
116    pub const fn count(&self) -> usize {
117        1
118    }
119}
120
121impl From<Position> for Cursors {
122    fn from(p: Position) -> Self {
123        Self::single(p)
124    }
125}
126
127/// A multi-cursor selection — ordered by primary first, then secondaries
128/// in insertion order.
129#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema)]
130pub struct Selection {
131    carets: Vec<Cursor>,
132    primary: usize,
133}
134
135impl Selection {
136    #[must_use]
137    pub fn single(cursor: Cursor) -> Self {
138        Self {
139            carets: vec![cursor],
140            primary: 0,
141        }
142    }
143
144    #[must_use]
145    pub fn carets(&self) -> &[Cursor] {
146        &self.carets
147    }
148
149    #[must_use]
150    pub fn primary(&self) -> &Cursor {
151        &self.carets[self.primary]
152    }
153
154    pub fn add(&mut self, c: Cursor) {
155        self.carets.push(c);
156    }
157
158    pub fn map_primary(&mut self, f: impl FnOnce(Cursor) -> Cursor) {
159        let idx = self.primary;
160        self.carets[idx] = f(self.carets[idx]);
161    }
162
163    pub fn map_all(&mut self, mut f: impl FnMut(Cursor) -> Cursor) {
164        for c in &mut self.carets {
165            *c = f(*c);
166        }
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn caret_is_empty_range() {
176        let c = Cursor::at(CaretId(0), Position::new(1, 3));
177        assert!(c.is_caret());
178        assert!(c.range().is_empty());
179    }
180
181    #[test]
182    fn extend_grows_but_anchor_stays() {
183        let c = Cursor::at(CaretId(0), Position::new(0, 0));
184        let grown = c.extend_to(Position::new(0, 5));
185        assert_eq!(grown.anchor, Position::new(0, 0));
186        assert_eq!(grown.head, Position::new(0, 5));
187    }
188
189    #[test]
190    fn collapse_makes_caret() {
191        let c = Cursor::new(CaretId(0), Position::new(0, 0), Position::new(0, 5));
192        let collapsed = c.collapse();
193        assert!(collapsed.is_caret());
194        assert_eq!(collapsed.head, Position::new(0, 5));
195    }
196
197    #[test]
198    fn primary_is_first_by_default() {
199        let s = Selection::single(Cursor::at(CaretId(0), Position::new(3, 2)));
200        assert_eq!(s.primary().head, Position::new(3, 2));
201    }
202
203    // ── Cursors newtype (phase-1 single-cursor home) ─────────────────────
204
205    #[test]
206    fn cursors_single_round_trips_position() {
207        let c = Cursors::single(Position::new(2, 5));
208        assert_eq!(c.primary(), Position::new(2, 5));
209        assert_eq!(c.count(), 1);
210    }
211
212    #[test]
213    fn cursors_set_primary_is_the_write_path() {
214        let mut c = Cursors::default();
215        assert_eq!(c.primary(), Position::ZERO);
216        c.set_primary(Position::new(4, 1));
217        assert_eq!(c.primary(), Position::new(4, 1));
218    }
219
220    #[test]
221    fn cursors_map_primary_transforms_in_place() {
222        let mut c = Cursors::single(Position::new(1, 1));
223        c.map_primary(|p| Position::new(p.line + 1, p.column + 2));
224        assert_eq!(c.primary(), Position::new(2, 3));
225    }
226
227    #[test]
228    fn cursors_from_position() {
229        let c: Cursors = Position::new(7, 7).into();
230        assert_eq!(c.primary(), Position::new(7, 7));
231    }
232}