brewery
notashelf /
ce24da6bc13d95ff7f15b25d019e4c98fbe0250c

beer

public

A terminal worth pouring time into

Star0tarball
clone
ssh://git@git.notashelf.dev:33/notashelf/beer.git
src/grid/selection.rsRust190 lines6.3 KB
1
use super::*;
2
3
impl Grid {
4
    /// Slide an active selection up by `n` rows after scrollback eviction,
5
    /// dropping it if either endpoint scrolled off the top.
6
    pub(super) fn shift_selection(&mut self, n: usize) {
7
        if let Some((a, b)) = self.selection {
8
            if a.row < n || b.row < n {
9
                self.selection = None;
10
            } else {
11
                self.selection = Some((
12
                    Point {
13
                        row: a.row - n,
14
                        ..a
15
                    },
16
                    Point {
17
                        row: b.row - n,
18
                        ..b
19
                    },
20
                ));
21
            }
22
        }
23
    }
24
25
    pub fn clear_selection(&mut self) {
26
        self.selection = None;
27
    }
28
29
    /// Begin a linear selection at an absolute point (drag anchor).
30
    pub fn start_selection(&mut self, row: usize, col: usize) {
31
        let p = Point { row, col };
32
        self.selection = Some((p, p));
33
        self.selection_block = false;
34
    }
35
36
    /// Begin a rectangular (block) selection at an absolute point.
37
    pub fn start_block_selection(&mut self, row: usize, col: usize) {
38
        let p = Point { row, col };
39
        self.selection = Some((p, p));
40
        self.selection_block = true;
41
    }
42
43
    /// Move the selection head (drag), keeping the anchor fixed.
44
    pub fn extend_selection(&mut self, row: usize, col: usize) {
45
        if let Some((_, head)) = self.selection.as_mut() {
46
            *head = Point { row, col };
47
        }
48
    }
49
50
    /// Select the word at an absolute point, breaking on whitespace and the
51
    /// default delimiter set.
52
    pub fn select_word(&mut self, row: usize, col: usize) {
53
        let delims = &self.word_delimiters;
54
        let line = self.abs_row(row);
55
        if col >= line.len() || !is_word(line[col].c, delims) {
56
            self.start_selection(row, col);
57
            return;
58
        }
59
        let mut lo = col;
60
        while lo > 0 && is_word(line[lo - 1].c, delims) {
61
            lo -= 1;
62
        }
63
        let mut hi = col;
64
        while hi + 1 < line.len() && is_word(line[hi + 1].c, delims) {
65
            hi += 1;
66
        }
67
        self.selection = Some((Point { row, col: lo }, Point { row, col: hi }));
68
        self.selection_block = false;
69
    }
70
71
    /// Select the whole line at an absolute row.
72
    pub fn select_line(&mut self, row: usize) {
73
        let last = self.abs_row(row).len().saturating_sub(1);
74
        self.selection = Some((Point { row, col: 0 }, Point { row, col: last }));
75
        self.selection_block = false;
76
    }
77
78
    /// The rectangle `(top_row, bottom_row, left_col, right_col)` of a block
79
    /// selection.
80
    fn block_rect(&self) -> Option<(usize, usize, usize, usize)> {
81
        let (a, b) = self.selection?;
82
        Some((
83
            a.row.min(b.row),
84
            a.row.max(b.row),
85
            a.col.min(b.col),
86
            a.col.max(b.col),
87
        ))
88
    }
89
90
    /// Normalized selection (start <= end in reading order), if any.
91
    fn ordered_selection(&self) -> Option<(Point, Point)> {
92
        self.selection.map(|(a, b)| {
93
            if (a.row, a.col) <= (b.row, b.col) {
94
                (a, b)
95
            } else {
96
                (b, a)
97
            }
98
        })
99
    }
100
101
    /// Whether the cell at an absolute `(row, col)` falls inside the selection.
102
    pub fn is_selected(&self, row: usize, col: usize) -> bool {
103
        if self.selection_block {
104
            let Some((r0, r1, c0, c1)) = self.block_rect() else {
105
                return false;
106
            };
107
            return row >= r0 && row <= r1 && col >= c0 && col <= c1;
108
        }
109
        let Some((start, end)) = self.ordered_selection() else {
110
            return false;
111
        };
112
        if row < start.row || row > end.row {
113
            return false;
114
        }
115
        let lo = if row == start.row { start.col } else { 0 };
116
        let hi = if row == end.row { end.col } else { usize::MAX };
117
        col >= lo && col <= hi
118
    }
119
120
    /// The inclusive `(lo, hi)` column span selected on absolute row `row`, if
121
    /// any part of that row is selected.
122
    pub fn selection_span_on(&self, row: usize) -> Option<(usize, usize)> {
123
        if self.selection_block {
124
            let (r0, r1, c0, c1) = self.block_rect()?;
125
            return (row >= r0 && row <= r1).then_some((c0, c1));
126
        }
127
        let (start, end) = self.ordered_selection()?;
128
        if row < start.row || row > end.row {
129
            return None;
130
        }
131
        let lo = if row == start.row { start.col } else { 0 };
132
        let hi = if row == end.row {
133
            end.col
134
        } else {
135
            self.abs_row(row).len().saturating_sub(1)
136
        };
137
        Some((lo, hi))
138
    }
139
140
    /// The selected text, with trailing blanks trimmed per line and rows joined
141
    /// by newlines. `None` if there is no selection.
142
    pub fn selection_text(&self) -> Option<String> {
143
        if self.selection_block {
144
            let (r0, r1, c0, c1) = self.block_rect()?;
145
            let mut out = String::new();
146
            for row in r0..=r1 {
147
                out.push_str(self.row_slice_text(row, c0, c1 + 1).trim_end());
148
                if row != r1 {
149
                    out.push('\n');
150
                }
151
            }
152
            return Some(out);
153
        }
154
        let (start, end) = self.ordered_selection()?;
155
        let mut out = String::new();
156
        for row in start.row..=end.row {
157
            let line = self.abs_row(row);
158
            let lo = if row == start.row { start.col } else { 0 };
159
            let hi = if row == end.row {
160
                (end.col + 1).min(line.len())
161
            } else {
162
                line.len()
163
            };
164
            out.push_str(self.row_slice_text(row, lo, hi).trim_end());
165
            if row != end.row {
166
                out.push('\n');
167
            }
168
        }
169
        Some(out)
170
    }
171
172
    /// The characters of an absolute row in `[from, to)`, skipping wide
173
    /// continuation cells.
174
    pub(super) fn row_slice_text(&self, row: usize, from: usize, to: usize) -> String {
175
        let mut out = String::new();
176
        for cell in self
177
            .abs_row(row)
178
            .get(from..to.min(self.abs_row(row).len()))
179
            .unwrap_or(&[])
180
            .iter()
181
            .filter(|c| !c.flags.contains(Flags::WIDE_CONT))
182
        {
183
            out.push(cell.c);
184
            if let Some(marks) = &cell.combining {
185
                out.push_str(marks);
186
            }
187
        }
188
        out
189
    }
190
}