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}