T35: review round 2: a program's link inside a prompt keeps its target and carries the prompt's number in its id, and a prompt that starts inside a link left open opens it again with the number, so a prompt all in links still starts on its first row; controls vte runs inside an escape go on before it, so only the ESC waits and a long run of them keeps the pty reading
This commit is contained in:
6 files changed
+136
-33
No files matched your search
+53
-24
@@ -7,12 +7,21 @@ use gpui::Context;
|
||||
|
||||
use super::{tap::MOST, view::TerminalView};
|
||||
|
||||
/// The target of the OSC 8 links prompts become, each named by its prompt's number; no program names a link so.
|
||||
/// The target of the OSC 8 link a prompt becomes; no program names a link so.
|
||||
pub(crate) const LINK: &str = "\u{7f}";
|
||||
|
||||
/// What a link's id starts with when it carries a prompt's number: the prompt's own link, or a program's inside it.
|
||||
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;
|
||||
|
||||
/// 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;
|
||||
|
||||
const ESC: u8 = 0x1b;
|
||||
|
||||
/// Controls vte skips inside an OSC.
|
||||
@@ -46,6 +55,8 @@ pub(crate) struct Prompts {
|
||||
prompt: Option<u32>,
|
||||
/// A program's own link is open.
|
||||
user: bool,
|
||||
/// Its target, when short enough to keep.
|
||||
target: Option<Vec<u8>>,
|
||||
/// The last prompt's number.
|
||||
count: u32,
|
||||
/// What goes to the parser from the last read.
|
||||
@@ -78,8 +89,6 @@ impl Prompts {
|
||||
State::Ground
|
||||
}
|
||||
(State::Escape, b']') => {
|
||||
// Controls vte ran inside the escape go on first, as it ran them.
|
||||
self.out.extend(self.held.drain(1..));
|
||||
self.held.push(byte);
|
||||
self.body.clear();
|
||||
self.overflow = false;
|
||||
@@ -90,15 +99,15 @@ impl Prompts {
|
||||
self.held.push(ESC);
|
||||
State::Escape
|
||||
}
|
||||
// vte runs these and stays in the escape.
|
||||
// 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) => {
|
||||
self.held.push(byte);
|
||||
self.out.push(byte);
|
||||
State::Escape
|
||||
}
|
||||
(State::Escape, _) => {
|
||||
// A full reset clears the link the parser writes with.
|
||||
if byte == b'c' {
|
||||
(self.prompt, self.user) = (None, false);
|
||||
(self.prompt, self.user, self.target) = (None, false, None);
|
||||
}
|
||||
self.held.push(byte);
|
||||
self.release();
|
||||
@@ -167,15 +176,25 @@ impl Prompts {
|
||||
if opens {
|
||||
self.count = self.count.wrapping_add(1);
|
||||
self.prompt = Some(self.count);
|
||||
(!self.user).then(|| link(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,
|
||||
}
|
||||
} else {
|
||||
let closes = self.prompt.take().is_some() && !self.user;
|
||||
closes.then(|| CLOSE.to_vec())
|
||||
match (self.prompt.take(), self.user, &self.target) {
|
||||
(Some(_), false, _) => Some(CLOSE.to_vec()),
|
||||
(Some(_), true, Some(target)) => Some(plain(target)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
} else if let Some(opens) = user(&self.body) {
|
||||
} else if let Some((opens, target)) = user(&self.body) {
|
||||
self.user = opens;
|
||||
// The program's link ends inside a prompt: the prompt's link takes up again.
|
||||
self.prompt.filter(|_| !opens).map(link)
|
||||
self.target = (opens && target.len() <= TARGET).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))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -208,27 +227,37 @@ fn mark(body: &[u8]) -> Option<bool> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether an OSC 8 body opens a program's link, as alacritty reads its target; none for no link at all.
|
||||
fn user(body: &[u8]) -> Option<bool> {
|
||||
/// Whether an OSC 8 body opens a program's link, as alacritty reads its target, and that target; none for no link at all.
|
||||
fn user(body: &[u8]) -> Option<(bool, &[u8])> {
|
||||
let rest = body.strip_prefix(b"8;")?;
|
||||
let target = &rest[rest.iter().position(|&byte| byte == b';')? + 1..];
|
||||
// alacritty joins the target's parts with `;`, each one empty when it is not UTF-8.
|
||||
Some(
|
||||
target.contains(&b';')
|
||||
|| std::str::from_utf8(target).is_ok_and(|target| !target.is_empty()),
|
||||
)
|
||||
let opens =
|
||||
target.contains(&b';') || std::str::from_utf8(target).is_ok_and(|text| !text.is_empty());
|
||||
Some((opens, target))
|
||||
}
|
||||
|
||||
/// The OSC 8 link that marks prompt `number`'s cells.
|
||||
fn link(number: u32) -> Vec<u8> {
|
||||
format!("\x1b]8;id={number};{LINK}\x07").into_bytes()
|
||||
/// A link to `target` whose id carries prompt `number`.
|
||||
fn link(number: u32, target: &[u8]) -> Vec<u8> {
|
||||
let mut link = format!("\x1b]8;id={NUMBERED}{number};").into_bytes();
|
||||
link.extend_from_slice(target);
|
||||
link.push(0x07);
|
||||
link
|
||||
}
|
||||
|
||||
/// The number of the prompt `row` shows, as its link names it.
|
||||
/// A program's link to `target`, its id its own again.
|
||||
fn plain(target: &[u8]) -> Vec<u8> {
|
||||
let mut link = b"\x1b]8;;".to_vec();
|
||||
link.extend_from_slice(target);
|
||||
link.push(0x07);
|
||||
link
|
||||
}
|
||||
|
||||
/// The number of the prompt `row` shows, as a link's id carries it.
|
||||
fn prompt(row: &Row<Cell>) -> Option<u32> {
|
||||
row.into_iter().find_map(|cell| {
|
||||
let link = cell.hyperlink().filter(|link| link.uri() == LINK)?;
|
||||
link.id().parse().ok()
|
||||
let link = cell.hyperlink()?;
|
||||
link.id().strip_prefix(NUMBERED)?.parse().ok()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -40,15 +40,15 @@ fn target(term: &Term<VoidListener>, line: i32, column: usize) -> Option<String>
|
||||
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=1;\x7f\x07$ \x1b]8;;\x07\x1b\\x"
|
||||
b"\x1b]8;id=\x7f1;\x7f\x07$ \x1b]8;;\x07\x1b\\x"
|
||||
);
|
||||
assert_eq!(
|
||||
passed(&[b"\x1b]133;A;k=i\x07\x1b]133;D;1\x18"]),
|
||||
b"\x1b]8;id=1;\x7f\x07\x1b]8;;\x07\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=1;\x7f\x07\x1b]8;;\x07\x1b]8;id=2;\x7f\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"]),
|
||||
@@ -109,11 +109,71 @@ fn other_sequences_pass_as_they_came() {
|
||||
);
|
||||
}
|
||||
|
||||
#[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(&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!(
|
||||
z.hyperlink()
|
||||
.is_some_and(|link| !link.id().starts_with(NUMBERED))
|
||||
);
|
||||
assert_eq!(
|
||||
target(&open, 0, 2).as_deref(),
|
||||
Some("file:///x"),
|
||||
"its own again past the prompt"
|
||||
);
|
||||
assert!(
|
||||
y.hyperlink()
|
||||
.is_some_and(|link| link.id().starts_with(NUMBERED))
|
||||
);
|
||||
}
|
||||
|
||||
#[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=1;\x7f\x07$"
|
||||
b"\n\x07\x1b]8;id=\x7f1;\x7f\x07$"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -338,6 +338,15 @@ mod tests {
|
||||
assert_eq!(ran.prompt, [0], "the prompt's cell, not the output's");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_long_escape_of_controls_keeps_the_pty_reading() {
|
||||
let ran = run(
|
||||
"printf '\\033'; head -c 3000000 /dev/zero; printf '[0mREADY'",
|
||||
(40, 4),
|
||||
);
|
||||
assert_eq!(ran.text, "READY");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_foreground_is_the_program_s_own_group() {
|
||||
let launch = Launch {
|
||||
|
||||
+8
-3
@@ -7,7 +7,12 @@ use alacritty_terminal::{
|
||||
use futures::channel::mpsc::UnboundedSender;
|
||||
use polling::{Event, PollMode, Poller};
|
||||
|
||||
use super::{marks, mouse::MouseMode, prompts::Prompts, pty::PtyEvent};
|
||||
use super::{
|
||||
marks,
|
||||
mouse::MouseMode,
|
||||
prompts::{self, Prompts},
|
||||
pty::PtyEvent,
|
||||
};
|
||||
|
||||
/// The longest OSC body kept; longer ones are dropped whole.
|
||||
pub(super) const MOST: usize = 4096;
|
||||
@@ -39,8 +44,8 @@ impl<P> Tapped<P> {
|
||||
|
||||
impl<P: EventedReadWrite> io::Read for Tapped<P> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
// A rewrite adds 14 bytes at most to a sequence of 6 or more, so a quarter of the room is read.
|
||||
let take = buf.len().saturating_sub(self.prompts.held() + 64) / 4;
|
||||
// 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;
|
||||
if take == 0 {
|
||||
return Ok(0);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user