From 294d0baef9dadd8122066802a1fe1954a54e3fbd Mon Sep 17 00:00:00 2001 From: kronberger-droid Date: Wed, 19 Aug 2026 11:17:59 +0200 Subject: [PATCH 1/2] test(engine): pin the history walk across a recalled multi-line entry Up recalls a two-line entry, Down moves to its second line, and the next Down should continue to the newer entry (the empty draft). The Up mirror walks back to the older entry from the first line. Both fail today: the in-entry line move ends history traversal, so the edge press starts a fresh cursor from the recalled text and finds nothing past it. --- src/engine.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/engine.rs b/src/engine.rs index f66b69bf..7c1dd58a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2855,6 +2855,57 @@ mod tests { assert_eq!(reedline.current_buffer_contents(), multiline_command); } + // --- history walk across a recalled multi-line entry (#1109 regression) --- + + /// History holds `older` then `one\ntwo` (newest). + fn two_entry_history_engine() -> Reedline { + let mut rl = seam_engine(Box::::default()); + for cmd in ["older", "one\ntwo"] { + rl.history + .save(HistoryItem::from_command_line(cmd)) + .expect("save history"); + } + rl + } + + #[test] + fn down_inside_recalled_multiline_entry_keeps_walking_forward() { + let mut rl = two_entry_history_engine(); + drive(&mut rl, &[key(KeyCode::Up)]); + assert_eq!(rl.editor.get_buffer(), "one\ntwo", "setup"); + assert_eq!(rl.editor.insertion_point(), 3, "setup: end of line 1"); + + drive(&mut rl, &[key(KeyCode::Down)]); + assert_eq!(rl.editor.get_buffer(), "one\ntwo", "moves to line 2 first"); + assert!(rl.editor.insertion_point() > 3, "setup: on line 2"); + + drive(&mut rl, &[key(KeyCode::Down)]); + assert_eq!( + rl.editor.get_buffer(), + "", + "from the last line, Down walks forward to the empty draft" + ); + } + + #[test] + fn up_inside_recalled_multiline_entry_keeps_walking_back() { + let mut rl = two_entry_history_engine(); + drive(&mut rl, &[key(KeyCode::Up), key(KeyCode::Down)]); + assert_eq!(rl.editor.get_buffer(), "one\ntwo", "setup"); + assert!(rl.editor.insertion_point() > 3, "setup: on line 2"); + + drive(&mut rl, &[key(KeyCode::Up)]); + assert_eq!(rl.editor.get_buffer(), "one\ntwo", "moves to line 1 first"); + assert!(rl.editor.insertion_point() <= 3, "setup: on line 1"); + + drive(&mut rl, &[key(KeyCode::Up)]); + assert_eq!( + rl.editor.get_buffer(), + "older", + "from the first line, Up walks back to the older entry" + ); + } + #[test] fn thread_safe() { fn f(_: S) {} From bf1b2037efdbd772470491d590aed17afada9022 Mon Sep 17 00:00:00 2001 From: kronberger-droid Date: Wed, 19 Aug 2026 11:18:37 +0200 Subject: [PATCH 2/2] fix(engine): keep history traversal alive while moving inside a recalled entry `up_command`/`down_command` move between the lines of a multi-line entry before they reach for history, and since #1109 they do so through `run_edit_commands` so the cursor settles under the mode's rest policy. That call also flips `InputMode::HistoryTraversal` back to `Regular`, so the next press at the buffer edge built a fresh `HistoryCursor` from the recalled text and found nothing past it: Up, Down, Down was stuck on the two-line entry instead of returning to the draft. Split the body into `apply_edit_commands`, which runs the commands under the rest policy without touching the input mode, and use that for the two in-entry line moves. `run_edit_commands` keeps its public contract of ending traversal on any edit. --- src/engine.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 7c1dd58a..c6a0d7fc 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -2014,7 +2014,12 @@ impl Reedline { if self.input_mode == InputMode::HistoryTraversal { self.input_mode = InputMode::Regular; } + self.apply_edit_commands(commands); + } + /// [`run_edit_commands`](Self::run_edit_commands) without ending history + /// traversal, for the engine's own line moves inside a recalled entry. + fn apply_edit_commands(&mut self, commands: &[EditCommand]) { // Adopt the current edit mode's rest policy so these commands resolve // under it (e.g. block-caret selection geometry) — but *without* // committing the cursor first. A commit here would apply the policy's @@ -2036,10 +2041,10 @@ impl Reedline { // If we're at the top, move to previous history self.previous_history(); } else { - // Through `run_edit_commands` so the cursor settles under the mode's + // Through `apply_edit_commands` so the cursor settles under the mode's // rest policy — a bare `editor.move_line_up` skips the commit boundary, // leaving a vi-normal caret past the last grapheme on a short line. - self.run_edit_commands(&[EditCommand::MoveLineUp { select: false }]); + self.apply_edit_commands(&[EditCommand::MoveLineUp { select: false }]); } } @@ -2050,7 +2055,7 @@ impl Reedline { self.next_history(); } else { // See `up_command`: settle under the rest policy via the commit boundary. - self.run_edit_commands(&[EditCommand::MoveLineDown { select: false }]); + self.apply_edit_commands(&[EditCommand::MoveLineDown { select: false }]); } }