use super::{AbsolutePosition, Screen};
use crate::cell::Cell;
use crate::row::Row;
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub enum SelectionMode {
Linear,
Line,
Word,
Block,
}
#[non_exhaustive]
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct SelectionRange {
pub start: AbsolutePosition,
pub end: AbsolutePosition,
pub mode: SelectionMode,
}
#[derive(Copy, Clone, Debug)]
pub(super) struct SelectionState {
pub(super) anchor: AbsolutePosition,
pub(super) cursor: AbsolutePosition,
pub(super) mode: SelectionMode,
}
impl SelectionState {
pub(super) fn new(anchor: AbsolutePosition, mode: SelectionMode) -> Self {
Self {
anchor,
cursor: anchor,
mode,
}
}
pub(super) fn extend(&mut self, to: AbsolutePosition) {
self.cursor = to;
}
pub(super) fn range(&self) -> SelectionRange {
let (start, end) =
if (self.anchor.row, self.anchor.col) <= (self.cursor.row, self.cursor.col) {
(self.anchor, self.cursor)
} else {
(self.cursor, self.anchor)
};
SelectionRange {
start,
end,
mode: self.mode,
}
}
}
pub(super) fn selected_text(screen: &Screen, state: &SelectionState) -> Option<String> {
let range = state.range();
let cols = screen.size().cols;
if cols == 0 {
return Some(String::new());
}
let last_col = cols - 1;
match range.mode {
SelectionMode::Linear => render_linear(screen, range.start, range.end, last_col),
SelectionMode::Line => render_linear(
screen,
AbsolutePosition {
row: range.start.row,
col: 0,
},
AbsolutePosition {
row: range.end.row,
col: last_col,
},
last_col,
),
SelectionMode::Word => {
let (start, end) = snap_word_endpoints(screen, range.start, range.end, last_col)?;
render_linear(screen, start, end, last_col)
}
SelectionMode::Block => render_block(screen, range.start, range.end),
}
}
fn render_linear(
screen: &Screen,
start: AbsolutePosition,
end: AbsolutePosition,
last_col: u16,
) -> Option<String> {
let mut result = String::new();
let mut prev_wrapped = false;
for abs_row in start.row..=end.row {
let row = screen.grid.row_at_absolute(abs_row)?;
let col_start = if abs_row == start.row { start.col } else { 0 };
let col_end = if abs_row == end.row {
end.col
} else {
last_col
};
if abs_row > start.row && !prev_wrapped {
result.push('\n');
}
let mut row_text = String::new();
let mut col = col_start;
while col <= col_end {
if let Some(cell) = row.get(col) {
if cell.is_wide_continuation() {
col += 1;
continue;
}
let c = cell.contents();
if c.is_empty() {
row_text.push(' ');
} else {
row_text.push_str(c);
}
}
col += 1;
}
result.push_str(row_text.trim_end());
prev_wrapped = row.wrapped();
}
Some(result)
}
fn render_block(screen: &Screen, start: AbsolutePosition, end: AbsolutePosition) -> Option<String> {
let cols = screen.size().cols;
if cols == 0 {
return Some(String::new());
}
let max_col_in_grid = cols - 1;
let raw_min = start.col.min(end.col);
let max_col = start.col.max(end.col).min(max_col_in_grid);
let mut result = String::new();
for abs_row in start.row..=end.row {
let row = screen.grid.row_at_absolute(abs_row)?;
if abs_row > start.row {
result.push('\n');
}
let min_col = if raw_min > 0
&& row
.get(raw_min)
.map(Cell::is_wide_continuation)
.unwrap_or(false)
{
raw_min - 1
} else {
raw_min
};
let mut row_text = String::new();
let mut col = min_col;
while col <= max_col {
if let Some(cell) = row.get(col) {
if cell.is_wide_continuation() {
col += 1;
continue;
}
let c = cell.contents();
if c.is_empty() {
row_text.push(' ');
} else {
row_text.push_str(c);
}
}
col += 1;
}
result.push_str(row_text.trim_end());
}
Some(result)
}
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
enum WordClass {
Whitespace,
Word,
Other,
}
fn cell_word_class(cell: &Cell) -> WordClass {
let s = cell.contents();
let Some(c) = s.chars().next() else {
return WordClass::Whitespace;
};
if c.is_whitespace() {
return WordClass::Whitespace;
}
if c.is_alphanumeric() || c == '_' {
return WordClass::Word;
}
WordClass::Other
}
fn class_at(row: &Row, col: u16) -> WordClass {
let Some(cell) = row.get(col) else {
return WordClass::Whitespace;
};
if cell.is_wide_continuation() && col > 0 {
return class_at(row, col - 1);
}
cell_word_class(cell)
}
fn snap_word_endpoints(
screen: &Screen,
start: AbsolutePosition,
end: AbsolutePosition,
last_col: u16,
) -> Option<(AbsolutePosition, AbsolutePosition)> {
let start_row = screen.grid.row_at_absolute(start.row)?;
let end_row = screen.grid.row_at_absolute(end.row)?;
let snapped_start_col = snap_left(start_row, start.col);
let snapped_end_col = snap_right(end_row, end.col, last_col);
Some((
AbsolutePosition {
row: start.row,
col: snapped_start_col,
},
AbsolutePosition {
row: end.row,
col: snapped_end_col,
},
))
}
fn snap_left(row: &Row, col: u16) -> u16 {
let target = class_at(row, col);
let mut cur = col;
while cur > 0 {
let next = cur - 1;
if class_at(row, next) != target {
break;
}
cur = next;
}
if let Some(cell) = row.get(cur)
&& cell.is_wide_continuation()
&& cur > 0
{
cur -= 1;
}
cur
}
fn snap_right(row: &Row, col: u16, last_col: u16) -> u16 {
let target = class_at(row, col);
let mut cur = col;
while cur < last_col {
let next = cur + 1;
if class_at(row, next) != target {
break;
}
cur = next;
}
if let Some(cell) = row.get(cur)
&& cell.is_wide()
&& cur < last_col
{
cur += 1;
}
cur
}