374 lines
12 KiB
Rust
374 lines
12 KiB
Rust
use alacritty_terminal::{
|
|
event::VoidListener,
|
|
index::Column,
|
|
term::{Config, test::TermSize},
|
|
vte::ansi::Processor,
|
|
};
|
|
|
|
use super::*;
|
|
use crate::term::marks::{self, Mark};
|
|
|
|
/// What the parser gets from `chunks`, each one read.
|
|
fn passed(chunks: &[&[u8]]) -> Vec<u8> {
|
|
let mut prompts = Prompts::default();
|
|
let mut out = Vec::new();
|
|
for chunk in chunks {
|
|
out.extend_from_slice(prompts.pass(chunk));
|
|
}
|
|
out
|
|
}
|
|
|
|
/// A terminal `columns` wide and `lines` high after `bytes` passed through the rewrite.
|
|
fn parsed(bytes: &[u8], columns: usize, lines: usize) -> Term<VoidListener> {
|
|
let mut term = Term::new(
|
|
Config::default(),
|
|
&TermSize::new(columns, lines),
|
|
VoidListener,
|
|
);
|
|
let mut parser: Processor = Processor::new();
|
|
parser.advance(&mut term, &passed(&[bytes]));
|
|
term
|
|
}
|
|
|
|
/// The target of the link at a cell.
|
|
fn target_of(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String> {
|
|
let cell = &term.grid()[Line(line)][Column(column)];
|
|
cell.hyperlink().map(|link| link.uri().to_string())
|
|
}
|
|
|
|
#[test]
|
|
fn a_mark_becomes_its_prompt_s_numbered_link() {
|
|
assert_eq!(
|
|
passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x1b\\x"]),
|
|
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07\x1b\\x"
|
|
);
|
|
assert_eq!(
|
|
passed(&[b"\x1b]133;A\x07$ \x1b]133;B\x07ls\x1b]133;C\x07out"]),
|
|
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;id=\x7f0i;\x7f\x07ls\x1b]8;;\x07out",
|
|
"the input runs from B to C"
|
|
);
|
|
assert_eq!(
|
|
passed(&[b"\x1b]133;A;k=i\x07\x1b]133;D;1\x18"]),
|
|
b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x18"
|
|
);
|
|
assert_eq!(
|
|
passed(&[b"\x1b]133;N\x07\x1b]133;C\x07\x1b]133;A\x07"]),
|
|
b"\x1b]8;id=\x7f1;\x7f\x07\x1b]8;;\x07\x1b]8;id=\x7f2;\x7f\x07"
|
|
);
|
|
assert_eq!(
|
|
passed(&[b"\x1b]133;D;0\x07"]),
|
|
b"\x1b]133;D;0\x07",
|
|
"an end with no prompt open passes"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_rewrite_reads_a_mark_as_the_session_hears_it() {
|
|
let bodies = [
|
|
"A", "A;k=i", "N", "B", "I", "C;x", "D;1", "D;abc", "D", "", "AB", "L", "P;k=i", "Q", "a",
|
|
];
|
|
for body in bodies {
|
|
let heard = marks::read(body.as_bytes()).map(|mark| match mark {
|
|
Mark::Prompt(_) => Edge::Prompt,
|
|
Mark::Input => Edge::Input,
|
|
Mark::Output | Mark::Done(_) => Edge::End,
|
|
});
|
|
assert_eq!(mark(body.as_bytes()), heard, "{body}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_mark_split_across_reads_comes_out_whole() {
|
|
let whole: &[u8] =
|
|
b"a\x1b]133;A;k=i\x07\x1b]8;;file:///w\x1b\\w\x1b]8;;\x1b\\$ \x1b]133;B\x1b\\b";
|
|
let once = passed(&[whole]);
|
|
for cut in 0..=whole.len() {
|
|
let (first, second) = whole.split_at(cut);
|
|
assert_eq!(passed(&[first, second]), once, "cut at {cut}");
|
|
}
|
|
let bytes: Vec<&[u8]> = whole.chunks(1).collect();
|
|
assert_eq!(passed(&bytes), once);
|
|
}
|
|
|
|
#[test]
|
|
fn other_sequences_pass_as_they_came() {
|
|
for bytes in [
|
|
"\x1b]133;AB\x07x",
|
|
"\x1b]1337;A\x07",
|
|
"\x1b]133;P;k=i\x07",
|
|
"\x1b]133;\x07",
|
|
"\x1b]0;133;A\x07",
|
|
"\x1b[31m\x1bc",
|
|
"\x1b]8;;file:///x\x07x\x1b]8;;\x07",
|
|
"text",
|
|
] {
|
|
assert_eq!(passed(&[bytes.as_bytes()]), bytes.as_bytes(), "{bytes:?}");
|
|
}
|
|
let mut long = b"\x1b]133;A;".to_vec();
|
|
long.extend([b'x'; MOST]);
|
|
long.push(0x07);
|
|
assert_eq!(passed(&[&long]), long, "past the scanner's bound");
|
|
let mut skipped = b"\x1b]133;A".to_vec();
|
|
skipped.extend([0; 3 * MOST]);
|
|
skipped.push(0x07);
|
|
assert_eq!(
|
|
passed(&[&skipped]),
|
|
skipped,
|
|
"skipped controls bound the bytes held"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_long_escape_of_controls_goes_on_as_it_comes() {
|
|
let mut whole = vec![ESC];
|
|
whole.extend(vec![0; 3 << 20]);
|
|
whole.extend_from_slice(b"[0mREADY");
|
|
let mut prompts = Prompts::default();
|
|
let mut out = Vec::new();
|
|
for chunk in whole.chunks(64 << 10) {
|
|
out.extend_from_slice(prompts.pass(chunk));
|
|
assert!(prompts.held() <= 1, "only the ESC waits");
|
|
}
|
|
assert_eq!(out.len(), whole.len());
|
|
assert!(out.ends_with(b"\x1b[0mREADY"));
|
|
}
|
|
|
|
#[test]
|
|
fn a_program_s_link_inside_a_prompt_carries_its_number() {
|
|
let whole = parsed(
|
|
b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work $ \x1b]8;;\x1b\\\x1b]133;B\x07",
|
|
12,
|
|
2,
|
|
);
|
|
assert_eq!(
|
|
prompt(&whole.grid()[Line(0)]),
|
|
Some(1),
|
|
"a prompt all in a link"
|
|
);
|
|
assert_eq!(target_of(&whole, 0, 6).as_deref(), Some("file:///work"));
|
|
let first = b"\x1b]133;A\x07\x1b]8;;file:///w\x1b\\/work\r\n\x1b]8;;\x1b\\$ \x1b]133;B\x07\r\no\r\no\r\no";
|
|
let term = parsed(first, 8, 3);
|
|
assert_eq!(jump(&term, true), Some(2), "its linked first row starts it");
|
|
let open = parsed(
|
|
b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]133;B\x07z",
|
|
8,
|
|
2,
|
|
);
|
|
assert_eq!(
|
|
prompt(&open.grid()[Line(0)]),
|
|
Some(1),
|
|
"a prompt inside a link left open"
|
|
);
|
|
let (y, z) = (
|
|
&open.grid()[Line(0)][Column(1)],
|
|
&open.grid()[Line(0)][Column(2)],
|
|
);
|
|
assert_eq!(part(z), Some(Part::Input(0)), "the input inside it");
|
|
assert_eq!(
|
|
target_of(&open, 0, 2).as_deref(),
|
|
Some("file:///x"),
|
|
"its own target past the prompt"
|
|
);
|
|
assert!(
|
|
y.hyperlink()
|
|
.is_some_and(|link| link.id().starts_with(NUMBERED))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_long_target_numbers_a_prompt_in_either_order() {
|
|
let target = format!("https://e.test/{}", "a".repeat(249));
|
|
let open = format!("\x1b]8;;{target}\x1b\\");
|
|
let before = format!("{open}x\x1b]133;A\x07y\x1b]133;B\x07z\x1b]8;;\x1b\\");
|
|
let inside = format!("x\x1b]133;A\x07{open}y\x1b]133;B\x07z\x1b]8;;\x1b\\");
|
|
for bytes in [before, inside] {
|
|
let term = parsed(bytes.as_bytes(), 8, 2);
|
|
let row = &term.grid()[Line(0)];
|
|
assert_eq!(prompt(row), Some(1), "{bytes:?}");
|
|
assert!(
|
|
prompted(&row[Column(1)]) && !prompted(&row[Column(2)]),
|
|
"the prompt's cell alone: {bytes:?}"
|
|
);
|
|
assert_eq!(part(&row[Column(2)]), Some(Part::Input(0)), "{bytes:?}");
|
|
assert_eq!(target_of(&term, 0, 2).as_deref(), Some(target.as_str()));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn controls_after_an_osc_s_closing_escape_run_where_vte_runs_them() {
|
|
let whole: &[u8] = b"LEFT\x1b]0;title\x1b\r\n\\RIGHT";
|
|
for cut in 0..=whole.len() {
|
|
let (first, second) = whole.split_at(cut);
|
|
assert_eq!(passed(&[first, second]), whole, "cut at {cut}");
|
|
}
|
|
let term = parsed(whole, 8, 2);
|
|
let line = |at: i32| -> String {
|
|
term.grid()[Line(at)]
|
|
.into_iter()
|
|
.map(|cell| cell.c)
|
|
.collect()
|
|
};
|
|
assert_eq!((line(0).trim_end(), line(1).trim_end()), ("LEFT", "RIGHT"));
|
|
let marked = parsed(b"\x1b]133;A\x1b\r\n\\$", 8, 2);
|
|
assert_eq!(prompt(&marked.grid()[Line(1)]), Some(1));
|
|
}
|
|
|
|
#[test]
|
|
fn del_and_high_bytes_in_an_escape_go_on_after_it() {
|
|
assert_eq!(
|
|
passed(&[b"\x1b\x7f]133;A\x07$\x1b\xff", b"]133;B\x07"]),
|
|
b"\x1b\x7f]8;id=\x7f1;\x7f\x07$\x1b\xff]8;id=\x7f0i;\x7f\x07"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn controls_vte_runs_inside_an_escape_go_on_before_the_link() {
|
|
assert_eq!(
|
|
passed(&[b"\x1b\n\x07]133;A\x07$"]),
|
|
b"\n\x07\x1b]8;id=\x7f1;\x7f\x07$"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_its_own() {
|
|
let term = parsed(
|
|
b"x\r\n\x1b]133;A\x07\x1b[31m$\x1b[0m \x1b]133;B\x07ls",
|
|
8,
|
|
2,
|
|
);
|
|
assert_eq!(prompt(&term.grid()[Line(0)]), None);
|
|
assert_eq!(prompt(&term.grid()[Line(1)]), Some(1));
|
|
assert_eq!(target_of(&term, 1, 1).as_deref(), Some(LINK));
|
|
assert_eq!(
|
|
part(&term.grid()[Line(1)][Column(2)]),
|
|
Some(Part::Input(0)),
|
|
"input"
|
|
);
|
|
let ran = parsed(
|
|
b"\x1b]133;A\x07$\x1b]133;B\x07ls\x1b]133;C\x07\r\nout",
|
|
8,
|
|
2,
|
|
);
|
|
assert_eq!(target_of(&ran, 1, 0), None, "output");
|
|
}
|
|
|
|
#[test]
|
|
fn a_program_s_own_link_and_the_prompt_s_keep_each_other() {
|
|
let term = parsed(
|
|
b"\x1b]133;A\x07\x1b]8;;file:///work\x1b\\/work\x1b]8;;\x1b\\ $ \x1b]133;B\x07",
|
|
12,
|
|
2,
|
|
);
|
|
assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///work"));
|
|
assert_eq!(target_of(&term, 0, 6).as_deref(), Some(LINK));
|
|
assert_eq!(prompt(&term.grid()[Line(0)]), Some(1));
|
|
let term = parsed(
|
|
b"\x1b]8;;file:///x\x1b\\x\x1b]133;A\x07y\x1b]8;;\x1b\\z\x1b]133;B\x07",
|
|
12,
|
|
2,
|
|
);
|
|
assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///x"));
|
|
assert_eq!(
|
|
target_of(&term, 0, 1).as_deref(),
|
|
Some("file:///x"),
|
|
"a prompt inside a link keeps it"
|
|
);
|
|
assert_eq!(target_of(&term, 0, 2).as_deref(), Some(LINK));
|
|
}
|
|
|
|
/// Prompts on lines 0, 3 and 6 of a history four lines high, the last on the screen.
|
|
fn history() -> Term<VoidListener> {
|
|
let mut bytes = Vec::new();
|
|
for _ in 0..3 {
|
|
bytes.extend_from_slice(
|
|
b"\x1b]133;A\x07$ \x1b]133;B\x07c\r\n\x1b]133;C\x07o\r\no\r\n\x1b]133;D;0\x07",
|
|
);
|
|
}
|
|
bytes.extend_from_slice(b"\x1b]133;A\x07$ \x1b]133;B\x07");
|
|
parsed(&bytes, 8, 4)
|
|
}
|
|
|
|
#[test]
|
|
fn up_and_down_go_to_the_prompts_beside_the_view_s_top() {
|
|
let mut term = history();
|
|
assert_eq!(term.grid().history_size(), 6);
|
|
// From the bottom: the prompt at the view's top is passed, the one before it found.
|
|
assert_eq!(jump(&term, true), Some(3));
|
|
term.scroll_display(Scroll::Delta(3));
|
|
assert_eq!(jump(&term, true), Some(6));
|
|
term.scroll_display(Scroll::Delta(3));
|
|
assert_eq!(jump(&term, true), None, "no prompt above the first");
|
|
assert_eq!(jump(&term, false), Some(3));
|
|
term.scroll_display(Scroll::Delta(-3));
|
|
assert_eq!(
|
|
jump(&term, false),
|
|
Some(0),
|
|
"a prompt on the screen means the bottom"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_prompt_over_rows_starts_at_its_first_blank_rows_within_it_too() {
|
|
let bytes = b"\x1b]133;A\x07FIRST\r\n\r\nLAST $ \x1b]133;B\x07\r\nout1\r\nout2\r\nout3\r\nout4";
|
|
let mut term = parsed(bytes, 8, 3);
|
|
assert_eq!(term.grid().history_size(), 4);
|
|
assert_eq!(jump(&term, true), Some(4), "FIRST, not LAST");
|
|
term.scroll_display(Scroll::Delta(4));
|
|
assert_eq!(jump(&term, false), None, "LAST is the same prompt");
|
|
let next = parsed(b"\x1b]133;A\x07$ \x1b]133;B\x07\r\n\x1b]133;A\x07$ ", 8, 2);
|
|
assert_eq!(
|
|
(prompt(&next.grid()[Line(0)]), prompt(&next.grid()[Line(1)])),
|
|
(Some(1), Some(2)),
|
|
"an empty line's next prompt is its own"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_prompt_keeps_its_start_through_a_resize() {
|
|
let bytes =
|
|
b"\x1b]133;A\x07~/folder $ \x1b]133;B\x07ls\r\nout\r\nout\r\nout\r\n\x1b]133;A\x07$ ";
|
|
let mut term = parsed(bytes, 12, 3);
|
|
term.resize(TermSize::new(6, 3));
|
|
let rows = term.topmost_line().0..=term.bottommost_line().0;
|
|
let numbers: Vec<Option<u32>> = rows.map(|line| prompt(&term.grid()[Line(line)])).collect();
|
|
let first = numbers.iter().position(|number| *number == Some(1));
|
|
// The first prompt wraps over two rows; the view jumps to the first of them.
|
|
assert_eq!(
|
|
numbers.iter().filter(|number| **number == Some(1)).count(),
|
|
2
|
|
);
|
|
let offset = jump(&term, true).expect("a prompt above");
|
|
let top = term.topmost_line().0 + first.expect("prompt 1") as i32;
|
|
assert_eq!(offset as i32, -top);
|
|
}
|
|
|
|
#[test]
|
|
fn input_carries_the_commands_ended_before_it() {
|
|
let line = b"\x1b]133;A\x07$\x1b]133;B\x07ab\x1b]133;A\x07R\x1b]133;B\x07c";
|
|
let term = parsed(line, 8, 2);
|
|
let at = |column: usize| part(&term.grid()[Line(0)][Column(column)]);
|
|
assert_eq!(
|
|
(at(0), at(1), at(3)),
|
|
(
|
|
Some(Part::Prompt(1)),
|
|
Some(Part::Input(0)),
|
|
Some(Part::Prompt(2))
|
|
)
|
|
);
|
|
assert_eq!(
|
|
at(4),
|
|
Some(Part::Input(0)),
|
|
"a prompt drawn again keeps the line"
|
|
);
|
|
let next = parsed(
|
|
b"\x1b]133;A\x07$\x1b]133;B\x07a\x1b]133;C\x07\x1b]133;D\x07\x1b]133;A\x07$\x1b]133;B\x07b",
|
|
8,
|
|
2,
|
|
);
|
|
assert_eq!(
|
|
part(&next.grid()[Line(0)][Column(3)]),
|
|
Some(Part::Input(2)),
|
|
"C and D each end it"
|
|
);
|
|
}
|