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