Skip to main content

scrive_core/
selection.rs

1//! Selections and the multi-cursor set.
2//!
3//! A [`Selection`] is a byte-offset range with a direction: `start <= end`
4//! always, and `reversed` says which end the caret (the *head*) is on, so the
5//! set is always stored in document order while the caret can face either way.
6//! Empty selections (`start == end`) are bare carets.
7//!
8//! A [`SelectionSet`] holds one or more disjoint selections, kept sorted and
9//! merged. Multi-cursor is a first-class model concept: the gestures that add
10//! cursors live in the UI layer, and this is the data they act on. Selections
11//! rebase through every edit's [`Patch`] eagerly, so a caret is always the
12//! current position and never a stale copy of an offset from before the edit.
13
14use std::cell::RefCell;
15
16use crate::coords::Bias;
17use crate::patch::Patch;
18
19/// Reused per-thread scratch for [`SelectionSet::rebase`]'s batch map: the
20/// `(offset, bias)` queries and their mapped results.
21type RebaseScratch = (Vec<(u32, Bias)>, Vec<u32>);
22
23/// Identity for a selection, monotonic within a [`SelectionSet`]. Larger ids are
24/// newer; used to pick the newest (autoscroll target) and oldest (Escape).
25#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
26pub struct SelectionId(pub usize);
27
28/// A directional byte-offset range. `start <= end` is an invariant; `reversed`
29/// records whether the caret is at `start` (`true`) or `end` (`false`).
30#[derive(Copy, Clone, PartialEq, Eq, Debug)]
31pub struct Selection {
32    /// Stable identity within its set.
33    pub id: SelectionId,
34    start: u32,
35    end: u32,
36    reversed: bool,
37    /// Goal for vertical movement: a **display cell** — the caret's visual
38    /// column, kept through tab stops and collapsed folds. Cleared by any
39    /// horizontal move or edit.
40    pub goal: Option<u32>,
41}
42
43impl Selection {
44    /// A caret (empty selection) with the given id at `offset`.
45    #[must_use]
46    pub fn caret(id: SelectionId, offset: u32) -> Self {
47        Self { id, start: offset, end: offset, reversed: false, goal: None }
48    }
49
50    /// A selection from `anchor` to `head` (either order); `reversed` is derived
51    /// so the caret sits at `head`.
52    #[must_use]
53    pub fn from_anchor(id: SelectionId, anchor: u32, head: u32) -> Self {
54        Self {
55            id,
56            start: anchor.min(head),
57            end: anchor.max(head),
58            reversed: head < anchor,
59            goal: None,
60        }
61    }
62
63    /// Lower bound (inclusive).
64    #[must_use]
65    pub fn start(&self) -> u32 {
66        self.start
67    }
68
69    /// Upper bound (exclusive-ish — the position past the selection).
70    #[must_use]
71    pub fn end(&self) -> u32 {
72        self.end
73    }
74
75    /// The moving end (where the caret is).
76    #[must_use]
77    pub fn head(&self) -> u32 {
78        if self.reversed {
79            self.start
80        } else {
81            self.end
82        }
83    }
84
85    /// The fixed end (the anchor).
86    #[must_use]
87    pub fn tail(&self) -> u32 {
88        if self.reversed {
89            self.end
90        } else {
91            self.start
92        }
93    }
94
95    /// Whether this is a bare caret.
96    #[must_use]
97    pub fn is_empty(&self) -> bool {
98        self.start == self.end
99    }
100
101    /// Move the head to `offset`, keeping the tail fixed (extends/shrinks the
102    /// selection, flipping direction if the head crosses the tail). Clears the
103    /// goal column.
104    pub fn set_head(&mut self, offset: u32) {
105        let tail = self.tail();
106        self.start = tail.min(offset);
107        self.end = tail.max(offset);
108        self.reversed = offset < tail;
109        self.goal = None;
110    }
111
112    /// Collapse to a caret at the head.
113    pub fn collapse_to_head(&mut self) {
114        let h = self.head();
115        self.start = h;
116        self.end = h;
117        self.reversed = false;
118    }
119
120    /// Collapse to a caret at `offset`, keeping this selection's id and clearing
121    /// the goal column. The movement layer's "plain move" primitive.
122    pub fn move_to_caret(&mut self, offset: u32) {
123        self.start = offset;
124        self.end = offset;
125        self.reversed = false;
126        self.goal = None;
127    }
128}
129
130/// A non-empty, sorted, disjoint set of selections. Merged after every
131/// mutation so no two selections overlap.
132#[derive(Clone, Debug)]
133pub struct SelectionSet {
134    selections: Vec<Selection>,
135    next_id: usize,
136}
137
138impl SelectionSet {
139    /// A set with a single caret at `offset`.
140    #[must_use]
141    pub fn new(offset: u32) -> Self {
142        Self { selections: vec![Selection::caret(SelectionId(0), offset)], next_id: 1 }
143    }
144
145    /// A set of carets at the given offsets (merged). Used by the verbs layer to
146    /// place carets after an edit. Panics if `offsets` is empty — a set is never
147    /// empty.
148    #[must_use]
149    pub fn from_offsets(offsets: &[u32]) -> Self {
150        assert!(!offsets.is_empty(), "a selection set is never empty");
151        let mut set = Self { selections: Vec::new(), next_id: 0 };
152        for &o in offsets {
153            let id = set.mint();
154            set.selections.push(Selection::caret(id, o));
155        }
156        set.normalize();
157        set
158    }
159
160    /// Build a set from `(anchor, head)` ranges (any order); the range at index
161    /// `newest` becomes the primary (largest id) — the autoscroll target. Used to
162    /// install a column (box) selection. Panics if `ranges` is empty or
163    /// `newest` is out of bounds.
164    #[must_use]
165    pub fn from_ranges(ranges: &[(u32, u32)], newest: usize) -> Self {
166        assert!(!ranges.is_empty(), "a selection set is never empty");
167        assert!(newest < ranges.len(), "newest index out of bounds");
168        let mut set = Self { selections: Vec::new(), next_id: 0 };
169        // Assign ids so `newest` gets the largest: push the others first, it last.
170        for (i, &(anchor, head)) in ranges.iter().enumerate() {
171            if i != newest {
172                let id = set.mint();
173                set.selections.push(Selection::from_anchor(id, anchor, head));
174            }
175        }
176        let id = set.mint();
177        let (anchor, head) = ranges[newest];
178        set.selections.push(Selection::from_anchor(id, anchor, head));
179        set.normalize();
180        set
181    }
182
183    /// The selections, in document order.
184    #[must_use]
185    pub fn all(&self) -> &[Selection] {
186        &self.selections
187    }
188
189    /// How many selections (always ≥ 1).
190    #[must_use]
191    pub fn len(&self) -> usize {
192        self.selections.len()
193    }
194
195    /// Always `false` — a set is never empty. Present for lint-friendliness
196    /// alongside [`SelectionSet::len`].
197    #[must_use]
198    pub fn is_empty(&self) -> bool {
199        false
200    }
201
202    /// The newest selection (largest id) — the autoscroll / primary target.
203    #[must_use]
204    pub fn newest(&self) -> &Selection {
205        self.selections.iter().max_by_key(|s| s.id).expect("set is non-empty")
206    }
207
208    /// Mint the next selection id.
209    fn mint(&mut self) -> SelectionId {
210        let id = SelectionId(self.next_id);
211        self.next_id += 1;
212        id
213    }
214
215    /// Add a caret at `offset`, merging into any selection it touches.
216    pub fn add_caret(&mut self, offset: u32) {
217        let id = self.mint();
218        self.selections.push(Selection::caret(id, offset));
219        self.normalize();
220    }
221
222    /// Add a selection from `anchor` to `head`, merging as needed.
223    pub fn add_selection(&mut self, anchor: u32, head: u32) {
224        let id = self.mint();
225        self.selections.push(Selection::from_anchor(id, anchor, head));
226        self.normalize();
227    }
228
229    /// Replace the whole set with a single selection (Escape collapses to the
230    /// oldest; here the caller supplies it).
231    pub fn set_single(&mut self, sel: Selection) {
232        self.selections = vec![sel];
233    }
234
235    /// Collapse to just the newest selection, as a caret at its head (the
236    /// standard Escape behavior for multi-cursor).
237    pub fn collapse_to_newest(&mut self) {
238        let mut n = *self.newest();
239        n.collapse_to_head();
240        self.selections = vec![n];
241    }
242
243    /// Collapse to just the oldest selection (smallest id), as a caret at its
244    /// head — the multi-cursor Escape that keeps the *primary* cursor.
245    /// With a single ranged selection this simply deselects it to a caret.
246    pub fn collapse_to_primary(&mut self) {
247        let mut primary = *self.selections.iter().min_by_key(|s| s.id).expect("set is non-empty");
248        primary.collapse_to_head();
249        self.selections = vec![primary];
250    }
251
252    /// Mutate every selection through `f`, then re-merge. The building block for
253    /// movement.
254    pub fn map_each(&mut self, mut f: impl FnMut(&mut Selection)) {
255        for s in &mut self.selections {
256            f(s);
257        }
258        self.normalize();
259    }
260
261    /// Rebase every selection through an edit's [`Patch`] — the eager mover that
262    /// keeps every endpoint current as text is inserted and deleted. Endpoints
263    /// use `Left` bias for the start and `Right` for the end so a selection
264    /// grows over text typed at its edges; carets follow the insertion.
265    /// Re-merges afterward.
266    pub fn rebase(&mut self, patch: &Patch) {
267        // One batch map through the patch instead of a per-selection loop of
268        // `map_offset` (which restarts the edit scan for every endpoint —
269        // O(carets²) when a document-scale multi-cursor edit produces a
270        // carets-sized patch). Each selection contributes one query (caret) or
271        // two (range); results are consumed back in the same order.
272        // The `queries`/`mapped` scratch is a thread-local reused across every
273        // rebase, so a committed edit costs no fresh allocation for the batch map
274        // (the mover runs on every keystroke that changes text, plus undo/redo).
275        // Thread-local rather than a `SelectionSet` field, so it adds no per-set
276        // memory and `clone` is unaffected; cleared before use and left empty at
277        // rest so the retained buffer doesn't pin memory between edits.
278        thread_local! {
279            static SCRATCH: RefCell<RebaseScratch> =
280                const { RefCell::new((Vec::new(), Vec::new())) };
281        }
282        SCRATCH.with(|cell| {
283            let (queries, mapped) = &mut *cell.borrow_mut();
284            queries.clear();
285            for s in &self.selections {
286                if s.start == s.end {
287                    // A caret follows the insertion (Right bias) so it stays a
288                    // caret rather than splitting into a selection.
289                    queries.push((s.start, Bias::Right));
290                } else {
291                    queries.push((s.start, Bias::Left));
292                    queries.push((s.end, Bias::Right));
293                }
294            }
295            patch.map_many(&queries[..], mapped);
296            let mut mi = 0;
297            for s in &mut self.selections {
298                if s.start == s.end {
299                    let o = mapped[mi];
300                    mi += 1;
301                    s.start = o;
302                    s.end = o;
303                } else {
304                    let (ns, ne) = (mapped[mi], mapped[mi + 1]);
305                    mi += 2;
306                    s.start = ns.min(ne);
307                    s.end = ne.max(ns);
308                }
309                s.goal = None;
310            }
311            queries.clear(); // don't pin the batch's memory between edits
312        });
313        self.normalize();
314    }
315
316    /// Sort by start and merge overlapping / touching-a-caret selections.
317    ///
318    /// Merge rule: merge on overlap, shared start, or a caret touching a
319    /// boundary; two *non-empty* selections that merely touch do NOT merge.
320    /// The surviving selection keeps the newest member's id, direction, and
321    /// goal.
322    fn normalize(&mut self) {
323        self.selections.sort_by_key(|s| (s.start, s.end));
324        // In-place compaction (dedup-style): the write cursor `w` keeps the merged
325        // prefix; each later selection either merges into `selections[w]` or is
326        // moved up to `selections[w + 1]`. Nothing merges in the common 1-few-caret
327        // case, so this allocates nothing — versus building a fresh `merged` Vec
328        // every time, and `normalize` rides both caret movement AND every edit.
329        // (`Selection: Copy`, so reading `s`/`prev` out by value is free.)
330        let mut w = 0;
331        for r in 1..self.selections.len() {
332            let s = self.selections[r];
333            if should_merge(&self.selections[w], &s) {
334                let prev = self.selections[w];
335                // Keep the newer member's identity/direction/goal.
336                let newer = if s.id > prev.id { s } else { prev };
337                let dst = &mut self.selections[w];
338                dst.start = prev.start.min(s.start);
339                dst.end = prev.end.max(s.end);
340                dst.id = newer.id;
341                dst.reversed = newer.reversed;
342                dst.goal = newer.goal;
343            } else {
344                w += 1;
345                self.selections[w] = s;
346            }
347        }
348        self.selections.truncate(w + 1);
349    }
350}
351
352/// Whether `b` (which starts at or after `a`) should merge into `a`.
353fn should_merge(a: &Selection, b: &Selection) -> bool {
354    debug_assert!(a.start <= b.start);
355    if b.start < a.end {
356        return true; // strict overlap
357    }
358    if a.start == b.start {
359        return true; // shared start
360    }
361    if b.start == a.end {
362        // Touching: merge only if at least one side is a caret.
363        return a.is_empty() || b.is_empty();
364    }
365    false
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::patch::Edit;
372
373    #[test]
374    fn head_tail_and_direction() {
375        let s = Selection::from_anchor(SelectionId(0), 2, 8);
376        assert_eq!((s.start(), s.end(), s.head(), s.tail()), (2, 8, 8, 2));
377        let r = Selection::from_anchor(SelectionId(0), 8, 2); // caret before anchor
378        assert_eq!((r.start(), r.end(), r.head(), r.tail()), (2, 8, 2, 8));
379    }
380
381    #[test]
382    fn set_head_flips_when_crossing_tail() {
383        let mut s = Selection::from_anchor(SelectionId(0), 5, 8); // tail 5, head 8
384        s.set_head(2); // head crosses tail to the left
385        assert_eq!((s.start(), s.end(), s.head(), s.tail()), (2, 5, 2, 5));
386    }
387
388    #[test]
389    fn overlapping_selections_merge() {
390        let mut set = SelectionSet::new(0);
391        set.set_single(Selection::from_anchor(SelectionId(0), 0, 5));
392        set.add_selection(3, 9); // overlaps 0..5
393        assert_eq!(set.len(), 1);
394        assert_eq!((set.all()[0].start(), set.all()[0].end()), (0, 9));
395    }
396
397    #[test]
398    fn nonempty_touching_selections_do_not_merge() {
399        let mut set = SelectionSet::new(0);
400        set.set_single(Selection::from_anchor(SelectionId(0), 0, 3));
401        set.add_selection(3, 6); // touches at 3, both non-empty
402        assert_eq!(set.len(), 2, "non-empty touching selections stay separate");
403    }
404
405    #[test]
406    fn caret_touching_a_boundary_merges() {
407        let mut set = SelectionSet::new(0);
408        set.set_single(Selection::from_anchor(SelectionId(0), 0, 3));
409        set.add_caret(3); // a caret at the boundary
410        assert_eq!(set.len(), 1);
411    }
412
413    #[test]
414    fn newest_wins_identity_on_merge() {
415        let mut set = SelectionSet::new(0);
416        set.set_single(Selection::from_anchor(SelectionId(0), 0, 5));
417        set.add_selection(3, 9); // id 1 (newer)
418        assert_eq!(set.all()[0].id, SelectionId(1));
419    }
420
421    #[test]
422    fn rebase_follows_an_insertion() {
423        let mut set = SelectionSet::new(0);
424        set.set_single(Selection::from_anchor(SelectionId(0), 5, 10));
425        // Insert 3 bytes at offset 0: everything shifts +3.
426        let patch = Patch::single(Edit { old: 0..0, new: 0..3 });
427        set.rebase(&patch);
428        assert_eq!((set.all()[0].start(), set.all()[0].end()), (8, 13));
429    }
430
431    #[test]
432    fn rebase_collapses_a_selection_inside_a_deletion() {
433        let mut set = SelectionSet::new(0);
434        set.set_single(Selection::from_anchor(SelectionId(0), 3, 7));
435        // Delete 0..10: the selection collapses to the deletion point.
436        let patch = Patch::single(Edit { old: 0..10, new: 0..0 });
437        set.rebase(&patch);
438        assert!(set.all()[0].is_empty());
439        assert_eq!(set.all()[0].start(), 0);
440    }
441
442    #[test]
443    fn from_ranges_installs_a_box_with_the_chosen_newest() {
444        // Three rows' worth of ranges; index 2 is the active row (newest).
445        let set = SelectionSet::from_ranges(&[(0, 3), (10, 13), (20, 23)], 2);
446        assert_eq!(set.len(), 3);
447        assert_eq!((set.newest().start(), set.newest().end()), (20, 23));
448    }
449
450    #[test]
451    fn collapse_to_primary_keeps_the_oldest_as_a_caret() {
452        let mut set = SelectionSet::new(0); // id 0 at offset 0
453        set.add_selection(5, 9); // id 1
454        set.add_caret(20); // id 2
455        assert!(set.len() >= 2);
456        set.collapse_to_primary();
457        assert_eq!(set.len(), 1);
458        assert_eq!(set.all()[0].id, SelectionId(0), "the oldest cursor is kept");
459        assert!(set.all()[0].is_empty());
460        assert_eq!(set.all()[0].head(), 0);
461    }
462
463    #[test]
464    fn collapse_to_newest_keeps_one_caret() {
465        let mut set = SelectionSet::new(0);
466        set.add_selection(5, 9);
467        set.add_caret(20);
468        assert!(set.len() >= 2);
469        set.collapse_to_newest();
470        assert_eq!(set.len(), 1);
471        assert!(set.all()[0].is_empty());
472    }
473}