vim: Fix cursor jumping past empty lines with inlay hints in visual mode (#35757)

**Summary**

Fixes #29134 - Visual mode cursor incorrectly jumps past empty lines
that contain inlay hints (type hints).

**Problem**

When in VIM visual mode, pressing j to move down from a longer line to
an empty line that contains an inlay hint would cause the cursor to skip
the empty line entirely and jump to the next line. This only occurred
when moving down (not up) and only in visual mode.

**Root Cause**

The issue was introduced by commit f9ee28db5e which added bias-based
navigation for handling multi-line inlay hints. When using Bias::Right
while moving down, the clipping logic would place the cursor past the
inlay hint, causing it to jump to the next line.

**Solution**
Added logic in up_down_buffer_rows to detect when clipping would place
the cursor within an inlay hint position. When detected, it uses the
buffer column position instead of the display column to avoid jumping
past the hint.

**Testing**

- Added comprehensive test case
test_visual_mode_with_inlay_hints_on_empty_line that reproduces the
exact scenario
- Manually verified the fix with the reproduction case from the issue
- All 356 tests pass with `cargo test -p vim`

**Release Notes:**
- Fixed VIM visual mode cursor jumping past empty lines with type hints
when navigating down
This commit is contained in:
Adam Mulvany 2025-08-22 13:20:22 +10:00 committed by GitHub
parent f5fd4ac670
commit 852439452c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1610,10 +1610,20 @@ fn up_down_buffer_rows(
map.line_len(begin_folded_line.row())
};
(
map.clip_point(DisplayPoint::new(begin_folded_line.row(), new_col), bias),
goal,
)
let point = DisplayPoint::new(begin_folded_line.row(), new_col);
let mut clipped_point = map.clip_point(point, bias);
// When navigating vertically in vim mode with inlay hints present,
// we need to handle the case where clipping moves us to a different row.
// This can happen when moving down (Bias::Right) and hitting an inlay hint.
// Re-clip with opposite bias to stay on the intended line.
//
// See: https://github.com/zed-industries/zed/issues/29134
if clipped_point.row() > point.row() {
clipped_point = map.clip_point(point, Bias::Left);
}
(clipped_point, goal)
}
fn down_display(
@ -3842,6 +3852,84 @@ mod test {
);
}
#[gpui::test]
async fn test_visual_mode_with_inlay_hints_on_empty_line(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
// Test the exact scenario from issue #29134
cx.set_state(
indoc! {"
fn main() {
let this_is_a_long_name = Vec::<u32>::new();
let new_oneˇ = this_is_a_long_name
.iter()
.map(|i| i + 1)
.map(|i| i * 2)
.collect::<Vec<_>>();
}
"},
Mode::Normal,
);
// Add type hint inlay on the empty line (line 3, after "this_is_a_long_name")
cx.update_editor(|editor, _window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
// The empty line is at line 3 (0-indexed)
let line_start = snapshot.anchor_after(Point::new(3, 0));
let inlay_text = ": Vec<u32>";
let inlay = Inlay::edit_prediction(1, line_start, inlay_text);
editor.splice_inlays(&[], vec![inlay], cx);
});
// Enter visual mode
cx.simulate_keystrokes("v");
cx.assert_state(
indoc! {"
fn main() {
let this_is_a_long_name = Vec::<u32>::new();
let new_one« ˇ»= this_is_a_long_name
.iter()
.map(|i| i + 1)
.map(|i| i * 2)
.collect::<Vec<_>>();
}
"},
Mode::Visual,
);
// Move down - should go to the beginning of line 4, not skip to line 5
cx.simulate_keystrokes("j");
cx.assert_state(
indoc! {"
fn main() {
let this_is_a_long_name = Vec::<u32>::new();
let new_one« = this_is_a_long_name
ˇ» .iter()
.map(|i| i + 1)
.map(|i| i * 2)
.collect::<Vec<_>>();
}
"},
Mode::Visual,
);
// Test with multiple movements
cx.set_state("let aˇ = 1;\nlet b = 2;\n\nlet c = 3;", Mode::Normal);
// Add type hint on the empty line
cx.update_editor(|editor, _window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let empty_line_start = snapshot.anchor_after(Point::new(2, 0));
let inlay_text = ": i32";
let inlay = Inlay::edit_prediction(2, empty_line_start, inlay_text);
editor.splice_inlays(&[], vec![inlay], cx);
});
// Enter visual mode and move down twice
cx.simulate_keystrokes("v j j");
cx.assert_state("let a« = 1;\nlet b = 2;\n\nˇ»let c = 3;", Mode::Visual);
}
#[gpui::test]
async fn test_go_to_percentage(cx: &mut gpui::TestAppContext) {
let mut cx = NeovimBackedTestContext::new(cx).await;