Skip to main content

ochrepad_core/
selection.rs

1//! Selections are always a set (PLAN 3.3). A plain cursor is a set of one empty
2//! selection.
3
4use std::ops::Range;
5
6use serde::{Deserialize, Serialize};
7
8use crate::position::{ByteOffset, VisualCol};
9use crate::text::Document;
10
11#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
12pub struct Selection {
13    pub anchor: ByteOffset,
14    pub head: ByteOffset,
15    /// Remembered visual column for consecutive vertical movement.
16    pub goal: Option<VisualCol>,
17}
18
19impl Selection {
20    pub fn caret(pos: ByteOffset) -> Self {
21        Self::new(pos, pos)
22    }
23
24    pub fn new(anchor: ByteOffset, head: ByteOffset) -> Self {
25        Self {
26            anchor,
27            head,
28            goal: None,
29        }
30    }
31
32    pub fn with_goal(mut self, goal: Option<VisualCol>) -> Self {
33        self.goal = goal;
34        self
35    }
36
37    pub fn start(&self) -> ByteOffset {
38        self.anchor.min(self.head)
39    }
40
41    pub fn end(&self) -> ByteOffset {
42        self.anchor.max(self.head)
43    }
44
45    pub fn range(&self) -> Range<ByteOffset> {
46        self.start()..self.end()
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.anchor == self.head
51    }
52
53    fn is_backward(&self) -> bool {
54        self.head < self.anchor
55    }
56}
57
58/// The rectangle a column selection was made from, kept alongside the set so
59/// rectangular copy/paste and column-keyed sort can use it. `start_*` is the
60/// anchor corner and `end_*` the head corner (it may be above or left of the
61/// anchor).
62#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
63pub struct Rectangle {
64    pub start_line: usize,
65    pub end_line: usize,
66    pub start_col: VisualCol,
67    pub end_col: VisualCol,
68}
69
70impl Rectangle {
71    pub fn top(&self) -> usize {
72        self.start_line.min(self.end_line)
73    }
74
75    pub fn bottom(&self) -> usize {
76        self.start_line.max(self.end_line)
77    }
78
79    pub fn left(&self) -> VisualCol {
80        self.start_col.min(self.end_col)
81    }
82
83    pub fn right(&self) -> VisualCol {
84        self.start_col.max(self.end_col)
85    }
86}
87
88/// Non-empty, sorted, non-overlapping selections plus the primary's index.
89#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
90pub struct SelectionSet {
91    ranges: Vec<Selection>,
92    primary: usize,
93    rect: Option<Rectangle>,
94}
95
96impl SelectionSet {
97    pub fn single(sel: Selection) -> Self {
98        Self {
99            ranges: vec![sel],
100            primary: 0,
101            rect: None,
102        }
103    }
104
105    pub fn caret(pos: ByteOffset) -> Self {
106        Self::single(Selection::caret(pos))
107    }
108
109    /// Builds a normalized set. `primary` indexes into `ranges` as given.
110    /// An empty `ranges` yields a caret at 0.
111    pub fn new(ranges: Vec<Selection>, primary: usize) -> Self {
112        if ranges.is_empty() {
113            return Self::caret(ByteOffset(0));
114        }
115        let primary = primary.min(ranges.len() - 1);
116        let mut set = Self {
117            ranges,
118            primary,
119            rect: None,
120        };
121        set.normalize();
122        set
123    }
124
125    pub fn ranges(&self) -> &[Selection] {
126        &self.ranges
127    }
128
129    /// Whether this is a well-formed set for `doc` (non-empty, sorted, apart
130    /// from each other as normalizing leaves them, every end on a character
131    /// boundary of the text, a rectangle within its lines). A set read back
132    /// from disk is checked with this before it is used.
133    pub fn fits(&self, doc: &Document) -> bool {
134        let ok = |p: ByteOffset| p <= doc.len() && doc.is_char_boundary(p);
135        !self.ranges.is_empty()
136            && self.primary < self.ranges.len()
137            && self.ranges.iter().all(|s| ok(s.anchor) && ok(s.head))
138            && self.ranges.windows(2).all(|w| w[0].end() < w[1].start())
139            && self
140                .rect
141                .as_ref()
142                .is_none_or(|r| r.start_line.max(r.end_line) < doc.line_count())
143    }
144
145    pub fn primary(&self) -> &Selection {
146        &self.ranges[self.primary]
147    }
148
149    pub fn primary_index(&self) -> usize {
150        self.primary
151    }
152
153    pub fn len(&self) -> usize {
154        self.ranges.len()
155    }
156
157    pub fn is_empty(&self) -> bool {
158        false
159    }
160
161    pub fn rect(&self) -> Option<&Rectangle> {
162        self.rect.as_ref()
163    }
164
165    pub fn with_rect(mut self, rect: Option<Rectangle>) -> Self {
166        self.rect = rect;
167        self
168    }
169
170    /// Applies `f` to every selection, renormalizes and drops the rectangle.
171    pub fn map(&self, mut f: impl FnMut(&Selection) -> Selection) -> Self {
172        Self::new(self.ranges.iter().map(&mut f).collect(), self.primary)
173    }
174
175    /// Sorts, then merges overlapping or touching selections. The merged
176    /// selection keeps the primary's direction if it absorbed the primary,
177    /// otherwise the first one's.
178    fn normalize(&mut self) {
179        let primary = self.primary;
180        let mut indexed: Vec<(usize, Selection)> = std::mem::take(&mut self.ranges)
181            .into_iter()
182            .enumerate()
183            .collect();
184        indexed.sort_by_key(|(_, s)| (s.start(), s.end()));
185
186        let mut out: Vec<Selection> = Vec::with_capacity(indexed.len());
187        let mut new_primary = 0;
188        // Which original selection decides the direction of `out.last()`.
189        let mut leader: Option<(usize, Selection)> = None;
190        for (i, sel) in indexed {
191            match out.last_mut() {
192                Some(last) if sel.start() <= last.end() => {
193                    let (start, end) = (last.start(), last.end().max(sel.end()));
194                    let (lead_idx, lead) = match leader.take() {
195                        Some(l) if l.0 == primary || i != primary => l,
196                        _ => (i, sel),
197                    };
198                    *last = if lead.is_backward() {
199                        Selection {
200                            anchor: end,
201                            head: start,
202                            goal: lead.goal,
203                        }
204                    } else {
205                        Selection {
206                            anchor: start,
207                            head: end,
208                            goal: lead.goal,
209                        }
210                    };
211                    if i == primary {
212                        new_primary = out.len() - 1;
213                    }
214                    leader = Some((lead_idx, lead));
215                }
216                _ => {
217                    if i == primary {
218                        new_primary = out.len();
219                    }
220                    leader = Some((i, sel.clone()));
221                    out.push(sel);
222                }
223            }
224        }
225        self.ranges = out;
226        self.primary = new_primary;
227    }
228
229    /// Checks the invariants; used by tests and debug assertions.
230    pub fn validate(&self, doc: &Document) -> Result<(), String> {
231        if self.ranges.is_empty() || self.primary >= self.ranges.len() {
232            return Err(format!("bad primary/len: {self:?}"));
233        }
234        for s in &self.ranges {
235            for p in [s.anchor, s.head] {
236                if p > doc.len() || !doc.is_char_boundary(p) {
237                    return Err(format!("{p:?} out of range or not a char boundary"));
238                }
239            }
240        }
241        for w in self.ranges.windows(2) {
242            if w[0].end() >= w[1].start() {
243                return Err(format!(
244                    "unsorted, overlapping or touching: {:?}",
245                    self.ranges
246                ));
247            }
248        }
249        Ok(())
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    fn sel(a: usize, h: usize) -> Selection {
258        Selection::new(ByteOffset(a), ByteOffset(h))
259    }
260
261    #[test]
262    fn normalize_sorts_and_merges() {
263        let set = SelectionSet::new(vec![sel(10, 12), sel(0, 2), sel(5, 5), sel(5, 5)], 0);
264        assert_eq!(set.ranges(), &[sel(0, 2), sel(5, 5), sel(10, 12)]);
265        assert_eq!(set.primary(), &sel(10, 12));
266    }
267
268    #[test]
269    fn touching_selections_merge_keeping_primary_direction() {
270        let set = SelectionSet::new(vec![sel(0, 3), sel(6, 3)], 1);
271        assert_eq!(set.ranges(), &[sel(6, 0)]);
272        assert_eq!(set.primary_index(), 0);
273
274        let set = SelectionSet::new(vec![sel(0, 3), sel(6, 3), sel(9, 9)], 2);
275        assert_eq!(set.ranges(), &[sel(0, 6), sel(9, 9)]);
276        assert_eq!(set.primary_index(), 1);
277    }
278
279    #[test]
280    fn caret_inside_selection_merges() {
281        let set = SelectionSet::new(vec![sel(2, 8), sel(4, 4)], 1);
282        assert_eq!(set.ranges(), &[sel(2, 8)]);
283        assert_eq!(set.primary_index(), 0);
284    }
285}