use justerm_core::{Engine, SelectionSpan, SelectionType, Side};
#[test]
fn char_select_one_line() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("hello"));
}
#[test]
fn char_select_multi_line_trims_and_joins_with_newline() {
let mut term = Engine::new(80, 24);
term.feed(b"ab\r\ncd");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(1, 1, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("ab\ncd"));
}
#[test]
fn char_select_soft_wrap_joins_and_keeps_interior_spaces() {
let mut term = Engine::new(4, 4);
term.feed(b"ab cd");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(1, 1, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("ab cd"));
}
#[test]
fn char_select_across_scrollback_boundary() {
let mut term = Engine::new(4, 2);
term.feed(b"L0\r\nL1\r\nL2\r\nL3");
term.scroll_up(1);
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(1, 1, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("L1\nL2"));
}
#[test]
fn selection_side_excludes_and_degenerate_is_empty() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Right, SelectionType::Char);
term.selection_extend(0, 4, Side::Left);
assert_eq!(term.selection_text().as_deref(), Some("ell"));
term.selection_begin(0, 2, Side::Right, SelectionType::Char);
term.selection_extend(0, 2, Side::Left);
assert_eq!(term.selection_text().as_deref(), Some(""));
}
#[test]
fn word_select_expands_to_word_boundaries() {
let mut term = Engine::new(80, 24);
term.feed(b"foo bar baz");
term.selection_begin(0, 5, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("bar"));
}
#[test]
fn word_select_crosses_soft_wrap() {
let mut term = Engine::new(4, 4);
term.feed(b"abcdef");
term.selection_begin(1, 0, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("abcdef"));
}
#[test]
fn word_select_on_alt_does_not_cross_into_primary_scrollback() {
let mut term = Engine::new(5, 2);
term.feed(b"abcdefghijklmno"); term.feed(b"\x1b[?1049h\x1b[H"); term.feed(b"WORD");
term.selection_begin(0, 0, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("WORD"));
}
#[test]
fn line_select_takes_whole_lines() {
let mut term = Engine::new(80, 24);
term.feed(b"hello world");
term.selection_begin(0, 3, Side::Left, SelectionType::Line);
assert_eq!(term.selection_text().as_deref(), Some("hello world"));
}
#[test]
fn block_select_is_rectangular() {
let mut term = Engine::new(80, 24);
term.feed(b"abcd\r\nefgh");
term.selection_begin(0, 1, Side::Left, SelectionType::Block);
term.selection_extend(1, 2, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("bc\nfg"));
}
#[test]
fn selection_skips_wide_char_spacer() {
let mut term = Engine::new(80, 24);
term.feed("한국".as_bytes());
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 3, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("한국"));
}
#[test]
fn word_select_expands_over_a_cjk_word() {
let mut term = Engine::new(80, 24);
term.feed("한글날".as_bytes());
term.selection_begin(0, 0, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("한글날"));
term.selection_begin(0, 2, Side::Left, SelectionType::Word); assert_eq!(term.selection_text().as_deref(), Some("한글날"));
}
#[test]
fn word_select_spans_a_mixed_ascii_cjk_word() {
let mut term = Engine::new(80, 24);
term.feed("ab한글cd".as_bytes());
term.selection_begin(0, 0, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("ab한글cd"));
}
#[test]
fn word_select_range_spans_full_wide_glyphs() {
let mut term = Engine::new(80, 24);
term.feed("한글날".as_bytes());
term.selection_begin(0, 0, Side::Left, SelectionType::Word);
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 0,
left: 0,
right: 5
}]
);
}
#[test]
fn word_select_does_not_start_on_a_wide_boundary_glyphs_spacer() {
let mut term = Engine::new(10, 3);
term.feed("\u{3000}abc".as_bytes());
term.selection_begin(0, 2, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("abc"));
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 0,
left: 2,
right: 4
}],
"the highlight starts at 'a', not on the ideographic space's second cell"
);
}
#[test]
fn word_select_stops_at_a_lead_less_orphan_spacer() {
let mut term = Engine::new(6, 4);
term.feed(b"\x1b[?2027h"); term.feed(b"\x1b[2;2H");
term.feed("한".as_bytes());
term.feed(b"\x1b[2;4Hxy");
term.feed(b"\x1b[1;6H");
term.feed("\u{25B6}\u{FE0F}".as_bytes());
term.selection_begin(1, 0, Side::Left, SelectionType::Word);
assert_eq!(
term.selection_text().as_deref(),
Some("\u{25B6}\u{FE0F}"),
"the walk stops at the orphan spacer — it must not swallow the following word"
);
}
#[test]
fn word_select_stops_at_a_mid_row_wrap_marker() {
let mut term = Engine::new(6, 4);
term.feed("abcde".as_bytes());
term.feed("한".as_bytes()); term.feed(b"\x1b[1;1H\x1b[2P");
term.selection_begin(0, 0, Side::Left, SelectionType::Word);
assert_eq!(term.selection_text().as_deref(), Some("cde"));
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 0,
left: 0,
right: 2
}]
);
}
#[test]
fn selection_range_reports_viewport_spans() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
assert_eq!(
term.selection_range(),
vec![SelectionSpan {
row: 0,
left: 0,
right: 4
}]
);
}
#[test]
fn selection_range_clips_to_viewport_when_scrolled() {
let mut term = Engine::new(4, 3);
term.feed(b"L0\r\nL1\r\nL2\r\nL3\r\nL4");
term.selection_begin(0, 0, Side::Left, SelectionType::Line); term.selection_extend(2, 0, Side::Left);
assert_eq!(
term.selection_range(),
vec![
SelectionSpan {
row: 0,
left: 0,
right: 3
},
SelectionSpan {
row: 1,
left: 0,
right: 3
},
SelectionSpan {
row: 2,
left: 0,
right: 3
},
]
);
term.scroll_up(1); assert_eq!(
term.selection_range(),
vec![
SelectionSpan {
row: 1,
left: 0,
right: 3
}, SelectionSpan {
row: 2,
left: 0,
right: 3
}, ]
);
}
#[test]
fn selection_follows_cap_eviction() {
let mut term = Engine::with_scrollback(4, 2, 2); term.feed(b"L0\r\nL1\r\nL2\r\nL3");
term.selection_begin(0, 0, Side::Left, SelectionType::Line);
assert_eq!(term.selection_text().as_deref(), Some("L2"));
term.feed(b"\r\nL4");
assert_eq!(term.selection_text().as_deref(), Some("L2"));
}
#[test]
fn selection_rotates_with_region_scroll() {
let mut term = Engine::new(4, 4);
term.feed(b"\x1b[2;4r"); term.feed(b"A\r\nB\r\nC\r\nD");
term.selection_begin(2, 0, Side::Left, SelectionType::Line); assert_eq!(term.selection_text().as_deref(), Some("C"));
term.feed(b"\r\n");
assert_eq!(term.selection_text().as_deref(), Some("C"));
}
#[test]
fn selection_rotates_with_reverse_index() {
let mut term = Engine::new(4, 4);
term.feed(b"\x1b[2;4r");
term.feed(b"A\r\nB\r\nC\r\nD"); term.feed(b"\x1b[2;1H");
term.selection_begin(2, 0, Side::Left, SelectionType::Line); assert_eq!(term.selection_text().as_deref(), Some("C"));
term.feed(b"\x1bM");
assert_eq!(term.selection_text().as_deref(), Some("C"));
}
#[test]
fn selection_survives_reflow_on_resize() {
let mut term = Engine::new(6, 4);
term.feed(b"abcdef");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 5, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("abcdef"));
term.resize(3, 4);
assert_eq!(term.selection_text().as_deref(), Some("abcdef"));
}
#[test]
fn selection_clears_on_alt_screen_switch() {
let mut term = Engine::new(80, 24);
term.feed(b"hello");
term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("hello"));
term.feed(b"\x1b[?1049h"); assert_eq!(term.selection_text(), None);
term.feed(b"\x1b[Hworld"); term.selection_begin(0, 0, Side::Left, SelectionType::Char);
term.selection_extend(0, 4, Side::Right);
assert_eq!(term.selection_text().as_deref(), Some("world"));
term.feed(b"\x1b[?1049l"); assert_eq!(term.selection_text(), None);
}