Integration pass over the terminal core (Codex/gpt-6-astra, reviewed): - Screen now owns the real kitty ImageStorage; new/reset/teardown, erase-display deletes and alternate-screen limit inheritance hook in where Zig does, and Terminal implements ExecTerminal so kitty APC commands execute against it. Exec tests use a real Terminal. - The PageList gains the APIs Screen/Terminal needed (pin iterators, cross-page row/partial-row clone, string allocator probing, dedup before allocating hyperlinks, Top viewport fixup after erase, append_ grapheme no longer marking rows dirty), and the Screen/Terminal workarounds for their absence are removed so the code mirrors Zig. Public signatures are unchanged; downstream crates build as before. Test Plan: - cargo test -p ghostty-terminal (1252 passed, 0 ignored) - cargo clippy -p ghostty-terminal --all-targets -- -D warnings
1043 lines
34 KiB
Rust
1043 lines
34 KiB
Rust
//! Represents a single selection within the terminal (i.e. a highlight region).
|
|
//!
|
|
//! Unlike Zig, tracked bounds hold [`TrackedPin`] handles into the owning
|
|
//! [`PageList`], so every method that must read (or move) the pins takes the
|
|
//! screen's page list explicitly (`&screen.pages`).
|
|
#![allow(clippy::manual_assert_eq)]
|
|
use crate::{
|
|
page::{Cell, Error},
|
|
page_list::{Direction, PageList, Pin, TrackedPin},
|
|
point::{Coordinate, Point, Tag},
|
|
};
|
|
|
|
// NOTE(mitchellh): I'm not very happy with how this is implemented, because
|
|
// the ordering operations which are used frequently require using
|
|
// pointFromPin which -- at the time of writing this -- is slow. The overall
|
|
// style of this struct is due to porting it from the previous implementation
|
|
// which had an efficient ordering operation.
|
|
//
|
|
// While reimplementing this, there were too many callers that already
|
|
// depended on this behavior so I kept it despite the inefficiency. In the
|
|
// future, we should take a look at this again!
|
|
|
|
/// The bounds of the selection. A selection bounds can be either tracked
|
|
/// or untracked. Untracked bounds are unsafe beyond the point the terminal
|
|
/// screen may be modified, since they may point to invalid memory. Tracked
|
|
/// bounds are always valid and will be updated as the screen changes, but
|
|
/// are more expensive to exist.
|
|
///
|
|
/// In all cases, start and end can be in any order. There is no guarantee that
|
|
/// start is before end or vice versa. If a user selects backwards,
|
|
/// start will be after end, and vice versa. Use the struct functions
|
|
/// to not have to worry about this.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Bounds {
|
|
Untracked { start: Pin, end: Pin },
|
|
Tracked { start: TrackedPin, end: TrackedPin },
|
|
}
|
|
|
|
/// Represents a single selection within the terminal (i.e. a highlight region).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub struct Selection {
|
|
/// The bounds of the selection.
|
|
pub bounds: Bounds,
|
|
|
|
/// Whether or not this selection refers to a rectangle, rather than whole
|
|
/// lines of a buffer. In this mode, start and end refer to the top left and
|
|
/// bottom right of the rectangle, or vice versa if the selection is backwards.
|
|
pub rectangle: bool,
|
|
}
|
|
|
|
/// The order of the selection:
|
|
///
|
|
/// * forward: start(x, y) is before end(x, y) (top-left to bottom-right).
|
|
/// * reverse: end(x, y) is before start(x, y) (bottom-right to top-left).
|
|
/// * mirrored_[forward|reverse]: special, rectangle selections only (see below).
|
|
///
|
|
/// For regular selections, the above also holds for top-right to bottom-left
|
|
/// (forward) and bottom-left to top-right (reverse). However, for rectangle
|
|
/// selections, both of these selections are *mirrored* as orientation
|
|
/// operations only flip the x or y axis, not both. Depending on the y axis
|
|
/// direction, this is either mirrored_forward or mirrored_reverse.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Order {
|
|
Forward,
|
|
Reverse,
|
|
MirroredForward,
|
|
MirroredReverse,
|
|
}
|
|
|
|
/// Possible adjustments to the selection.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Adjustment {
|
|
Left,
|
|
Right,
|
|
Up,
|
|
Down,
|
|
Home,
|
|
End,
|
|
PageUp,
|
|
PageDown,
|
|
BeginningOfLine,
|
|
EndOfLine,
|
|
}
|
|
|
|
fn screen_coord(list: &PageList, p: Pin) -> Coordinate {
|
|
list.point_from_pin(Tag::Screen, p)
|
|
.expect("selection pin must be in the screen")
|
|
.coord()
|
|
}
|
|
|
|
impl Selection {
|
|
/// Initialize a new (untracked) selection with the given start and end
|
|
/// pins on the screen.
|
|
pub fn new(start_pin: Pin, end_pin: Pin, rect: bool) -> Self {
|
|
Self {
|
|
bounds: Bounds::Untracked {
|
|
start: start_pin,
|
|
end: end_pin,
|
|
},
|
|
rectangle: rect,
|
|
}
|
|
}
|
|
|
|
/// Release the tracked pins (if any).
|
|
pub fn deinit(self, list: &mut PageList) {
|
|
if let Bounds::Tracked { start, end } = self.bounds {
|
|
list.untrack_pin(start);
|
|
list.untrack_pin(end);
|
|
}
|
|
}
|
|
|
|
/// Returns true if this selection is equal to another selection.
|
|
pub fn eql(&self, list: &PageList, other: &Selection) -> bool {
|
|
self.start(list).eql(other.start(list))
|
|
&& self.end(list).eql(other.end(list))
|
|
&& self.rectangle == other.rectangle
|
|
}
|
|
|
|
/// The starting pin of the selection. This is NOT ordered.
|
|
pub fn start(&self, list: &PageList) -> Pin {
|
|
match self.bounds {
|
|
Bounds::Untracked { start, .. } => start,
|
|
Bounds::Tracked { start, .. } => list
|
|
.tracked_pin(start)
|
|
.expect("tracked selection pin must be live"),
|
|
}
|
|
}
|
|
|
|
/// The ending pin of the selection. This is NOT ordered.
|
|
pub fn end(&self, list: &PageList) -> Pin {
|
|
match self.bounds {
|
|
Bounds::Untracked { end, .. } => end,
|
|
Bounds::Tracked { end, .. } => list
|
|
.tracked_pin(end)
|
|
.expect("tracked selection pin must be live"),
|
|
}
|
|
}
|
|
|
|
/// Set the starting pin (Zig: writing through `startPtr`).
|
|
pub fn set_start(&mut self, list: &mut PageList, pin: Pin) {
|
|
match &mut self.bounds {
|
|
Bounds::Untracked { start, .. } => *start = pin,
|
|
Bounds::Tracked { start, .. } => {
|
|
list.set_tracked_pin(*start, pin)
|
|
.expect("selection pin must be valid");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Set the ending pin (Zig: writing through `endPtr`).
|
|
pub fn set_end(&mut self, list: &mut PageList, pin: Pin) {
|
|
match &mut self.bounds {
|
|
Bounds::Untracked { end, .. } => *end = pin,
|
|
Bounds::Tracked { end, .. } => {
|
|
list.set_tracked_pin(*end, pin)
|
|
.expect("selection pin must be valid");
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns true if this is a tracked selection.
|
|
pub fn tracked(&self) -> bool {
|
|
matches!(self.bounds, Bounds::Tracked { .. })
|
|
}
|
|
|
|
/// Convert this selection a tracked selection. It is asserted this is
|
|
/// an untracked selection. The tracked selection is returned.
|
|
pub fn track(&self, list: &mut PageList) -> Result<Selection, Error> {
|
|
let Bounds::Untracked { start, end } = self.bounds else {
|
|
panic!("selection is already tracked");
|
|
};
|
|
|
|
// Track our pins
|
|
let tracked_start = list.track_pin(start)?;
|
|
let tracked_end = match list.track_pin(end) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
list.untrack_pin(tracked_start);
|
|
return Err(e);
|
|
}
|
|
};
|
|
|
|
Ok(Selection {
|
|
bounds: Bounds::Tracked {
|
|
start: tracked_start,
|
|
end: tracked_end,
|
|
},
|
|
rectangle: self.rectangle,
|
|
})
|
|
}
|
|
|
|
/// Returns the top left point of the selection.
|
|
pub fn top_left(&self, list: &PageList) -> Pin {
|
|
match self.order(list) {
|
|
Order::Forward => self.start(list),
|
|
Order::Reverse => self.end(list),
|
|
Order::MirroredForward => {
|
|
let mut p = self.start(list);
|
|
p.x = self.end(list).x;
|
|
p
|
|
}
|
|
Order::MirroredReverse => {
|
|
let mut p = self.end(list);
|
|
p.x = self.start(list).x;
|
|
p
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Returns the bottom right point of the selection.
|
|
pub fn bottom_right(&self, list: &PageList) -> Pin {
|
|
match self.order(list) {
|
|
Order::Forward => self.end(list),
|
|
Order::Reverse => self.start(list),
|
|
Order::MirroredForward => {
|
|
let mut p = self.end(list);
|
|
p.x = self.start(list).x;
|
|
p
|
|
}
|
|
Order::MirroredReverse => {
|
|
let mut p = self.start(list);
|
|
p.x = self.end(list).x;
|
|
p
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn order(&self, list: &PageList) -> Order {
|
|
let start_pt = screen_coord(list, self.start(list));
|
|
let end_pt = screen_coord(list, self.end(list));
|
|
|
|
if self.rectangle {
|
|
// Reverse (also handles single-column)
|
|
if start_pt.y > end_pt.y && start_pt.x >= end_pt.x {
|
|
return Order::Reverse;
|
|
}
|
|
if start_pt.y >= end_pt.y && start_pt.x > end_pt.x {
|
|
return Order::Reverse;
|
|
}
|
|
|
|
// Mirror, bottom-left to top-right
|
|
if start_pt.y > end_pt.y && start_pt.x < end_pt.x {
|
|
return Order::MirroredReverse;
|
|
}
|
|
|
|
// Mirror, top-right to bottom-left
|
|
if start_pt.y < end_pt.y && start_pt.x > end_pt.x {
|
|
return Order::MirroredForward;
|
|
}
|
|
|
|
// Forward
|
|
return Order::Forward;
|
|
}
|
|
|
|
if start_pt.y < end_pt.y {
|
|
return Order::Forward;
|
|
}
|
|
if start_pt.y > end_pt.y {
|
|
return Order::Reverse;
|
|
}
|
|
if start_pt.x <= end_pt.x {
|
|
return Order::Forward;
|
|
}
|
|
Order::Reverse
|
|
}
|
|
|
|
/// Returns the selection in the given order.
|
|
///
|
|
/// The returned selection is always a new untracked selection.
|
|
///
|
|
/// Note that only forward and reverse are useful desired orders for this
|
|
/// function. All other orders act as if forward order was desired.
|
|
pub fn ordered(&self, list: &PageList, desired: Order) -> Selection {
|
|
if self.order(list) == desired {
|
|
return Selection::new(self.start(list), self.end(list), self.rectangle);
|
|
}
|
|
|
|
let tl = self.top_left(list);
|
|
let br = self.bottom_right(list);
|
|
match desired {
|
|
Order::Reverse => Selection::new(br, tl, self.rectangle),
|
|
_ => Selection::new(tl, br, self.rectangle),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the selection contains the given point.
|
|
///
|
|
/// This recalculates top left and bottom right each call. If you have
|
|
/// many points to check, it is cheaper to do the containment logic
|
|
/// yourself and cache the topleft/bottomright.
|
|
pub fn contains(&self, list: &PageList, pin: Pin) -> bool {
|
|
let tl_pin = self.top_left(list);
|
|
let br_pin = self.bottom_right(list);
|
|
|
|
// This is definitely not very efficient. Low-hanging fruit to
|
|
// improve this.
|
|
let tl = screen_coord(list, tl_pin);
|
|
let br = screen_coord(list, br_pin);
|
|
let p = screen_coord(list, pin);
|
|
|
|
// If we're in rectangle select, we can short-circuit with an easy check
|
|
// here
|
|
if self.rectangle {
|
|
return p.y >= tl.y && p.y <= br.y && p.x >= tl.x && p.x <= br.x;
|
|
}
|
|
|
|
// If tl/br are same line
|
|
if tl.y == br.y {
|
|
return p.y == tl.y && p.x >= tl.x && p.x <= br.x;
|
|
}
|
|
|
|
// If on top line, just has to be left of X
|
|
if p.y == tl.y {
|
|
return p.x >= tl.x;
|
|
}
|
|
|
|
// If on bottom line, just has to be right of X
|
|
if p.y == br.y {
|
|
return p.x <= br.x;
|
|
}
|
|
|
|
// If between the top/bottom, always good.
|
|
p.y > tl.y && p.y < br.y
|
|
}
|
|
|
|
/// Get a selection for a single row in the screen. This will return None
|
|
/// if the row is not included in the selection.
|
|
///
|
|
/// This is a very expensive operation. It has to traverse the linked list
|
|
/// of pages for the top-left, bottom-right, and the given pin to find
|
|
/// the coordinates. If you are calling this repeatedly, prefer
|
|
/// `contained_row_cached`.
|
|
pub fn contained_row(&self, list: &PageList, pin: Pin) -> Option<Selection> {
|
|
let tl_pin = self.top_left(list);
|
|
let br_pin = self.bottom_right(list);
|
|
|
|
// This is definitely not very efficient. Low-hanging fruit to
|
|
// improve this. Callers should prefer contained_row_cached if they
|
|
// can swing it.
|
|
let tl = screen_coord(list, tl_pin);
|
|
let br = screen_coord(list, br_pin);
|
|
let p = screen_coord(list, pin);
|
|
|
|
self.contained_row_cached(list, tl_pin, br_pin, pin, tl, br, p)
|
|
}
|
|
|
|
/// Same as `contained_row` but useful if you're calling it repeatedly
|
|
/// so that the pins can be cached across calls. Advanced.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub fn contained_row_cached(
|
|
&self,
|
|
list: &PageList,
|
|
tl_pin: Pin,
|
|
br_pin: Pin,
|
|
pin: Pin,
|
|
tl: Coordinate,
|
|
br: Coordinate,
|
|
p: Coordinate,
|
|
) -> Option<Selection> {
|
|
if p.y < tl.y || p.y > br.y {
|
|
return None;
|
|
}
|
|
|
|
// Rectangle case: we can return early as the x range will always be the
|
|
// same. We've already validated that the row is in the selection.
|
|
if self.rectangle {
|
|
return Some(Selection::new(
|
|
Pin { x: tl.x, ..pin },
|
|
Pin { x: br.x, ..pin },
|
|
true,
|
|
));
|
|
}
|
|
|
|
if p.y == tl.y {
|
|
// If the selection is JUST this line, return it as-is.
|
|
if p.y == br.y {
|
|
return Some(Selection::new(tl_pin, br_pin, false));
|
|
}
|
|
|
|
// Selection top-left line matches only.
|
|
return Some(Selection::new(
|
|
tl_pin,
|
|
Pin {
|
|
x: list.cols - 1,
|
|
..pin
|
|
},
|
|
false,
|
|
));
|
|
}
|
|
|
|
// Row is our bottom selection, so we return the selection from the
|
|
// beginning of the line to the br. We know our selection is more than
|
|
// one line (due to conditionals above)
|
|
if p.y == br.y {
|
|
assert!(p.y != tl.y);
|
|
return Some(Selection::new(Pin { x: 0, ..pin }, br_pin, false));
|
|
}
|
|
|
|
// Row is somewhere between our selection lines so we return the full line.
|
|
Some(Selection::new(
|
|
Pin { x: 0, ..pin },
|
|
Pin {
|
|
x: list.cols - 1,
|
|
..pin
|
|
},
|
|
false,
|
|
))
|
|
}
|
|
|
|
/// Adjust the selection by some given adjustment. An adjustment allows
|
|
/// a selection to be expanded slightly left, right, up, down, etc.
|
|
pub fn adjust(&mut self, list: &mut PageList, adjustment: Adjustment) {
|
|
// Note that we always adjust "end" because end always represents
|
|
// the last point of the selection by mouse, not necessarily the
|
|
// top/bottom visually. So this results in the correct behavior
|
|
// whether the user drags up or down.
|
|
let end_pin = self.end(list);
|
|
match adjustment {
|
|
Adjustment::Up => {
|
|
if let Some(new_end) = end_pin.up(list, 1) {
|
|
self.set_end(list, new_end);
|
|
} else {
|
|
self.adjust(list, Adjustment::BeginningOfLine);
|
|
}
|
|
}
|
|
|
|
Adjustment::Down => {
|
|
// Find the next non-blank row
|
|
let mut current = end_pin;
|
|
loop {
|
|
let Some(next) = current.down(list, 1) else {
|
|
// If we're at the bottom, just go to the end of the line
|
|
self.adjust(list, Adjustment::EndOfLine);
|
|
break;
|
|
};
|
|
let has_text = list
|
|
.node(next.node)
|
|
.and_then(|n| n.data.get_cells(usize::from(next.y)))
|
|
.is_some_and(Cell::has_text_any);
|
|
if has_text {
|
|
self.set_end(list, next);
|
|
break;
|
|
}
|
|
current = next;
|
|
}
|
|
}
|
|
|
|
Adjustment::Left => {
|
|
let found = end_pin
|
|
.cell_iterator(list, Direction::LeftUp, None)
|
|
.skip(1)
|
|
.find(|p| cell_has_text(list, *p));
|
|
if let Some(next) = found {
|
|
self.set_end(list, next);
|
|
}
|
|
}
|
|
|
|
Adjustment::Right => {
|
|
// Step right, wrapping to the next row down at the start of each new line,
|
|
// until we find a non-empty cell.
|
|
let found = end_pin
|
|
.cell_iterator(list, Direction::RightDown, None)
|
|
.skip(1)
|
|
.find(|p| cell_has_text(list, *p));
|
|
if let Some(next) = found {
|
|
self.set_end(list, next);
|
|
}
|
|
}
|
|
|
|
Adjustment::PageUp => {
|
|
if let Some(new_end) = end_pin.up(list, usize::from(list.rows)) {
|
|
self.set_end(list, new_end);
|
|
} else {
|
|
self.adjust(list, Adjustment::Home);
|
|
}
|
|
}
|
|
|
|
Adjustment::PageDown => {
|
|
if let Some(new_end) = end_pin.down(list, usize::from(list.rows)) {
|
|
self.set_end(list, new_end);
|
|
} else {
|
|
self.adjust(list, Adjustment::End);
|
|
}
|
|
}
|
|
|
|
Adjustment::Home => {
|
|
let p = list
|
|
.pin(Point::Screen(Coordinate { x: 0, y: 0 }))
|
|
.expect("screen has a top-left");
|
|
self.set_end(list, p);
|
|
}
|
|
|
|
Adjustment::End => {
|
|
let found = list
|
|
.row_iterator(
|
|
Direction::LeftUp,
|
|
Point::Screen(Coordinate::default()),
|
|
None,
|
|
)
|
|
.find_map(|next| {
|
|
let cells = list.node(next.node)?.data.get_cells(usize::from(next.y))?;
|
|
if Cell::has_text_any(cells) {
|
|
Some(Pin {
|
|
x: (cells.len() - 1) as u16,
|
|
..next
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
if let Some(p) = found {
|
|
self.set_end(list, p);
|
|
}
|
|
}
|
|
|
|
Adjustment::BeginningOfLine => {
|
|
self.set_end(list, Pin { x: 0, ..end_pin });
|
|
}
|
|
|
|
Adjustment::EndOfLine => {
|
|
let cols = list
|
|
.node(end_pin.node)
|
|
.map_or(list.cols, |n| n.data.size.cols);
|
|
self.set_end(
|
|
list,
|
|
Pin {
|
|
x: cols - 1,
|
|
..end_pin
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn cell_has_text(list: &PageList, p: Pin) -> bool {
|
|
p.row_and_cell(list).is_some_and(|(_, c)| c.has_text())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::screen::{Options, Screen};
|
|
|
|
fn init(cols: u16, rows: u16, max_scrollback: usize) -> Screen {
|
|
Screen::new(Options::new(cols, rows, max_scrollback)).unwrap()
|
|
}
|
|
|
|
/// `s.pages.pin(.{ .screen = .{ .x, .y } }).?`
|
|
fn sp(s: &Screen, x: u16, y: u32) -> Pin {
|
|
s.pages.pin(Point::Screen(Coordinate { x, y })).unwrap()
|
|
}
|
|
|
|
/// `s.pages.pointFromPin(.screen, pin).?`
|
|
fn spt(s: &Screen, p: Pin) -> Point {
|
|
s.pages.point_from_pin(Tag::Screen, p).unwrap()
|
|
}
|
|
|
|
fn pt(x: u16, y: u32) -> Point {
|
|
Point::Screen(Coordinate { x, y })
|
|
}
|
|
|
|
fn sel_of(s: &Screen, a: (u16, u32), b: (u16, u32), rect: bool) -> Selection {
|
|
Selection::new(sp(s, a.0, a.1), sp(s, b.0, b.1), rect)
|
|
}
|
|
|
|
/// Adjust a fresh selection and check the resulting start/end points.
|
|
fn check_adjust(
|
|
s: &mut Screen,
|
|
start: (u16, u32),
|
|
end: (u16, u32),
|
|
adj: Adjustment,
|
|
want_start: (u16, u32),
|
|
want_end: (u16, u32),
|
|
) {
|
|
let mut sel = sel_of(s, start, end, false);
|
|
sel.adjust(&mut s.pages, adj);
|
|
assert_eq!(pt(want_start.0, want_start.1), spt(s, sel.start(&s.pages)));
|
|
assert_eq!(pt(want_end.0, want_end.1), spt(s, sel.end(&s.pages)));
|
|
sel.deinit(&mut s.pages);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_right() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A1234\nB5678\nC1234\nD5678").unwrap();
|
|
|
|
// Simple movement right
|
|
check_adjust(&mut s, (5, 1), (3, 3), Adjustment::Right, (5, 1), (4, 3));
|
|
|
|
// Already at end of the line.
|
|
check_adjust(&mut s, (4, 1), (4, 2), Adjustment::Right, (4, 1), (0, 3));
|
|
|
|
// Already at end of the screen
|
|
check_adjust(&mut s, (5, 1), (4, 3), Adjustment::Right, (5, 1), (4, 3));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_left() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A1234\nB5678\nC1234\nD5678").unwrap();
|
|
|
|
// Simple movement left
|
|
check_adjust(&mut s, (5, 1), (3, 3), Adjustment::Left, (5, 1), (2, 3));
|
|
|
|
// Already at beginning of the line.
|
|
check_adjust(&mut s, (5, 1), (0, 3), Adjustment::Left, (5, 1), (4, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_left_skips_blanks() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A1234\nB5678\nC12\nD56").unwrap();
|
|
|
|
// Same line
|
|
check_adjust(&mut s, (5, 1), (4, 3), Adjustment::Left, (5, 1), (2, 3));
|
|
|
|
// Edge
|
|
check_adjust(&mut s, (5, 1), (0, 3), Adjustment::Left, (5, 1), (2, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_up() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A\nB\nC\nD\nE").unwrap();
|
|
|
|
// Not on the first line
|
|
check_adjust(&mut s, (5, 1), (3, 3), Adjustment::Up, (5, 1), (3, 2));
|
|
|
|
// On the first line
|
|
check_adjust(&mut s, (5, 1), (3, 0), Adjustment::Up, (5, 1), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_down() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A\nB\nC\nD\nE").unwrap();
|
|
|
|
// Not on the first line
|
|
check_adjust(&mut s, (5, 1), (3, 3), Adjustment::Down, (5, 1), (3, 4));
|
|
|
|
// On the last line
|
|
check_adjust(&mut s, (4, 1), (3, 4), Adjustment::Down, (4, 1), (9, 4));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_down_with_not_full_screen() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A\nB\nC").unwrap();
|
|
|
|
// On the last line
|
|
check_adjust(&mut s, (4, 1), (3, 2), Adjustment::Down, (4, 1), (9, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_home() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A\nB\nC").unwrap();
|
|
|
|
// On the last line
|
|
check_adjust(&mut s, (4, 1), (1, 2), Adjustment::Home, (4, 1), (0, 0));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_end_with_not_full_screen() {
|
|
let mut s = init(10, 10, 0);
|
|
s.test_write_string("A\nB\nC").unwrap();
|
|
|
|
// On the last line
|
|
check_adjust(&mut s, (4, 0), (1, 1), Adjustment::End, (4, 0), (9, 2));
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_beginning_of_line() {
|
|
let mut s = init(8, 10, 0);
|
|
s.test_write_string("A12 B34\nC12 D34").unwrap();
|
|
|
|
// Not at beginning of the line
|
|
check_adjust(
|
|
&mut s,
|
|
(5, 1),
|
|
(5, 1),
|
|
Adjustment::BeginningOfLine,
|
|
(5, 1),
|
|
(0, 1),
|
|
);
|
|
|
|
// Already at beginning of the line
|
|
check_adjust(
|
|
&mut s,
|
|
(5, 1),
|
|
(0, 1),
|
|
Adjustment::BeginningOfLine,
|
|
(5, 1),
|
|
(0, 1),
|
|
);
|
|
|
|
// End pin moves to start pin
|
|
check_adjust(
|
|
&mut s,
|
|
(0, 1),
|
|
(5, 1),
|
|
Adjustment::BeginningOfLine,
|
|
(0, 1),
|
|
(0, 1),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_adjust_end_of_line() {
|
|
let mut s = init(8, 10, 0);
|
|
s.test_write_string("A12 B34\nC12 D34").unwrap();
|
|
|
|
// Not at end of the line
|
|
check_adjust(
|
|
&mut s,
|
|
(1, 0),
|
|
(1, 0),
|
|
Adjustment::EndOfLine,
|
|
(1, 0),
|
|
(7, 0),
|
|
);
|
|
|
|
// Already at end of the line
|
|
check_adjust(
|
|
&mut s,
|
|
(1, 0),
|
|
(7, 0),
|
|
Adjustment::EndOfLine,
|
|
(1, 0),
|
|
(7, 0),
|
|
);
|
|
|
|
// End pin moves to start pin
|
|
check_adjust(
|
|
&mut s,
|
|
(7, 0),
|
|
(1, 0),
|
|
Adjustment::EndOfLine,
|
|
(7, 0),
|
|
(7, 0),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_order_standard() {
|
|
let s = init(100, 100, 1);
|
|
|
|
// forward, multi-line
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (2, 1), (2, 2), false).order(&s.pages)
|
|
);
|
|
// reverse, multi-line
|
|
assert_eq!(
|
|
Order::Reverse,
|
|
sel_of(&s, (2, 2), (2, 1), false).order(&s.pages)
|
|
);
|
|
// forward, same-line
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (2, 1), (3, 1), false).order(&s.pages)
|
|
);
|
|
// forward, single char
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (2, 1), (2, 1), false).order(&s.pages)
|
|
);
|
|
// reverse, single line
|
|
assert_eq!(
|
|
Order::Reverse,
|
|
sel_of(&s, (2, 1), (1, 1), false).order(&s.pages)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn selection_order_rectangle() {
|
|
let s = init(100, 100, 1);
|
|
|
|
// Conventions:
|
|
// TL - top left
|
|
// BL - bottom left
|
|
// TR - top right
|
|
// BR - bottom right
|
|
|
|
// forward (TL -> BR)
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (1, 1), (2, 2), true).order(&s.pages)
|
|
);
|
|
// reverse (BR -> TL)
|
|
assert_eq!(
|
|
Order::Reverse,
|
|
sel_of(&s, (2, 2), (1, 1), true).order(&s.pages)
|
|
);
|
|
// mirrored_forward (TR -> BL)
|
|
assert_eq!(
|
|
Order::MirroredForward,
|
|
sel_of(&s, (3, 1), (1, 3), true).order(&s.pages)
|
|
);
|
|
// mirrored_reverse (BL -> TR)
|
|
assert_eq!(
|
|
Order::MirroredReverse,
|
|
sel_of(&s, (1, 3), (3, 1), true).order(&s.pages)
|
|
);
|
|
// forward, single line (left -> right )
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (1, 1), (3, 1), true).order(&s.pages)
|
|
);
|
|
// reverse, single line (right -> left)
|
|
assert_eq!(
|
|
Order::Reverse,
|
|
sel_of(&s, (3, 1), (1, 1), true).order(&s.pages)
|
|
);
|
|
// forward, single column (top -> bottom)
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (2, 1), (2, 3), true).order(&s.pages)
|
|
);
|
|
// reverse, single column (bottom -> top)
|
|
assert_eq!(
|
|
Order::Reverse,
|
|
sel_of(&s, (2, 3), (2, 1), true).order(&s.pages)
|
|
);
|
|
// forward, single cell
|
|
assert_eq!(
|
|
Order::Forward,
|
|
sel_of(&s, (1, 1), (1, 1), true).order(&s.pages)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn top_left() {
|
|
let s = init(10, 10, 0);
|
|
// forward
|
|
let sel = sel_of(&s, (1, 1), (3, 1), true);
|
|
assert_eq!(pt(1, 1), spt(&s, sel.top_left(&s.pages)));
|
|
// reverse
|
|
let sel = sel_of(&s, (3, 1), (1, 1), true);
|
|
assert_eq!(pt(1, 1), spt(&s, sel.top_left(&s.pages)));
|
|
// mirrored_forward
|
|
let sel = sel_of(&s, (3, 1), (1, 3), true);
|
|
assert_eq!(pt(1, 1), spt(&s, sel.top_left(&s.pages)));
|
|
// mirrored_reverse
|
|
let sel = sel_of(&s, (1, 3), (3, 1), true);
|
|
assert_eq!(pt(1, 1), spt(&s, sel.top_left(&s.pages)));
|
|
}
|
|
|
|
#[test]
|
|
fn bottom_right() {
|
|
let s = init(10, 10, 0);
|
|
// forward
|
|
let sel = sel_of(&s, (1, 1), (3, 1), false);
|
|
assert_eq!(pt(3, 1), spt(&s, sel.bottom_right(&s.pages)));
|
|
// reverse
|
|
let sel = sel_of(&s, (3, 1), (1, 1), false);
|
|
assert_eq!(pt(3, 1), spt(&s, sel.bottom_right(&s.pages)));
|
|
// mirrored_forward
|
|
let sel = sel_of(&s, (3, 1), (1, 3), true);
|
|
assert_eq!(pt(3, 3), spt(&s, sel.bottom_right(&s.pages)));
|
|
// mirrored_reverse
|
|
let sel = sel_of(&s, (1, 3), (3, 1), true);
|
|
assert_eq!(pt(3, 3), spt(&s, sel.bottom_right(&s.pages)));
|
|
}
|
|
|
|
#[test]
|
|
fn ordered() {
|
|
let s = init(10, 10, 0);
|
|
let l = &s.pages;
|
|
{
|
|
// forward
|
|
let sel = sel_of(&s, (1, 1), (3, 1), false);
|
|
let sel_reverse = sel_of(&s, (3, 1), (1, 1), false);
|
|
assert!(sel.ordered(l, Order::Forward).eql(l, &sel));
|
|
assert!(sel.ordered(l, Order::Reverse).eql(l, &sel_reverse));
|
|
assert!(sel.ordered(l, Order::MirroredForward).eql(l, &sel));
|
|
}
|
|
{
|
|
// reverse
|
|
let sel = sel_of(&s, (3, 1), (1, 1), false);
|
|
let sel_forward = sel_of(&s, (1, 1), (3, 1), false);
|
|
assert!(sel.ordered(l, Order::Forward).eql(l, &sel_forward));
|
|
assert!(sel.ordered(l, Order::Reverse).eql(l, &sel));
|
|
assert!(sel.ordered(l, Order::MirroredForward).eql(l, &sel_forward));
|
|
}
|
|
{
|
|
// mirrored_forward
|
|
let sel = sel_of(&s, (3, 1), (1, 3), true);
|
|
let sel_forward = sel_of(&s, (1, 1), (3, 3), true);
|
|
let sel_reverse = sel_of(&s, (3, 3), (1, 1), true);
|
|
assert!(sel.ordered(l, Order::Forward).eql(l, &sel_forward));
|
|
assert!(sel.ordered(l, Order::Reverse).eql(l, &sel_reverse));
|
|
assert!(sel.ordered(l, Order::MirroredReverse).eql(l, &sel_forward));
|
|
}
|
|
{
|
|
// mirrored_reverse
|
|
let sel = sel_of(&s, (1, 3), (3, 1), true);
|
|
let sel_forward = sel_of(&s, (1, 1), (3, 3), true);
|
|
let sel_reverse = sel_of(&s, (3, 3), (1, 1), true);
|
|
assert!(sel.ordered(l, Order::Forward).eql(l, &sel_forward));
|
|
assert!(sel.ordered(l, Order::Reverse).eql(l, &sel_reverse));
|
|
assert!(sel.ordered(l, Order::MirroredForward).eql(l, &sel_forward));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn selection_contains() {
|
|
let s = init(10, 10, 0);
|
|
let l = &s.pages;
|
|
{
|
|
let sel = sel_of(&s, (5, 1), (3, 2), false);
|
|
|
|
assert!(sel.contains(l, sp(&s, 6, 1)));
|
|
assert!(sel.contains(l, sp(&s, 1, 2)));
|
|
assert!(!sel.contains(l, sp(&s, 1, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 5, 2)));
|
|
}
|
|
|
|
// Reverse
|
|
{
|
|
let sel = sel_of(&s, (3, 2), (5, 1), false);
|
|
|
|
assert!(sel.contains(l, sp(&s, 6, 1)));
|
|
assert!(sel.contains(l, sp(&s, 1, 2)));
|
|
assert!(!sel.contains(l, sp(&s, 1, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 5, 2)));
|
|
}
|
|
|
|
// Single line
|
|
{
|
|
let sel = sel_of(&s, (5, 1), (8, 1), false);
|
|
|
|
assert!(sel.contains(l, sp(&s, 6, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 2, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 9, 1)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn selection_contains_rectangle() {
|
|
let s = init(15, 15, 0);
|
|
let l = &s.pages;
|
|
for (a, b) in [((3, 3), (7, 9)), ((7, 9), (3, 3))] {
|
|
// The second iteration is the "Reverse" case.
|
|
let sel = sel_of(&s, a, b, true);
|
|
|
|
assert!(sel.contains(l, sp(&s, 5, 6))); // Center
|
|
assert!(sel.contains(l, sp(&s, 3, 6))); // Left border
|
|
assert!(sel.contains(l, sp(&s, 7, 6))); // Right border
|
|
assert!(sel.contains(l, sp(&s, 5, 3))); // Top border
|
|
assert!(sel.contains(l, sp(&s, 5, 9))); // Bottom border
|
|
|
|
assert!(!sel.contains(l, sp(&s, 5, 2))); // Above center
|
|
assert!(!sel.contains(l, sp(&s, 5, 10))); // Below center
|
|
assert!(!sel.contains(l, sp(&s, 2, 6))); // Left center
|
|
assert!(!sel.contains(l, sp(&s, 8, 6))); // Right center
|
|
assert!(!sel.contains(l, sp(&s, 8, 3))); // Just right of top right
|
|
assert!(!sel.contains(l, sp(&s, 2, 9))); // Just left of bottom left
|
|
}
|
|
|
|
// Single line
|
|
// NOTE: This is the same as normal selection but we just do it for brevity
|
|
{
|
|
let sel = sel_of(&s, (5, 1), (10, 1), true);
|
|
|
|
assert!(sel.contains(l, sp(&s, 6, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 2, 1)));
|
|
assert!(!sel.contains(l, sp(&s, 12, 1)));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn selection_contained_row() {
|
|
let s = init(10, 5, 0);
|
|
let l = &s.pages;
|
|
let last = s.pages.cols - 1;
|
|
|
|
{
|
|
let sel = sel_of(&s, (5, 1), (3, 3), false);
|
|
|
|
// Not contained
|
|
assert!(sel.contained_row(l, sp(&s, 1, 4)).is_none());
|
|
|
|
// Start line
|
|
assert_eq!(
|
|
Selection::new(sel.start(l), sp(&s, last, 1), false),
|
|
sel.contained_row(l, sp(&s, 1, 1)).unwrap()
|
|
);
|
|
|
|
// End line
|
|
assert_eq!(
|
|
Selection::new(sp(&s, 0, 3), sel.end(l), false),
|
|
sel.contained_row(l, sp(&s, 2, 3)).unwrap()
|
|
);
|
|
|
|
// Middle line
|
|
assert_eq!(
|
|
Selection::new(sp(&s, 0, 2), sp(&s, last, 2), false),
|
|
sel.contained_row(l, sp(&s, 2, 2)).unwrap()
|
|
);
|
|
}
|
|
|
|
// Rectangle
|
|
{
|
|
let sel = sel_of(&s, (3, 1), (6, 3), true);
|
|
|
|
// Not contained
|
|
assert!(sel.contained_row(l, sp(&s, 1, 4)).is_none());
|
|
|
|
// Start line
|
|
assert_eq!(
|
|
Selection::new(sp(&s, 3, 1), sp(&s, 6, 1), true),
|
|
sel.contained_row(l, sp(&s, 1, 1)).unwrap()
|
|
);
|
|
|
|
// End line
|
|
assert_eq!(
|
|
Selection::new(sp(&s, 3, 3), sp(&s, 6, 3), true),
|
|
sel.contained_row(l, sp(&s, 2, 3)).unwrap()
|
|
);
|
|
|
|
// Middle line
|
|
assert_eq!(
|
|
Selection::new(sp(&s, 3, 2), sp(&s, 6, 2), true),
|
|
sel.contained_row(l, sp(&s, 2, 2)).unwrap()
|
|
);
|
|
}
|
|
|
|
// Single-line selection
|
|
{
|
|
let sel = sel_of(&s, (2, 1), (6, 1), false);
|
|
|
|
// Not contained
|
|
assert!(sel.contained_row(l, sp(&s, 1, 0)).is_none());
|
|
assert!(sel.contained_row(l, sp(&s, 1, 2)).is_none());
|
|
|
|
// Contained
|
|
assert_eq!(sel, sel.contained_row(l, sp(&s, 1, 1)).unwrap());
|
|
}
|
|
}
|
|
}
|