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.
22    Block { anchor: Position, head: Position },
23}
24
25/// Bounds of a selection on a particular row, expressed as inclusive
26/// char-column range. `None` means the row is outside the selection.
27/// `Some((0, usize::MAX))` is the convention for "whole row" — the
28/// renderer caps it at the row's actual length.
29pub type RowSpan = Option<(usize, usize)>;
30
31impl Selection {
32    /// Where the cursor end of the selection lives. After
33    /// [`Selection::extend_to`] this is the freshly-set value.
34    pub fn head(self) -> Position {
35        match self {
36            Selection::Char { head, .. } => head,
37            Selection::Line { head_row, .. } => Position::new(head_row, 0),
38            Selection::Block { head, .. } => head,
39        }
40    }
41
42    /// The opposite end of the selection — fixed when the user
43    /// entered visual mode.
44    pub fn anchor(self) -> Position {
45        match self {
46            Selection::Char { anchor, .. } => anchor,
47            Selection::Line { anchor_row, .. } => Position::new(anchor_row, 0),
48            Selection::Block { anchor, .. } => anchor,
49        }
50    }
51
52    /// Move the cursor end of the selection to `pos`. Anchor stays
53    /// put; for `Line` we drop the column since rows are all that
54    /// matter.
55    pub fn extend_to(&mut self, pos: Position) {
56        match self {
57            Selection::Char { head, .. } => *head = pos,
58            Selection::Line { head_row, .. } => *head_row = pos.row,
59            Selection::Block { head, .. } => *head = pos,
60        }
61    }
62
63    /// What columns of `row` the selection covers. Used by the
64    /// render layer to paint the selection bg without having to
65    /// know each variant's quirks.
66    ///
67    /// - `Char` on a single row: `[min_col, max_col]`.
68    /// - `Char` spanning rows: from `head/anchor.col` on the start
69    ///   row to end-of-line, then full rows in between, then
70    ///   `0..=end.col` on the last row.
71    /// - `Line`: `(0, usize::MAX)` for every row in range.
72    /// - `Block`: `[min_col, max_col]` regardless of which row.
73    pub fn row_span(self, row: usize) -> RowSpan {
74        match self {
75            Selection::Char { anchor, head } => {
76                let (start, end) = order(anchor, head);
77                if row < start.row || row > end.row {
78                    return None;
79                }
80                let lo = if row == start.row { start.col } else { 0 };
81                let hi = if row == end.row { end.col } else { usize::MAX };
82                Some((lo, hi))
83            }
84            Selection::Line {
85                anchor_row,
86                head_row,
87            } => {
88                let (lo, hi) = if anchor_row <= head_row {
89                    (anchor_row, head_row)
90                } else {
91                    (head_row, anchor_row)
92                };
93                if row < lo || row > hi {
94                    None
95                } else {
96                    Some((0, usize::MAX))
97                }
98            }
99            Selection::Block { anchor, head } => {
100                let (top, bot) = (anchor.row.min(head.row), anchor.row.max(head.row));
101                if row < top || row > bot {
102                    return None;
103                }
104                let (left, right) = (anchor.col.min(head.col), anchor.col.max(head.col));
105                Some((left, right))
106            }
107        }
108    }
109
110    /// Inclusive `(top_row, bottom_row)` covered by the selection.
111    pub fn row_bounds(self) -> (usize, usize) {
112        match self {
113            Selection::Char { anchor, head } => {
114                let (s, e) = order(anchor, head);
115                (s.row, e.row)
116            }
117            Selection::Line {
118                anchor_row,
119                head_row,
120            } => (anchor_row.min(head_row), anchor_row.max(head_row)),
121            Selection::Block { anchor, head } => {
122                (anchor.row.min(head.row), anchor.row.max(head.row))
123            }
124        }
125    }
126}
127
128/// Order a pair of positions row-major.
129fn order(a: Position, b: Position) -> (Position, Position) {
130    if a <= b { (a, b) } else { (b, a) }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136
137    #[test]
138    fn char_single_row_inclusive() {
139        let sel = Selection::Char {
140            anchor: Position::new(0, 2),
141            head: Position::new(0, 5),
142        };
143        assert_eq!(sel.row_span(0), Some((2, 5)));
144        assert_eq!(sel.row_span(1), None);
145    }
146
147    #[test]
148    fn char_multi_row_clips_endpoints() {
149        let sel = Selection::Char {
150            anchor: Position::new(1, 3),
151            head: Position::new(3, 7),
152        };
153        assert_eq!(sel.row_span(0), None);
154        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
155        assert_eq!(sel.row_span(2), Some((0, usize::MAX)));
156        assert_eq!(sel.row_span(3), Some((0, 7)));
157        assert_eq!(sel.row_span(4), None);
158    }
159
160    #[test]
161    fn char_handles_reversed_endpoints() {
162        // Cursor moved up-left of anchor.
163        let sel = Selection::Char {
164            anchor: Position::new(3, 7),
165            head: Position::new(1, 3),
166        };
167        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
168        assert_eq!(sel.row_span(3), Some((0, 7)));
169    }
170
171    #[test]
172    fn line_covers_whole_rows_only() {
173        let sel = Selection::Line {
174            anchor_row: 5,
175            head_row: 7,
176        };
177        assert_eq!(sel.row_span(4), None);
178        assert_eq!(sel.row_span(5), Some((0, usize::MAX)));
179        assert_eq!(sel.row_span(6), Some((0, usize::MAX)));
180        assert_eq!(sel.row_span(7), Some((0, usize::MAX)));
181        assert_eq!(sel.row_span(8), None);
182    }
183
184    #[test]
185    fn block_inclusive_rect() {
186        let sel = Selection::Block {
187            anchor: Position::new(2, 4),
188            head: Position::new(5, 8),
189        };
190        for row in 2..=5 {
191            assert_eq!(sel.row_span(row), Some((4, 8)));
192        }
193        assert_eq!(sel.row_span(1), None);
194        assert_eq!(sel.row_span(6), None);
195    }
196
197    #[test]
198    fn block_normalises_corners() {
199        // Anchor bottom-right, head top-left.
200        let sel = Selection::Block {
201            anchor: Position::new(5, 8),
202            head: Position::new(2, 4),
203        };
204        for row in 2..=5 {
205            assert_eq!(sel.row_span(row), Some((4, 8)));
206        }
207    }
208
209    #[test]
210    fn extend_to_updates_head() {
211        let mut sel = Selection::Char {
212            anchor: Position::new(0, 0),
213            head: Position::new(0, 3),
214        };
215        sel.extend_to(Position::new(2, 9));
216        assert_eq!(sel.head(), Position::new(2, 9));
217        assert_eq!(sel.anchor(), Position::new(0, 0));
218    }
219
220    #[test]
221    fn line_extend_to_drops_column() {
222        let mut sel = Selection::Line {
223            anchor_row: 1,
224            head_row: 1,
225        };
226        sel.extend_to(Position::new(4, 50));
227        assert_eq!(sel.head(), Position::new(4, 0));
228    }
229
230    #[test]
231    fn row_bounds_each_kind() {
232        let c = Selection::Char {
233            anchor: Position::new(2, 0),
234            head: Position::new(5, 0),
235        };
236        assert_eq!(c.row_bounds(), (2, 5));
237        let l = Selection::Line {
238            anchor_row: 7,
239            head_row: 3,
240        };
241        assert_eq!(l.row_bounds(), (3, 7));
242        let b = Selection::Block {
243            anchor: Position::new(8, 1),
244            head: Position::new(2, 9),
245        };
246        assert_eq!(b.row_bounds(), (2, 8));
247    }
248}