diff --git a/src/core_editor/editor.rs b/src/core_editor/editor.rs index 0d7673ba..feb64542 100644 --- a/src/core_editor/editor.rs +++ b/src/core_editor/editor.rs @@ -306,7 +306,16 @@ impl Editor { } pub(crate) fn is_cursor_at_buffer_end(&self) -> bool { - self.line_buffer.insertion_point() == self.get_buffer().len() + let pos = self.line_buffer.insertion_point(); + let len = self.get_buffer().len(); + if pos == len { + return true; + } + // In Vi normal mode the cursor sits *on* the last character rather + // than after it. Treat that position as "at buffer end" so that + // prefix history search still triggers after pressing Escape. + matches!(self.edit_mode, PromptEditMode::Vi(PromptViMode::Normal)) + && self.line_buffer.grapheme_right_index() == len } pub(crate) fn reset_undo_stack(&mut self) { @@ -2168,4 +2177,39 @@ mod test { assert_eq!(bracket_result, expected_bracket); assert_eq!(quote_result, expected_quote); } + + #[test] + fn test_is_cursor_at_buffer_end_vi_normal_mode() { + // In Vi normal mode, the cursor on the last character should count + // as "at buffer end" for prefix history search purposes. + let mut editor = editor_with("ls"); + editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Normal)); + + // Cursor on last char 's' (position 1) + editor.line_buffer.set_insertion_point(1); + assert!(editor.is_cursor_at_buffer_end()); + + // Cursor on first char 'l' (position 0) — not at end + editor.line_buffer.set_insertion_point(0); + assert!(!editor.is_cursor_at_buffer_end()); + + // Cursor after last char (position 2) — still at end + editor.line_buffer.set_insertion_point(2); + assert!(editor.is_cursor_at_buffer_end()); + } + + #[test] + fn test_is_cursor_at_buffer_end_insert_mode() { + // In insert mode, only cursor after the last character counts. + let mut editor = editor_with("ls"); + editor.set_edit_mode(PromptEditMode::Vi(PromptViMode::Insert)); + + // Cursor on last char 's' (position 1) — NOT at end in insert mode + editor.line_buffer.set_insertion_point(1); + assert!(!editor.is_cursor_at_buffer_end()); + + // Cursor after last char (position 2) — at end + editor.line_buffer.set_insertion_point(2); + assert!(editor.is_cursor_at_buffer_end()); + } } diff --git a/src/edit_mode/vi/mod.rs b/src/edit_mode/vi/mod.rs index 721e45d9..216a1fc3 100644 --- a/src/edit_mode/vi/mod.rs +++ b/src/edit_mode/vi/mod.rs @@ -156,8 +156,20 @@ impl EditMode for Vi { } (_, KeyModifiers::NONE, KeyCode::Esc) => { self.cache.clear(); + let was_insert = self.mode == ViMode::Insert; self.mode = ViMode::Normal; - ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) + // In Vi, exiting insert mode moves the cursor one position + // left because insert mode places the cursor between + // characters while normal mode places it on a character. + if was_insert { + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveLeft { select: false }]), + ReedlineEvent::Esc, + ReedlineEvent::Repaint, + ]) + } else { + ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint]) + } } (ViMode::Normal | ViMode::Visual, _, _) => self .normal_keybindings @@ -231,13 +243,41 @@ mod test { use pretty_assertions::assert_eq; #[test] - fn esc_leads_to_normal_mode_test() { + fn esc_from_insert_mode_moves_cursor_left() { let mut vi = Vi::default(); + assert!(matches!(vi.mode, ViMode::Insert)); + + let esc = + ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))) + .unwrap(); + let result = vi.parse_event(esc); + + // Exiting insert mode should move the cursor left (Vi standard + // behavior: insert mode cursor is between characters, normal mode + // cursor is on a character). + assert_eq!( + result, + ReedlineEvent::Multiple(vec![ + ReedlineEvent::Edit(vec![EditCommand::MoveLeft { select: false }]), + ReedlineEvent::Esc, + ReedlineEvent::Repaint, + ]) + ); + assert!(matches!(vi.mode, ViMode::Normal)); + } + + #[test] + fn esc_from_normal_mode_does_not_move_cursor() { + let mut vi = Vi { + mode: ViMode::Normal, + ..Default::default() + }; let esc = ReedlineRawEvent::try_from(Event::Key(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE))) .unwrap(); let result = vi.parse_event(esc); + // Esc from normal mode should NOT move the cursor left. assert_eq!( result, ReedlineEvent::Multiple(vec![ReedlineEvent::Esc, ReedlineEvent::Repaint])