Skip to main content

termesh_editor/
selection.rs

1//! Cursors and selections, and how they survive somebody else's edit.
2//!
3//! A cursor is just a zero-width [`Range`]. V1 ships a single cursor (ARCHITECTURE.md
4//! §10's ship discipline), but the model is multi-range from the start so multi-cursor is
5//! additive rather than a rewrite.
6
7use crate::change::{Assoc, ChangeSet};
8
9/// A selection with a fixed `anchor` and a moving `head`, in char offsets.
10///
11/// `head < anchor` is legal and means the selection was dragged backwards — the direction
12/// is information (it decides which end moves next), so it is preserved rather than
13/// normalized away.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub struct Range {
16    pub anchor: usize,
17    pub head: usize,
18}
19
20impl Range {
21    pub fn new(anchor: usize, head: usize) -> Self {
22        Self { anchor, head }
23    }
24
25    /// A zero-width range — an ordinary cursor.
26    pub fn point(at: usize) -> Self {
27        Self { anchor: at, head: at }
28    }
29
30    pub fn is_empty(&self) -> bool {
31        self.anchor == self.head
32    }
33
34    pub fn start(&self) -> usize {
35        self.anchor.min(self.head)
36    }
37
38    pub fn end(&self) -> usize {
39        self.anchor.max(self.head)
40    }
41
42    pub fn len(&self) -> usize {
43        self.end() - self.start()
44    }
45
46    /// Carry this range through a change made elsewhere in the document.
47    ///
48    /// The head maps with [`Assoc::After`] so that typing at the cursor pushes it along
49    /// rather than leaving it stranded behind the character just inserted.
50    pub fn map(&self, changes: &ChangeSet) -> Self {
51        Self {
52            anchor: changes.map_pos(self.anchor, Assoc::After),
53            head: changes.map_pos(self.head, Assoc::After),
54        }
55    }
56}
57
58/// The set of ranges owned by a buffer. Always non-empty.
59#[derive(Debug, Clone, PartialEq, Eq)]
60pub struct Selection {
61    ranges: Vec<Range>,
62    primary: usize,
63}
64
65impl Selection {
66    pub fn single(range: Range) -> Self {
67        Self { ranges: vec![range], primary: 0 }
68    }
69
70    pub fn point(at: usize) -> Self {
71        Self::single(Range::point(at))
72    }
73
74    pub fn ranges(&self) -> &[Range] {
75        &self.ranges
76    }
77
78    /// The range that drives scrolling and single-cursor operations.
79    pub fn primary(&self) -> Range {
80        self.ranges[self.primary]
81    }
82
83    /// Carry every range through a change (ADR-0006 §7).
84    pub fn map(&self, changes: &ChangeSet) -> Self {
85        Self { ranges: self.ranges.iter().map(|r| r.map(changes)).collect(), primary: self.primary }
86    }
87}
88
89impl Default for Selection {
90    fn default() -> Self {
91        Self::point(0)
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn a_cursor_is_an_empty_range() {
101        let c = Range::point(4);
102        assert!(c.is_empty());
103        assert_eq!(c.len(), 0);
104        assert_eq!((c.start(), c.end()), (4, 4));
105    }
106
107    #[test]
108    fn a_backwards_range_keeps_its_direction_but_orders_its_bounds() {
109        let r = Range::new(9, 3);
110        assert_eq!((r.start(), r.end()), (3, 9));
111        assert_eq!(r.len(), 6);
112        assert_eq!(r.head, 3, "direction is information; it must survive");
113    }
114
115    #[test]
116    fn an_edit_before_the_cursor_pushes_it_along() {
117        let cursor = Range::point(10);
118        let insert = ChangeSet::replace(20, 0, 0, "abc");
119        assert_eq!(cursor.map(&insert), Range::point(13));
120    }
121
122    #[test]
123    fn an_edit_after_the_cursor_leaves_it_alone() {
124        let cursor = Range::point(4);
125        let insert = ChangeSet::replace(20, 10, 10, "abc");
126        assert_eq!(cursor.map(&insert), Range::point(4));
127    }
128
129    #[test]
130    fn typing_at_the_cursor_carries_it_forward() {
131        // The reason heads map with Assoc::After: otherwise the cursor would sit behind
132        // every character you type.
133        let cursor = Range::point(5);
134        let typed = ChangeSet::replace(10, 5, 5, "x");
135        assert_eq!(cursor.map(&typed), Range::point(6));
136    }
137
138    #[test]
139    fn a_selection_spanning_a_deletion_collapses_onto_it() {
140        let selection = Range::new(3, 9);
141        let deleted = ChangeSet::replace(20, 2, 12, "");
142        assert_eq!(selection.map(&deleted), Range::new(2, 2));
143    }
144
145    #[test]
146    fn every_range_in_a_selection_maps() {
147        let sel = Selection { ranges: vec![Range::point(1), Range::point(8)], primary: 1 };
148        let mapped = sel.map(&ChangeSet::replace(20, 0, 0, "xx"));
149        assert_eq!(mapped.ranges(), [Range::point(3), Range::point(10)]);
150        assert_eq!(mapped.primary(), Range::point(10), "the primary index survives");
151    }
152
153    #[test]
154    fn a_default_selection_is_a_cursor_at_the_start() {
155        assert_eq!(Selection::default().primary(), Range::point(0));
156    }
157}