| 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 | } |