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}