rustfmt.toml sets group_imports, imports_granularity and imports_layout, which are nightly-only; agents formatted with stable rustfmt, which ignored them. This is a formatting-only pass with `cargo +nightly fmt`. Test Plan: - cargo clippy --workspace --all-targets -- -D warnings - cargo test --workspace
1060 lines
25 KiB
Rust
1060 lines
25 KiB
Rust
use ghostty_vt::{color::Rgb, modes::Mode, sgr::Attribute as A};
|
|
|
|
use super::*;
|
|
use crate::{page::ContentTag, point::Tag};
|
|
|
|
fn cr_lf(t: &mut Terminal) {
|
|
t.carriage_return();
|
|
t.linefeed().unwrap();
|
|
}
|
|
|
|
/// "ABC\nDEF\nGHI" written with CR+LF between lines (no trailing CR/LF).
|
|
fn abc_def_ghi(t: &mut Terminal) {
|
|
print_str(t, "ABC");
|
|
cr_lf(t);
|
|
print_str(t, "DEF");
|
|
cr_lf(t);
|
|
print_str(t, "GHI");
|
|
}
|
|
|
|
fn dirty(t: &Terminal, y: u32) -> bool {
|
|
t.is_dirty(pt_active(0, y))
|
|
}
|
|
|
|
fn print_chars(t: &mut Terminal, s: &str) {
|
|
for c in s.chars() {
|
|
t.print(u32::from(c)).unwrap();
|
|
}
|
|
}
|
|
|
|
/// (row.hyperlink, cell.hyperlink, lookup id, hyperlink_set.count)
|
|
fn link_info(t: &Terminal, pt: Point) -> (bool, bool, Option<u16>, usize) {
|
|
let p = pin_at(t, pt);
|
|
let page = t.screen().page(p.node);
|
|
let (row, cell) = page
|
|
.get_row_and_cell(usize::from(p.x), usize::from(p.y))
|
|
.unwrap();
|
|
let i = page.cell_index(usize::from(p.x), usize::from(p.y)).unwrap();
|
|
(
|
|
row.hyperlink(),
|
|
cell.hyperlink(),
|
|
page.lookup_hyperlink(i),
|
|
page.hyperlink_set.count(),
|
|
)
|
|
}
|
|
|
|
fn expect_linked(t: &Terminal, x: u16, y: u32) {
|
|
let (row, cell, id, count) = link_info(t, pt_viewport(x, y));
|
|
assert!(row);
|
|
assert!(cell);
|
|
assert_eq!(Some(1), id);
|
|
assert_eq!(1, count);
|
|
}
|
|
|
|
fn expect_unlinked_cell(t: &Terminal, x: u16, y: u32) {
|
|
let (_, cell, id, _) = link_info(t, pt_viewport(x, y));
|
|
assert!(!cell);
|
|
assert!(id.is_none());
|
|
}
|
|
|
|
fn expect_unlinked_row(t: &Terminal, x: u16, y: u32) {
|
|
let (row, cell, id, _) = link_info(t, pt_viewport(x, y));
|
|
assert!(!row);
|
|
assert!(!cell);
|
|
assert!(id.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_top_and_bottom_margin_top_and_bottom() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_top_and_bottom_margin(1, 2);
|
|
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
|
|
assert!(dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(!dirty(&t, 2));
|
|
assert_eq!("\nABC\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_top_and_bottom_margin_top_equal_to_bottom() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_top_and_bottom_margin(2, 2);
|
|
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_left_and_right_margin_simple() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_left_and_right_margin(0, 0);
|
|
|
|
t.clear_dirty();
|
|
t.erase_chars(1);
|
|
|
|
assert!(dirty(&t, 0));
|
|
assert!(!dirty(&t, 1));
|
|
assert_eq!(" BC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_left_and_right_margin_left_only() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_left_and_right_margin(2, 0);
|
|
assert_eq!(1, t.scrolling_region.left);
|
|
assert_eq!(t.cols - 1, t.scrolling_region.right);
|
|
t.set_cursor_pos(1, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!("A\nDBC\nGEF\n HI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_left_and_right_margin_left_and_right() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_left_and_right_margin(1, 2);
|
|
t.set_cursor_pos(1, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!(" C\nABF\nDEI\nGH", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_left_and_right_margin_left_equal_right() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_left_and_right_margin(2, 2);
|
|
t.set_cursor_pos(1, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_set_left_and_right_margin_mode_69_unset() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, false);
|
|
t.set_left_and_right_margin(1, 2);
|
|
t.set_cursor_pos(1, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_simple() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
assert!(dirty(&t, 3));
|
|
assert_eq!("ABC\n\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_colors_with_bg_color() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
t.set_attribute(A::DirectColorBg(Rgb {
|
|
r: 0xFF,
|
|
g: 0,
|
|
b: 0,
|
|
}))
|
|
.unwrap();
|
|
t.insert_lines(1);
|
|
|
|
assert_eq!("ABC\n\nDEF\nGHI", plain(&t));
|
|
|
|
for x in 0..t.cols {
|
|
let cell = cell_at(&t, pt_active(x, 1));
|
|
assert!(cell.content_tag() == ContentTag::BgColorRgb);
|
|
assert_eq!(
|
|
Rgb {
|
|
r: 0xFF,
|
|
g: 0,
|
|
b: 0
|
|
},
|
|
cell.color_rgb()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_handles_style_refs() {
|
|
let mut t = init(5, 3);
|
|
|
|
print_str(&mut t, "ABC");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF");
|
|
cr_lf(&mut t);
|
|
|
|
// For the line being deleted, create a refcounted style
|
|
t.set_attribute(A::Bold).unwrap();
|
|
print_str(&mut t, "GHI");
|
|
t.set_attribute(A::Unset).unwrap();
|
|
|
|
// verify we have styles in our style map
|
|
assert_eq!(1, t.screen().cursor_page().styles.count());
|
|
|
|
t.set_cursor_pos(2, 2);
|
|
t.insert_lines(1);
|
|
|
|
assert_eq!("ABC\n\nDEF", plain(&t));
|
|
|
|
// verify we have no styles in our style map
|
|
assert_eq!(0, t.screen().cursor_page().styles.count());
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_outside_of_scroll_region() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_top_and_bottom_margin(3, 4);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
assert!(!dirty(&t, 1));
|
|
assert!(!dirty(&t, 2));
|
|
assert_eq!("ABC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_top_bottom_scroll_region() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "123");
|
|
t.set_top_and_bottom_margin(1, 3);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
assert!(!dirty(&t, 3));
|
|
assert_eq!("ABC\n\nDEF\n123", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_across_page_boundary_marks_all_shifted_rows_dirty() {
|
|
let mut t = init_scrollback(10, 5, 1024);
|
|
|
|
let first = t.screen().pages.first.unwrap();
|
|
let first_page_nrows = usize::from(t.screen().page(first).capacity.rows);
|
|
|
|
// Fill up the first page minus 3 rows
|
|
for _ in 0..first_page_nrows - 3 {
|
|
t.linefeed().unwrap();
|
|
}
|
|
|
|
// Add content that will cross a page boundary
|
|
print_str(&mut t, "1AAAA");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "2BBBB");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "3CCCC");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "4DDDD");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "5EEEE");
|
|
|
|
// Verify we now have a second page
|
|
assert!(t.screen().pages.node(first).unwrap().next.is_some());
|
|
|
|
t.set_cursor_pos(1, 1);
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
for y in 0..5 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
assert_eq!("\n1AAAA\n2BBBB\n3CCCC\n4DDDD", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_legacy_test() {
|
|
let mut t = init(2, 5);
|
|
|
|
// Initial value
|
|
for (i, c) in "ABCDE".chars().enumerate() {
|
|
if i > 0 {
|
|
cr_lf(&mut t);
|
|
}
|
|
t.print(u32::from(c)).unwrap();
|
|
}
|
|
|
|
// Move to row 2
|
|
t.set_cursor_pos(2, 1);
|
|
|
|
// Insert two lines
|
|
t.insert_lines(2);
|
|
|
|
assert_eq!("A\n\n\nB\nC", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_zero() {
|
|
let mut t = init(2, 5);
|
|
|
|
// This should do nothing
|
|
t.set_cursor_pos(1, 1);
|
|
t.insert_lines(0);
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_with_scroll_region() {
|
|
let mut t = init(2, 6);
|
|
|
|
// Initial value
|
|
for (i, c) in "ABCDE".chars().enumerate() {
|
|
if i > 0 {
|
|
cr_lf(&mut t);
|
|
}
|
|
t.print(u32::from(c)).unwrap();
|
|
}
|
|
|
|
t.set_top_and_bottom_margin(1, 2);
|
|
t.set_cursor_pos(1, 1);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
assert!(dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(!dirty(&t, 2));
|
|
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("X\nA\nC\nD\nE", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_more_than_remaining() {
|
|
let mut t = init(2, 5);
|
|
|
|
// Initial value
|
|
for (i, c) in "ABCDE".chars().enumerate() {
|
|
if i > 0 {
|
|
cr_lf(&mut t);
|
|
}
|
|
t.print(u32::from(c)).unwrap();
|
|
}
|
|
|
|
// Move to row 2
|
|
t.set_cursor_pos(2, 1);
|
|
|
|
// Insert a bunch of lines
|
|
t.clear_dirty();
|
|
t.insert_lines(20);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
|
|
assert_eq!("A", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_resets_pending_wrap() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, "ABCDE");
|
|
assert!(t.screen().cursor.pending_wrap);
|
|
t.insert_lines(1);
|
|
assert!(!t.screen().cursor.pending_wrap);
|
|
t.print(u32::from('B')).unwrap();
|
|
|
|
assert_eq!("B\nABCDE", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_resets_wrap() {
|
|
let mut t = init(3, 3);
|
|
|
|
t.print(u32::from('1')).unwrap();
|
|
cr_lf(&mut t);
|
|
print_chars(&mut t, "ABCDEF");
|
|
t.set_cursor_pos(1, 1);
|
|
t.insert_lines(1);
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("X\n1\nABC", plain(&t));
|
|
|
|
assert!(!row_at(&t, pt_active(0, 2)).wrap());
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_multi_codepoint_graphemes() {
|
|
let mut t = init(5, 5);
|
|
|
|
// Disable grapheme clustering
|
|
t.modes.set(Mode::GraphemeCluster, true);
|
|
|
|
print_str(&mut t, "ABC");
|
|
cr_lf(&mut t);
|
|
|
|
// This is: 👨👩👧 (which may or may not render correctly)
|
|
t.print(0x1F468).unwrap();
|
|
t.print(0x200D).unwrap();
|
|
t.print(0x1F469).unwrap();
|
|
t.print(0x200D).unwrap();
|
|
t.print(0x1F467).unwrap();
|
|
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI");
|
|
t.set_cursor_pos(2, 2);
|
|
t.insert_lines(1);
|
|
|
|
assert_eq!("ABC\n\n👨👩👧\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_insert_lines_left_right_scroll_region() {
|
|
let mut t = init(10, 10);
|
|
|
|
print_str(&mut t, "ABC123");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF456");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
t.clear_dirty();
|
|
t.insert_lines(1);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
assert!(dirty(&t, 3));
|
|
|
|
assert_eq!("ABC123\nD 56\nGEF489\n HI7", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_simple() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
let viewport_before = t.screen().pages.get_top_left(Tag::Viewport).unwrap();
|
|
t.scroll_up(1).unwrap();
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
// Viewport should have moved. Our entire page should've scrolled!
|
|
// The viewport moving will cause our render state to make the full
|
|
// frame as dirty.
|
|
let viewport_after = t.screen().pages.get_top_left(Tag::Viewport).unwrap();
|
|
assert!(!viewport_before.eql(viewport_after));
|
|
|
|
assert_eq!("DEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_moves_hyperlink() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_str(&mut t, "ABC");
|
|
cr_lf(&mut t);
|
|
t.screen_mut()
|
|
.start_hyperlink(b"http://example.com", None)
|
|
.unwrap();
|
|
print_str(&mut t, "DEF");
|
|
t.screen_mut().end_hyperlink();
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI");
|
|
t.set_cursor_pos(2, 2);
|
|
t.scroll_up(1).unwrap();
|
|
|
|
assert_eq!("DEF\nGHI", plain(&t));
|
|
|
|
for x in 0..3 {
|
|
expect_linked(&t, x, 0);
|
|
}
|
|
for x in 0..3 {
|
|
expect_unlinked_row(&t, x, 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_clears_hyperlink() {
|
|
let mut t = init(5, 5);
|
|
|
|
t.screen_mut()
|
|
.start_hyperlink(b"http://example.com", None)
|
|
.unwrap();
|
|
print_str(&mut t, "ABC");
|
|
t.screen_mut().end_hyperlink();
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI");
|
|
t.set_cursor_pos(2, 2);
|
|
t.scroll_up(1).unwrap();
|
|
|
|
assert_eq!("DEF\nGHI", plain(&t));
|
|
|
|
for x in 0..3 {
|
|
expect_unlinked_row(&t, x, 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_top_bottom_scroll_region() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_top_and_bottom_margin(2, 3);
|
|
t.set_cursor_pos(1, 1);
|
|
|
|
t.clear_dirty();
|
|
t.scroll_up(1).unwrap();
|
|
|
|
// This is dirty because the cursor moves from this row
|
|
assert!(dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
|
|
assert_eq!("ABC\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_left_right_scroll_region() {
|
|
let mut t = init(10, 10);
|
|
|
|
print_str(&mut t, "ABC123");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF456");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
t.clear_dirty();
|
|
t.scroll_up(1).unwrap();
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
assert!(dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
|
|
assert_eq!("AEF423\nDHI756\nG 89", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_left_right_scroll_region_hyperlink() {
|
|
let mut t = init(10, 10);
|
|
|
|
print_str(&mut t, "ABC123");
|
|
cr_lf(&mut t);
|
|
t.screen_mut()
|
|
.start_hyperlink(b"http://example.com", None)
|
|
.unwrap();
|
|
print_str(&mut t, "DEF456");
|
|
t.screen_mut().end_hyperlink();
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(2, 2);
|
|
t.scroll_up(1).unwrap();
|
|
|
|
assert_eq!("AEF423\nDHI756\nG 89", plain(&t));
|
|
|
|
// First row gets some hyperlinks
|
|
for x in 0..1 {
|
|
expect_unlinked_cell(&t, x, 0);
|
|
}
|
|
for x in 1..4 {
|
|
expect_linked(&t, x, 0);
|
|
}
|
|
for x in 4..6 {
|
|
expect_unlinked_cell(&t, x, 0);
|
|
}
|
|
|
|
// Second row preserves hyperlink where we didn't scroll
|
|
for x in 0..1 {
|
|
expect_linked(&t, x, 1);
|
|
}
|
|
for x in 1..4 {
|
|
expect_unlinked_cell(&t, x, 1);
|
|
}
|
|
for x in 4..6 {
|
|
expect_linked(&t, x, 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_preserves_pending_wrap() {
|
|
let mut t = init(5, 5);
|
|
|
|
t.set_cursor_pos(1, 5);
|
|
t.print(u32::from('A')).unwrap();
|
|
t.set_cursor_pos(2, 5);
|
|
t.print(u32::from('B')).unwrap();
|
|
t.set_cursor_pos(3, 5);
|
|
t.print(u32::from('C')).unwrap();
|
|
t.scroll_up(1).unwrap();
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!(" B\n C\n\nX", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_full_top_bottom_region() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_str(&mut t, "top");
|
|
t.set_cursor_pos(5, 1);
|
|
print_str(&mut t, "ABCDE");
|
|
t.set_top_and_bottom_margin(2, 5);
|
|
|
|
t.clear_dirty();
|
|
t.scroll_up(4).unwrap();
|
|
|
|
// This is dirty because the cursor moves from this row
|
|
assert!(dirty(&t, 0));
|
|
assert!(dirty(&t, 1));
|
|
|
|
assert_eq!("top", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_full_top_bottomleft_right_scroll_region() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_str(&mut t, "top");
|
|
t.set_cursor_pos(5, 1);
|
|
print_str(&mut t, "ABCDE");
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_top_and_bottom_margin(2, 5);
|
|
t.set_left_and_right_margin(2, 4);
|
|
|
|
t.clear_dirty();
|
|
t.scroll_up(4).unwrap();
|
|
|
|
// This is dirty because the cursor moves from this row
|
|
assert!(dirty(&t, 0));
|
|
for y in 1..5 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
|
|
assert_eq!("top\n\n\n\nA E", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_creates_scrollback_in_primary_screen() {
|
|
// When in primary screen with full-width scroll region at top,
|
|
// scrollUp (CSI S) should push lines into scrollback like xterm.
|
|
let mut t = init_scrollback(5, 5, 10);
|
|
|
|
// Fill the screen with content
|
|
print_str(&mut t, "AAAAA");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "BBBBB");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "CCCCC");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DDDDD");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "EEEEE");
|
|
|
|
t.clear_dirty();
|
|
|
|
// Scroll up by 1, which should push "AAAAA" into scrollback
|
|
t.scroll_up(1).unwrap();
|
|
|
|
// The cursor row (new empty row) should be dirty
|
|
assert!(t.screen().cursor_row().dirty());
|
|
|
|
// The active screen should now show BBBBB through EEEEE plus one blank line
|
|
assert_eq!("BBBBB\nCCCCC\nDDDDD\nEEEEE", plain(&t));
|
|
|
|
// Now scroll to the top to see scrollback - AAAAA should be there
|
|
t.screen_mut().scroll(Scroll::Top);
|
|
assert_eq!("AAAAA\nBBBBB\nCCCCC\nDDDDD\nEEEEE", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_with_max_scrollback_zero() {
|
|
// When max_scrollback is 0, scrollUp should still work but not retain history
|
|
let mut t = init_scrollback(5, 5, 0);
|
|
|
|
print_str(&mut t, "AAAAA");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "BBBBB");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "CCCCC");
|
|
|
|
t.scroll_up(1).unwrap();
|
|
|
|
// Active screen should show scrolled content
|
|
assert_eq!("BBBBB\nCCCCC", plain(&t));
|
|
|
|
// Scroll to top - should be same as active since no scrollback
|
|
t.screen_mut().scroll(Scroll::Top);
|
|
assert_eq!("BBBBB\nCCCCC", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_with_max_scrollback_zero_and_top_margin() {
|
|
// When max_scrollback is 0 and top margin is set, should use deleteLines path
|
|
let mut t = init_scrollback(5, 5, 0);
|
|
|
|
print_str(&mut t, "AAAAA");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "BBBBB");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "CCCCC");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DDDDD");
|
|
|
|
// Set top margin (not at row 0)
|
|
t.set_top_and_bottom_margin(2, 5);
|
|
|
|
t.scroll_up(1).unwrap();
|
|
|
|
// First row preserved, rest scrolled
|
|
assert_eq!("AAAAA\nCCCCC\nDDDDD", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_up_with_max_scrollback_zero_and_left_right_margin() {
|
|
// When max_scrollback is 0 with left/right margins, uses deleteLines path
|
|
let mut t = init_scrollback(10, 5, 0);
|
|
|
|
print_str(&mut t, "AAAAABBBBB");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "CCCCCDDDDD");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "EEEEEFFFFF");
|
|
|
|
// Set left/right margins (columns 2-6, 1-indexed = indices 1-5)
|
|
t.modes.set(Mode::EnableLeftAndRightMargin, true);
|
|
t.set_left_and_right_margin(2, 6);
|
|
|
|
t.scroll_up(1).unwrap();
|
|
|
|
// cols 1-5 scroll, col 0 and cols 6+ preserved
|
|
assert_eq!("ACCCCDBBBB\nCEEEEFDDDD\nE FFFF", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_simple() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
for y in 0..5 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
|
|
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_hyperlink_moves() {
|
|
let mut t = init(5, 5);
|
|
|
|
t.screen_mut()
|
|
.start_hyperlink(b"http://example.com", None)
|
|
.unwrap();
|
|
print_str(&mut t, "ABC");
|
|
t.screen_mut().end_hyperlink();
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI");
|
|
t.set_cursor_pos(2, 2);
|
|
t.scroll_down(1);
|
|
|
|
assert_eq!("\nABC\nDEF\nGHI", plain(&t));
|
|
|
|
for x in 0..3 {
|
|
expect_linked(&t, x, 1);
|
|
}
|
|
for x in 0..3 {
|
|
expect_unlinked_row(&t, x, 0);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_outside_of_scroll_region() {
|
|
let mut t = init(5, 5);
|
|
abc_def_ghi(&mut t);
|
|
t.set_top_and_bottom_margin(3, 4);
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
assert!(!dirty(&t, 0));
|
|
|
|
// This is dirty because the cursor moves from this row
|
|
assert!(dirty(&t, 1));
|
|
assert!(dirty(&t, 2));
|
|
assert!(dirty(&t, 3));
|
|
|
|
assert_eq!("ABC\nDEF\n\nGHI", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_left_right_scroll_region() {
|
|
let mut t = init(10, 10);
|
|
|
|
print_str(&mut t, "ABC123");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF456");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(2, 2);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
|
|
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_left_right_scroll_region_hyperlink() {
|
|
let mut t = init(10, 10);
|
|
|
|
t.screen_mut()
|
|
.start_hyperlink(b"http://example.com", None)
|
|
.unwrap();
|
|
print_str(&mut t, "ABC123");
|
|
t.screen_mut().end_hyperlink();
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF456");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(2, 2);
|
|
t.scroll_down(1);
|
|
|
|
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
|
|
|
|
// First row preserves hyperlink where we didn't scroll
|
|
for x in 0..1 {
|
|
expect_linked(&t, x, 0);
|
|
}
|
|
for x in 1..4 {
|
|
expect_unlinked_cell(&t, x, 0);
|
|
}
|
|
for x in 4..6 {
|
|
expect_linked(&t, x, 0);
|
|
}
|
|
|
|
// Second row gets some hyperlinks
|
|
for x in 0..1 {
|
|
expect_unlinked_cell(&t, x, 1);
|
|
}
|
|
for x in 1..4 {
|
|
expect_linked(&t, x, 1);
|
|
}
|
|
for x in 4..6 {
|
|
expect_unlinked_cell(&t, x, 1);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_outside_of_left_right_scroll_region() {
|
|
let mut t = init(10, 10);
|
|
|
|
print_str(&mut t, "ABC123");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "DEF456");
|
|
cr_lf(&mut t);
|
|
print_str(&mut t, "GHI789");
|
|
t.scrolling_region.left = 1;
|
|
t.scrolling_region.right = 3;
|
|
t.set_cursor_pos(1, 1);
|
|
|
|
let (cx, cy) = (t.screen().cursor.x, t.screen().cursor.y);
|
|
t.clear_dirty();
|
|
t.scroll_down(1);
|
|
assert_eq!(cx, t.screen().cursor.x);
|
|
assert_eq!(cy, t.screen().cursor.y);
|
|
|
|
for y in 0..4 {
|
|
assert!(dirty(&t, y));
|
|
}
|
|
|
|
assert_eq!("A 23\nDBC156\nGEF489\n HI7", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_scroll_down_preserves_pending_wrap() {
|
|
let mut t = init(5, 10);
|
|
|
|
t.set_cursor_pos(1, 5);
|
|
t.print(u32::from('A')).unwrap();
|
|
t.set_cursor_pos(2, 5);
|
|
t.print(u32::from('B')).unwrap();
|
|
t.set_cursor_pos(3, 5);
|
|
t.print(u32::from('C')).unwrap();
|
|
t.scroll_down(1);
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("\n A\n B\nX C", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_simple_operation() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, "ABC");
|
|
t.set_cursor_pos(1, 1);
|
|
t.clear_dirty();
|
|
t.erase_chars(2);
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert!(dirty(&t, 0));
|
|
assert!(!dirty(&t, 1));
|
|
|
|
assert_eq!("X C", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_minimum_one() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, "ABC");
|
|
t.set_cursor_pos(1, 1);
|
|
t.clear_dirty();
|
|
t.erase_chars(0);
|
|
t.print(u32::from('X')).unwrap();
|
|
assert!(dirty(&t, 0));
|
|
|
|
assert_eq!("XBC", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_beyond_screen_edge() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, " ABC");
|
|
t.set_cursor_pos(1, 4);
|
|
t.erase_chars(10);
|
|
|
|
assert_eq!(" A", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_wide_character() {
|
|
let mut t = init(5, 5);
|
|
|
|
t.print(u32::from('橋')).unwrap();
|
|
print_chars(&mut t, "BC");
|
|
t.set_cursor_pos(1, 1);
|
|
t.erase_chars(1);
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("X BC", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_resets_pending_wrap() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, "ABCDE");
|
|
assert!(t.screen().cursor.pending_wrap);
|
|
t.erase_chars(1);
|
|
assert!(!t.screen().cursor.pending_wrap);
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("ABCDX", plain(&t));
|
|
}
|
|
|
|
#[test]
|
|
fn terminal_erase_chars_resets_wrap() {
|
|
let mut t = init(5, 5);
|
|
|
|
print_chars(&mut t, "ABCDE123");
|
|
assert!(row_at(&t, pt_active(0, 0)).wrap());
|
|
|
|
t.set_cursor_pos(1, 1);
|
|
t.erase_chars(1);
|
|
|
|
assert!(!row_at(&t, pt_active(0, 0)).wrap());
|
|
|
|
t.print(u32::from('X')).unwrap();
|
|
|
|
assert_eq!("XBCDE\n123", plain(&t));
|
|
}
|