Skip to content

fix(vi): enforce cursor invariant in normal mode#1069

Open
sim590 wants to merge 7 commits into
nushell:mainfrom
sim590:cursor-clamp
Open

fix(vi): enforce cursor invariant in normal mode#1069
sim590 wants to merge 7 commits into
nushell:mainfrom
sim590:cursor-clamp

Conversation

@sim590
Copy link
Copy Markdown

@sim590 sim590 commented May 8, 2026

Summary

In Vi normal mode, the cursor sits on a character and must not go past the last grapheme. This was not enforced, causing the cursor to land one position past the end of the line after various operations (movements, deletions, paste, undo, etc.).

This PR implements the approach discussed in #1066 (Approach C1): the Vi mode owns its cursor invariant and emits EditCommand::ClampCursorToNormalMode after each editing command that stays in normal mode. LineBuffer remains fully mode-agnostic.

Changes

  • Add EditCommand::ClampCursorToNormalMode, implemented in Editor::clamp_cursor()
  • Emit ClampCursorToNormalMode after every Vi normal mode editing command in command.rs (to_reedline and to_reedline_with_motion)
  • Add clamp_cursor() guards on rightward movement methods in Editor (move_right, move_to_end, move_to_line_end, move_word_right*, move_big_word_right*)
  • Guard set_buffer so that history load, completion accept, and paste also respect the invariant
  • Move cursor left on Escape (insert to normal transition), matching Vim behavior
  • Propagate edit mode to Editor on Escape event so the cursor invariant is enforced immediately
  • Adjust is_cursor_at_buffer_end() to treat "cursor on last grapheme" as "at buffer end" in Vi normal mode, preserving prefix history search and inline hints

Test plan

  • 868 tests pass, clippy clean
  • New tests verify that normal mode commands (x, p, P, u, dd, D, ~, r, dw, db, d$, yw) emit ClampCursorToNormalMode
  • New tests verify that insert-transitioning commands (i, a, cc, cw, s) do not emit it
  • Existing parser tests updated to expect the new command in their output

Closes #694 (cursor past end in normal mode)
Relates to #1066

sim590 added 6 commits May 4, 2026 17:16
In Vi normal mode the cursor sits *on* a character and must not go
past the last grapheme. Add a clamp_cursor() method to Editor that
enforces this invariant whenever the edit mode is Vi normal.

Call clamp_cursor() after every EditCommand in run_edit_command(),
and in set_buffer(), move_to_end() and move_to_line_end() so that
history loading and end-of-line motions always respect the bound.

Also adjust is_cursor_at_buffer_end() to treat "cursor on the last
grapheme" as equivalent to "at buffer end" in Vi normal mode, so
that prefix history search and inline hints continue to work.

Fixes nushell#694 (pt 2), nushell#788 (pt 4)
In Vi, insert mode places the cursor between characters while normal
mode places it on a character. Pressing Escape to leave insert mode
must move the cursor one position left to match standard Vi behavior.

When Escape is pressed from normal mode the cursor is not moved.

Fixes nushell#694 (pt 1), nushell#788 (pt 1)
ReedlineEvent::Esc is handled outside of run_edit_commands(), so the
editor's edit_mode was never updated on mode transitions triggered by
Escape. Sync the editor's mode and clamp the cursor at that point so
that the cursor invariant is enforced immediately.
Add EditCommand::ClampCursorToNormalMode, implemented in Editor as a
call to clamp_cursor(). The Vi normal mode emits this command after
every editing operation (delete, yank, paste, undo, replace) via
command.rs and to_reedline_with_motion.

Add clamp_cursor() guards to move_right, move_word_right* and
move_big_word_right* in Editor so that pure motion commands also
respect the Vi normal mode cursor invariant.

LineBuffer remains fully mode-agnostic. The Vi mode owns its own
cursor invariant at the dispatch boundary, per the design discussed
in nushell#1066.
All Vi normal mode editing commands now emit ClampCursorToNormalMode
after their edit command. Update the expected ReedlineEvent values in
test_reedline_move, test_reedline_memory_move and
test_reedline_move_in_visual_mode accordingly.
Add tests verifying that Vi normal mode commands emit
ClampCursorToNormalMode when they should (x, p, P, u, dd, D, ~, r,
dw, db, d$, yw) and do not emit it when they transition to insert
mode (i, a, cc, cw, s).
False positive from the multibyte cursor clamping test that uses
the string "café" and asserts against "caf".len().
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

vi mode cursor position isn't vi-like

1 participant