T35: review round 3: a program's link target is kept whole, as the scanner keeps it, so a prompt inside a long link is numbered and gives the link back plain at its end, and the tap reads a 517th of its room to hold the growth; an ESC that ends an OSC the parser has goes on at once, so controls after it run where vte runs them
This commit is contained in:
5 files changed
+104
-49
No files matched your search
+46
-36
@@ -16,11 +16,11 @@ const NUMBERED: char = '\u{7f}';
|
||||
/// What ends a prompt's link.
|
||||
const CLOSE: &[u8] = b"\x1b]8;;\x07";
|
||||
|
||||
/// The longest target of a program's link kept, to open it again with a prompt's number.
|
||||
const TARGET: usize = 256;
|
||||
/// What a held sequence's rewrite comes to at most: a link with the longest target the scanner keeps.
|
||||
pub(super) const SLACK: usize = MOST + 32;
|
||||
|
||||
/// How many times its size a rewrite makes a sequence at most: a mark of 8 bytes becomes a link of 276.
|
||||
pub(super) const GROWTH: usize = 36;
|
||||
/// How many times its size a rewrite makes a sequence at most: a mark of 8 bytes inside a program's link becomes that link again.
|
||||
pub(super) const GROWTH: usize = SLACK / 8 + 1;
|
||||
|
||||
const ESC: u8 = 0x1b;
|
||||
|
||||
@@ -33,8 +33,8 @@ fn ignored(byte: u8) -> bool {
|
||||
enum State {
|
||||
#[default]
|
||||
Ground,
|
||||
/// After an ESC, held until the next byte says whether an OSC starts.
|
||||
Escape,
|
||||
/// After an ESC, which waits while the parser stands where it stood before it, else has gone on.
|
||||
Escape { sent: bool },
|
||||
/// An OSC held whole, since it may be a mark or a link to rewrite.
|
||||
Held,
|
||||
/// An OSC passed on as it comes.
|
||||
@@ -51,11 +51,11 @@ pub(crate) struct Prompts {
|
||||
body: Vec<u8>,
|
||||
/// The body passed the scanner's bound, so it counts for nothing.
|
||||
overflow: bool,
|
||||
/// The held OSC's ESC has gone on: it ended the OSC before.
|
||||
sent: bool,
|
||||
/// The prompt open now, from its start mark to its end.
|
||||
prompt: Option<u32>,
|
||||
/// A program's own link is open.
|
||||
user: bool,
|
||||
/// Its target, when short enough to keep.
|
||||
/// The target of a program's link open now, to open it again with a prompt's number and without.
|
||||
target: Option<Vec<u8>>,
|
||||
/// The last prompt's number.
|
||||
count: u32,
|
||||
@@ -82,32 +82,33 @@ impl Prompts {
|
||||
self.state = match (self.state, byte) {
|
||||
(State::Ground, ESC) => {
|
||||
self.held.push(ESC);
|
||||
State::Escape
|
||||
State::Escape { sent: false }
|
||||
}
|
||||
(State::Ground, _) => {
|
||||
self.out.push(byte);
|
||||
State::Ground
|
||||
}
|
||||
(State::Escape, b']') => {
|
||||
(State::Escape { sent }, b']') => {
|
||||
self.held.push(byte);
|
||||
self.sent = sent;
|
||||
self.body.clear();
|
||||
self.overflow = false;
|
||||
State::Held
|
||||
}
|
||||
(State::Escape, ESC) => {
|
||||
(State::Escape { .. }, ESC) => {
|
||||
self.release();
|
||||
self.held.push(ESC);
|
||||
State::Escape
|
||||
State::Escape { sent: false }
|
||||
}
|
||||
// vte runs these and stays in the escape, so they go on before it, and only the ESC waits.
|
||||
(State::Escape, _) if byte == 0x07 || ignored(byte) => {
|
||||
// vte runs these and stays in the escape; one that waits lets them on first, where the parser stands the same.
|
||||
(State::Escape { sent }, _) if byte == 0x07 || ignored(byte) => {
|
||||
self.out.push(byte);
|
||||
State::Escape
|
||||
State::Escape { sent }
|
||||
}
|
||||
(State::Escape, _) => {
|
||||
(State::Escape { .. }, _) => {
|
||||
// A full reset clears the link the parser writes with.
|
||||
if byte == b'c' {
|
||||
(self.prompt, self.user, self.target) = (None, false, None);
|
||||
(self.prompt, self.target) = (None, None);
|
||||
}
|
||||
self.held.push(byte);
|
||||
self.release();
|
||||
@@ -117,10 +118,15 @@ impl Prompts {
|
||||
self.end(Some(byte));
|
||||
State::Ground
|
||||
}
|
||||
// An ESC ends the OSC: after a rewrite the parser stands in ground, else inside the OSC, so the ESC goes on.
|
||||
(State::Held | State::Passing, ESC) => {
|
||||
self.end(None);
|
||||
self.held.push(ESC);
|
||||
State::Escape
|
||||
if self.end(None) {
|
||||
self.held.push(ESC);
|
||||
State::Escape { sent: false }
|
||||
} else {
|
||||
self.out.push(ESC);
|
||||
State::Escape { sent: true }
|
||||
}
|
||||
}
|
||||
(State::Held, _) => {
|
||||
self.held.push(byte);
|
||||
@@ -168,8 +174,8 @@ impl Prompts {
|
||||
self.out.append(&mut self.held);
|
||||
}
|
||||
|
||||
/// The OSC ends at `end`, BEL, CAN or SUB, or at an ESC when none.
|
||||
fn end(&mut self, end: Option<u8>) {
|
||||
/// The OSC ends at `end`, BEL, CAN or SUB, or at an ESC when none; true when it was rewritten.
|
||||
fn end(&mut self, end: Option<u8>) -> bool {
|
||||
let rewrite = if self.overflow {
|
||||
None
|
||||
} else if let Some(opens) = self.body.strip_prefix(b"133;").and_then(mark) {
|
||||
@@ -177,21 +183,19 @@ impl Prompts {
|
||||
self.count = self.count.wrapping_add(1);
|
||||
self.prompt = Some(self.count);
|
||||
// A prompt inside a program's link carries its number on that link.
|
||||
match (self.user, &self.target) {
|
||||
(false, _) => Some(link(self.count, LINK.as_bytes())),
|
||||
(true, Some(target)) => Some(link(self.count, target)),
|
||||
(true, None) => None,
|
||||
}
|
||||
Some(match &self.target {
|
||||
Some(target) => link(self.count, target),
|
||||
None => link(self.count, LINK.as_bytes()),
|
||||
})
|
||||
} else {
|
||||
match (self.prompt.take(), self.user, &self.target) {
|
||||
(Some(_), false, _) => Some(CLOSE.to_vec()),
|
||||
(Some(_), true, Some(target)) => Some(plain(target)),
|
||||
_ => None,
|
||||
match (self.prompt.take(), &self.target) {
|
||||
(Some(_), Some(target)) => Some(plain(target)),
|
||||
(Some(_), None) => Some(CLOSE.to_vec()),
|
||||
(None, _) => None,
|
||||
}
|
||||
}
|
||||
} else if let Some((opens, target)) = user(&self.body) {
|
||||
self.user = opens;
|
||||
self.target = (opens && target.len() <= TARGET).then(|| target.to_vec());
|
||||
self.target = opens.then(|| target.to_vec());
|
||||
// Inside a prompt a program's link carries the prompt's number; its end takes the prompt's link up again.
|
||||
let target = if opens { target } else { LINK.as_bytes() };
|
||||
self.prompt.map(|number| link(number, target))
|
||||
@@ -201,15 +205,21 @@ impl Prompts {
|
||||
match (rewrite, self.state) {
|
||||
(Some(bytes), State::Held) => {
|
||||
self.held.clear();
|
||||
self.out.extend_from_slice(&bytes);
|
||||
// An ESC that went on already starts the link.
|
||||
self.out.extend_from_slice(&bytes[usize::from(self.sent)..]);
|
||||
// CAN and SUB end the OSC and run as well.
|
||||
self.out.extend(end.filter(|&byte| byte != 0x07));
|
||||
true
|
||||
}
|
||||
(_, State::Held) => {
|
||||
self.held.extend(end);
|
||||
self.release();
|
||||
false
|
||||
}
|
||||
_ => {
|
||||
self.out.extend(end);
|
||||
false
|
||||
}
|
||||
_ => self.out.extend(end),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+52
-10
@@ -31,7 +31,7 @@ fn parsed(bytes: &[u8], columns: usize, lines: usize) -> Term<VoidListener> {
|
||||
}
|
||||
|
||||
/// The target of the link at a cell.
|
||||
fn target(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String> {
|
||||
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())
|
||||
}
|
||||
@@ -136,7 +136,7 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
|
||||
Some(1),
|
||||
"a prompt all in a link"
|
||||
);
|
||||
assert_eq!(target(&whole, 0, 6).as_deref(), Some("file:///work"));
|
||||
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");
|
||||
@@ -159,7 +159,7 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
|
||||
.is_some_and(|link| !link.id().starts_with(NUMBERED))
|
||||
);
|
||||
assert_eq!(
|
||||
target(&open, 0, 2).as_deref(),
|
||||
target_of(&open, 0, 2).as_deref(),
|
||||
Some("file:///x"),
|
||||
"its own again past the prompt"
|
||||
);
|
||||
@@ -169,6 +169,48 @@ fn a_program_s_link_inside_a_prompt_carries_its_number() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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)];
|
||||
let numbered = |column: usize| {
|
||||
row[Column(column)]
|
||||
.hyperlink()
|
||||
.is_some_and(|link| link.id().starts_with(NUMBERED))
|
||||
};
|
||||
assert_eq!(prompt(row), Some(1), "{bytes:?}");
|
||||
assert!(
|
||||
numbered(1) && !numbered(2),
|
||||
"the prompt's cell alone: {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 controls_vte_runs_inside_an_escape_go_on_before_the_link() {
|
||||
assert_eq!(
|
||||
@@ -186,8 +228,8 @@ fn a_prompt_s_cells_carry_its_link_through_sgr_0_and_its_input_none() {
|
||||
);
|
||||
assert_eq!(prompt(&term.grid()[Line(0)]), None);
|
||||
assert_eq!(prompt(&term.grid()[Line(1)]), Some(1));
|
||||
assert_eq!(target(&term, 1, 1).as_deref(), Some(LINK));
|
||||
assert_eq!(target(&term, 1, 2), None, "input");
|
||||
assert_eq!(target_of(&term, 1, 1).as_deref(), Some(LINK));
|
||||
assert_eq!(target_of(&term, 1, 2), None, "input");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -197,21 +239,21 @@ fn a_program_s_own_link_and_the_prompt_s_keep_each_other() {
|
||||
12,
|
||||
2,
|
||||
);
|
||||
assert_eq!(target(&term, 0, 0).as_deref(), Some("file:///work"));
|
||||
assert_eq!(target(&term, 0, 6).as_deref(), Some(LINK));
|
||||
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(&term, 0, 0).as_deref(), Some("file:///x"));
|
||||
assert_eq!(target_of(&term, 0, 0).as_deref(), Some("file:///x"));
|
||||
assert_eq!(
|
||||
target(&term, 0, 1).as_deref(),
|
||||
target_of(&term, 0, 1).as_deref(),
|
||||
Some("file:///x"),
|
||||
"a prompt inside a link keeps it"
|
||||
);
|
||||
assert_eq!(target(&term, 0, 2).as_deref(), Some(LINK));
|
||||
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.
|
||||
|
||||
+4
-1
@@ -45,7 +45,10 @@ impl<P> Tapped<P> {
|
||||
impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
// What a rewrite adds stays inside the buffer, with room for the held sequence's own.
|
||||
let take = buf.len().saturating_sub(self.prompts.held() + 300) / prompts::GROWTH;
|
||||
let room = buf
|
||||
.len()
|
||||
.saturating_sub(self.prompts.held() + prompts::SLACK);
|
||||
let take = room / prompts::GROWTH;
|
||||
if take == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user