Problem
Reedline's LineBuffer::insertion_point is a plain byte offset with no mode-aware bounds checking. In Vi, the two modes have fundamentally different cursor semantics:
- Insert mode — cursor is between characters; valid range is
0..=len()
- Normal mode — cursor is on a character; valid range is
0..last_grapheme_start
Reedline makes no such distinction. The cursor can always reach len() regardless of mode, causing a cascade of off-by-one bugs.
Affected issues
All of the following share this single root cause:
Affected code paths
Every function that can place the cursor at len() is incorrect when in Vi normal mode:
| Function |
File |
Problem |
set_insertion_point |
line_buffer.rs:71 |
No bounds check |
move_to_end / move_to_line_end |
line_buffer.rs:132-139 |
Always goes to len() |
move_right |
line_buffer.rs:453 |
l can move past last character |
set_buffer |
line_buffer.rs:81 |
After history load, cursor lands at len() |
All *_word_right_* |
line_buffer.rs |
Fallback to len() instead of last grapheme |
previous_history |
engine.rs:1548 |
Calls move_to_end() after loading history |
Proposed approaches
Approach A — Centralized clamping (less invasive)
Add a clamp_cursor_for_vi_normal() method in Editor and call it from run_edit_command() whenever the mode is Vi normal, and after history navigation / buffer changes in the engine.
fn clamp_cursor_for_vi_normal(&mut self) {
let len = self.get_buffer().len();
if !self.get_buffer().is_empty() && self.insertion_point() >= len {
let last = self.line_buffer.grapheme_left_index_from_pos(len);
self.line_buffer.set_insertion_point(last);
}
}
Pros: minimal API changes, single enforcement point, fixes all symptoms at once
Cons: requires careful placement at every entry point (history load, buffer set, etc.) — risk of missing a spot
Approach B — Mode-aware movement functions (more invasive)
Pass the current PromptEditMode to each movement function so they bound themselves:
pub fn move_right(&mut self, mode: PromptEditMode) {
let next = self.grapheme_right_index();
let limit = match mode {
PromptEditMode::Vi(PromptViMode::Normal) =>
self.grapheme_left_index_from_pos(self.lines.len()),
_ => self.lines.len(),
};
self.insertion_point = next.min(limit);
}
Pros: impossible to miss a case — correctness is enforced at the source
Cons: large API change across LineBuffer, Editor, and all callers
Question for maintainers
@blindFS @ysthakur @fdncred — which approach aligns better with the direction you have in mind for reedline? Or is there a third path you'd prefer? We'd like to implement this properly rather than continuing with piecemeal fixes.
Problem
Reedline's
LineBuffer::insertion_pointis a plain byte offset with no mode-aware bounds checking. In Vi, the two modes have fundamentally different cursor semantics:0..=len()0..last_grapheme_startReedline makes no such distinction. The cursor can always reach
len()regardless of mode, causing a cascade of off-by-one bugs.Affected issues
All of the following share this single root cause:
Move(Big)WordRightEndis off-by-1 in insert mode #766 —MoveWordRightEndis off-by-1 in insert modeAffected code paths
Every function that can place the cursor at
len()is incorrect when in Vi normal mode:set_insertion_pointline_buffer.rs:71move_to_end/move_to_line_endline_buffer.rs:132-139len()move_rightline_buffer.rs:453lcan move past last characterset_bufferline_buffer.rs:81len()*_word_right_*line_buffer.rslen()instead of last graphemeprevious_historyengine.rs:1548move_to_end()after loading historyProposed approaches
Approach A — Centralized clamping (less invasive)
Add a
clamp_cursor_for_vi_normal()method inEditorand call it fromrun_edit_command()whenever the mode is Vi normal, and after history navigation / buffer changes in the engine.Pros: minimal API changes, single enforcement point, fixes all symptoms at once
Cons: requires careful placement at every entry point (history load, buffer set, etc.) — risk of missing a spot
Approach B — Mode-aware movement functions (more invasive)
Pass the current
PromptEditModeto each movement function so they bound themselves:Pros: impossible to miss a case — correctness is enforced at the source
Cons: large API change across
LineBuffer,Editor, and all callersQuestion for maintainers
@blindFS @ysthakur @fdncred — which approach aligns better with the direction you have in mind for reedline? Or is there a third path you'd prefer? We'd like to implement this properly rather than continuing with piecemeal fixes.