use crate::terminal::frame::{CellSelection, TerminalFrame};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Cell {
pub row: u16,
pub column: u16,
}
impl Cell {
#[must_use]
pub fn new(column: u16, row: u16) -> Self {
Self { row, column }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Selection {
anchor: Cell,
head: Cell,
revision: u64,
rectangle: bool,
dragging: bool,
}
impl Selection {
#[must_use]
pub fn new(anchor: Cell, revision: u64, rectangle: bool) -> Self {
Self {
anchor,
head: anchor,
revision,
rectangle,
dragging: true,
}
}
pub fn extend(&mut self, head: Cell) {
self.head = head;
}
#[must_use]
pub fn is_dragging(self) -> bool {
self.dragging
}
pub fn finish(&mut self) {
self.dragging = false;
}
#[must_use]
pub fn refresh(mut self, previous: &TerminalFrame, next: &TerminalFrame) -> Option<Self> {
if previous.viewport != next.viewport
|| previous.scroll != next.scroll
|| previous.modes.alternate_screen != next.modes.alternate_screen
{
return None;
}
if previous.content_revision != next.content_revision {
let (start, end) = self.ordered();
for index in start.row..=end.row {
let old = previous.rows.get(usize::from(index))?;
let new = next.rows.get(usize::from(index))?;
if old.text != new.text
|| old.cells != new.cells
|| old.soft_wrapped != new.soft_wrapped
{
return None;
}
}
}
self.revision = next.content_revision;
Some(self)
}
#[must_use]
pub fn is_empty(self) -> bool {
self.anchor == self.head
}
fn ordered(self) -> (Cell, Cell) {
if self.head < self.anchor {
(self.head, self.anchor)
} else {
(self.anchor, self.head)
}
}
#[must_use]
pub fn columns(self, row: u16, columns: u16) -> std::ops::Range<u16> {
let (start, end) = self.ordered();
if row < start.row || row > end.row {
return 0..0;
}
if self.rectangle {
let left = start.column.min(end.column);
let right = start.column.max(end.column);
return left..right.min(columns);
}
let left = if row == start.row { start.column } else { 0 };
let right = if row == end.row { end.column } else { columns };
left..right.min(columns)
}
#[must_use]
pub fn to_cells(self) -> CellSelection {
let (start, end) = self.ordered();
CellSelection::new(
(start.column, start.row),
(end.column.saturating_sub(1), end.row),
self.rectangle,
)
}
#[must_use]
pub fn text(self, frame: &TerminalFrame) -> String {
let mut text = String::new();
let mut needs_newline = false;
for (index, row) in frame.rows.iter().enumerate() {
let Ok(row_index) = u16::try_from(index) else {
break;
};
let columns = self.columns(row_index, frame.viewport.columns());
if columns.is_empty() {
continue;
}
if needs_newline {
text.push('\n');
}
for cell in row.cells.iter() {
if cell.spacer
|| cell.column >= columns.end
|| columns.start >= cell.column.saturating_add(u16::from(cell.width))
{
continue;
}
if let Some(grapheme) = row.text.get(cell.text.clone()) {
text.push_str(grapheme);
}
}
needs_newline = self.rectangle || !row.soft_wrapped;
}
text
}
}
pub(crate) fn trim_lines(text: &str) -> String {
text.lines()
.map(str::trim_end)
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_click_without_movement_selects_nothing() {
let selection = Selection::new(Cell::new(3, 1), 7, false);
assert!(selection.is_empty());
assert!(selection.columns(1, 80).is_empty());
}
#[test]
fn a_multi_row_drag_selects_to_the_row_edges_in_either_direction() {
let mut forward = Selection::new(Cell::new(5, 1), 7, false);
forward.extend(Cell::new(2, 3));
assert_eq!(forward.columns(0, 80), 0..0);
assert_eq!(forward.columns(1, 80), 5..80);
assert_eq!(forward.columns(2, 80), 0..80);
assert_eq!(forward.columns(3, 80), 0..2);
let mut backward = Selection::new(Cell::new(2, 3), 7, false);
backward.extend(Cell::new(5, 1));
for row in 0..5 {
assert_eq!(forward.columns(row, 80), backward.columns(row, 80));
}
}
#[test]
fn a_rectangular_drag_uses_the_same_columns_on_every_row() {
let mut selection = Selection::new(Cell::new(5, 1), 7, true);
selection.extend(Cell::new(2, 3));
for row in 1..=3 {
assert_eq!(selection.columns(row, 80), 2..5);
}
assert_eq!(selection.columns(4, 80), 0..0);
}
#[test]
fn the_engine_selection_is_ordered_and_inclusive() {
let mut selection = Selection::new(Cell::new(5, 3), 42, false);
selection.extend(Cell::new(2, 1));
let cells = selection.to_cells();
assert_eq!(cells.start, (2, 1));
assert_eq!(cells.end, (4, 3));
}
fn frame(columns: u16, rows: u16, bytes: &str) -> std::sync::Arc<TerminalFrame> {
use crate::terminal::core::{Core, Effects};
use crate::terminal::{EngineOptions, TerminalColors, Viewport};
let mut core = Core::new(
Viewport::new(columns, rows, 8, 16).unwrap(),
&EngineOptions::default(),
TerminalColors::default(),
)
.unwrap();
core.write(bytes.as_bytes(), &mut Effects::default());
core.frame(true).unwrap().unwrap()
}
fn pick(frame: &TerminalFrame, start: (u16, u16), end: (u16, u16), rectangle: bool) -> String {
let mut selection = Selection::new(
Cell::new(start.0, start.1),
frame.content_revision,
rectangle,
);
selection.extend(Cell::new(end.0, end.1));
selection.text(frame)
}
#[test]
fn copying_uses_half_open_boundaries_including_the_last_column() {
let frame = frame(6, 2, "alphaX\r\nbetaYZ");
for (start, end, expected) in [
((0, 0), (5, 0), "alpha"),
((0, 0), (1, 0), "a"),
((5, 0), (0, 0), "alpha"),
((0, 0), (6, 0), "alphaX"),
((0, 0), (0, 1), "alphaX"),
] {
assert_eq!(
pick(&frame, start, end, false),
expected,
"{start:?} to {end:?}"
);
}
assert_eq!(pick(&frame, (0, 0), (6, 1), false), "alphaX\nbetaYZ");
}
#[test]
fn copying_joins_soft_wrapped_rows_and_keeps_hard_line_breaks() {
let frame = frame(6, 3, "alpha bravo\r\nlast");
assert!(frame.rows[0].soft_wrapped);
assert!(!frame.rows[1].soft_wrapped);
let copied = pick(&frame, (0, 0), (4, 2), false);
assert_eq!(copied, "alpha bravo \nlast");
assert_eq!(trim_lines(&copied), "alpha bravo\nlast");
}
#[test]
fn copying_a_rectangle_keeps_wrapped_rows_separate() {
let frame = frame(6, 2, "abcdefghijkl");
assert!(frame.rows[0].soft_wrapped);
assert_eq!(pick(&frame, (1, 0), (4, 1), true), "bcd\nhij");
assert_eq!(pick(&frame, (4, 1), (1, 0), true), "bcd\nhij");
}
#[test]
fn copying_either_half_of_a_wide_cell_keeps_the_whole_grapheme() {
let frame = frame(10, 1, "\u{6f22}e\u{301}X");
for (start, end, expected) in [
(0, 1, "\u{6f22}"),
(1, 2, "\u{6f22}"),
(0, 2, "\u{6f22}"),
(2, 3, "e\u{301}"),
(0, 4, "\u{6f22}e\u{301}X"),
] {
assert_eq!(
pick(&frame, (start, 0), (end, 0), false),
expected,
"columns {start}..{end}"
);
}
}
}