Skip to main content

strop_core/
selection.rs

1//! One selection model for everything (0014 wave 2): normal mode is a
2//! collapsed selection, visual mode is a stretched one, multicursor is
3//! several. Cursor / anchor / extra-cursors used to be three fields that
4//! could disagree; the set owns them with the invariants in one place.
5
6/// One selection: the anchor sits, the head moves. Collapsed (equal) is
7/// a cursor. Byte offsets, always char boundaries.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub struct Selection {
10    pub anchor: usize,
11    pub head: usize,
12}
13
14impl Selection {
15    pub fn cursor(at: usize) -> Self {
16        Self {
17            anchor: at,
18            head: at,
19        }
20    }
21
22    pub fn collapsed(self) -> bool {
23        self.anchor == self.head
24    }
25
26    /// (start, end) ordered.
27    pub fn range(self) -> (usize, usize) {
28        (self.anchor.min(self.head), self.anchor.max(self.head))
29    }
30}
31
32/// The editor's selections: a primary plus zero or more extras.
33/// Invariants (enforced by `normalize`): extras sorted, deduped, none
34/// equal to the primary's head.
35#[derive(Debug, Clone)]
36pub struct SelectionSet {
37    primary: Selection,
38    extras: Vec<Selection>,
39}
40
41impl Default for SelectionSet {
42    fn default() -> Self {
43        Self {
44            primary: Selection::cursor(0),
45            extras: Vec::new(),
46        }
47    }
48}
49
50impl SelectionSet {
51    pub fn primary(&self) -> Selection {
52        self.primary
53    }
54
55    /// Every head, primary first (0013 §3 cascade order).
56    pub fn heads(&self) -> Vec<usize> {
57        std::iter::once(self.primary.head)
58            .chain(self.extras.iter().map(|s| s.head))
59            .collect()
60    }
61
62    pub fn extra_heads(&self) -> &[Selection] {
63        &self.extras
64    }
65
66    pub fn count(&self) -> usize {
67        1 + self.extras.len()
68    }
69
70    /// Move the primary head (motions, edits).
71    pub fn set_head(&mut self, head: usize) {
72        self.primary.head = head;
73    }
74
75    /// Move head and anchor together (leaving visual mode, plain moves).
76    pub fn collapse_primary(&mut self, at: usize) {
77        self.primary = Selection::cursor(at);
78    }
79
80    /// Enter/extend visual: the anchor stays, the head walks.
81    pub fn stretch_primary(&mut self, anchor: usize, head: usize) {
82        self.primary = Selection { anchor, head };
83    }
84
85    /// `Q`: drop the extra under the primary, else plant one there.
86    pub fn toggle_extra(&mut self) {
87        if let Some(i) = self.extras.iter().position(|s| s.head == self.primary.head) {
88            self.extras.remove(i);
89        } else {
90            self.extras.push(self.primary);
91        }
92        self.normalize();
93    }
94
95    /// Plant an extra cursor (both ends at `at`). Never on the
96    /// primary's head — `Space c` and match-planting skip that spot;
97    /// stacking on the primary is `Q`'s job (toggle_extra).
98    pub fn plant_extra(&mut self, at: usize) {
99        if at != self.primary.head && !self.extras.iter().any(|s| s.head == at) {
100            self.extras.push(Selection::cursor(at));
101        }
102        self.normalize();
103    }
104
105    /// Sorted, deduped. An extra MAY sit on the primary's head — `Q`
106    /// plants exactly there, then a motion walks them apart (0013).
107    pub fn normalize(&mut self) {
108        self.extras.sort_by_key(|s| s.head);
109        self.extras.dedup();
110    }
111
112    /// Esc: extras die, primary stays.
113    pub fn collapse_extras(&mut self) {
114        self.extras.clear();
115    }
116
117    /// Replace the extras wholesale — the motion cascade replants
118    /// computed heads, and stacked-on-primary extras survive (they were
119    /// planted by Q on purpose, 0013 §3).
120    pub fn set_extras(&mut self, heads: impl IntoIterator<Item = usize>) {
121        self.extras = heads.into_iter().map(Selection::cursor).collect();
122        self.normalize();
123    }
124
125    /// After an edit shifted bytes: remap every head/anchor by delta at
126    /// a point (the mirrored-edit cascade's bookkeeping).
127    pub fn remap(&mut self, at: usize, delta: isize) {
128        let shift = |p: &mut usize| {
129            if *p >= at {
130                *p = (*p as isize + delta).max(0) as usize;
131            }
132        };
133        shift(&mut self.primary.head);
134        shift(&mut self.primary.anchor);
135        for s in &mut self.extras {
136            shift(&mut s.head);
137            shift(&mut s.anchor);
138        }
139        self.normalize();
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn invariants_hold() {
149        let mut s = SelectionSet::default();
150        s.set_head(5);
151        s.toggle_extra(); // Q plants ON the primary (0013 semantics)
152        assert_eq!(s.count(), 2);
153        s.plant_extra(2);
154        s.plant_extra(2); // dup dies
155        assert_eq!(s.count(), 3);
156        assert_eq!(s.heads(), vec![5, 2, 5]);
157        s.toggle_extra(); // an extra sits under the primary → drops it
158        assert_eq!(s.count(), 2);
159        assert_eq!(s.heads(), vec![5, 2]);
160        s.collapse_extras();
161        assert_eq!(s.count(), 1);
162    }
163
164    #[test]
165    fn remap_shifts_past_the_edit() {
166        let mut s = SelectionSet::default();
167        s.collapse_primary(10);
168        s.plant_extra(20);
169        s.remap(5, 3); // 3 bytes inserted at 5
170        assert_eq!(s.heads(), vec![13, 23]);
171        s.remap(5, -3);
172        assert_eq!(s.heads(), vec![10, 20]);
173    }
174}