mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-09-21 16:02:20 +00:00
fix(terminal): submit the line the shell is still holding, not an empty one (#800)
Refs #433. Adopts the held seed at the editor's own doors rather than at submit time, so a recalled history entry, a ctrl-U, a ghost suggestion or a completion is no longer glued to the front of the gap text. Also closes a paste-provenance hole left by #790: release_hold()/dump_hold() pushed the hold's contents into the typeahead record as plain text, dropping the paste mark, so a paste made during a gap that outlived the hold window came back looking typed and was submitted raw through the shell's binding table.
This commit is contained in:
@@ -4,11 +4,20 @@ const RECORD_CAP: usize = 4096;
|
||||
pub struct Typeahead {
|
||||
text: String,
|
||||
tainted: bool,
|
||||
pasted: bool,
|
||||
}
|
||||
|
||||
pub enum RawInput<'a> {
|
||||
Text(&'a str),
|
||||
Key { key: &'a str, plain: bool },
|
||||
/// [`Text`](Self::Text) for text that came off the clipboard rather than
|
||||
/// the keyboard. The record replays into the editor's buffer, so the
|
||||
/// provenance has to survive the round trip or a paste comes back looking
|
||||
/// typed and is submitted as typed (#660).
|
||||
Pasted(&'a str),
|
||||
Key {
|
||||
key: &'a str,
|
||||
plain: bool,
|
||||
},
|
||||
Interrupt,
|
||||
}
|
||||
|
||||
@@ -22,6 +31,10 @@ impl Typeahead {
|
||||
RawInput::Interrupt => self.discard(),
|
||||
_ if externally_owned => {}
|
||||
RawInput::Text(s) => self.record_text(s),
|
||||
RawInput::Pasted(s) => {
|
||||
self.record_text(s);
|
||||
self.pasted = true;
|
||||
}
|
||||
RawInput::Key {
|
||||
key: "enter",
|
||||
plain: true,
|
||||
@@ -40,10 +53,30 @@ impl Typeahead {
|
||||
*self = Self::default();
|
||||
}
|
||||
|
||||
/// Whether any of the recorded text came off the clipboard. Read it before
|
||||
/// [`drain`](Self::drain) or [`adopt`](Self::adopt), which clear it with
|
||||
/// the seed — the same contract `GapHold::pasted` carries.
|
||||
pub fn pasted(&self) -> bool {
|
||||
self.pasted
|
||||
}
|
||||
|
||||
pub fn drain(&mut self) -> Option<String> {
|
||||
std::mem::take(self).flush()
|
||||
}
|
||||
|
||||
/// Hand the seed to the local editor while leaving the wipe owed.
|
||||
///
|
||||
/// The shell is still sitting on this text, so the `^U` that erases it has
|
||||
/// to go out eventually — but not necessarily now. Taking the seed out and
|
||||
/// keeping the record in its tainted (wipe, seed nothing) shape lets the
|
||||
/// editor own the whole line straight away, and the next `drain` still
|
||||
/// produces the wipe.
|
||||
pub fn adopt(&mut self) -> Option<String> {
|
||||
let seed = self.drain()?;
|
||||
self.tainted = true;
|
||||
Some(seed)
|
||||
}
|
||||
|
||||
fn record_text(&mut self, s: &str) {
|
||||
if s.chars().any(char::is_control) {
|
||||
self.tainted = true;
|
||||
@@ -201,6 +234,62 @@ mod tests {
|
||||
assert_eq!(Typeahead::new().drain(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_moves_the_seed_out_and_leaves_the_wipe_owed() {
|
||||
let mut t = Typeahead::new();
|
||||
t.observe(RawInput::Text("echo"), false);
|
||||
assert_eq!(t.adopt(), Some("echo".to_string()));
|
||||
// The seed is the editor's now, but the shell is still holding it.
|
||||
assert_eq!(t.drain(), Some(String::new()));
|
||||
assert_eq!(t.drain(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_an_empty_record_owes_nothing() {
|
||||
let mut t = Typeahead::new();
|
||||
assert_eq!(t.adopt(), None);
|
||||
assert_eq!(t.drain(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_twice_seeds_once() {
|
||||
let mut t = Typeahead::new();
|
||||
t.observe(RawInput::Text("echo"), false);
|
||||
assert_eq!(t.adopt(), Some("echo".to_string()));
|
||||
assert_eq!(t.adopt(), Some(String::new()));
|
||||
assert_eq!(t.drain(), Some(String::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_pasted_gap_replays_as_a_paste() {
|
||||
let mut t = Typeahead::new();
|
||||
assert!(!t.pasted(), "a fresh record carries nothing pasted");
|
||||
t.observe(RawInput::Text("cat "), false);
|
||||
assert!(!t.pasted());
|
||||
t.observe(RawInput::Pasted("/tmp/x"), false);
|
||||
assert!(t.pasted(), "the whole seed is pasted once any of it is");
|
||||
assert_eq!(t.drain(), Some("cat /tmp/x".to_string()));
|
||||
assert!(!t.pasted(), "the drain hands the mark over with the seed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adopting_hands_the_paste_mark_over_with_the_seed() {
|
||||
let mut t = Typeahead::new();
|
||||
t.observe(RawInput::Pasted("ls"), false);
|
||||
assert_eq!(t.adopt(), Some("ls".to_string()));
|
||||
assert!(!t.pasted(), "what is left is the owed wipe, not a paste");
|
||||
assert_eq!(t.drain(), Some(String::new()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discarding_an_adopted_record_drops_the_owed_wipe() {
|
||||
let mut t = Typeahead::new();
|
||||
t.observe(RawInput::Text("echo"), false);
|
||||
assert_eq!(t.adopt(), Some("echo".to_string()));
|
||||
t.discard();
|
||||
assert_eq!(t.drain(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_text_is_wiped_and_seeded() {
|
||||
let mut p = Typeahead::new();
|
||||
|
||||
+415
-8
@@ -2370,6 +2370,7 @@ impl TerminalView {
|
||||
let key = ks.key.as_str();
|
||||
self.cursor_visible = true;
|
||||
self.jump_to_prompt();
|
||||
self.adopt_typeahead();
|
||||
|
||||
let aliased;
|
||||
let ks = if m.control && !m.platform && !m.alt && matches!(key, "p" | "n") {
|
||||
@@ -4050,12 +4051,36 @@ impl TerminalView {
|
||||
}
|
||||
|
||||
fn flush_typeahead(&mut self) {
|
||||
let pasted = self.typeahead.pasted();
|
||||
let Some(seed) = self.typeahead.drain() else {
|
||||
return;
|
||||
};
|
||||
self.terminal.write(vec![0x15]);
|
||||
if !seed.is_empty() {
|
||||
self.cmd.prepend_str(&seed);
|
||||
self.prepend_into_editor(&seed, pasted);
|
||||
}
|
||||
}
|
||||
|
||||
/// Take the record into the editor without paying the wipe yet. Every door
|
||||
/// into the editor opens with this.
|
||||
///
|
||||
/// `at_prompt` comes back on the `D` mark, a whole prompt draw ahead of the
|
||||
/// `B` that arms `zle_reading`, and this editor is live for that whole
|
||||
/// window. Everything it offers rewrites the line — history recall and the
|
||||
/// ghost suggestion replace it wholesale, ⌃U empties it, completion filters
|
||||
/// on it — so the line has to be whole *before* those run, not stitched
|
||||
/// back together at submit time in front of whatever replaced it. Folding
|
||||
/// it in that late made `↑` then Enter run the recalled entry with the gap
|
||||
/// text glued to its front, and ⌃U then Enter bring back the text ⌃U had
|
||||
/// just cleared.
|
||||
///
|
||||
/// The `^U` stays owed until `flush_typeahead`, which keeps it where it has
|
||||
/// always been on the wire: immediately before the line. Sending it here
|
||||
/// instead would put it out before the shell's own editor is reading.
|
||||
fn adopt_typeahead(&mut self) {
|
||||
let pasted = self.typeahead.pasted();
|
||||
if let Some(seed) = self.typeahead.adopt() {
|
||||
self.prepend_into_editor(&seed, pasted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4096,7 +4121,7 @@ impl TerminalView {
|
||||
self.release_hold();
|
||||
}
|
||||
self.terminal.write(bytes);
|
||||
self.observe_typeahead(RawInput::Text(text));
|
||||
self.observe_gap_text(text, pasted);
|
||||
}
|
||||
|
||||
/// Move whatever the gap hold collected into the editor's buffer, keeping
|
||||
@@ -4112,17 +4137,36 @@ impl TerminalView {
|
||||
let Some(net) = self.hold.engage() else {
|
||||
return;
|
||||
};
|
||||
self.prepend_into_editor(&net, pasted);
|
||||
}
|
||||
|
||||
/// Put text in front of the editor's line that the editor did not receive
|
||||
/// through its own keys, keeping the provenance that decides how the line
|
||||
/// is submitted (#660). Both routes that do this — the gap hold and the
|
||||
/// typeahead record — carry a `pasted()` to read before they hand over.
|
||||
fn prepend_into_editor(&mut self, text: &str, pasted: bool) {
|
||||
if pasted {
|
||||
self.cmd.prepend_pasted(&net);
|
||||
self.cmd.prepend_pasted(text);
|
||||
} else {
|
||||
self.cmd.prepend_str(&net);
|
||||
self.cmd.prepend_str(text);
|
||||
}
|
||||
}
|
||||
|
||||
/// A gap's text on its way to the record, which replays it into the editor
|
||||
/// later — so a paste has to be recorded as one.
|
||||
fn observe_gap_text(&mut self, text: &str, pasted: bool) {
|
||||
self.observe_typeahead(if pasted {
|
||||
RawInput::Pasted(text)
|
||||
} else {
|
||||
RawInput::Text(text)
|
||||
});
|
||||
}
|
||||
|
||||
fn release_hold(&mut self) {
|
||||
let pasted = self.hold.pasted();
|
||||
if let Some((net, bytes)) = self.hold.release() {
|
||||
self.terminal.write(bytes);
|
||||
self.observe_typeahead(RawInput::Text(&net));
|
||||
self.observe_gap_text(&net, pasted);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4139,9 +4183,10 @@ impl TerminalView {
|
||||
let _ = self.hold.timeout(epoch);
|
||||
return;
|
||||
}
|
||||
let pasted = self.hold.pasted();
|
||||
if let Some((net, bytes)) = self.hold.timeout(epoch) {
|
||||
self.terminal.write(bytes);
|
||||
self.observe_typeahead(RawInput::Text(&net));
|
||||
self.observe_gap_text(&net, pasted);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
@@ -4192,6 +4237,16 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.engage_hold_into_editor();
|
||||
// The shell is still holding the recorded text on its own line, and the
|
||||
// ^U that erases it has not gone out yet: `at_prompt` comes back on the
|
||||
// `D` mark, before the prompt is even drawn, while the wipe waits for
|
||||
// `B`. The key that got here has folded the seed into the line already,
|
||||
// so this is usually just paying the wipe that left owed; where nothing
|
||||
// has, the drain still puts the seed back the way every other drain
|
||||
// does. Dropping it submitted only what was typed after the handover,
|
||||
// and an empty command when that was nothing, which is the blank line
|
||||
// #433 reports.
|
||||
self.flush_typeahead();
|
||||
let line = self.cmd.text();
|
||||
if !line.trim().is_empty() {
|
||||
let cwd = self.cwd();
|
||||
@@ -4227,7 +4282,6 @@ impl TerminalView {
|
||||
self.history_prefix.clear();
|
||||
self.close_completion();
|
||||
|
||||
self.wipe_pending_typeahead();
|
||||
let bracketed = self
|
||||
.terminal
|
||||
.term
|
||||
@@ -4463,13 +4517,18 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
self.engage_hold_into_editor();
|
||||
// Same reason as `submit_command`: what the record holds is on the
|
||||
// shell's own line, so it belongs in front of the line handed back.
|
||||
// The wipe waits until past the multi-line bail, which hands nothing
|
||||
// over and so must put nothing on the wire either.
|
||||
self.adopt_typeahead();
|
||||
let line = self.cmd.text();
|
||||
if line.contains('\n') {
|
||||
cx.notify();
|
||||
return;
|
||||
}
|
||||
self.close_completion();
|
||||
self.wipe_pending_typeahead();
|
||||
self.flush_typeahead();
|
||||
let tail = line.chars().count().saturating_sub(self.cmd.cursor());
|
||||
if !line.is_empty() {
|
||||
self.terminal.write(line.into_bytes());
|
||||
@@ -4944,6 +5003,7 @@ impl TerminalView {
|
||||
return;
|
||||
}
|
||||
if self.input_active() {
|
||||
self.adopt_typeahead();
|
||||
self.cmd.insert_str(text);
|
||||
self.history_nav = None;
|
||||
self.editor_goal_col = None;
|
||||
@@ -14207,3 +14267,350 @@ mod gpui_tests {
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// The window between the shell reporting a prompt and its line editor
|
||||
/// actually reading, which is where a fast typist's line goes missing (#433).
|
||||
///
|
||||
/// These drive a real `TerminalView` over a pane link on every platform, so
|
||||
/// they are not gated to unix the way `gpui_tests` is.
|
||||
#[cfg(test)]
|
||||
mod prompt_handover_tests {
|
||||
use super::*;
|
||||
use crate::daemon::protocol::{ClientMsg, DaemonMsg};
|
||||
use crate::daemon::transport::Stream;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
fn harness(cx: &mut TestAppContext) -> (gpui::WindowHandle<TerminalView>, Stream) {
|
||||
crate::core::config::pin_test_config_dir();
|
||||
cx.executor().allow_parking();
|
||||
let (client_side, daemon_side) = test_stream_pair();
|
||||
cx.update(|cx| {
|
||||
gpui_component::init(cx);
|
||||
cx.set_global(Config::default());
|
||||
});
|
||||
let window = cx.add_window(|window, cx| {
|
||||
let terminal = RemoteTerminal::from_stream(client_side, TermSize::new(80, 24))
|
||||
.expect("link-backed terminal");
|
||||
TerminalView::with_terminal(terminal, 1, window, cx)
|
||||
});
|
||||
(window, daemon_side)
|
||||
}
|
||||
|
||||
/// Everything the pane has written to the PTY, in order.
|
||||
fn drain(daemon: &mut Stream) -> Vec<u8> {
|
||||
daemon
|
||||
.set_read_timeout(Some(std::time::Duration::from_millis(150)))
|
||||
.unwrap();
|
||||
let mut out = Vec::new();
|
||||
loop {
|
||||
match ClientMsg::read(daemon) {
|
||||
Ok(ClientMsg::Input(bytes)) => out.extend_from_slice(&bytes),
|
||||
Ok(_) => continue,
|
||||
Err(e)
|
||||
if matches!(
|
||||
e.kind(),
|
||||
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
|
||||
) =>
|
||||
{
|
||||
break;
|
||||
}
|
||||
Err(e) => panic!("pane link failed: {e}"),
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn settle(
|
||||
cx: &mut TestAppContext,
|
||||
window: &gpui::WindowHandle<TerminalView>,
|
||||
what: &str,
|
||||
f: impl Fn(&TerminalView) -> bool,
|
||||
) {
|
||||
for _ in 0..300 {
|
||||
cx.run_until_parked();
|
||||
if window.update(cx, |view, _, _| f(view)).unwrap() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
}
|
||||
panic!("never settled: {what}");
|
||||
}
|
||||
|
||||
/// Printable text arrives the way the platform delivers it — through the
|
||||
/// text-input path, which is what the gap hold and the typeahead record see.
|
||||
fn type_text(window: &gpui::WindowHandle<TerminalView>, cx: &mut TestAppContext, text: &str) {
|
||||
for ch in text.chars() {
|
||||
window
|
||||
.update(cx, |view, _, cx| view.commit_text(&ch.to_string(), cx))
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn press(window: &gpui::WindowHandle<TerminalView>, cx: &mut TestAppContext, key: &str) {
|
||||
window
|
||||
.update(cx, |view, window, cx| {
|
||||
view.on_key_down(
|
||||
&KeyDownEvent {
|
||||
keystroke: gpui::Keystroke::parse(key).unwrap(),
|
||||
is_held: false,
|
||||
prefer_character_input: false,
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn prompt(daemon: &mut Stream, at_prompt: bool) {
|
||||
DaemonMsg::Prompt {
|
||||
active: true,
|
||||
at_prompt,
|
||||
last_exit: None,
|
||||
}
|
||||
.encode(daemon)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Types `text` into the gap of a running command and lets the hold window
|
||||
/// expire, so the bytes go to the PTY and are recorded for replay. Then
|
||||
/// puts the pane back at a prompt the way the `D` mark does — before the
|
||||
/// prompt is drawn, so the shell's line editor is not reading yet.
|
||||
fn typed_into_the_gap_then_handed_back(
|
||||
cx: &mut TestAppContext,
|
||||
window: &gpui::WindowHandle<TerminalView>,
|
||||
daemon: &mut Stream,
|
||||
text: &str,
|
||||
) {
|
||||
prompt(daemon, true);
|
||||
DaemonMsg::Output(b"\x1b]133;B\x07".to_vec())
|
||||
.encode(daemon)
|
||||
.unwrap();
|
||||
settle(cx, window, "the editor takes the first prompt", |view| {
|
||||
view.input_active() && view.terminal.zle_reading()
|
||||
});
|
||||
|
||||
prompt(daemon, false);
|
||||
DaemonMsg::Output(b"\x1b]133;C\x07".to_vec())
|
||||
.encode(daemon)
|
||||
.unwrap();
|
||||
settle(cx, window, "a command takes the pane", |view| {
|
||||
!view.input_active()
|
||||
});
|
||||
|
||||
type_text(window, cx, text);
|
||||
cx.executor().advance_clock(HOLD_WINDOW * 2);
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(daemon),
|
||||
text.as_bytes(),
|
||||
"the hold window gives up and dumps what it held"
|
||||
);
|
||||
|
||||
prompt(daemon, true);
|
||||
settle(cx, window, "the editor takes the prompt back", |view| {
|
||||
view.input_active()
|
||||
});
|
||||
assert!(
|
||||
!window
|
||||
.update(cx, |view, _, _| view.terminal.zle_reading())
|
||||
.unwrap(),
|
||||
"this is the D-to-B window: the shell is not reading its line yet"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_line_typed_into_the_gap_survives_a_prompt_that_is_not_reading_yet(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
typed_into_the_gap_then_handed_back(cx, &window, &mut daemon, "echo hi");
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15echo hi\r".to_vec(),
|
||||
"the line the shell is holding must be erased and submitted whole, \
|
||||
not erased and replaced by an empty command"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn a_prompt_handover_keeps_the_held_text_in_front_of_what_follows_it(cx: &mut TestAppContext) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
typed_into_the_gap_then_handed_back(cx, &window, &mut daemon, "echo");
|
||||
|
||||
// Typing carries straight on into the editor that just took the prompt.
|
||||
type_text(&window, cx, " hi");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
Vec::<u8>::new(),
|
||||
"the editor owns these keys, so none of them reach the PTY"
|
||||
);
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15echo hi\r".to_vec(),
|
||||
"what the shell was holding leads the line, not the tail alone"
|
||||
);
|
||||
}
|
||||
|
||||
/// The editor takes the held line over the moment it is touched, before it
|
||||
/// edits anything — and takes it over without putting the wipe on the wire,
|
||||
/// which is still the shell's line editor's to receive when it starts
|
||||
/// reading.
|
||||
#[gpui::test]
|
||||
fn the_editor_takes_the_held_line_over_before_it_edits_it(cx: &mut TestAppContext) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
typed_into_the_gap_then_handed_back(cx, &window, &mut daemon, "echo");
|
||||
|
||||
// `home` moves the caret and nothing else: any editor key is enough.
|
||||
press(&window, cx, "home");
|
||||
cx.run_until_parked();
|
||||
window
|
||||
.update(cx, |view, _, _| {
|
||||
assert_eq!(
|
||||
view.cmd.text(),
|
||||
"echo",
|
||||
"the line the shell is sitting on is the editor's line now"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
Vec::<u8>::new(),
|
||||
"taking the line over owes the wipe, it does not send it early"
|
||||
);
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15echo\r".to_vec(),
|
||||
"the owed wipe is paid on submit, still in front of the line"
|
||||
);
|
||||
}
|
||||
|
||||
/// The half of the window the seed alone does not cover: the editor is
|
||||
/// live, so the user can *replace* the line before submitting it. Recalling
|
||||
/// history and pressing Enter has to run the entry recalled — not that
|
||||
/// entry with the text the shell was holding glued to its front.
|
||||
#[gpui::test]
|
||||
fn recalling_history_in_the_gap_window_replaces_the_held_line(cx: &mut TestAppContext) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
typed_into_the_gap_then_handed_back(cx, &window, &mut daemon, "echo");
|
||||
window
|
||||
.update(cx, |view, _, _| {
|
||||
view.history.push("echo from history".to_string());
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
press(&window, cx, "up");
|
||||
cx.run_until_parked();
|
||||
window
|
||||
.update(cx, |view, _, _| {
|
||||
assert_eq!(
|
||||
view.cmd.text(),
|
||||
"echo from history",
|
||||
"the recall searches on the whole line, held text included"
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15echo from history\r".to_vec(),
|
||||
"the recalled entry runs on its own, with the held text replaced \
|
||||
rather than prefixed to it"
|
||||
);
|
||||
}
|
||||
|
||||
/// A paste that landed in the gap is still a paste after the handover. The
|
||||
/// record replays it into the editor, and a line that arrives there looking
|
||||
/// typed is submitted raw through the shell's binding table (#660) — the
|
||||
/// hole `GapHold::pasted` closed for the hold's own route.
|
||||
#[gpui::test]
|
||||
fn a_paste_held_in_the_gap_is_still_a_paste_after_the_handover(cx: &mut TestAppContext) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
// Bracketed paste is what a live prompt advertises; without it there is
|
||||
// no framing to lose in the first place.
|
||||
DaemonMsg::Output(b"\x1b[?2004h".to_vec())
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
settle(cx, &window, "the shell turns bracketed paste on", |view| {
|
||||
view.terminal
|
||||
.term
|
||||
.lock()
|
||||
.mode()
|
||||
.contains(TermMode::BRACKETED_PASTE)
|
||||
});
|
||||
|
||||
prompt(&mut daemon, true);
|
||||
DaemonMsg::Output(b"\x1b]133;B\x07".to_vec())
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
settle(cx, &window, "the editor takes the first prompt", |view| {
|
||||
view.input_active() && view.terminal.zle_reading()
|
||||
});
|
||||
prompt(&mut daemon, false);
|
||||
DaemonMsg::Output(b"\x1b]133;C\x07".to_vec())
|
||||
.encode(&mut daemon)
|
||||
.unwrap();
|
||||
settle(cx, &window, "a command takes the pane", |view| {
|
||||
!view.input_active()
|
||||
});
|
||||
|
||||
window
|
||||
.update(cx, |view, _, cx| view.paste("echo hi".to_string(), cx))
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(HOLD_WINDOW * 2);
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x1b[200~echo hi\x1b[201~".to_vec(),
|
||||
"the hold window gives up and dumps the paste as a paste"
|
||||
);
|
||||
|
||||
prompt(&mut daemon, true);
|
||||
settle(cx, &window, "the editor takes the prompt back", |view| {
|
||||
view.input_active()
|
||||
});
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15\x1b[200~echo hi\x1b[201~\r".to_vec(),
|
||||
"the replayed line keeps its framing instead of being typed at the \
|
||||
shell's binding table"
|
||||
);
|
||||
}
|
||||
|
||||
/// The same for an emptied line: ⌃U clears what the editor is holding, and
|
||||
/// the shell's copy of it goes too instead of coming back at submit.
|
||||
#[gpui::test]
|
||||
fn clearing_the_line_in_the_gap_window_clears_the_held_text_too(cx: &mut TestAppContext) {
|
||||
let (window, mut daemon) = harness(cx);
|
||||
typed_into_the_gap_then_handed_back(cx, &window, &mut daemon, "echo");
|
||||
|
||||
press(&window, cx, "ctrl-u");
|
||||
cx.run_until_parked();
|
||||
window
|
||||
.update(cx, |view, _, _| assert_eq!(view.cmd.text(), ""))
|
||||
.unwrap();
|
||||
|
||||
press(&window, cx, "enter");
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
drain(&mut daemon),
|
||||
b"\x15\r".to_vec(),
|
||||
"an emptied line submits empty: the wipe is still owed, the seed is not"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user