Skip to main content

lattice_protocol/
selection.rs

1//! Selections.
2//!
3//! A `Selection` is an `(anchor, head)` pair plus a visual mode hint. The
4//! `head` is the active cursor end; the `anchor` is the other end of any
5//! visual extent. When `anchor == head` and `visual` is `None`, the selection
6//! is a degenerate cursor.
7//!
8//! `SelectionSet` is a non-empty set with one designated *primary* selection.
9//! v1 invariants assume exactly one selection; the set form is preserved so
10//! multi-cursor (post-1.0 per §5.2) is a clean extension.
11
12use serde::{Deserialize, Serialize};
13
14use crate::position::Position;
15
16/// One cursor or visual extent in a buffer.
17///
18/// `anchor` and `head` are *not* ordered: moving backwards in Visual mode puts
19/// `head` before `anchor`, and consumers normalise (`min`/`max`) when they need
20/// a span. With [`visual`](Self::visual) set, the extent follows vim's
21/// inclusive convention — Charwise covers the character *at* `head` too (a
22/// renderer converts to a half-open [`Range`](crate::Range) by extending
23/// `end` one character), Linewise covers whole lines regardless of the byte
24/// columns, and Blockwise covers the rectangle the two corners span.
25///
26/// # Examples
27///
28/// ```
29/// use lattice_protocol::{Position, Selection, VisualMode};
30///
31/// let caret = Selection::cursor(Position::new(4, 2));
32/// assert!(caret.is_cursor());
33///
34/// // A backwards charwise selection: head precedes anchor.
35/// let backwards = Selection {
36///     anchor: Position::new(0, 8),
37///     head: Position::new(0, 3),
38///     visual: Some(VisualMode::Charwise),
39/// };
40/// assert!(!backwards.is_cursor());
41/// assert!(backwards.head < backwards.anchor);
42/// ```
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44pub struct Selection {
45    /// The fixed end — where Visual mode was entered. Equals `head` for a
46    /// plain cursor.
47    pub anchor: Position,
48    /// The moving end: the cursor the user sees and motions move.
49    pub head: Position,
50    /// The visual-mode shape of the extent, or `None` outside Visual mode.
51    pub visual: Option<VisualMode>,
52}
53
54/// How a visual selection's two ends are interpreted — vim's `v`, `V` and
55/// `<C-v>`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
57pub enum VisualMode {
58    /// `v`: every character from one end to the other, both ends included.
59    Charwise,
60    /// `V`: every line from one end's line to the other's; columns ignored.
61    Linewise,
62    /// `<C-v>`: the rectangle whose opposite corners are the two ends.
63    Blockwise,
64}
65
66impl Selection {
67    /// A plain cursor at `at`: `anchor == head`, no visual extent.
68    pub const fn cursor(at: Position) -> Self {
69        Self {
70            anchor: at,
71            head: at,
72            visual: None,
73        }
74    }
75
76    /// `true` for a plain cursor: collapsed *and* not in Visual mode. A
77    /// one-character visual selection (`anchor == head`, `visual` set) is not
78    /// a cursor — it selects that character.
79    pub fn is_cursor(&self) -> bool {
80        self.anchor == self.head && self.visual.is_none()
81    }
82}
83
84/// A selection set. Always non-empty. Index `primary` points at the primary
85/// selection; in v1 the set has exactly one entry and `primary == 0`.
86///
87/// The fields are private so the invariant cannot be broken: every
88/// constructor yields at least one selection and an in-range primary, so
89/// [`Self::primary`] never panics.
90///
91/// # Examples
92///
93/// ```
94/// use lattice_protocol::{Position, Selection, SelectionSet};
95///
96/// let mut set = SelectionSet::default(); // one cursor at the origin
97/// assert_eq!(set.all().len(), 1);
98/// assert_eq!(set.primary().head, Position::ZERO);
99///
100/// set.replace_primary(Selection::cursor(Position::new(3, 0)));
101/// assert_eq!(set.primary().head.line, 3);
102///
103/// // Rebuilding from parts repairs what would break the invariant.
104/// let repaired = SelectionSet::from_parts(vec![], 5);
105/// assert_eq!(repaired, SelectionSet::cursor_at_origin());
106/// let clamped = SelectionSet::from_parts(vec![Selection::cursor(Position::ZERO)], 5);
107/// assert_eq!(clamped.primary_index(), 0);
108/// ```
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct SelectionSet {
111    selections: Vec<Selection>,
112    primary: usize,
113}
114
115impl SelectionSet {
116    /// A set holding just `selection`, which is primary.
117    pub fn single(selection: Selection) -> Self {
118        Self {
119            selections: vec![selection],
120            primary: 0,
121        }
122    }
123
124    /// One plain cursor at [`Position::ZERO`] — also the [`Default`].
125    pub fn cursor_at_origin() -> Self {
126        Self::single(Selection::cursor(Position::ZERO))
127    }
128
129    /// Reconstruct a set from its parts — the counterpart to [`Self::all`] +
130    /// [`Self::primary_index`], used to rebuild a `SelectionSet` that was
131    /// projected into an owned form (e.g. the plugin-host WIT boundary). The
132    /// non-empty invariant is preserved: an empty `selections` collapses to a
133    /// single origin cursor, and `primary` is clamped into range.
134    pub fn from_parts(selections: Vec<Selection>, primary: usize) -> Self {
135        if selections.is_empty() {
136            return Self::cursor_at_origin();
137        }
138        let primary = primary.min(selections.len() - 1);
139        Self {
140            selections,
141            primary,
142        }
143    }
144
145    /// The primary selection — the one single-cursor code acts on.
146    pub fn primary(&self) -> &Selection {
147        // SAFETY-equivalent: every constructor and mutator preserves the
148        // non-empty invariant, so primary is always a valid index.
149        &self.selections[self.primary]
150    }
151
152    /// Mutable access to the primary selection.
153    pub fn primary_mut(&mut self) -> &mut Selection {
154        &mut self.selections[self.primary]
155    }
156
157    /// Every selection, in stored order (never empty).
158    pub fn all(&self) -> &[Selection] {
159        &self.selections
160    }
161
162    /// Index of the primary within [`Self::all`].
163    pub fn primary_index(&self) -> usize {
164        self.primary
165    }
166
167    /// Overwrite the primary selection in place; the others are untouched.
168    pub fn replace_primary(&mut self, selection: Selection) {
169        self.selections[self.primary] = selection;
170    }
171}
172
173impl Default for SelectionSet {
174    fn default() -> Self {
175        Self::cursor_at_origin()
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    #![allow(clippy::unwrap_used, clippy::panic)]
182    use super::*;
183
184    #[test]
185    fn cursor_constructor_collapses_anchor_and_head() {
186        let p = Position::new(2, 4);
187        let sel = Selection::cursor(p);
188        assert_eq!(sel.anchor, p);
189        assert_eq!(sel.head, p);
190        assert_eq!(sel.visual, None);
191        assert!(sel.is_cursor());
192    }
193
194    #[test]
195    fn selection_with_distinct_endpoints_is_not_a_cursor() {
196        let sel = Selection {
197            anchor: Position::new(0, 0),
198            head: Position::new(0, 3),
199            visual: None,
200        };
201        assert!(!sel.is_cursor());
202    }
203
204    #[test]
205    fn selection_with_visual_extent_is_not_a_cursor_even_when_collapsed() {
206        let sel = Selection {
207            anchor: Position::ZERO,
208            head: Position::ZERO,
209            visual: Some(VisualMode::Charwise),
210        };
211        assert!(!sel.is_cursor());
212    }
213
214    #[test]
215    fn selection_set_default_is_a_single_origin_cursor() {
216        let s = SelectionSet::default();
217        assert_eq!(s.all().len(), 1);
218        assert_eq!(s.primary_index(), 0);
219        assert!(s.primary().is_cursor());
220        assert_eq!(s.primary().head, Position::ZERO);
221    }
222
223    #[test]
224    fn from_parts_preserves_selections_and_primary() {
225        let a = Selection::cursor(Position::new(0, 0));
226        let b = Selection::cursor(Position::new(2, 3));
227        let s = SelectionSet::from_parts(vec![a, b], 1);
228        assert_eq!(s.all().len(), 2);
229        assert_eq!(s.primary_index(), 1);
230        assert_eq!(s.primary(), &b);
231    }
232
233    #[test]
234    fn from_parts_clamps_out_of_range_primary_and_repairs_empty() {
235        // Out-of-range primary clamps to the last selection (invariant kept).
236        let only = Selection::cursor(Position::new(1, 1));
237        let clamped = SelectionSet::from_parts(vec![only], 9);
238        assert_eq!(clamped.primary_index(), 0);
239        // Empty input collapses to a single origin cursor (never empty).
240        let repaired = SelectionSet::from_parts(vec![], 3);
241        assert_eq!(repaired.all().len(), 1);
242        assert_eq!(repaired.primary_index(), 0);
243        assert!(repaired.primary().is_cursor());
244    }
245
246    #[test]
247    fn selection_set_single_uses_provided_selection() {
248        let sel = Selection::cursor(Position::new(5, 6));
249        let s = SelectionSet::single(sel);
250        assert_eq!(s.primary(), &sel);
251        assert_eq!(s.all().len(), 1);
252    }
253
254    #[test]
255    fn replace_primary_swaps_in_place_without_changing_count() {
256        let mut s = SelectionSet::default();
257        let new_sel = Selection::cursor(Position::new(7, 0));
258        s.replace_primary(new_sel);
259        assert_eq!(s.primary(), &new_sel);
260        assert_eq!(s.all().len(), 1);
261        assert_eq!(s.primary_index(), 0);
262    }
263
264    #[test]
265    fn primary_mut_allows_in_place_mutation() {
266        let mut s = SelectionSet::default();
267        s.primary_mut().head = Position::new(0, 4);
268        assert_eq!(s.primary().head, Position::new(0, 4));
269    }
270
271    #[test]
272    fn visual_modes_are_distinct() {
273        // Documents the intent: charwise / linewise / blockwise are not equal.
274        assert_ne!(Some(VisualMode::Charwise), Some(VisualMode::Linewise));
275        assert_ne!(Some(VisualMode::Linewise), Some(VisualMode::Blockwise));
276    }
277}