Fix accidental visual selection on scroll (#2927)

Conrad Irwin created

Release Notes:

- vim: Fix bug where scrolling vertically would sometimes enter visual
mode

Change summary

crates/editor/src/editor.rs               |  2 
crates/editor/src/scroll/scroll_amount.rs |  2 
crates/vim/src/normal/scroll.rs           | 62 +++++++++++++++++++++++-
3 files changed, 61 insertions(+), 5 deletions(-)

Detailed changes

crates/editor/src/editor.rs πŸ”—

@@ -1654,7 +1654,7 @@ impl Editor {
             .excerpt_containing(self.selections.newest_anchor().head(), cx)
     }
 
-    fn style(&self, cx: &AppContext) -> EditorStyle {
+    pub fn style(&self, cx: &AppContext) -> EditorStyle {
         build_style(
             settings::get::<ThemeSettings>(cx),
             self.get_field_editor_theme.as_deref(),

crates/editor/src/scroll/scroll_amount.rs πŸ”—

@@ -39,7 +39,7 @@ impl ScrollAmount {
                 .visible_line_count()
                 // subtract one to leave an anchor line
                 // round towards zero (so page-up and page-down are symmetric)
-                .map(|l| ((l - 1.) * count).trunc())
+                .map(|l| (l * count).trunc() - count.signum())
                 .unwrap_or(0.),
         }
     }

crates/vim/src/normal/scroll.rs πŸ”—

@@ -67,7 +67,8 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
         let top_anchor = editor.scroll_manager.anchor().anchor;
 
         editor.change_selections(None, cx, |s| {
-            s.move_heads_with(|map, head, goal| {
+            s.move_with(|map, selection| {
+                let head = selection.head();
                 let top = top_anchor.to_display_point(map);
                 let min_row = top.row() + VERTICAL_SCROLL_MARGIN as u32;
                 let max_row = top.row() + visible_rows - VERTICAL_SCROLL_MARGIN as u32 - 1;
@@ -79,7 +80,11 @@ fn scroll_editor(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContex
                 } else {
                     head
                 };
-                (new_head, goal)
+                if selection.is_empty() {
+                    selection.collapse_to(new_head, selection.goal)
+                } else {
+                    selection.set_head(new_head, selection.goal)
+                };
             })
         });
     }
@@ -90,12 +95,35 @@ mod test {
     use crate::{state::Mode, test::VimTestContext};
     use gpui::geometry::vector::vec2f;
     use indoc::indoc;
+    use language::Point;
 
     #[gpui::test]
     async fn test_scroll(cx: &mut gpui::TestAppContext) {
         let mut cx = VimTestContext::new(cx, true).await;
 
-        cx.set_state(indoc! {"Λ‡a\nb\nc\nd\ne\n"}, Mode::Normal);
+        let window = cx.window;
+        let line_height =
+            cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache()));
+        window.simulate_resize(vec2f(1000., 8.0 * line_height - 1.0), &mut cx);
+
+        cx.set_state(
+            indoc!(
+                "Λ‡one
+                two
+                three
+                four
+                five
+                six
+                seven
+                eight
+                nine
+                ten
+                eleven
+                twelve
+            "
+            ),
+            Mode::Normal,
+        );
 
         cx.update_editor(|editor, cx| {
             assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
@@ -112,5 +140,33 @@ mod test {
         cx.update_editor(|editor, cx| {
             assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.))
         });
+
+        // does not select in normal mode
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
+        });
+        cx.simulate_keystrokes(["ctrl-d"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(
+                editor.selections.newest(cx).range(),
+                Point::new(5, 0)..Point::new(5, 0)
+            )
+        });
+
+        // does select in visual mode
+        cx.simulate_keystrokes(["g", "g"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.))
+        });
+        cx.simulate_keystrokes(["v", "ctrl-d"]);
+        cx.update_editor(|editor, cx| {
+            assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 2.0));
+            assert_eq!(
+                editor.selections.newest(cx).range(),
+                Point::new(0, 0)..Point::new(5, 1)
+            )
+        });
     }
 }