Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion src/core_editor/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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());
}
}
44 changes: 42 additions & 2 deletions src/edit_mode/vi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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])
Expand Down
Loading