Skip to main content

hjkl_buffer/
selection.rs

1use crate::Position;
2
3/// First-class vim selection. Each variant carries the kind directly
4/// rather than relying on a single char-range primitive with separate
5/// "treat as line / block" overlays — that's the whole point of
6/// owning the buffer model. Anchor is where the user pressed
7/// `v` / `V` / `Ctrl-V`; head moves with the cursor and is updated
8/// via [`Selection::extend_to`].
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum Selection {
11    /// `v` — character-wise. Covers `anchor..=head` inclusive,
12    /// row-major. Empty rows in the middle of a multi-row span are
13    /// treated as having one virtual cell so the highlight is
14    /// visible (matches vim).
15    Char { anchor: Position, head: Position },
16    /// `V` — line-wise. Both endpoints are pure row indices; column
17    /// is irrelevant (the whole row is always covered).
18    Line { anchor_row: usize, head_row: usize },
19    /// `Ctrl-V` — block-wise. Covers the inclusive rectangle whose
20    /// corners are anchor and head. Row range is `min..=max`; column
21    /// range is `min..=max` independently of the corner diagonals. The
22    /// columns are display columns, which [`Selection::row_span_on`] turns
23    /// into each row's characters (the same on a line without tabs or wide
24    /// characters).
25    Block { anchor: Position, head: Position },
26}
27
28/// Bounds of a selection on a particular row, expressed as inclusive
29/// char-column range. `None` means the row is outside the selection.
30/// `Some((0, usize::MAX))` is the convention for "whole row" — the
31/// renderer caps it at the row's actual length.
32pub type RowSpan = Option<(usize, usize)>;
33
34impl Selection {
35    /// Where the cursor end of the selection lives. After
36    /// [`Selection::extend_to`] this is the freshly-set value.
37    pub fn head(self) -> Position {
38        match self {
39            Self::Char { head, .. } => head,
40            Self::Line { head_row, .. } => Position::new(head_row, 0),
41            Self::Block { head, .. } => head,
42        }
43    }
44
45    /// The opposite end of the selection — fixed when the user
46    /// entered visual mode.
47    pub fn anchor(self) -> Position {
48        match self {
49            Self::Char { anchor, .. } => anchor,
50            Self::Line { anchor_row, .. } => Position::new(anchor_row, 0),
51            Self::Block { anchor, .. } => anchor,
52        }
53    }
54
55    /// Move the cursor end of the selection to `pos`. Anchor stays
56    /// put; for `Line` we drop the column since rows are all that
57    /// matter.
58    pub fn extend_to(&mut self, pos: Position) {
59        match self {
60            Self::Char { head, .. } => *head = pos,
61            Self::Line { head_row, .. } => *head_row = pos.row,
62            Self::Block { head, .. } => *head = pos,
63        }
64    }
65
66    /// What columns of `row` the selection covers. Used by the
67    /// render layer to paint the selection bg without having to
68    /// know each variant's quirks.
69    ///
70    /// - `Char` on a single row: `[min_col, max_col]`.
71    /// - `Char` spanning rows: from `head/anchor.col` on the start
72    ///   row to end-of-line, then full rows in between, then
73    ///   `0..=end.col` on the last row.
74    /// - `Line`: `(0, usize::MAX)` for every row in range.
75    /// - `Block`: `[min_col, max_col]` regardless of which row.
76    pub fn row_span(self, row: usize) -> RowSpan {
77        match self {
78            Self::Char { anchor, head } => {
79                let (start, end) = order(anchor, head);
80                if row < start.row || row > end.row {
81                    return None;
82                }
83                let lo = if row == start.row { start.col } else { 0 };
84                let hi = if row == end.row { end.col } else { usize::MAX };
85                Some((lo, hi))
86            }
87            Self::Line {
88                anchor_row,
89                head_row,
90            } => {
91                let (lo, hi) = if anchor_row <= head_row {
92                    (anchor_row, head_row)
93                } else {
94                    (head_row, anchor_row)
95                };
96                if row < lo || row > hi {
97                    None
98                } else {
99                    Some((0, usize::MAX))
100                }
101            }
102            Self::Block { anchor, head } => {
103                let (top, bot) = (anchor.row.min(head.row), anchor.row.max(head.row));
104                if row < top || row > bot {
105                    return None;
106                }
107                let (left, right) = (anchor.col.min(head.col), anchor.col.max(head.col));
108                Some((left, right))
109            }
110        }
111    }
112
113    /// [`Selection::row_span`] as characters of `line`, the text of `row`.
114    /// A `Block`'s columns are display columns (vim's blockwise Visual covers
115    /// the same screen columns on every row), so on `line` it covers every
116    /// character with a cell between them, a tab or wide character cut by an
117    /// edge included; past the line's end it counts one character per cell,
118    /// as the other variants' columns do there. `Char` and `Line` are
119    /// [`Selection::row_span`] unchanged.
120    pub fn row_span_on(self, row: usize, line: &str, tab_width: usize) -> RowSpan {
121        let Self::Block { .. } = self else {
122            return self.row_span(row);
123        };
124        let (left, right) = self.row_span(row)?;
125        let mut first = None;
126        let mut last = None;
127        let mut vcol = 0;
128        let mut count = 0;
129        let tab_width = tab_width.max(1);
130        for (i, ch) in line.chars().enumerate() {
131            let next = if ch == '\t' {
132                vcol + tab_width - vcol % tab_width
133            } else {
134                vcol + crate::geom::char_col_to_visual_col(ch.encode_utf8(&mut [0; 4]), 1, 1)
135            };
136            if vcol <= right && next > left {
137                first.get_or_insert(i);
138                last = Some(i);
139            }
140            vcol = next;
141            count = i + 1;
142        }
143        let past = |col: usize| count + col.saturating_sub(vcol);
144        let lo = first.unwrap_or_else(|| past(left));
145        let hi = if right == usize::MAX {
146            usize::MAX
147        } else {
148            match last {
149                Some(i) if right < vcol => i,
150                _ => past(right),
151            }
152        };
153        Some((lo, hi))
154    }
155
156    /// Inclusive `(top_row, bottom_row)` covered by the selection.
157    pub fn row_bounds(self) -> (usize, usize) {
158        match self {
159            Self::Char { anchor, head } => {
160                let (s, e) = order(anchor, head);
161                (s.row, e.row)
162            }
163            Self::Line {
164                anchor_row,
165                head_row,
166            } => (anchor_row.min(head_row), anchor_row.max(head_row)),
167            Self::Block { anchor, head } => (anchor.row.min(head.row), anchor.row.max(head.row)),
168        }
169    }
170}
171
172/// Order a pair of positions row-major.
173fn order(a: Position, b: Position) -> (Position, Position) {
174    if a <= b { (a, b) } else { (b, a) }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    fn block(top: usize, left: usize, bot: usize, right: usize) -> Selection {
182        Selection::Block {
183            anchor: Position::new(top, left),
184            head: Position::new(bot, right),
185        }
186    }
187
188    #[test]
189    fn block_row_span_on_plain_text_is_row_span() {
190        let sel = block(0, 1, 1, 3);
191        assert_eq!(sel.row_span_on(0, "abcdef", 4), sel.row_span(0));
192    }
193
194    #[test]
195    fn block_row_span_on_maps_display_columns_through_a_tab() {
196        // `\tab` at tab width 4: the tab covers cells 0-3, `a` 4, `b` 5.
197        let sel = block(0, 2, 1, 4);
198        assert_eq!(sel.row_span_on(0, "\tab", 4), Some((0, 1)));
199        assert_eq!(sel.row_span_on(1, "abcdefgh", 4), Some((2, 4)));
200    }
201
202    #[test]
203    fn block_row_span_on_past_the_line_counts_cells() {
204        // Cells 6-9 lie past the end of `\tab` (width 6).
205        let sel = block(0, 6, 1, 9);
206        assert_eq!(sel.row_span_on(0, "\tab", 4), Some((3, 6)));
207    }
208
209    #[test]
210    fn block_row_span_on_dollar_runs_to_the_end() {
211        let sel = block(0, 1, 0, usize::MAX);
212        assert_eq!(sel.row_span_on(0, "\tab", 4), Some((0, usize::MAX)));
213    }
214
215    #[test]
216    fn char_single_row_inclusive() {
217        let sel = Selection::Char {
218            anchor: Position::new(0, 2),
219            head: Position::new(0, 5),
220        };
221        assert_eq!(sel.row_span(0), Some((2, 5)));
222        assert_eq!(sel.row_span(1), None);
223    }
224
225    #[test]
226    fn char_multi_row_clips_endpoints() {
227        let sel = Selection::Char {
228            anchor: Position::new(1, 3),
229            head: Position::new(3, 7),
230        };
231        assert_eq!(sel.row_span(0), None);
232        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
233        assert_eq!(sel.row_span(2), Some((0, usize::MAX)));
234        assert_eq!(sel.row_span(3), Some((0, 7)));
235        assert_eq!(sel.row_span(4), None);
236    }
237
238    #[test]
239    fn char_handles_reversed_endpoints() {
240        // Cursor moved up-left of anchor.
241        let sel = Selection::Char {
242            anchor: Position::new(3, 7),
243            head: Position::new(1, 3),
244        };
245        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
246        assert_eq!(sel.row_span(3), Some((0, 7)));
247    }
248
249    #[test]
250    fn line_covers_whole_rows_only() {
251        let sel = Selection::Line {
252            anchor_row: 5,
253            head_row: 7,
254        };
255        assert_eq!(sel.row_span(4), None);
256        assert_eq!(sel.row_span(5), Some((0, usize::MAX)));
257        assert_eq!(sel.row_span(6), Some((0, usize::MAX)));
258        assert_eq!(sel.row_span(7), Some((0, usize::MAX)));
259        assert_eq!(sel.row_span(8), None);
260    }
261
262    #[test]
263    fn block_inclusive_rect() {
264        let sel = Selection::Block {
265            anchor: Position::new(2, 4),
266            head: Position::new(5, 8),
267        };
268        for row in 2..=5 {
269            assert_eq!(sel.row_span(row), Some((4, 8)));
270        }
271        assert_eq!(sel.row_span(1), None);
272        assert_eq!(sel.row_span(6), None);
273    }
274
275    #[test]
276    fn block_normalises_corners() {
277        // Anchor bottom-right, head top-left.
278        let sel = Selection::Block {
279            anchor: Position::new(5, 8),
280            head: Position::new(2, 4),
281        };
282        for row in 2..=5 {
283            assert_eq!(sel.row_span(row), Some((4, 8)));
284        }
285    }
286
287    #[test]
288    fn extend_to_updates_head() {
289        let mut sel = Selection::Char {
290            anchor: Position::new(0, 0),
291            head: Position::new(0, 3),
292        };
293        sel.extend_to(Position::new(2, 9));
294        assert_eq!(sel.head(), Position::new(2, 9));
295        assert_eq!(sel.anchor(), Position::new(0, 0));
296    }
297
298    #[test]
299    fn line_extend_to_drops_column() {
300        let mut sel = Selection::Line {
301            anchor_row: 1,
302            head_row: 1,
303        };
304        sel.extend_to(Position::new(4, 50));
305        assert_eq!(sel.head(), Position::new(4, 0));
306    }
307
308    #[test]
309    fn row_bounds_each_kind() {
310        let c = Selection::Char {
311            anchor: Position::new(2, 0),
312            head: Position::new(5, 0),
313        };
314        assert_eq!(c.row_bounds(), (2, 5));
315        let l = Selection::Line {
316            anchor_row: 7,
317            head_row: 3,
318        };
319        assert_eq!(l.row_bounds(), (3, 7));
320        let b = Selection::Block {
321            anchor: Position::new(8, 1),
322            head: Position::new(2, 9),
323        };
324        assert_eq!(b.row_bounds(), (2, 8));
325    }
326}