Fix vim mouse selections

Conrad Irwin created

Closes #27720

Change summary

crates/vim/src/test.rs | 33 +++++++++++++++++++++++++++++++
crates/vim/src/vim.rs  | 46 ++++++++++++++++++++++++++++++++-----------
2 files changed, 67 insertions(+), 12 deletions(-)

Detailed changes

crates/vim/src/test.rs 🔗

@@ -1303,6 +1303,39 @@ async fn test_mouse_selection(cx: &mut TestAppContext) {
     cx.assert_state("one «ˇtwo» three", Mode::Visual)
 }
 
+#[gpui::test]
+async fn test_mouse_drag_across_anchor_does_not_drift(cx: &mut TestAppContext) {
+    let mut cx = VimTestContext::new(cx, true).await;
+
+    cx.set_state("ˇone two three four", Mode::Normal);
+
+    let click_pos = cx.pixel_position("one ˇtwo three four");
+    let drag_left = cx.pixel_position("ˇone two three four");
+    let anchor_pos = cx.pixel_position("one tˇwo three four");
+
+    cx.simulate_mouse_down(click_pos, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+
+    cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+    cx.assert_state("«ˇone t»wo three four", Mode::Visual);
+
+    cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+
+    cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+    cx.assert_state("«ˇone t»wo three four", Mode::Visual);
+
+    cx.simulate_mouse_move(anchor_pos, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+    cx.simulate_mouse_move(drag_left, MouseButton::Left, Modifiers::none());
+    cx.run_until_parked();
+    cx.assert_state("«ˇone t»wo three four", Mode::Visual);
+
+    cx.simulate_mouse_up(drag_left, MouseButton::Left, Modifiers::none());
+}
+
 #[perf]
 #[gpui::test]
 async fn test_lowercase_marks(cx: &mut TestAppContext) {

crates/vim/src/vim.rs 🔗

@@ -23,8 +23,9 @@ use crate::normal::paste::Paste as VimPaste;
 use collections::HashMap;
 use editor::{
     Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset,
-    SelectionEffects, ToPoint,
+    SelectionEffects,
     actions::Paste,
+    display_map::ToDisplayPoint,
     movement::{self, FindRange},
 };
 use gpui::{
@@ -37,6 +38,8 @@ use language::{
 };
 pub use mode_indicator::ModeIndicator;
 use motion::Motion;
+use multi_buffer::ToPoint as _;
+use multi_buffer::ToPoint as _;
 use normal::search::SearchSubmit;
 use object::Object;
 use schemars::JsonSchema;
@@ -503,6 +506,7 @@ pub(crate) struct Vim {
     pub(crate) current_anchor: Option<Selection<Anchor>>,
     pub(crate) undo_modes: HashMap<TransactionId, Mode>,
     pub(crate) undo_last_line_tx: Option<TransactionId>,
+    extended_pending_selection_id: Option<usize>,
 
     selected_register: Option<char>,
     pub search: SearchState,
@@ -561,6 +565,7 @@ impl Vim {
             current_tx: None,
             undo_last_line_tx: None,
             current_anchor: None,
+            extended_pending_selection_id: None,
             undo_modes: HashMap::default(),
 
             status_label: None,
@@ -1218,17 +1223,32 @@ impl Vim {
                     s.select_anchor_ranges(vec![pos..pos])
                 }
 
-                let snapshot = s.display_snapshot();
-                if let Some(pending) = s.pending_anchor_mut()
-                    && pending.reversed
+                let mut should_extend_pending = false;
+                if !last_mode.is_visual()
                     && mode.is_visual()
-                    && !last_mode.is_visual()
+                    && let Some(pending) = s.pending_anchor()
                 {
-                    let mut end = pending.end.to_point(&snapshot.buffer_snapshot());
-                    end = snapshot
-                        .buffer_snapshot()
-                        .clip_point(end + Point::new(0, 1), Bias::Right);
-                    pending.end = snapshot.buffer_snapshot().anchor_before(end);
+                    let snapshot = s.display_snapshot();
+                    let is_empty = pending
+                        .start
+                        .cmp(&pending.end, &snapshot.buffer_snapshot())
+                        .is_eq();
+                    should_extend_pending = pending.reversed
+                        && !is_empty
+                        && vim.extended_pending_selection_id != Some(pending.id);
+                };
+
+                if should_extend_pending {
+                    let snapshot = s.display_snapshot();
+                    if let Some(pending) = s.pending_anchor_mut() {
+                        let end = pending.end.to_point(&snapshot.buffer_snapshot());
+                        let end = end.to_display_point(&snapshot);
+                        let new_end = movement::right(&snapshot, end);
+                        pending.end = snapshot
+                            .buffer_snapshot()
+                            .anchor_before(new_end.to_point(&snapshot));
+                    }
+                    vim.extended_pending_selection_id = s.pending_anchor().map(|p| p.id)
                 }
 
                 s.move_with(|map, selection| {
@@ -1240,8 +1260,10 @@ impl Vim {
                             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() {
-                        selection.end = movement::right(map, selection.start);
+                    } else if !last_mode.is_visual() && mode.is_visual() {
+                        if selection.is_empty() {
+                            selection.end = movement::right(map, selection.start);
+                        }
                     }
                 });
             })