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            Self::Char { head, .. } => head,
37            Self::Line { head_row, .. } => Position::new(head_row, 0),
38            Self::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            Self::Char { anchor, .. } => anchor,
47            Self::Line { anchor_row, .. } => Position::new(anchor_row, 0),
48            Self::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            Self::Char { head, .. } => *head = pos,
58            Self::Line { head_row, .. } => *head_row = pos.row,
59            Self::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            Self::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            Self::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            Self::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            Self::Char { anchor, head } => {
114                let (s, e) = order(anchor, head);
115                (s.row, e.row)
116            }
117            Self::Line {
118                anchor_row,
119                head_row,
120            } => (anchor_row.min(head_row), anchor_row.max(head_row)),
121            Self::Block { anchor, head } => (anchor.row.min(head.row), anchor.row.max(head.row)),
122        }
123    }
124}
125
126/// Order a pair of positions row-major.
127fn order(a: Position, b: Position) -> (Position, Position) {
128    if a <= b { (a, b) } else { (b, a) }
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn char_single_row_inclusive() {
137        let sel = Selection::Char {
138            anchor: Position::new(0, 2),
139            head: Position::new(0, 5),
140        };
141        assert_eq!(sel.row_span(0), Some((2, 5)));
142        assert_eq!(sel.row_span(1), None);
143    }
144
145    #[test]
146    fn char_multi_row_clips_endpoints() {
147        let sel = Selection::Char {
148            anchor: Position::new(1, 3),
149            head: Position::new(3, 7),
150        };
151        assert_eq!(sel.row_span(0), None);
152        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
153        assert_eq!(sel.row_span(2), Some((0, usize::MAX)));
154        assert_eq!(sel.row_span(3), Some((0, 7)));
155        assert_eq!(sel.row_span(4), None);
156    }
157
158    #[test]
159    fn char_handles_reversed_endpoints() {
160        // Cursor moved up-left of anchor.
161        let sel = Selection::Char {
162            anchor: Position::new(3, 7),
163            head: Position::new(1, 3),
164        };
165        assert_eq!(sel.row_span(1), Some((3, usize::MAX)));
166        assert_eq!(sel.row_span(3), Some((0, 7)));
167    }
168
169    #[test]
170    fn line_covers_whole_rows_only() {
171        let sel = Selection::Line {
172            anchor_row: 5,
173            head_row: 7,
174        };
175        assert_eq!(sel.row_span(4), None);
176        assert_eq!(sel.row_span(5), Some((0, usize::MAX)));
177        assert_eq!(sel.row_span(6), Some((0, usize::MAX)));
178        assert_eq!(sel.row_span(7), Some((0, usize::MAX)));
179        assert_eq!(sel.row_span(8), None);
180    }
181
182    #[test]
183    fn block_inclusive_rect() {
184        let sel = Selection::Block {
185            anchor: Position::new(2, 4),
186            head: Position::new(5, 8),
187        };
188        for row in 2..=5 {
189            assert_eq!(sel.row_span(row), Some((4, 8)));
190        }
191        assert_eq!(sel.row_span(1), None);
192        assert_eq!(sel.row_span(6), None);
193    }
194
195    #[test]
196    fn block_normalises_corners() {
197        // Anchor bottom-right, head top-left.
198        let sel = Selection::Block {
199            anchor: Position::new(5, 8),
200            head: Position::new(2, 4),
201        };
202        for row in 2..=5 {
203            assert_eq!(sel.row_span(row), Some((4, 8)));
204        }
205    }
206
207    #[test]
208    fn extend_to_updates_head() {
209        let mut sel = Selection::Char {
210            anchor: Position::new(0, 0),
211            head: Position::new(0, 3),
212        };
213        sel.extend_to(Position::new(2, 9));
214        assert_eq!(sel.head(), Position::new(2, 9));
215        assert_eq!(sel.anchor(), Position::new(0, 0));
216    }
217
218    #[test]
219    fn line_extend_to_drops_column() {
220        let mut sel = Selection::Line {
221            anchor_row: 1,
222            head_row: 1,
223        };
224        sel.extend_to(Position::new(4, 50));
225        assert_eq!(sel.head(), Position::new(4, 0));
226    }
227
228    #[test]
229    fn row_bounds_each_kind() {
230        let c = Selection::Char {
231            anchor: Position::new(2, 0),
232            head: Position::new(5, 0),
233        };
234        assert_eq!(c.row_bounds(), (2, 5));
235        let l = Selection::Line {
236            anchor_row: 7,
237            head_row: 3,
238        };
239        assert_eq!(l.row_bounds(), (3, 7));
240        let b = Selection::Block {
241            anchor: Position::new(8, 1),
242            head: Position::new(2, 9),
243        };
244        assert_eq!(b.row_bounds(), (2, 8));
245    }
246}