From 2759f541da451bd297ead1c1719126a807d336d1 Mon Sep 17 00:00:00 2001 From: Dino Date: Tue, 23 Sep 2025 16:39:12 +0100 Subject: [PATCH] vim: Fix cursor position being set to end of line in normal mode (#38161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address an issue where, in Vim mode, clicking past the end of a line after selecting the entire line would place the cursor on the newline character instead of the last character of the line, which is inconsistent with Vim's normal mode expectations. I believe the root cause was that the cursor’s position was updated to the end of the line before the mode switch from Visual to Normal, at which point `DisplayMap.clip_at_line_ends` was still set to `false`. As a result, the cursor could end up in an invalid position for Normal mode. The fix ensures that when switching between these two modes, and if the selection is empty, the selection point is properly clipped, preventing the cursor from being placed past the end of the line. Related #38049 Release Notes: - Fixed issue in Vim mode where switching from any mode to normal mode could end up with the cursor in the newline character --------- Co-authored-by: Conrad Irwin --- crates/editor/src/editor.rs | 4 +++ crates/vim/src/test.rs | 49 +++++++++++++++++++++++++++++++++++++ crates/vim/src/vim.rs | 4 ++- 3 files changed, 56 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1d6ac4bcca2c1dfb9e34b461965adc97e4a6b6eb..eec6e367dc597eb61a37ad33e116fd6688ba7c66 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2518,6 +2518,10 @@ impl Editor { key_context } + pub fn last_bounds(&self) -> Option<&Bounds> { + self.last_bounds.as_ref() + } + fn show_mouse_cursor(&mut self, cx: &mut Context) { if self.mouse_cursor_hidden { self.mouse_cursor_hidden = false; diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 03adfc8af15cf92f7ee6c4c857c0f154e2c969f3..867b0829dde3eef418049f2bb4eec2985fabcfad 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -2213,3 +2213,52 @@ async fn test_multi_cursor_replay(cx: &mut gpui::TestAppContext) { Mode::Normal, ); } + +#[gpui::test] +async fn test_clipping_on_mode_change(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state( + indoc! { + " + ˇverylongline + andsomelinebelow + " + }, + Mode::Normal, + ); + + cx.simulate_keystrokes("v e"); + cx.assert_state( + indoc! { + " + «verylonglineˇ» + andsomelinebelow + " + }, + Mode::Visual, + ); + + let mut pixel_position = cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let current_head = editor.selections.newest_display(cx).end; + editor.last_bounds().unwrap().origin + + editor + .display_to_pixel_point(current_head, &snapshot, window) + .unwrap() + }); + pixel_position.x += px(100.); + // click beyond end of the line + cx.simulate_click(pixel_position, Modifiers::default()); + cx.run_until_parked(); + + cx.assert_state( + indoc! { + " + verylonglinˇe + andsomelinebelow + " + }, + Mode::Normal, + ); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c7fb8ffa35ea090296f137b11f08379db968ce3d..1c2ea0dccbc6e84d7a871f7a4d30fb6c6949044e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1092,6 +1092,8 @@ impl Vim { let mut point = selection.head(); if !selection.reversed && !selection.is_empty() { point = movement::left(map, selection.head()); + } else if selection.is_empty() { + point = map.clip_point(point, Bias::Left); } selection.collapse_to(point, selection.goal) } else if !last_mode.is_visual() && mode.is_visual() && selection.is_empty() { @@ -1608,7 +1610,7 @@ impl Vim { && !is_multicursor && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&self.mode) { - self.switch_mode(Mode::Normal, true, window, cx); + self.switch_mode(Mode::Normal, false, window, cx); } }