[BUG] Vim mode: "o" and "O" insert new lines at end of text instead of at cursor position
Description
In vim mode, pressing o (open line below) and O (open line above) inserts a new line at the wrong position — typically at the end of the text — instead of relative to the current cursor line.
Steps to reproduce
- Enable vim mode in Claude Code
- Type a multi-line message (e.g., three lines of text)
- Press
Escto enter normal mode - Move the cursor to the first or middle line
- Press
oorO
Expected behavior
oshould insert a new line below the current cursor line and enter insert modeOshould insert a new line above the current cursor line and enter insert mode
Actual behavior
Both o and O insert a new line at the wrong position (typically the end of the text), regardless of where the cursor is.
Root cause (from source analysis)
The openLine handler (minified as otH) splits the text by \n to get logical lines, but uses cursor.getPosition().line to index into them — which returns a wrapped (visual) line index, not a logical line index.
When text wraps (i.e., a line is longer than the terminal width), the visual line number is larger than the logical line number, causing the splice to happen at the wrong index — often past the end of the logical lines array.
Relevant code (deminified):
function openLine(direction, ctx) {
let lines = ctx.text.split('\n'); // logical lines
let { line } = ctx.cursor.getPosition(); // BUG: returns wrapped/visual line index
let insertAt = direction === "below" ? line + 1 : line;
let newLines = [...lines.slice(0, insertAt), "", ...lines.slice(insertAt)];
ctx.setText(newLines.join('\n'));
ctx.enterInsert(calcOffset(newLines, insertAt));
}
getPosition() delegates to measuredText.getPositionFromOffset(), which iterates over this.wrappedLines and returns the visual line index. The fix should convert the visual line to a logical line before using it to index into the split('\n') array.
Other cursor operations (like up/down/startOfLine) correctly work with wrapped lines since they stay within the cursor's coordinate system. openLine is the one that crosses the boundary by mixing visual coordinates with logical text manipulation.
Suggested fix
Compute the logical line from the cursor's raw offset instead of using the visual line:
function openLine(direction, ctx) {
let lines = ctx.text.split('\n');
// Use offset to derive the logical line index
let logicalLine = ctx.text.substring(0, ctx.cursor.offset).split('\n').length - 1;
let insertAt = direction === "below" ? logicalLine + 1 : logicalLine;
let newLines = [...lines.slice(0, insertAt), "", ...lines.slice(insertAt)];
ctx.setText(newLines.join('\n'));
ctx.enterInsert(calcOffset(newLines, insertAt));
}
Alternatively, the cursor class already has findLogicalLineStart() / findLogicalLineEnd() methods that could be used to stay consistent with the existing codebase.
Possibly related
- #17788 —
acommand at end of line incorrectly creates newline (likely same root cause)
Environment
- Claude Code v2.1.80
- Linux (bash)
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗