Files
ghostty-rs/crates/ghostty-terminal/src/screen/tests/p2.rs
T
ddidderr 1ba5af0658 feat(terminal): port Screen, ScreenSet, Tabstops and Selection
Ports ZIG/src/terminal/{Screen,ScreenSet,Tabstops,Selection}.zig on top
of the page/PageList layer. The cursor holds a TrackedPin; Zig's cached
page_row/page_cell pointers become accessor methods. Selection methods
take the PageList explicitly because tracked bounds are handles.

Screen tests use minimal plain-text dump/selection-string helpers that
follow the formatter's semantics; they will delegate to the real
formatter once formatter.zig is ported.

Gaps found in the PageList layer are recorded for the parity pass in
docs/rewrite/notes/ghostty-terminal-pagelist-requests.md; three resize
tests are #[ignore]d pending those fixes (PageList::resize keeps
trailing blank rows Zig drops). Kitty image storage and StringMap are
placeholders.

Test Plan:
- cargo test -p ghostty-terminal (443 passed, 3 ignored; Screen 183/184,
  Selection 18/18, Tabstops 4/5 - skipped tests use allocator failure
  injection)
2026-10-11 22:28:03 +02:00

1081 lines
32 KiB
Rust

use super::*;
use ghostty_vt::{color::Rgb, sgr::Attribute as A};
fn bg155() -> A<'static> {
A::DirectColorBg(Rgb { r: 155, g: 0, b: 0 })
}
fn assert_bg155(s: &Screen, x: u16, y: u32) {
let cell = s.pages.get_cell(active(x, y)).unwrap();
assert!(cell.content_tag() == ContentTag::BgColorRgb);
assert_eq!(Rgb { r: 155, g: 0, b: 0 }, cell.color_rgb());
}
fn bold_style(s: &Screen) -> bool {
let page = s.cursor_page();
page.styles.get(s.cursor.style_id).unwrap().flags.bold
}
/// Scroll down enough to go to another page. Returns the start page.
fn scroll_to_new_page(s: &mut Screen) -> NodeId {
let start_page = s.pages.last.unwrap();
let rem = s.page(start_page).capacity.rows;
s.page_mut(start_page).pause_integrity_checks(true);
for _ in 0..rem {
s.cursor_down_or_scroll().unwrap();
}
s.page_mut(start_page).pause_integrity_checks(false);
start_page
}
#[test]
fn screen_cursor_down_across_pages_preserves_style() {
let mut s = init(10, 3, 1);
// Scroll down enough to go to another page
let start_page = scroll_to_new_page(&mut s);
// We need our page to change for this test o make sense. If this
// assertion fails then the bug is in the test: we should be scrolling
// above enough for a new page to show up.
assert!(start_page != s.cursor_pin().node);
// Scroll back to the previous page
s.cursor_up(1);
assert!(start_page == s.cursor_pin().node);
// Go back up, set a style
s.set_attribute(A::Bold).unwrap();
assert!(bold_style(&s));
// Go back down into the next page and we should have that style
s.cursor_down(1);
assert!(bold_style(&s));
}
#[test]
fn screen_cursor_up_across_pages_preserves_style() {
let mut s = init(10, 3, 1);
let start_page = scroll_to_new_page(&mut s);
assert!(start_page != s.cursor_pin().node);
// Go back up, set a style
s.set_attribute(A::Bold).unwrap();
assert!(bold_style(&s));
// Go back down into the prev page and we should have that style
s.cursor_up(1);
assert!(start_page == s.cursor_pin().node);
assert!(bold_style(&s));
}
#[test]
fn screen_cursor_absolute_across_pages_preserves_style() {
let mut s = init(10, 3, 1);
let start_page = scroll_to_new_page(&mut s);
assert!(start_page != s.cursor_pin().node);
// Go back up, set a style
s.set_attribute(A::Bold).unwrap();
assert!(bold_style(&s));
// Go back down into the prev page and we should have that style
s.cursor_absolute(1, 1);
assert!(start_page == s.cursor_pin().node);
assert!(bold_style(&s));
}
#[test]
fn screen_cursor_absolute_to_page_with_insufficient_capacity() {
// This test checks for a very specific edge case
// which previously resulted in memory corruption.
//
// The conditions for this edge case are as such:
// - The cursor has an associated style or other managed memory.
// - The cursor moves to a different page.
// - The new page is at capacity and must have its capacity adjusted.
let mut s = init(10, 3, 1);
let start_page = scroll_to_new_page(&mut s);
let new_page = s.cursor_pin().node;
// We need our page to change for this test to make sense. If this
// assertion fails then the bug is in the test: we should be scrolling
// above enough for a new page to show up.
assert!(start_page != new_page);
// Add styles to the start page until it reaches capacity.
{
// Pause integrity checks because they're slow and
// we're not testing this, this is just setup.
s.page_mut(start_page).pause_integrity_checks(true);
let mut n: u32 = 1;
while s
.page_mut(start_page)
.styles
.add(Style {
bg_color: style::Color::Rgb(Rgb {
r: (n & 0xff) as u8,
g: ((n >> 8) & 0xff) as u8,
b: ((n >> 16) & 0xff) as u8,
}),
..Style::default()
})
.is_ok()
{
n += 1;
}
s.page_mut(start_page).pause_integrity_checks(false);
s.page(start_page).assert_integrity();
}
// Set a style on the cursor.
s.set_attribute(A::Bold).unwrap();
{
let styleval = s.page(new_page).styles.get(s.cursor.style_id).unwrap();
assert!(styleval.flags.bold);
}
// Go back up into the start page and we should still have that style.
s.cursor_absolute(1, 1);
{
let cur_page = s.cursor_pin().node;
// The page we're on now should NOT equal start_page, since its
// capacity should have been adjusted, which invalidates our ptr.
assert!(start_page != cur_page);
// To make sure we DID change pages we check we're not on new_page.
assert!(new_page != cur_page);
let styleval = s.page(cur_page).styles.get(s.cursor.style_id).unwrap();
assert!(styleval.flags.bold);
}
s.cursor_page().assert_integrity();
s.page(new_page).assert_integrity();
}
#[test]
fn screen_scrolling() {
let mut s = init(10, 3, 0);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
// Scroll down, should still be bottom
s.cursor_down_scroll().unwrap();
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 2);
// Everything is dirty because we have no scrollback
assert!(s.pages.is_dirty(active(0, 0)));
assert!(s.pages.is_dirty(active(0, 1)));
assert!(s.pages.is_dirty(active(0, 2)));
// Scrolling to the bottom does nothing
s.scroll(Scroll::Active);
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrolling_with_a_single_row_screen_no_scrollback() {
let mut s = init(10, 1, 0);
s.test_write_string("1ABCD").unwrap();
// Scroll down, should still be bottom
s.cursor_down_scroll().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
// Screen should be dirty
assert!(s.pages.is_dirty(active(0, 0)));
}
#[test]
fn screen_scrolling_with_a_single_row_screen_with_scrollback() {
let mut s = init(10, 1, 1);
s.test_write_string("1ABCD").unwrap();
// Scroll down, should still be bottom
s.cursor_down_scroll().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
// Active should be dirty
assert!(s.pages.is_dirty(active(0, 0)));
// Scrollback also dirty because cursor moved from there
assert!(s.pages.is_dirty(screen_pt(0, 0)));
s.scroll(Scroll::DeltaRow(-1));
assert_eq!("1ABCD", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrolling_across_pages_preserves_style() {
let mut s = init(10, 3, 1);
s.set_attribute(A::Bold).unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
let start_page = s.pages.last.unwrap();
// Scroll down enough to go to another page
let rem = {
let p = s.page(start_page);
p.capacity.rows - p.size.rows + 1
};
s.page_mut(start_page).pause_integrity_checks(true);
for _ in 0..rem {
s.cursor_down_or_scroll().unwrap();
}
s.page_mut(start_page).pause_integrity_checks(false);
// We need our page to change for this test o make sense. If this
// assertion fails then the bug is in the test: we should be scrolling
// above enough for a new page to show up.
let page = s.pages.last.unwrap();
assert!(start_page != page);
let styleval = s.page(page).styles.get(s.cursor.style_id).unwrap();
assert!(styleval.flags.bold);
}
#[test]
fn screen_scroll_down_from_0() {
let mut s = init(10, 3, 0);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
// Scrolling up does nothing, but allows it
s.scroll(Scroll::DeltaRow(-1));
assert!(s.pages.viewport == Viewport::Active);
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrollback_various_cases() {
let mut s = init(10, 3, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.cursor_down_scroll().unwrap();
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling to the bottom
s.scroll(Scroll::Active);
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling back should make it visible again
s.scroll(Scroll::DeltaRow(-1));
assert!(s.pages.viewport != Viewport::Active);
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling back again should do nothing
s.scroll(Scroll::DeltaRow(-1));
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling to the bottom
s.scroll(Scroll::Active);
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling forward with no grow should do nothing
s.scroll(Scroll::DeltaRow(1));
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling to the top should work
s.scroll(Scroll::Top);
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Should be able to easily clear active area only
s.clear_rows(active(0, 0), None, false);
assert_eq!("1ABCD", dump(&s, viewport(0, 0)));
// Scrolling to the bottom
s.scroll(Scroll::Active);
assert_eq!("", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrollback_with_multi_row_delta() {
let mut s = init(10, 3, 3);
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH\n6IJKL")
.unwrap();
// Scroll to top
s.scroll(Scroll::Top);
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scroll down multiple
s.scroll(Scroll::DeltaRow(5));
assert!(s.pages.viewport == Viewport::Active);
assert_eq!("4ABCD\n5EFGH\n6IJKL", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrollback_empty() {
let mut s = init(10, 3, 50);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.scroll(Scroll::DeltaRow(1));
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrollback_doesnt_move_viewport_if_not_at_bottom() {
let mut s = init(10, 3, 3);
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n4ABCD\n5EFGH")
.unwrap();
// First test: we scroll up by 1, so we're not at the bottom anymore.
s.scroll(Scroll::DeltaRow(-1));
assert_eq!("2EFGH\n3IJKL\n4ABCD", dump(&s, viewport(0, 0)));
// Next, we scroll back down by 1, this grows the scrollback but we
// shouldn't move.
s.cursor_down_scroll().unwrap();
assert_eq!("2EFGH\n3IJKL\n4ABCD", dump(&s, viewport(0, 0)));
// Scroll again, this clears scrollback so we should move viewports
// but still see the same thing since our original view fits.
s.cursor_down_scroll().unwrap();
assert_eq!("2EFGH\n3IJKL\n4ABCD", dump(&s, viewport(0, 0)));
}
#[test]
fn screen_scrolling_moves_selection() {
let mut s = init(5, 3, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
// Select a single line
let sel = Selection::new(
pin(&s, active(0, 1)),
pin(&s, active(s.pages.cols - 1, 1)),
false,
);
s.select(Some(sel)).unwrap();
// Scroll down, should still be bottom
s.cursor_down_scroll().unwrap();
// Our selection should've moved up
{
let sel = s.selection.unwrap();
assert_eq!(
active(0, 0),
pt_from_pin(&s, Tag::Active, sel.start(&s.pages))
);
assert_eq!(
active(s.pages.cols - 1, 0),
pt_from_pin(&s, Tag::Active, sel.end(&s.pages))
);
}
// Test our contents rotated
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scrolling to the bottom does nothing
s.scroll(Scroll::Active);
// Our selection should've stayed the same
{
let sel = s.selection.unwrap();
assert_eq!(
active(0, 0),
pt_from_pin(&s, Tag::Active, sel.start(&s.pages))
);
assert_eq!(
active(s.pages.cols - 1, 0),
pt_from_pin(&s, Tag::Active, sel.end(&s.pages))
);
}
// Test our contents rotated
assert_eq!("2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
// Scroll up again
s.cursor_down_scroll().unwrap();
// Test our contents rotated
assert_eq!("3IJKL", dump(&s, viewport(0, 0)));
// Our selection should be null because it left the screen.
{
let sel = s.selection.unwrap();
assert!(
s.pages
.point_from_pin(Tag::Active, sel.start(&s.pages))
.is_none()
);
assert!(
s.pages
.point_from_pin(Tag::Active, sel.end(&s.pages))
.is_none()
);
}
}
#[test]
fn screen_scrolling_moves_viewport() {
let mut s = init(10, 3, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n").unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.scroll(Scroll::DeltaRow(-2));
// Test our contents rotated
assert_eq!("2EFGH\n3IJKL\n1ABCD", dump(&s, viewport(0, 0)));
let tl = s.pages.get_top_left(Tag::Viewport).unwrap();
assert_eq!(screen_pt(0, 1), pt_from_pin(&s, Tag::Screen, tl));
}
#[test]
fn screen_scrolling_when_viewport_is_pruned() {
let mut s = init(215, 3, 1);
// Write some to create scrollback and move back into our scrollback.
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n").unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.scroll(Scroll::DeltaRow(-2));
// Our viewport is now somewhere pinned. Create so much scrollback
// that we prune it.
s.test_write_string("\n").unwrap();
for _ in 0..1000 {
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n").unwrap();
}
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
let tl = s.pages.get_top_left(Tag::Viewport).unwrap();
assert_eq!(screen_pt(0, 0), pt_from_pin(&s, Tag::Screen, tl));
}
#[test]
fn screen_scroll_and_clear_full_screen() {
let mut s = init(10, 3, 5);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, viewport(0, 0)));
s.scroll_clear().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
assert_eq!("1ABCD\n2EFGH\n3IJKL", dump(&s, screen_pt(0, 0)));
}
#[test]
fn screen_scroll_and_clear_partial_screen() {
let mut s = init(10, 3, 5);
s.test_write_string("1ABCD\n2EFGH").unwrap();
assert_eq!("1ABCD\n2EFGH", dump(&s, viewport(0, 0)));
s.scroll_clear().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
assert_eq!("1ABCD\n2EFGH", dump(&s, screen_pt(0, 0)));
}
#[test]
fn screen_scroll_and_clear_empty_screen() {
let mut s = init(10, 3, 5);
s.scroll_clear().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
assert_eq!("", dump(&s, screen_pt(0, 0)));
}
#[test]
fn screen_scroll_and_clear_ignore_blank_lines() {
let mut s = init(10, 3, 10);
s.test_write_string("1ABCD\n2EFGH").unwrap();
s.scroll_clear().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
// Move back to top-left
s.cursor_absolute(0, 0);
// Write and clear
s.test_write_string("3ABCD\n").unwrap();
assert_eq!("3ABCD", dump(&s, active(0, 0)));
s.scroll_clear().unwrap();
assert_eq!("", dump(&s, viewport(0, 0)));
// Move back to top-left
s.cursor_absolute(0, 0);
s.test_write_string("X").unwrap();
assert_eq!("1ABCD\n2EFGH\n3ABCD\nX", dump(&s, screen_pt(0, 0)));
}
#[test]
fn screen_scroll_above_same_page() {
let mut s = init(10, 3, 10);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// At this point:
// +-------------+ ACTIVE
// +----------+ : = PAGE 0
// 0 |1ABCD00000| | 0
// 1 |2EFGH00000| | 1
// :^ : : = PIN 0
// 2 |3IJKL00000| | 2
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// 0 |1ABCD00000|
// +-------------+ ACTIVE
// 1 |2EFGH00000| | 0
// 2 | | | 1
// :^ : : = PIN 0
// 3 |3IJKL00000| | 2
// +----------+ :
// +-------------+
assert_eq!("2EFGH\n\n3IJKL", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// Page 0 row 2 (active row 1) is dirty because it was cleared.
assert!(s.pages.is_dirty(active(0, 1)));
// Page 0 row 3 (active row 2) is dirty because it's new.
assert!(s.pages.is_dirty(active(0, 2)));
}
#[test]
fn screen_scroll_above_same_page_but_cursor_on_previous_page() {
let mut s = init(10, 5, 10);
// We need to get the cursor to a new page
let first = s.pages.first.unwrap();
let first_page_size = s.page(first).capacity.rows;
s.page_mut(first).pause_integrity_checks(true);
for _ in 0..first_page_size - 3 {
s.test_write_string("\n").unwrap();
}
s.page_mut(first).pause_integrity_checks(false);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1A\n2B\n3C\n4D\n5E").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// Ensure we're still on the first page and have a second
assert!(s.cursor_pin().node == s.pages.first.unwrap());
assert!(s.pages.node(s.pages.first.unwrap()).unwrap().next.is_some());
// At this point:
// +----------+ = PAGE 0
// ... : :
// +-------------+ ACTIVE
// 4305 |1A00000000| | 0
// 4306 |2B00000000| | 1
// :^ : : = PIN 0
// 4307 |3C00000000| | 2
// +----------+ :
// +----------+ : = PAGE 1
// 0 |4D00000000| | 3
// 1 |5E00000000| | 4
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// ... : :
// 4305 |1A00000000|
// +-------------+ ACTIVE
// 4306 |2B00000000| | 0
// 4307 | | | 1
// :^ : : = PIN 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 |3C00000000| | 2
// 1 |4D00000000| | 3
// 2 |5E00000000| | 4
// +----------+ :
// +-------------+
assert_eq!("2B\n\n3C\n4D\n5E", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0's penultimate row is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// The rest of the rows are dirty because they've been modified or are new.
assert!(s.pages.is_dirty(active(0, 1)));
assert!(s.pages.is_dirty(active(0, 2)));
assert!(s.pages.is_dirty(active(0, 3)));
assert!(s.pages.is_dirty(active(0, 4)));
}
#[test]
fn screen_scroll_above_same_page_but_cursor_on_previous_page_last_row() {
let mut s = init(10, 5, 10);
// We need to get the cursor to a new page
let first = s.pages.first.unwrap();
let first_page_size = s.page(first).capacity.rows;
s.page_mut(first).pause_integrity_checks(true);
for _ in 0..first_page_size - 2 {
s.test_write_string("\n").unwrap();
}
s.page_mut(first).pause_integrity_checks(false);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1A\n2B\n3C\n4D\n5E").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// Ensure we're still on the first page and have a second
assert!(s.cursor_pin().node == s.pages.first.unwrap());
assert!(s.pages.node(s.pages.first.unwrap()).unwrap().next.is_some());
// At this point:
// +----------+ = PAGE 0
// ... : :
// +-------------+ ACTIVE
// 4306 |1A00000000| | 0
// 4307 |2B00000000| | 1
// :^ : : = PIN 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 |3C00000000| | 2
// 1 |4D00000000| | 3
// 2 |5E00000000| | 4
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// ... : :
// 4306 |1A00000000|
// +-------------+ ACTIVE
// 4307 |2B00000000| | 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 | | | 1
// :^ : : = PIN 0
// 1 |3C00000000| | 2
// 2 |4D00000000| | 3
// 3 |5E00000000| | 4
// +----------+ :
// +-------------+
assert_eq!("2B\n\n3C\n4D\n5E", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0's final row is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// Page 1's rows are all dirty because every row was moved.
assert!(s.pages.is_dirty(active(0, 1)));
assert!(s.pages.is_dirty(active(0, 2)));
assert!(s.pages.is_dirty(active(0, 3)));
assert!(s.pages.is_dirty(active(0, 4)));
// Attempt to clear the style from the cursor and
// then assert the integrity of both of our pages.
//
// This catches a case of memory corruption where the cursor
// is moved between pages without accounting for style refs.
s.set_attribute(A::ResetBg).unwrap();
s.page(s.pages.first.unwrap()).assert_integrity();
s.page(s.pages.last.unwrap()).assert_integrity();
}
#[test]
fn screen_scroll_above_creates_new_page() {
let mut s = init(10, 3, 10);
// We need to get the cursor to a new page
let first = s.pages.first.unwrap();
let first_page_size = s.page(first).capacity.rows;
s.page_mut(first).pause_integrity_checks(true);
for _ in 0..first_page_size - 3 {
s.test_write_string("\n").unwrap();
}
s.page_mut(first).pause_integrity_checks(false);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// Ensure we're still on the first page
assert!(s.cursor_pin().node == s.pages.first.unwrap());
// At this point:
// +----------+ = PAGE 0
// ... : :
// +-------------+ ACTIVE
// 4305 |1ABCD00000| | 0
// 4306 |2EFGH00000| | 1
// :^ : : = PIN 0
// 4307 |3IJKL00000| | 2
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// ... : :
// 4305 |1ABCD00000|
// +-------------+ ACTIVE
// 4306 |2EFGH00000| | 0
// 4307 | | | 1
// :^ : : = PIN 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 |3IJKL00000| | 2
// +----------+ :
// +-------------+
assert_eq!("2EFGH\n\n3IJKL", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0's penultimate row is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// Page 0's final row is dirty because it was cleared.
assert!(s.pages.is_dirty(active(0, 1)));
// Page 1's row is dirty because it's new.
assert!(s.pages.is_dirty(active(0, 2)));
}
#[test]
fn screen_scroll_above_with_cursor_on_non_final_row() {
let mut s = init(10, 4, 10);
// Get the cursor to be 2 rows above a new page
let first = s.pages.first.unwrap();
let first_page_size = s.page(first).capacity.rows;
s.page_mut(first).pause_integrity_checks(true);
for _ in 0..first_page_size - 3 {
s.test_write_string("\n").unwrap();
}
s.page_mut(first).pause_integrity_checks(false);
// Write 3 lines of text, forcing the last line into the first
// row of a new page. Move our cursor onto the previous page.
s.set_attribute(bg155()).unwrap();
s.test_write_string("1AB\n2BC\n3DE\n4FG").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// Ensure we're still on the first page. So our cursor is on the first
// page but we have two pages of data.
assert!(s.cursor_pin().node == s.pages.first.unwrap());
// +----------+ = PAGE 0
// ... : :
// +-------------+ ACTIVE
// 4305 |1AB0000000| | 0
// 4306 |2BC0000000| | 1
// :^ : : = PIN 0
// 4307 |3DE0000000| | 2
// +----------+ :
// +----------+ : = PAGE 1
// 0 |4FG0000000| | 3
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// ... : :
// 4305 |1AB0000000|
// +-------------+ ACTIVE
// 4306 |2BC0000000| | 0
// 4307 | | | 1
// :^ : : = PIN 0
// +----------+ :
// +----------+ : = PAGE 1
// 0 |3DE0000000| | 2
// 1 |4FG0000000| | 3
// +----------+ :
// +-------------+
assert_eq!("2BC\n\n3DE\n4FG", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0's penultimate row is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// Page 0's final row is dirty because it was cleared.
assert!(s.pages.is_dirty(active(0, 1)));
// Page 1's row is dirty because it's new.
assert!(s.pages.is_dirty(active(0, 2)));
}
#[test]
fn screen_scroll_above_no_scrollback_bottom_of_page() {
let mut s = init(10, 3, 0);
let first = s.pages.first.unwrap();
let first_page_size = s.page(first).capacity.rows;
s.page_mut(first).pause_integrity_checks(true);
for _ in 0..first_page_size - 3 {
s.test_write_string("\n").unwrap();
}
s.page_mut(first).pause_integrity_checks(false);
s.set_attribute(bg155()).unwrap();
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s.cursor_absolute(0, 1);
s.pages.clear_dirty();
// At this point:
// +-------------+ ACTIVE
// +----------+ : = PAGE 0
// 0 |1ABCD00000| | 0
// 1 |2EFGH00000| | 1
// :^ : : = PIN 0
// 2 |3IJKL00000| | 2
// +----------+ :
// +-------------+
s.cursor_scroll_above().unwrap();
// +----------+ = PAGE 0
// 0 |1ABCD00000|
// +-------------+ ACTIVE
// 1 |2EFGH00000| | 0
// 2 | | | 1
// :^ : : = PIN 0
// 3 |3IJKL00000| | 2
// +----------+ :
// +-------------+
assert_eq!("2EFGH\n\n3IJKL", dump(&s, viewport(0, 0)));
assert_bg155(&s, 0, 1);
// Page 0 row 1 (active row 0) is dirty because the cursor moved off of it.
assert!(s.pages.is_dirty(active(0, 0)));
// Page 0 row 2 (active row 1) is dirty because it was cleared.
assert!(s.pages.is_dirty(active(0, 1)));
// Page 0 row 3 (active row 2) is dirty because it is new.
assert!(s.pages.is_dirty(active(0, 2)));
}
#[test]
fn screen_clone() {
let mut s = init(10, 3, 10);
s.test_write_string("1ABCD\n2EFGH").unwrap();
assert_eq!("1ABCD\n2EFGH", dump(&s, active(0, 0)));
assert_eq!(5, s.cursor.x);
assert_eq!(1, s.cursor.y);
// Clone
let s2 = s.clone_screen(active(0, 0), None).unwrap();
assert_eq!("1ABCD\n2EFGH", dump(&s2, active(0, 0)));
assert_eq!(5, s2.cursor.x);
assert_eq!(1, s2.cursor.y);
// Write to s1, should not be in s2
s.test_write_string("\n34567").unwrap();
assert_eq!("1ABCD\n2EFGH\n34567", dump(&s, active(0, 0)));
assert_eq!("1ABCD\n2EFGH", dump(&s2, active(0, 0)));
assert_eq!(5, s2.cursor.x);
assert_eq!(1, s2.cursor.y);
}
#[test]
fn screen_clone_partial() {
let mut s = init(10, 3, 10);
s.test_write_string("1ABCD\n2EFGH").unwrap();
assert_eq!("1ABCD\n2EFGH", dump(&s, active(0, 0)));
assert_eq!(5, s.cursor.x);
assert_eq!(1, s.cursor.y);
// Clone
let s2 = s.clone_screen(active(0, 1), None).unwrap();
assert_eq!("2EFGH", dump(&s2, active(0, 0)));
// Cursor is shifted since we cloned partial
assert_eq!(5, s2.cursor.x);
assert_eq!(0, s2.cursor.y);
}
#[test]
fn screen_clone_partial_cursor_out_of_bounds() {
let mut s = init(10, 3, 10);
s.test_write_string("1ABCD\n2EFGH").unwrap();
assert_eq!("1ABCD\n2EFGH", dump(&s, active(0, 0)));
assert_eq!(5, s.cursor.x);
assert_eq!(1, s.cursor.y);
// Clone
let s2 = s.clone_screen(active(0, 0), Some(active(0, 0))).unwrap();
assert_eq!("1ABCD", dump(&s2, active(0, 0)));
// Cursor is shifted since we cloned partial
assert_eq!(0, s2.cursor.x);
assert_eq!(0, s2.cursor.y);
}
/// Common setup: 5x3 with scrollback 1 containing three lines.
fn sel_screen() -> Screen {
let mut s = init(5, 3, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
s
}
fn sel_points(s: &Screen) -> (Point, Point) {
let sel = s.selection.unwrap();
(
pt_from_pin(s, Tag::Active, sel.start(&s.pages)),
pt_from_pin(s, Tag::Active, sel.end(&s.pages)),
)
}
#[test]
fn screen_clone_contains_full_selection() {
let mut s = sel_screen();
// Select a single line
let sel = Selection::new(
pin(&s, active(0, 1)),
pin(&s, active(s.pages.cols - 1, 1)),
false,
);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 0), None).unwrap();
// Our selection should remain valid
let (a, b) = sel_points(&s2);
assert_eq!(active(0, 1), a);
assert_eq!(active(s2.pages.cols - 1, 1), b);
}
#[test]
fn screen_clone_contains_none_of_selection() {
let mut s = sel_screen();
// Select a single line
let sel = Selection::new(
pin(&s, active(0, 0)),
pin(&s, active(s.pages.cols - 1, 0)),
false,
);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 1), None).unwrap();
// Our selection should be null
assert!(s2.selection.is_none());
}
#[test]
fn screen_clone_contains_selection_start_cutoff() {
let mut s = sel_screen();
// Select a single line
let sel = Selection::new(
pin(&s, active(0, 0)),
pin(&s, active(s.pages.cols - 1, 1)),
false,
);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 1), None).unwrap();
// Our selection should remain valid
let (a, b) = sel_points(&s2);
assert_eq!(active(0, 0), a);
assert_eq!(active(s2.pages.cols - 1, 0), b);
}
#[test]
fn screen_clone_contains_selection_end_cutoff() {
let mut s = sel_screen();
// Select a single line
let sel = Selection::new(pin(&s, active(0, 1)), pin(&s, active(2, 2)), false);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 0), Some(active(0, 1))).unwrap();
// Our selection should remain valid
let (a, b) = sel_points(&s2);
assert_eq!(active(0, 1), a);
assert_eq!(active(s2.pages.cols - 1, 2), b);
}
#[test]
fn screen_clone_contains_selection_end_cutoff_reversed() {
let mut s = sel_screen();
// Select a single line
let sel = Selection::new(pin(&s, active(2, 2)), pin(&s, active(0, 1)), false);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 0), Some(active(0, 1))).unwrap();
// Our selection should remain valid
let (a, b) = sel_points(&s2);
assert_eq!(active(0, 1), a);
assert_eq!(active(s2.pages.cols - 1, 2), b);
}
#[test]
fn screen_clone_contains_subset_of_selection() {
let mut s = init(5, 4, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n4ABCD").unwrap();
// Select the full screen
let sel = Selection::new(pin(&s, active(0, 0)), pin(&s, active(0, 3)), false);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 1), Some(active(0, 2))).unwrap();
// Our selection should remain valid
let (a, b) = sel_points(&s2);
assert_eq!(active(0, 0), a);
assert_eq!(active(s2.pages.cols - 1, 3), b);
}
#[test]
fn screen_clone_contains_subset_of_rectangle_selection() {
let mut s = init(5, 4, 1);
s.test_write_string("1ABCD\n2EFGH\n3IJKL\n4ABCD").unwrap();
// Select the full screen from x=1 to x=3
let sel = Selection::new(pin(&s, active(1, 0)), pin(&s, active(3, 3)), true);
s.select(Some(sel)).unwrap();
// Clone
let s2 = s.clone_screen(active(0, 1), Some(active(0, 2))).unwrap();
// Our selection should remain valid and be properly clipped
// preserving the columns of the start and end points of the
// selection.
let (a, b) = sel_points(&s2);
assert_eq!(active(1, 0), a);
assert_eq!(active(3, 3), b);
}
#[test]
fn screen_clone_basic() {
let mut s = init(10, 3, 0);
s.test_write_string("1ABCD\n2EFGH\n3IJKL").unwrap();
{
let s2 = s.clone_screen(active(0, 1), Some(active(0, 1))).unwrap();
// Test our contents rotated
assert_eq!("2EFGH", dump(&s2, active(0, 0)));
}
{
let s2 = s.clone_screen(active(0, 1), Some(active(0, 2))).unwrap();
// Test our contents rotated
assert_eq!("2EFGH\n3IJKL", dump(&s2, active(0, 0)));
}
}