fix(terminal): adopt the held line at the editor's own doors, not in render

The previous commit folded the seed in from `render`, which never runs in
these tests — all four handover tests failed on `never settled`, and in
the app a keystroke that beat the frame would have been edited against a
line that was still missing its front.

`handle_editor_key` and `commit_text` are the two doors into the editor,
and both now open with `adopt_typeahead`, so the line is whole before
anything reads or rewrites it. `render` goes back to what it was.
This commit is contained in:
l0ng-ai
2026-09-07 22:31:46 +08:00
parent 15f020c19e
commit bc9b1ec94a
+54 -23
View File
@@ -2256,6 +2256,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") {
@@ -3945,16 +3946,22 @@ impl TerminalView {
}
}
/// Take the record into the editor without paying the wipe yet.
/// 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. The `^U`
/// stays owed until `flush_typeahead`, which keeps it where it has always
/// been on the wire: immediately before the line.
/// 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) {
if let Some(seed) = self.typeahead.adopt() {
self.cmd.prepend_str(&seed);
@@ -4072,12 +4079,12 @@ impl TerminalView {
// 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`. `adopt_typeahead` has normally already folded the seed into the
// line by now, and this pays the wipe it left owed; on the frame where
// it has not, the drain here 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.
// `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() {
@@ -4836,6 +4843,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;
@@ -6360,8 +6368,6 @@ impl Render for TerminalView {
}
if self.terminal.zle_reading() {
self.flush_typeahead();
} else {
self.adopt_typeahead();
}
}
let entity = cx.entity();
@@ -14054,17 +14060,6 @@ mod prompt_handover_tests {
.unwrap(),
"this is the D-to-B window: the shell is not reading its line yet"
);
settle(
cx,
window,
"the editor adopts the line the shell is holding",
|view| view.cmd.text() == text,
);
assert_eq!(
drain(daemon),
Vec::<u8>::new(),
"adopting the line owes the wipe, it does not send it early"
);
}
#[gpui::test]
@@ -14107,6 +14102,42 @@ mod prompt_handover_tests {
);
}
/// 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