Merge pull request #1944 from zed-industries/vim-page-movement

Kay Simmons created

Add scroll commands to vim mode

Change summary

assets/keymaps/vim.json                       |  74 ++
crates/diagnostics/src/diagnostics.rs         |   5 
crates/editor/src/editor.rs                   | 609 +-------------------
crates/editor/src/editor_tests.rs             |  21 
crates/editor/src/element.rs                  |  15 
crates/editor/src/items.rs                    |  66 +-
crates/editor/src/scroll.rs                   | 348 ++++++++++++
crates/editor/src/scroll/actions.rs           | 159 +++++
crates/editor/src/scroll/autoscroll.rs        | 246 ++++++++
crates/editor/src/scroll/scroll_amount.rs     |  48 +
crates/editor/src/selections_collection.rs    |   2 
crates/go_to_line/src/go_to_line.rs           |   2 
crates/gpui/src/app.rs                        | 173 +++++
crates/gpui/src/keymap.rs                     |  15 
crates/journal/src/journal.rs                 |   2 
crates/outline/src/outline.rs                 |   4 
crates/project_symbols/src/project_symbols.rs |   3 
crates/search/src/project_search.rs           |   4 
crates/util/src/lib.rs                        |   4 
crates/vim/src/editor_events.rs               |  17 
crates/vim/src/insert.rs                      |   2 
crates/vim/src/normal.rs                      |  61 ++
crates/vim/src/normal/change.rs               |   4 
crates/vim/src/normal/delete.rs               |   2 
crates/vim/src/state.rs                       |   2 
crates/vim/src/test/vim_test_context.rs       |   3 
crates/vim/src/vim.rs                         |  19 
crates/vim/src/visual.rs                      |   4 
crates/workspace/src/dock.rs                  |   8 
crates/zed/src/zed.rs                         |   5 
30 files changed, 1,251 insertions(+), 676 deletions(-)

Detailed changes

assets/keymaps/vim.json πŸ”—

@@ -8,6 +8,22 @@
                     "Namespace": "G"
                 }
             ],
+            "i": [
+                "vim::PushOperator",
+                {
+                    "Object": {
+                        "around": false
+                    }
+                }
+            ],
+            "a": [
+                "vim::PushOperator",
+                {
+                    "Object": {
+                        "around": true
+                    }
+                }
+            ],
             "h": "vim::Left",
             "backspace": "vim::Backspace",
             "j": "vim::Down",
@@ -38,22 +54,6 @@
             ],
             "%": "vim::Matching",
             "escape": "editor::Cancel",
-            "i": [
-                "vim::PushOperator",
-                {
-                    "Object": {
-                        "around": false
-                    }
-                }
-            ],
-            "a": [
-                "vim::PushOperator",
-                {
-                    "Object": {
-                        "around": true
-                    }
-                }
-            ],
             "0": "vim::StartOfLine", // When no number operator present, use start of line motion
             "1": [
                 "vim::Number",
@@ -110,6 +110,12 @@
                 "vim::PushOperator",
                 "Yank"
             ],
+            "z": [
+                "vim::PushOperator",
+                {
+                    "Namespace": "Z"
+                }
+            ],
             "i": [
                 "vim::SwitchMode",
                 "Insert"
@@ -147,6 +153,30 @@
                 {
                     "focus": true
                 }
+            ],
+            "ctrl-f": [
+                "vim::Scroll",
+                "PageDown"
+            ],
+            "ctrl-b": [
+                "vim::Scroll",
+                "PageUp"
+            ],
+            "ctrl-d": [
+                "vim::Scroll",
+                "HalfPageDown"
+            ],
+            "ctrl-u": [
+                "vim::Scroll",
+                "HalfPageUp"
+            ],
+            "ctrl-e": [
+                "vim::Scroll",
+                "LineDown"
+            ],
+            "ctrl-y": [
+                "vim::Scroll",
+                "LineUp"
             ]
         }
     },
@@ -188,6 +218,18 @@
             "y": "vim::CurrentLine"
         }
     },
+    {
+        "context": "Editor && vim_operator == z",
+        "bindings": {
+            "t": "editor::ScrollCursorTop",
+            "z": "editor::ScrollCursorCenter",
+            "b": "editor::ScrollCursorBottom",
+            "escape": [
+                "vim::SwitchMode",
+                "Normal"
+            ]
+        }
+    },
     {
         "context": "Editor && VimObject",
         "bindings": {

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

@@ -5,8 +5,9 @@ use collections::{BTreeMap, HashSet};
 use editor::{
     diagnostic_block_renderer,
     display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock},
-    highlight_diagnostic_message, Autoscroll, Editor, ExcerptId, ExcerptRange, MultiBuffer,
-    ToOffset,
+    highlight_diagnostic_message,
+    scroll::autoscroll::Autoscroll,
+    Editor, ExcerptId, ExcerptRange, MultiBuffer, ToOffset,
 };
 use gpui::{
     actions, elements::*, fonts::TextStyle, impl_internal_actions, serde_json, AnyViewHandle,

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

@@ -10,6 +10,7 @@ mod mouse_context_menu;
 pub mod movement;
 mod multi_buffer;
 mod persistence;
+pub mod scroll;
 pub mod selections_collection;
 
 #[cfg(test)]
@@ -33,13 +34,13 @@ use gpui::{
     elements::*,
     executor,
     fonts::{self, HighlightStyle, TextStyle},
-    geometry::vector::{vec2f, Vector2F},
+    geometry::vector::Vector2F,
     impl_actions, impl_internal_actions,
     platform::CursorStyle,
     serde_json::json,
-    text_layout, AnyViewHandle, AppContext, AsyncAppContext, Axis, ClipboardItem, Element,
-    ElementBox, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription,
-    Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Element, ElementBox, Entity,
+    ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, Task, View,
+    ViewContext, ViewHandle, WeakViewHandle,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -61,11 +62,13 @@ pub use multi_buffer::{
 use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
 use ordered_float::OrderedFloat;
 use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction};
+use scroll::{
+    autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide,
+};
 use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use smallvec::SmallVec;
-use smol::Timer;
 use snippet::Snippet;
 use std::{
     any::TypeId,
@@ -86,11 +89,9 @@ use workspace::{ItemNavHistory, Workspace, WorkspaceId};
 use crate::git::diff_hunk_to_display;
 
 const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
-const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 const MAX_LINE_LEN: usize = 1024;
 const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
 const MAX_SELECTION_HISTORY_LEN: usize = 1024;
-pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
 
 pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
 
@@ -100,12 +101,6 @@ pub struct SelectNext {
     pub replace_newest: bool,
 }
 
-#[derive(Clone, PartialEq)]
-pub struct Scroll {
-    pub scroll_position: Vector2F,
-    pub axis: Option<Axis>,
-}
-
 #[derive(Clone, PartialEq)]
 pub struct Select(pub SelectPhase);
 
@@ -258,7 +253,7 @@ impl_actions!(
     ]
 );
 
-impl_internal_actions!(editor, [Scroll, Select, Jump]);
+impl_internal_actions!(editor, [Select, Jump]);
 
 enum DocumentHighlightRead {}
 enum DocumentHighlightWrite {}
@@ -270,12 +265,8 @@ pub enum Direction {
     Next,
 }
 
-#[derive(Default)]
-struct ScrollbarAutoHide(bool);
-
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::new_file);
-    cx.add_action(Editor::scroll);
     cx.add_action(Editor::select);
     cx.add_action(Editor::cancel);
     cx.add_action(Editor::newline);
@@ -305,12 +296,9 @@ pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(Editor::redo);
     cx.add_action(Editor::move_up);
     cx.add_action(Editor::move_page_up);
-    cx.add_action(Editor::page_up);
     cx.add_action(Editor::move_down);
     cx.add_action(Editor::move_page_down);
-    cx.add_action(Editor::page_down);
     cx.add_action(Editor::next_screen);
-
     cx.add_action(Editor::move_left);
     cx.add_action(Editor::move_right);
     cx.add_action(Editor::move_to_previous_word_start);
@@ -370,6 +358,7 @@ pub fn init(cx: &mut MutableAppContext) {
     hover_popover::init(cx);
     link_go_to_definition::init(cx);
     mouse_context_menu::init(cx);
+    scroll::actions::init(cx);
 
     workspace::register_project_item::<Editor>(cx);
     workspace::register_followable_item::<Editor>(cx);
@@ -411,46 +400,6 @@ pub enum SelectMode {
     All,
 }
 
-#[derive(PartialEq, Eq)]
-pub enum Autoscroll {
-    Next,
-    Strategy(AutoscrollStrategy),
-}
-
-impl Autoscroll {
-    pub fn fit() -> Self {
-        Self::Strategy(AutoscrollStrategy::Fit)
-    }
-
-    pub fn newest() -> Self {
-        Self::Strategy(AutoscrollStrategy::Newest)
-    }
-
-    pub fn center() -> Self {
-        Self::Strategy(AutoscrollStrategy::Center)
-    }
-}
-
-#[derive(PartialEq, Eq, Default)]
-pub enum AutoscrollStrategy {
-    Fit,
-    Newest,
-    #[default]
-    Center,
-    Top,
-    Bottom,
-}
-
-impl AutoscrollStrategy {
-    fn next(&self) -> Self {
-        match self {
-            AutoscrollStrategy::Center => AutoscrollStrategy::Top,
-            AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
-            _ => AutoscrollStrategy::Center,
-        }
-    }
-}
-
 #[derive(Copy, Clone, PartialEq, Eq)]
 pub enum EditorMode {
     SingleLine,
@@ -477,74 +426,12 @@ type CompletionId = usize;
 type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
 type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
 
-#[derive(Clone, Copy)]
-pub struct OngoingScroll {
-    last_timestamp: Instant,
-    axis: Option<Axis>,
-}
-
-impl OngoingScroll {
-    fn initial() -> OngoingScroll {
-        OngoingScroll {
-            last_timestamp: Instant::now() - SCROLL_EVENT_SEPARATION,
-            axis: None,
-        }
-    }
-
-    fn update(&mut self, axis: Option<Axis>) {
-        self.last_timestamp = Instant::now();
-        self.axis = axis;
-    }
-
-    pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
-        const UNLOCK_PERCENT: f32 = 1.9;
-        const UNLOCK_LOWER_BOUND: f32 = 6.;
-        let mut axis = self.axis;
-
-        let x = delta.x().abs();
-        let y = delta.y().abs();
-        let duration = Instant::now().duration_since(self.last_timestamp);
-        if duration > SCROLL_EVENT_SEPARATION {
-            //New ongoing scroll will start, determine axis
-            axis = if x <= y {
-                Some(Axis::Vertical)
-            } else {
-                Some(Axis::Horizontal)
-            };
-        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
-            //Check if the current ongoing will need to unlock
-            match axis {
-                Some(Axis::Vertical) => {
-                    if x > y && x >= y * UNLOCK_PERCENT {
-                        axis = None;
-                    }
-                }
-
-                Some(Axis::Horizontal) => {
-                    if y > x && y >= x * UNLOCK_PERCENT {
-                        axis = None;
-                    }
-                }
-
-                None => {}
-            }
-        }
-
-        match axis {
-            Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
-            Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
-            None => {}
-        }
-
-        axis
-    }
-}
-
 pub struct Editor {
     handle: WeakViewHandle<Self>,
     buffer: ModelHandle<MultiBuffer>,
     display_map: ModelHandle<DisplayMap>,
     pub selections: SelectionsCollection,
+    pub scroll_manager: ScrollManager,
     columnar_selection_tail: Option<Anchor>,
     add_selections_state: Option<AddSelectionsState>,
     select_next_state: Option<SelectNextState>,
@@ -554,10 +441,6 @@ pub struct Editor {
     select_larger_syntax_node_stack: Vec<Box<[Selection<usize>]>>,
     ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
-    ongoing_scroll: OngoingScroll,
-    scroll_position: Vector2F,
-    scroll_top_anchor: Anchor,
-    autoscroll_request: Option<(Autoscroll, bool)>,
     soft_wrap_mode_override: Option<settings::SoftWrap>,
     get_field_editor_theme: Option<Arc<GetFieldEditorTheme>>,
     override_text_style: Option<Box<OverrideTextStyle>>,
@@ -565,10 +448,7 @@ pub struct Editor {
     focused: bool,
     blink_manager: ModelHandle<BlinkManager>,
     show_local_selections: bool,
-    show_scrollbars: bool,
-    hide_scrollbar_task: Option<Task<()>>,
     mode: EditorMode,
-    vertical_scroll_margin: f32,
     placeholder_text: Option<Arc<str>>,
     highlighted_rows: Option<Range<u32>>,
     #[allow(clippy::type_complexity)]
@@ -590,8 +470,6 @@ pub struct Editor {
     leader_replica_id: Option<u16>,
     hover_state: HoverState,
     link_go_to_definition_state: LinkGoToDefinitionState,
-    visible_line_count: Option<f32>,
-    last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -600,9 +478,8 @@ pub struct EditorSnapshot {
     pub display_snapshot: DisplaySnapshot,
     pub placeholder_text: Option<Arc<str>>,
     is_focused: bool,
+    scroll_anchor: ScrollAnchor,
     ongoing_scroll: OngoingScroll,
-    scroll_position: Vector2F,
-    scroll_top_anchor: Anchor,
 }
 
 #[derive(Clone, Debug)]
@@ -1090,12 +967,9 @@ pub struct ClipboardSelection {
 
 #[derive(Debug)]
 pub struct NavigationData {
-    // Matching offsets for anchor and scroll_top_anchor allows us to recreate the anchor if the buffer
-    // has since been closed
     cursor_anchor: Anchor,
     cursor_position: Point,
-    scroll_position: Vector2F,
-    scroll_top_anchor: Anchor,
+    scroll_anchor: ScrollAnchor,
     scroll_top_row: u32,
 }
 
@@ -1163,9 +1037,8 @@ impl Editor {
                 display_map.set_state(&snapshot, cx);
             });
         });
-        clone.selections.set_state(&self.selections);
-        clone.scroll_position = self.scroll_position;
-        clone.scroll_top_anchor = self.scroll_top_anchor;
+        clone.selections.clone_state(&self.selections);
+        clone.scroll_manager.clone_state(&self.scroll_manager);
         clone.searchable = self.searchable;
         clone
     }
@@ -1200,6 +1073,7 @@ impl Editor {
             buffer: buffer.clone(),
             display_map: display_map.clone(),
             selections,
+            scroll_manager: ScrollManager::new(),
             columnar_selection_tail: None,
             add_selections_state: None,
             select_next_state: None,
@@ -1212,17 +1086,10 @@ impl Editor {
             soft_wrap_mode_override: None,
             get_field_editor_theme,
             project,
-            ongoing_scroll: OngoingScroll::initial(),
-            scroll_position: Vector2F::zero(),
-            scroll_top_anchor: Anchor::min(),
-            autoscroll_request: None,
             focused: false,
             blink_manager: blink_manager.clone(),
             show_local_selections: true,
-            show_scrollbars: true,
-            hide_scrollbar_task: None,
             mode,
-            vertical_scroll_margin: 3.0,
             placeholder_text: None,
             highlighted_rows: None,
             background_highlights: Default::default(),
@@ -1244,8 +1111,6 @@ impl Editor {
             leader_replica_id: None,
             hover_state: Default::default(),
             link_go_to_definition_state: Default::default(),
-            visible_line_count: None,
-            last_autoscroll: None,
             _subscriptions: vec![
                 cx.observe(&buffer, Self::on_buffer_changed),
                 cx.subscribe(&buffer, Self::on_buffer_event),
@@ -1254,7 +1119,7 @@ impl Editor {
             ],
         };
         this.end_selection(cx);
-        this.make_scrollbar_visible(cx);
+        this.scroll_manager.show_scrollbar(cx);
 
         let editor_created_event = EditorCreated(cx.handle());
         cx.emit_global(editor_created_event);
@@ -1307,9 +1172,8 @@ impl Editor {
         EditorSnapshot {
             mode: self.mode,
             display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)),
-            ongoing_scroll: self.ongoing_scroll,
-            scroll_position: self.scroll_position,
-            scroll_top_anchor: self.scroll_top_anchor,
+            scroll_anchor: self.scroll_manager.anchor(),
+            ongoing_scroll: self.scroll_manager.ongoing_scroll(),
             placeholder_text: self.placeholder_text.clone(),
             is_focused: self
                 .handle
@@ -1348,64 +1212,6 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
-        self.vertical_scroll_margin = margin_rows as f32;
-        cx.notify();
-    }
-
-    pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
-        self.set_scroll_position_internal(scroll_position, true, cx);
-    }
-
-    fn set_scroll_position_internal(
-        &mut self,
-        scroll_position: Vector2F,
-        local: bool,
-        cx: &mut ViewContext<Self>,
-    ) {
-        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-
-        if scroll_position.y() <= 0. {
-            self.scroll_top_anchor = Anchor::min();
-            self.scroll_position = scroll_position.max(vec2f(0., 0.));
-        } else {
-            let scroll_top_buffer_offset =
-                DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
-            let anchor = map
-                .buffer_snapshot
-                .anchor_at(scroll_top_buffer_offset, Bias::Right);
-            self.scroll_position = vec2f(
-                scroll_position.x(),
-                scroll_position.y() - anchor.to_display_point(&map).row() as f32,
-            );
-            self.scroll_top_anchor = anchor;
-        }
-
-        self.make_scrollbar_visible(cx);
-        self.autoscroll_request.take();
-        hide_hover(self, cx);
-
-        cx.emit(Event::ScrollPositionChanged { local });
-        cx.notify();
-    }
-
-    fn set_visible_line_count(&mut self, lines: f32) {
-        self.visible_line_count = Some(lines)
-    }
-
-    fn set_scroll_top_anchor(
-        &mut self,
-        anchor: Anchor,
-        position: Vector2F,
-        cx: &mut ViewContext<Self>,
-    ) {
-        self.scroll_top_anchor = anchor;
-        self.scroll_position = position;
-        self.make_scrollbar_visible(cx);
-        cx.emit(Event::ScrollPositionChanged { local: false });
-        cx.notify();
-    }
-
     pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
         self.cursor_shape = cursor_shape;
         cx.notify();
@@ -1431,199 +1237,6 @@ impl Editor {
         self.input_enabled = input_enabled;
     }
 
-    pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor)
-    }
-
-    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
-        if max < self.scroll_position.x() {
-            self.scroll_position.set_x(max);
-            true
-        } else {
-            false
-        }
-    }
-
-    pub fn autoscroll_vertically(
-        &mut self,
-        viewport_height: f32,
-        line_height: f32,
-        cx: &mut ViewContext<Self>,
-    ) -> bool {
-        let visible_lines = viewport_height / line_height;
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let mut scroll_position =
-            compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor);
-        let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
-            (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
-        } else {
-            display_map.max_point().row() as f32
-        };
-        if scroll_position.y() > max_scroll_top {
-            scroll_position.set_y(max_scroll_top);
-            self.set_scroll_position(scroll_position, cx);
-        }
-
-        let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() {
-            autoscroll
-        } else {
-            return false;
-        };
-
-        let first_cursor_top;
-        let last_cursor_bottom;
-        if let Some(highlighted_rows) = &self.highlighted_rows {
-            first_cursor_top = highlighted_rows.start as f32;
-            last_cursor_bottom = first_cursor_top + 1.;
-        } else if autoscroll == Autoscroll::newest() {
-            let newest_selection = self.selections.newest::<Point>(cx);
-            first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
-            last_cursor_bottom = first_cursor_top + 1.;
-        } else {
-            let selections = self.selections.all::<Point>(cx);
-            first_cursor_top = selections
-                .first()
-                .unwrap()
-                .head()
-                .to_display_point(&display_map)
-                .row() as f32;
-            last_cursor_bottom = selections
-                .last()
-                .unwrap()
-                .head()
-                .to_display_point(&display_map)
-                .row() as f32
-                + 1.0;
-        }
-
-        let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
-            0.
-        } else {
-            ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor()
-        };
-        if margin < 0.0 {
-            return false;
-        }
-
-        let strategy = match autoscroll {
-            Autoscroll::Strategy(strategy) => strategy,
-            Autoscroll::Next => {
-                let last_autoscroll = &self.last_autoscroll;
-                if let Some(last_autoscroll) = last_autoscroll {
-                    if self.scroll_position == last_autoscroll.0
-                        && first_cursor_top == last_autoscroll.1
-                        && last_cursor_bottom == last_autoscroll.2
-                    {
-                        last_autoscroll.3.next()
-                    } else {
-                        AutoscrollStrategy::default()
-                    }
-                } else {
-                    AutoscrollStrategy::default()
-                }
-            }
-        };
-
-        match strategy {
-            AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
-                let margin = margin.min(self.vertical_scroll_margin);
-                let target_top = (first_cursor_top - margin).max(0.0);
-                let target_bottom = last_cursor_bottom + margin;
-                let start_row = scroll_position.y();
-                let end_row = start_row + visible_lines;
-
-                if target_top < start_row {
-                    scroll_position.set_y(target_top);
-                    self.set_scroll_position_internal(scroll_position, local, cx);
-                } else if target_bottom >= end_row {
-                    scroll_position.set_y(target_bottom - visible_lines);
-                    self.set_scroll_position_internal(scroll_position, local, cx);
-                }
-            }
-            AutoscrollStrategy::Center => {
-                scroll_position.set_y((first_cursor_top - margin).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
-            }
-            AutoscrollStrategy::Top => {
-                scroll_position.set_y((first_cursor_top).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
-            }
-            AutoscrollStrategy::Bottom => {
-                scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
-                self.set_scroll_position_internal(scroll_position, local, cx);
-            }
-        }
-
-        self.last_autoscroll = Some((
-            self.scroll_position,
-            first_cursor_top,
-            last_cursor_bottom,
-            strategy,
-        ));
-
-        true
-    }
-
-    pub fn autoscroll_horizontally(
-        &mut self,
-        start_row: u32,
-        viewport_width: f32,
-        scroll_width: f32,
-        max_glyph_width: f32,
-        layouts: &[text_layout::Line],
-        cx: &mut ViewContext<Self>,
-    ) -> bool {
-        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
-        let selections = self.selections.all::<Point>(cx);
-
-        let mut target_left;
-        let mut target_right;
-
-        if self.highlighted_rows.is_some() {
-            target_left = 0.0_f32;
-            target_right = 0.0_f32;
-        } else {
-            target_left = std::f32::INFINITY;
-            target_right = 0.0_f32;
-            for selection in selections {
-                let head = selection.head().to_display_point(&display_map);
-                if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
-                    let start_column = head.column().saturating_sub(3);
-                    let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
-                    target_left = target_left.min(
-                        layouts[(head.row() - start_row) as usize]
-                            .x_for_index(start_column as usize),
-                    );
-                    target_right = target_right.max(
-                        layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
-                            + max_glyph_width,
-                    );
-                }
-            }
-        }
-
-        target_right = target_right.min(scroll_width);
-
-        if target_right - target_left > viewport_width {
-            return false;
-        }
-
-        let scroll_left = self.scroll_position.x() * max_glyph_width;
-        let scroll_right = scroll_left + viewport_width;
-
-        if target_left < scroll_left {
-            self.scroll_position.set_x(target_left / max_glyph_width);
-            true
-        } else if target_right > scroll_right {
-            self.scroll_position
-                .set_x((target_right - viewport_width) / max_glyph_width);
-            true
-        } else {
-            false
-        }
-    }
-
     fn selections_did_change(
         &mut self,
         local: bool,
@@ -1746,11 +1359,6 @@ impl Editor {
         });
     }
 
-    fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
-        self.ongoing_scroll.update(action.axis);
-        self.set_scroll_position(action.scroll_position, cx);
-    }
-
     fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext<Self>) {
         self.hide_context_menu(cx);
 
@@ -4073,23 +3681,6 @@ impl Editor {
         })
     }
 
-    pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) {
-        if self.take_rename(true, cx).is_some() {
-            return;
-        }
-
-        if let Some(_) = self.context_menu.as_mut() {
-            return;
-        }
-
-        if matches!(self.mode, EditorMode::SingleLine) {
-            cx.propagate_action();
-            return;
-        }
-
-        self.request_autoscroll(Autoscroll::Next, cx);
-    }
-
     pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
         if self.take_rename(true, cx).is_some() {
             return;
@@ -4123,10 +3714,13 @@ impl Editor {
             return;
         }
 
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_first(cx) {
-                return;
-            }
+        if self
+            .context_menu
+            .as_mut()
+            .map(|menu| menu.select_first(cx))
+            .unwrap_or(false)
+        {
+            return;
         }
 
         if matches!(self.mode, EditorMode::SingleLine) {
@@ -4134,9 +3728,10 @@ impl Editor {
             return;
         }
 
-        let row_count = match self.visible_line_count {
-            Some(row_count) => row_count as u32 - 1,
-            None => return,
+        let row_count = if let Some(row_count) = self.visible_line_count() {
+            row_count as u32 - 1
+        } else {
+            return;
         };
 
         let autoscroll = if action.center_cursor {
@@ -4158,32 +3753,6 @@ impl Editor {
         });
     }
 
-    pub fn page_up(&mut self, _: &PageUp, cx: &mut ViewContext<Self>) {
-        if self.take_rename(true, cx).is_some() {
-            return;
-        }
-
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_first(cx) {
-                return;
-            }
-        }
-
-        if matches!(self.mode, EditorMode::SingleLine) {
-            cx.propagate_action();
-            return;
-        }
-
-        let lines = match self.visible_line_count {
-            Some(lines) => lines,
-            None => return,
-        };
-
-        let cur_position = self.scroll_position(cx);
-        let new_pos = cur_position - vec2f(0., lines + 1.);
-        self.set_scroll_position(new_pos, cx);
-    }
-
     pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext<Self>) {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, goal| movement::up(map, head, goal, false))
@@ -4221,10 +3790,13 @@ impl Editor {
             return;
         }
 
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_last(cx) {
-                return;
-            }
+        if self
+            .context_menu
+            .as_mut()
+            .map(|menu| menu.select_last(cx))
+            .unwrap_or(false)
+        {
+            return;
         }
 
         if matches!(self.mode, EditorMode::SingleLine) {
@@ -4232,9 +3804,10 @@ impl Editor {
             return;
         }
 
-        let row_count = match self.visible_line_count {
-            Some(row_count) => row_count as u32 - 1,
-            None => return,
+        let row_count = if let Some(row_count) = self.visible_line_count() {
+            row_count as u32 - 1
+        } else {
+            return;
         };
 
         let autoscroll = if action.center_cursor {
@@ -4256,32 +3829,6 @@ impl Editor {
         });
     }
 
-    pub fn page_down(&mut self, _: &PageDown, cx: &mut ViewContext<Self>) {
-        if self.take_rename(true, cx).is_some() {
-            return;
-        }
-
-        if let Some(context_menu) = self.context_menu.as_mut() {
-            if context_menu.select_last(cx) {
-                return;
-            }
-        }
-
-        if matches!(self.mode, EditorMode::SingleLine) {
-            cx.propagate_action();
-            return;
-        }
-
-        let lines = match self.visible_line_count {
-            Some(lines) => lines,
-            None => return,
-        };
-
-        let cur_position = self.scroll_position(cx);
-        let new_pos = cur_position + vec2f(0., lines - 1.);
-        self.set_scroll_position(new_pos, cx);
-    }
-
     pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext<Self>) {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, goal| movement::down(map, head, goal, false))
@@ -4602,18 +4149,19 @@ impl Editor {
 
     fn push_to_nav_history(
         &self,
-        position: Anchor,
+        cursor_anchor: Anchor,
         new_position: Option<Point>,
         cx: &mut ViewContext<Self>,
     ) {
         if let Some(nav_history) = &self.nav_history {
             let buffer = self.buffer.read(cx).read(cx);
-            let point = position.to_point(&buffer);
-            let scroll_top_row = self.scroll_top_anchor.to_point(&buffer).row;
+            let cursor_position = cursor_anchor.to_point(&buffer);
+            let scroll_state = self.scroll_manager.anchor();
+            let scroll_top_row = scroll_state.top_row(&buffer);
             drop(buffer);
 
             if let Some(new_position) = new_position {
-                let row_delta = (new_position.row as i64 - point.row as i64).abs();
+                let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs();
                 if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA {
                     return;
                 }
@@ -4621,10 +4169,9 @@ impl Editor {
 
             nav_history.push(
                 Some(NavigationData {
-                    cursor_anchor: position,
-                    cursor_position: point,
-                    scroll_position: self.scroll_position,
-                    scroll_top_anchor: self.scroll_top_anchor,
+                    cursor_anchor,
+                    cursor_position,
+                    scroll_anchor: scroll_state,
                     scroll_top_row,
                 }),
                 cx,
@@ -5922,16 +5469,6 @@ impl Editor {
         });
     }
 
-    pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
-        self.autoscroll_request = Some((autoscroll, true));
-        cx.notify();
-    }
-
-    fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
-        self.autoscroll_request = Some((autoscroll, false));
-        cx.notify();
-    }
-
     pub fn transact(
         &mut self,
         cx: &mut ViewContext<Self>,
@@ -6340,31 +5877,6 @@ impl Editor {
         self.blink_manager.read(cx).visible() && self.focused
     }
 
-    pub fn show_scrollbars(&self) -> bool {
-        self.show_scrollbars
-    }
-
-    fn make_scrollbar_visible(&mut self, cx: &mut ViewContext<Self>) {
-        if !self.show_scrollbars {
-            self.show_scrollbars = true;
-            cx.notify();
-        }
-
-        if cx.default_global::<ScrollbarAutoHide>().0 {
-            self.hide_scrollbar_task = Some(cx.spawn_weak(|this, mut cx| async move {
-                Timer::after(SCROLLBAR_SHOW_INTERVAL).await;
-                if let Some(this) = this.upgrade(&cx) {
-                    this.update(&mut cx, |this, cx| {
-                        this.show_scrollbars = false;
-                        cx.notify();
-                    });
-                }
-            }));
-        } else {
-            self.hide_scrollbar_task = None;
-        }
-    }
-
     fn on_buffer_changed(&mut self, _: ModelHandle<MultiBuffer>, cx: &mut ViewContext<Self>) {
         cx.notify();
     }
@@ -6561,11 +6073,7 @@ impl EditorSnapshot {
     }
 
     pub fn scroll_position(&self) -> Vector2F {
-        compute_scroll_position(
-            &self.display_snapshot,
-            self.scroll_position,
-            &self.scroll_top_anchor,
-        )
+        self.scroll_anchor.scroll_position(&self.display_snapshot)
     }
 }
 
@@ -6577,20 +6085,6 @@ impl Deref for EditorSnapshot {
     }
 }
 
-fn compute_scroll_position(
-    snapshot: &DisplaySnapshot,
-    mut scroll_position: Vector2F,
-    scroll_top_anchor: &Anchor,
-) -> Vector2F {
-    if *scroll_top_anchor != Anchor::min() {
-        let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32;
-        scroll_position.set_y(scroll_top + scroll_position.y());
-    } else {
-        scroll_position.set_y(0.);
-    }
-    scroll_position
-}
-
 #[derive(Copy, Clone, Debug, PartialEq, Eq)]
 pub enum Event {
     BufferEdited,
@@ -6603,7 +6097,6 @@ pub enum Event {
     SelectionsChanged { local: bool },
     ScrollPositionChanged { local: bool },
     Closed,
-    IgnoredInput,
 }
 
 pub struct EditorFocused(pub ViewHandle<Editor>);
@@ -6789,7 +6282,6 @@ impl View for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
-            cx.emit(Event::IgnoredInput);
             return;
         }
 
@@ -6826,7 +6318,6 @@ impl View for Editor {
         cx: &mut ViewContext<Self>,
     ) {
         if !self.input_enabled {
-            cx.emit(Event::IgnoredInput);
             return;
         }
 

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

@@ -12,7 +12,7 @@ use crate::test::{
 };
 use gpui::{
     executor::Deterministic,
-    geometry::rect::RectF,
+    geometry::{rect::RectF, vector::vec2f},
     platform::{WindowBounds, WindowOptions},
 };
 use language::{FakeLspAdapter, LanguageConfig, LanguageRegistry, Point};
@@ -544,31 +544,30 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
 
         // Set scroll position to check later
         editor.set_scroll_position(Vector2F::new(5.5, 5.5), cx);
-        let original_scroll_position = editor.scroll_position;
-        let original_scroll_top_anchor = editor.scroll_top_anchor;
+        let original_scroll_position = editor.scroll_manager.anchor();
 
         // Jump to the end of the document and adjust scroll
         editor.move_to_end(&MoveToEnd, cx);
         editor.set_scroll_position(Vector2F::new(-2.5, -0.5), cx);
-        assert_ne!(editor.scroll_position, original_scroll_position);
-        assert_ne!(editor.scroll_top_anchor, original_scroll_top_anchor);
+        assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
 
         let nav_entry = pop_history(&mut editor, cx).unwrap();
         editor.navigate(nav_entry.data.unwrap(), cx);
-        assert_eq!(editor.scroll_position, original_scroll_position);
-        assert_eq!(editor.scroll_top_anchor, original_scroll_top_anchor);
+        assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
 
         // Ensure we don't panic when navigation data contains invalid anchors *and* points.
-        let mut invalid_anchor = editor.scroll_top_anchor;
+        let mut invalid_anchor = editor.scroll_manager.anchor().top_anchor;
         invalid_anchor.text_anchor.buffer_id = Some(999);
         let invalid_point = Point::new(9999, 0);
         editor.navigate(
             Box::new(NavigationData {
                 cursor_anchor: invalid_anchor,
                 cursor_position: invalid_point,
-                scroll_top_anchor: invalid_anchor,
+                scroll_anchor: ScrollAnchor {
+                    top_anchor: invalid_anchor,
+                    offset: Default::default(),
+                },
                 scroll_top_row: invalid_point.row,
-                scroll_position: Default::default(),
             }),
             cx,
         );
@@ -5034,7 +5033,7 @@ fn test_following(cx: &mut gpui::MutableAppContext) {
             .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx)
             .unwrap();
         assert_eq!(follower.scroll_position(cx), initial_scroll_position);
-        assert!(follower.autoscroll_request.is_some());
+        assert!(follower.scroll_manager.has_autoscroll_request());
     });
     assert_eq!(follower.read(cx).selections.ranges(cx), vec![0..0]);
 

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

@@ -1,7 +1,7 @@
 use super::{
     display_map::{BlockContext, ToDisplayPoint},
-    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Scroll, Select, SelectPhase,
-    SoftWrap, ToPoint, MAX_LINE_LEN,
+    Anchor, DisplayPoint, Editor, EditorMode, EditorSnapshot, Select, SelectPhase, SoftWrap,
+    ToPoint, MAX_LINE_LEN,
 };
 use crate::{
     display_map::{BlockStyle, DisplaySnapshot, TransformBlock},
@@ -13,6 +13,7 @@ use crate::{
         GoToFetchedDefinition, GoToFetchedTypeDefinition, UpdateGoToDefinitionLink,
     },
     mouse_context_menu::DeployMouseContextMenu,
+    scroll::actions::Scroll,
     EditorStyle,
 };
 use clock::ReplicaId;
@@ -955,7 +956,7 @@ impl EditorElement {
                     move |_, cx| {
                         if let Some(view) = view.upgrade(cx.deref_mut()) {
                             view.update(cx.deref_mut(), |view, cx| {
-                                view.make_scrollbar_visible(cx);
+                                view.scroll_manager.show_scrollbar(cx);
                             });
                         }
                     }
@@ -977,7 +978,7 @@ impl EditorElement {
                                     position.set_y(top_row as f32);
                                     view.set_scroll_position(position, cx);
                                 } else {
-                                    view.make_scrollbar_visible(cx);
+                                    view.scroll_manager.show_scrollbar(cx);
                                 }
                             });
                         }
@@ -1298,7 +1299,7 @@ impl EditorElement {
         };
 
         let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
-        let scroll_x = snapshot.scroll_position.x();
+        let scroll_x = snapshot.scroll_anchor.offset.x();
         let (fixed_blocks, non_fixed_blocks) = snapshot
             .blocks_in_range(rows.clone())
             .partition::<Vec<_>, _>(|(_, block)| match block {
@@ -1670,7 +1671,7 @@ impl Element for EditorElement {
                 ));
             }
 
-            show_scrollbars = view.show_scrollbars();
+            show_scrollbars = view.scroll_manager.scrollbars_visible();
             include_root = view
                 .project
                 .as_ref()
@@ -1725,7 +1726,7 @@ impl Element for EditorElement {
         );
 
         self.update_view(cx.app, |view, cx| {
-            let clamped = view.clamp_scroll_left(scroll_max.x());
+            let clamped = view.scroll_manager.clamp_scroll_left(scroll_max.x());
 
             let autoscrolled = if autoscroll_horizontally {
                 view.autoscroll_horizontally(

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

@@ -26,8 +26,9 @@ use workspace::{
 
 use crate::{
     display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
-    movement::surrounding_word, persistence::DB, Anchor, Autoscroll, Editor, Event, ExcerptId,
-    MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
+    movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor,
+    Event, ExcerptId, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _,
+    FORMAT_TIMEOUT,
 };
 
 pub const MAX_TAB_TITLE_LEN: usize = 24;
@@ -87,14 +88,17 @@ impl FollowableItem for Editor {
                 }
 
                 if let Some(anchor) = state.scroll_top_anchor {
-                    editor.set_scroll_top_anchor(
-                        Anchor {
-                            buffer_id: Some(state.buffer_id as usize),
-                            excerpt_id,
-                            text_anchor: language::proto::deserialize_anchor(anchor)
-                                .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                    editor.set_scroll_anchor_internal(
+                        ScrollAnchor {
+                            top_anchor: Anchor {
+                                buffer_id: Some(state.buffer_id as usize),
+                                excerpt_id,
+                                text_anchor: language::proto::deserialize_anchor(anchor)
+                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                            },
+                            offset: vec2f(state.scroll_x, state.scroll_y),
                         },
-                        vec2f(state.scroll_x, state.scroll_y),
+                        false,
                         cx,
                     );
                 }
@@ -132,13 +136,14 @@ impl FollowableItem for Editor {
 
     fn to_state_proto(&self, cx: &AppContext) -> Option<proto::view::Variant> {
         let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id();
+        let scroll_anchor = self.scroll_manager.anchor();
         Some(proto::view::Variant::Editor(proto::view::Editor {
             buffer_id,
             scroll_top_anchor: Some(language::proto::serialize_anchor(
-                &self.scroll_top_anchor.text_anchor,
+                &scroll_anchor.top_anchor.text_anchor,
             )),
-            scroll_x: self.scroll_position.x(),
-            scroll_y: self.scroll_position.y(),
+            scroll_x: scroll_anchor.offset.x(),
+            scroll_y: scroll_anchor.offset.y(),
             selections: self
                 .selections
                 .disjoint_anchors()
@@ -160,11 +165,12 @@ impl FollowableItem for Editor {
         match update {
             proto::update_view::Variant::Editor(update) => match event {
                 Event::ScrollPositionChanged { .. } => {
+                    let scroll_anchor = self.scroll_manager.anchor();
                     update.scroll_top_anchor = Some(language::proto::serialize_anchor(
-                        &self.scroll_top_anchor.text_anchor,
+                        &scroll_anchor.top_anchor.text_anchor,
                     ));
-                    update.scroll_x = self.scroll_position.x();
-                    update.scroll_y = self.scroll_position.y();
+                    update.scroll_x = scroll_anchor.offset.x();
+                    update.scroll_y = scroll_anchor.offset.y();
                     true
                 }
                 Event::SelectionsChanged { .. } => {
@@ -207,14 +213,16 @@ impl FollowableItem for Editor {
                     self.set_selections_from_remote(selections, cx);
                     self.request_autoscroll_remotely(Autoscroll::newest(), cx);
                 } else if let Some(anchor) = message.scroll_top_anchor {
-                    self.set_scroll_top_anchor(
-                        Anchor {
-                            buffer_id: Some(buffer_id),
-                            excerpt_id,
-                            text_anchor: language::proto::deserialize_anchor(anchor)
-                                .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                    self.set_scroll_anchor(
+                        ScrollAnchor {
+                            top_anchor: Anchor {
+                                buffer_id: Some(buffer_id),
+                                excerpt_id,
+                                text_anchor: language::proto::deserialize_anchor(anchor)
+                                    .ok_or_else(|| anyhow!("invalid scroll top"))?,
+                            },
+                            offset: vec2f(message.scroll_x, message.scroll_y),
                         },
-                        vec2f(message.scroll_x, message.scroll_y),
                         cx,
                     );
                 }
@@ -279,13 +287,12 @@ impl Item for Editor {
                 buffer.clip_point(data.cursor_position, Bias::Left)
             };
 
-            let scroll_top_anchor = if buffer.can_resolve(&data.scroll_top_anchor) {
-                data.scroll_top_anchor
-            } else {
-                buffer.anchor_before(
+            let mut scroll_anchor = data.scroll_anchor;
+            if !buffer.can_resolve(&scroll_anchor.top_anchor) {
+                scroll_anchor.top_anchor = buffer.anchor_before(
                     buffer.clip_point(Point::new(data.scroll_top_row, 0), Bias::Left),
-                )
-            };
+                );
+            }
 
             drop(buffer);
 
@@ -293,8 +300,7 @@ impl Item for Editor {
                 false
             } else {
                 let nav_history = self.nav_history.take();
-                self.scroll_position = data.scroll_position;
-                self.scroll_top_anchor = scroll_top_anchor;
+                self.set_scroll_anchor(scroll_anchor, cx);
                 self.change_selections(Some(Autoscroll::fit()), cx, |s| {
                     s.select_ranges([offset..offset])
                 });

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

@@ -0,0 +1,348 @@
+pub mod actions;
+pub mod autoscroll;
+pub mod scroll_amount;
+
+use std::{
+    cmp::Ordering,
+    time::{Duration, Instant},
+};
+
+use gpui::{
+    geometry::vector::{vec2f, Vector2F},
+    Axis, MutableAppContext, Task, ViewContext,
+};
+use language::Bias;
+
+use crate::{
+    display_map::{DisplaySnapshot, ToDisplayPoint},
+    hover_popover::hide_hover,
+    Anchor, DisplayPoint, Editor, EditorMode, Event, MultiBufferSnapshot, ToPoint,
+};
+
+use self::{
+    autoscroll::{Autoscroll, AutoscrollStrategy},
+    scroll_amount::ScrollAmount,
+};
+
+pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
+const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
+
+#[derive(Default)]
+pub struct ScrollbarAutoHide(pub bool);
+
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub struct ScrollAnchor {
+    pub offset: Vector2F,
+    pub top_anchor: Anchor,
+}
+
+impl ScrollAnchor {
+    fn new() -> Self {
+        Self {
+            offset: Vector2F::zero(),
+            top_anchor: Anchor::min(),
+        }
+    }
+
+    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+        let mut scroll_position = self.offset;
+        if self.top_anchor != Anchor::min() {
+            let scroll_top = self.top_anchor.to_display_point(snapshot).row() as f32;
+            scroll_position.set_y(scroll_top + scroll_position.y());
+        } else {
+            scroll_position.set_y(0.);
+        }
+        scroll_position
+    }
+
+    pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
+        self.top_anchor.to_point(buffer).row
+    }
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct OngoingScroll {
+    last_event: Instant,
+    axis: Option<Axis>,
+}
+
+impl OngoingScroll {
+    fn new() -> Self {
+        Self {
+            last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
+            axis: None,
+        }
+    }
+
+    pub fn filter(&self, delta: &mut Vector2F) -> Option<Axis> {
+        const UNLOCK_PERCENT: f32 = 1.9;
+        const UNLOCK_LOWER_BOUND: f32 = 6.;
+        let mut axis = self.axis;
+
+        let x = delta.x().abs();
+        let y = delta.y().abs();
+        let duration = Instant::now().duration_since(self.last_event);
+        if duration > SCROLL_EVENT_SEPARATION {
+            //New ongoing scroll will start, determine axis
+            axis = if x <= y {
+                Some(Axis::Vertical)
+            } else {
+                Some(Axis::Horizontal)
+            };
+        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
+            //Check if the current ongoing will need to unlock
+            match axis {
+                Some(Axis::Vertical) => {
+                    if x > y && x >= y * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                Some(Axis::Horizontal) => {
+                    if y > x && y >= x * UNLOCK_PERCENT {
+                        axis = None;
+                    }
+                }
+
+                None => {}
+            }
+        }
+
+        match axis {
+            Some(Axis::Vertical) => *delta = vec2f(0., delta.y()),
+            Some(Axis::Horizontal) => *delta = vec2f(delta.x(), 0.),
+            None => {}
+        }
+
+        axis
+    }
+}
+
+pub struct ScrollManager {
+    vertical_scroll_margin: f32,
+    anchor: ScrollAnchor,
+    ongoing: OngoingScroll,
+    autoscroll_request: Option<(Autoscroll, bool)>,
+    last_autoscroll: Option<(Vector2F, f32, f32, AutoscrollStrategy)>,
+    show_scrollbars: bool,
+    hide_scrollbar_task: Option<Task<()>>,
+    visible_line_count: Option<f32>,
+}
+
+impl ScrollManager {
+    pub fn new() -> Self {
+        ScrollManager {
+            vertical_scroll_margin: 3.0,
+            anchor: ScrollAnchor::new(),
+            ongoing: OngoingScroll::new(),
+            autoscroll_request: None,
+            show_scrollbars: true,
+            hide_scrollbar_task: None,
+            last_autoscroll: None,
+            visible_line_count: None,
+        }
+    }
+
+    pub fn clone_state(&mut self, other: &Self) {
+        self.anchor = other.anchor;
+        self.ongoing = other.ongoing;
+    }
+
+    pub fn anchor(&self) -> ScrollAnchor {
+        self.anchor
+    }
+
+    pub fn ongoing_scroll(&self) -> OngoingScroll {
+        self.ongoing
+    }
+
+    pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
+        self.ongoing.last_event = Instant::now();
+        self.ongoing.axis = axis;
+    }
+
+    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> Vector2F {
+        self.anchor.scroll_position(snapshot)
+    }
+
+    fn set_scroll_position(
+        &mut self,
+        scroll_position: Vector2F,
+        map: &DisplaySnapshot,
+        local: bool,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let new_anchor = if scroll_position.y() <= 0. {
+            ScrollAnchor {
+                top_anchor: Anchor::min(),
+                offset: scroll_position.max(vec2f(0., 0.)),
+            }
+        } else {
+            let scroll_top_buffer_offset =
+                DisplayPoint::new(scroll_position.y() as u32, 0).to_offset(&map, Bias::Right);
+            let top_anchor = map
+                .buffer_snapshot
+                .anchor_at(scroll_top_buffer_offset, Bias::Right);
+
+            ScrollAnchor {
+                top_anchor,
+                offset: vec2f(
+                    scroll_position.x(),
+                    scroll_position.y() - top_anchor.to_display_point(&map).row() as f32,
+                ),
+            }
+        };
+
+        self.set_anchor(new_anchor, local, cx);
+    }
+
+    fn set_anchor(&mut self, anchor: ScrollAnchor, local: bool, cx: &mut ViewContext<Editor>) {
+        self.anchor = anchor;
+        cx.emit(Event::ScrollPositionChanged { local });
+        self.show_scrollbar(cx);
+        self.autoscroll_request.take();
+        cx.notify();
+    }
+
+    pub fn show_scrollbar(&mut self, cx: &mut ViewContext<Editor>) {
+        if !self.show_scrollbars {
+            self.show_scrollbars = true;
+            cx.notify();
+        }
+
+        if cx.default_global::<ScrollbarAutoHide>().0 {
+            self.hide_scrollbar_task = Some(cx.spawn_weak(|editor, mut cx| async move {
+                cx.background().timer(SCROLLBAR_SHOW_INTERVAL).await;
+                if let Some(editor) = editor.upgrade(&cx) {
+                    editor.update(&mut cx, |editor, cx| {
+                        editor.scroll_manager.show_scrollbars = false;
+                        cx.notify();
+                    });
+                }
+            }));
+        } else {
+            self.hide_scrollbar_task = None;
+        }
+    }
+
+    pub fn scrollbars_visible(&self) -> bool {
+        self.show_scrollbars
+    }
+
+    pub fn has_autoscroll_request(&self) -> bool {
+        self.autoscroll_request.is_some()
+    }
+
+    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
+        if max < self.anchor.offset.x() {
+            self.anchor.offset.set_x(max);
+            true
+        } else {
+            false
+        }
+    }
+}
+
+impl Editor {
+    pub fn vertical_scroll_margin(&mut self) -> usize {
+        self.scroll_manager.vertical_scroll_margin as usize
+    }
+
+    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut ViewContext<Self>) {
+        self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
+        cx.notify();
+    }
+
+    pub fn visible_line_count(&self) -> Option<f32> {
+        self.scroll_manager.visible_line_count
+    }
+
+    pub(crate) fn set_visible_line_count(&mut self, lines: f32) {
+        self.scroll_manager.visible_line_count = Some(lines)
+    }
+
+    pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext<Self>) {
+        self.set_scroll_position_internal(scroll_position, true, cx);
+    }
+
+    pub(crate) fn set_scroll_position_internal(
+        &mut self,
+        scroll_position: Vector2F,
+        local: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+
+        hide_hover(self, cx);
+        self.scroll_manager
+            .set_scroll_position(scroll_position, &map, local, cx);
+    }
+
+    pub fn scroll_position(&self, cx: &mut ViewContext<Self>) -> Vector2F {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        self.scroll_manager.anchor.scroll_position(&display_map)
+    }
+
+    pub fn set_scroll_anchor(&mut self, scroll_anchor: ScrollAnchor, cx: &mut ViewContext<Self>) {
+        self.set_scroll_anchor_internal(scroll_anchor, true, cx);
+    }
+
+    pub(crate) fn set_scroll_anchor_internal(
+        &mut self,
+        scroll_anchor: ScrollAnchor,
+        local: bool,
+        cx: &mut ViewContext<Self>,
+    ) {
+        hide_hover(self, cx);
+        self.scroll_manager.set_anchor(scroll_anchor, local, cx);
+    }
+
+    pub fn scroll_screen(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) {
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return;
+        }
+
+        if self.take_rename(true, cx).is_some() {
+            return;
+        }
+
+        if amount.move_context_menu_selection(self, cx) {
+            return;
+        }
+
+        let cur_position = self.scroll_position(cx);
+        let new_pos = cur_position + vec2f(0., amount.lines(self) - 1.);
+        self.set_scroll_position(new_pos, cx);
+    }
+
+    /// Returns an ordering. The newest selection is:
+    ///     Ordering::Equal => on screen
+    ///     Ordering::Less => above the screen
+    ///     Ordering::Greater => below the screen
+    pub fn newest_selection_on_screen(&self, cx: &mut MutableAppContext) -> Ordering {
+        let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let newest_head = self
+            .selections
+            .newest_anchor()
+            .head()
+            .to_display_point(&snapshot);
+        let screen_top = self
+            .scroll_manager
+            .anchor
+            .top_anchor
+            .to_display_point(&snapshot);
+
+        if screen_top > newest_head {
+            return Ordering::Less;
+        }
+
+        if let Some(visible_lines) = self.visible_line_count() {
+            if newest_head.row() < screen_top.row() + visible_lines as u32 {
+                return Ordering::Equal;
+            }
+        }
+
+        Ordering::Greater
+    }
+}

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

@@ -0,0 +1,159 @@
+use gpui::{
+    actions, geometry::vector::Vector2F, impl_internal_actions, Axis, MutableAppContext,
+    ViewContext,
+};
+use language::Bias;
+
+use crate::{Editor, EditorMode};
+
+use super::{autoscroll::Autoscroll, scroll_amount::ScrollAmount, ScrollAnchor};
+
+actions!(
+    editor,
+    [
+        LineDown,
+        LineUp,
+        HalfPageDown,
+        HalfPageUp,
+        PageDown,
+        PageUp,
+        NextScreen,
+        ScrollCursorTop,
+        ScrollCursorCenter,
+        ScrollCursorBottom,
+    ]
+);
+
+#[derive(Clone, PartialEq)]
+pub struct Scroll {
+    pub scroll_position: Vector2F,
+    pub axis: Option<Axis>,
+}
+
+impl_internal_actions!(editor, [Scroll]);
+
+pub fn init(cx: &mut MutableAppContext) {
+    cx.add_action(Editor::next_screen);
+    cx.add_action(Editor::scroll);
+    cx.add_action(Editor::scroll_cursor_top);
+    cx.add_action(Editor::scroll_cursor_center);
+    cx.add_action(Editor::scroll_cursor_bottom);
+    cx.add_action(|this: &mut Editor, _: &LineDown, cx| {
+        this.scroll_screen(&ScrollAmount::LineDown, cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &LineUp, cx| {
+        this.scroll_screen(&ScrollAmount::LineUp, cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &HalfPageDown, cx| {
+        this.scroll_screen(&ScrollAmount::HalfPageDown, cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &HalfPageUp, cx| {
+        this.scroll_screen(&ScrollAmount::HalfPageUp, cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &PageDown, cx| {
+        this.scroll_screen(&ScrollAmount::PageDown, cx)
+    });
+    cx.add_action(|this: &mut Editor, _: &PageUp, cx| {
+        this.scroll_screen(&ScrollAmount::PageUp, cx)
+    });
+}
+
+impl Editor {
+    pub fn next_screen(&mut self, _: &NextScreen, cx: &mut ViewContext<Editor>) -> Option<()> {
+        if self.take_rename(true, cx).is_some() {
+            return None;
+        }
+
+        self.context_menu.as_mut()?;
+
+        if matches!(self.mode, EditorMode::SingleLine) {
+            cx.propagate_action();
+            return None;
+        }
+
+        self.request_autoscroll(Autoscroll::Next, cx);
+
+        Some(())
+    }
+
+    fn scroll(&mut self, action: &Scroll, cx: &mut ViewContext<Self>) {
+        self.scroll_manager.update_ongoing_scroll(action.axis);
+        self.set_scroll_position(action.scroll_position, cx);
+    }
+
+    fn scroll_cursor_top(editor: &mut Editor, _: &ScrollCursorTop, cx: &mut ViewContext<Editor>) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(scroll_margin_rows);
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                top_anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+
+    fn scroll_cursor_center(
+        editor: &mut Editor,
+        _: &ScrollCursorCenter,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top.row().saturating_sub(visible_rows / 2);
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                top_anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+
+    fn scroll_cursor_bottom(
+        editor: &mut Editor,
+        _: &ScrollCursorBottom,
+        cx: &mut ViewContext<Editor>,
+    ) {
+        let snapshot = editor.snapshot(cx).display_snapshot;
+        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let mut new_screen_top = editor.selections.newest_display(cx).head();
+        *new_screen_top.row_mut() = new_screen_top
+            .row()
+            .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows));
+        *new_screen_top.column_mut() = 0;
+        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
+        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
+
+        editor.set_scroll_anchor(
+            ScrollAnchor {
+                top_anchor: new_anchor,
+                offset: Default::default(),
+            },
+            cx,
+        )
+    }
+}

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

@@ -0,0 +1,246 @@
+use std::cmp;
+
+use gpui::{text_layout, ViewContext};
+use language::Point;
+
+use crate::{display_map::ToDisplayPoint, Editor, EditorMode};
+
+#[derive(PartialEq, Eq)]
+pub enum Autoscroll {
+    Next,
+    Strategy(AutoscrollStrategy),
+}
+
+impl Autoscroll {
+    pub fn fit() -> Self {
+        Self::Strategy(AutoscrollStrategy::Fit)
+    }
+
+    pub fn newest() -> Self {
+        Self::Strategy(AutoscrollStrategy::Newest)
+    }
+
+    pub fn center() -> Self {
+        Self::Strategy(AutoscrollStrategy::Center)
+    }
+}
+
+#[derive(PartialEq, Eq, Default)]
+pub enum AutoscrollStrategy {
+    Fit,
+    Newest,
+    #[default]
+    Center,
+    Top,
+    Bottom,
+}
+
+impl AutoscrollStrategy {
+    fn next(&self) -> Self {
+        match self {
+            AutoscrollStrategy::Center => AutoscrollStrategy::Top,
+            AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
+            _ => AutoscrollStrategy::Center,
+        }
+    }
+}
+
+impl Editor {
+    pub fn autoscroll_vertically(
+        &mut self,
+        viewport_height: f32,
+        line_height: f32,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        let visible_lines = viewport_height / line_height;
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
+        let max_scroll_top = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+            (display_map.max_point().row() as f32 - visible_lines + 1.).max(0.)
+        } else {
+            display_map.max_point().row() as f32
+        };
+        if scroll_position.y() > max_scroll_top {
+            scroll_position.set_y(max_scroll_top);
+            self.set_scroll_position(scroll_position, cx);
+        }
+
+        let (autoscroll, local) =
+            if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() {
+                autoscroll
+            } else {
+                return false;
+            };
+
+        let first_cursor_top;
+        let last_cursor_bottom;
+        if let Some(highlighted_rows) = &self.highlighted_rows {
+            first_cursor_top = highlighted_rows.start as f32;
+            last_cursor_bottom = first_cursor_top + 1.;
+        } else if autoscroll == Autoscroll::newest() {
+            let newest_selection = self.selections.newest::<Point>(cx);
+            first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32;
+            last_cursor_bottom = first_cursor_top + 1.;
+        } else {
+            let selections = self.selections.all::<Point>(cx);
+            first_cursor_top = selections
+                .first()
+                .unwrap()
+                .head()
+                .to_display_point(&display_map)
+                .row() as f32;
+            last_cursor_bottom = selections
+                .last()
+                .unwrap()
+                .head()
+                .to_display_point(&display_map)
+                .row() as f32
+                + 1.0;
+        }
+
+        let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
+            0.
+        } else {
+            ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor()
+        };
+        if margin < 0.0 {
+            return false;
+        }
+
+        let strategy = match autoscroll {
+            Autoscroll::Strategy(strategy) => strategy,
+            Autoscroll::Next => {
+                let last_autoscroll = &self.scroll_manager.last_autoscroll;
+                if let Some(last_autoscroll) = last_autoscroll {
+                    if self.scroll_manager.anchor.offset == last_autoscroll.0
+                        && first_cursor_top == last_autoscroll.1
+                        && last_cursor_bottom == last_autoscroll.2
+                    {
+                        last_autoscroll.3.next()
+                    } else {
+                        AutoscrollStrategy::default()
+                    }
+                } else {
+                    AutoscrollStrategy::default()
+                }
+            }
+        };
+
+        match strategy {
+            AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
+                let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
+                let target_top = (first_cursor_top - margin).max(0.0);
+                let target_bottom = last_cursor_bottom + margin;
+                let start_row = scroll_position.y();
+                let end_row = start_row + visible_lines;
+
+                if target_top < start_row {
+                    scroll_position.set_y(target_top);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
+                } else if target_bottom >= end_row {
+                    scroll_position.set_y(target_bottom - visible_lines);
+                    self.set_scroll_position_internal(scroll_position, local, cx);
+                }
+            }
+            AutoscrollStrategy::Center => {
+                scroll_position.set_y((first_cursor_top - margin).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, cx);
+            }
+            AutoscrollStrategy::Top => {
+                scroll_position.set_y((first_cursor_top).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, cx);
+            }
+            AutoscrollStrategy::Bottom => {
+                scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0));
+                self.set_scroll_position_internal(scroll_position, local, cx);
+            }
+        }
+
+        self.scroll_manager.last_autoscroll = Some((
+            self.scroll_manager.anchor.offset,
+            first_cursor_top,
+            last_cursor_bottom,
+            strategy,
+        ));
+
+        true
+    }
+
+    pub fn autoscroll_horizontally(
+        &mut self,
+        start_row: u32,
+        viewport_width: f32,
+        scroll_width: f32,
+        max_glyph_width: f32,
+        layouts: &[text_layout::Line],
+        cx: &mut ViewContext<Self>,
+    ) -> bool {
+        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
+        let selections = self.selections.all::<Point>(cx);
+
+        let mut target_left;
+        let mut target_right;
+
+        if self.highlighted_rows.is_some() {
+            target_left = 0.0_f32;
+            target_right = 0.0_f32;
+        } else {
+            target_left = std::f32::INFINITY;
+            target_right = 0.0_f32;
+            for selection in selections {
+                let head = selection.head().to_display_point(&display_map);
+                if head.row() >= start_row && head.row() < start_row + layouts.len() as u32 {
+                    let start_column = head.column().saturating_sub(3);
+                    let end_column = cmp::min(display_map.line_len(head.row()), head.column() + 3);
+                    target_left = target_left.min(
+                        layouts[(head.row() - start_row) as usize]
+                            .x_for_index(start_column as usize),
+                    );
+                    target_right = target_right.max(
+                        layouts[(head.row() - start_row) as usize].x_for_index(end_column as usize)
+                            + max_glyph_width,
+                    );
+                }
+            }
+        }
+
+        target_right = target_right.min(scroll_width);
+
+        if target_right - target_left > viewport_width {
+            return false;
+        }
+
+        let scroll_left = self.scroll_manager.anchor.offset.x() * max_glyph_width;
+        let scroll_right = scroll_left + viewport_width;
+
+        if target_left < scroll_left {
+            self.scroll_manager
+                .anchor
+                .offset
+                .set_x(target_left / max_glyph_width);
+            true
+        } else if target_right > scroll_right {
+            self.scroll_manager
+                .anchor
+                .offset
+                .set_x((target_right - viewport_width) / max_glyph_width);
+            true
+        } else {
+            false
+        }
+    }
+
+    pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext<Self>) {
+        self.scroll_manager.autoscroll_request = Some((autoscroll, true));
+        cx.notify();
+    }
+
+    pub(crate) fn request_autoscroll_remotely(
+        &mut self,
+        autoscroll: Autoscroll,
+        cx: &mut ViewContext<Self>,
+    ) {
+        self.scroll_manager.autoscroll_request = Some((autoscroll, false));
+        cx.notify();
+    }
+}

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

@@ -0,0 +1,48 @@
+use gpui::ViewContext;
+use serde::Deserialize;
+use util::iife;
+
+use crate::Editor;
+
+#[derive(Clone, PartialEq, Deserialize)]
+pub enum ScrollAmount {
+    LineUp,
+    LineDown,
+    HalfPageUp,
+    HalfPageDown,
+    PageUp,
+    PageDown,
+}
+
+impl ScrollAmount {
+    pub fn move_context_menu_selection(
+        &self,
+        editor: &mut Editor,
+        cx: &mut ViewContext<Editor>,
+    ) -> bool {
+        iife!({
+            let context_menu = editor.context_menu.as_mut()?;
+
+            match self {
+                Self::LineDown | Self::HalfPageDown => context_menu.select_next(cx),
+                Self::LineUp | Self::HalfPageUp => context_menu.select_prev(cx),
+                Self::PageDown => context_menu.select_last(cx),
+                Self::PageUp => context_menu.select_first(cx),
+            }
+            .then_some(())
+        })
+        .is_some()
+    }
+
+    pub fn lines(&self, editor: &mut Editor) -> f32 {
+        match self {
+            Self::LineDown => 1.,
+            Self::LineUp => -1.,
+            Self::HalfPageDown => editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
+            Self::HalfPageUp => -editor.visible_line_count().map(|l| l / 2.).unwrap_or(1.),
+            // Minus 1. here so that there is a pivot line that stays on the screen
+            Self::PageDown => editor.visible_line_count().unwrap_or(1.) - 1.,
+            Self::PageUp => -editor.visible_line_count().unwrap_or(1.) - 1.,
+        }
+    }
+}

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

@@ -61,7 +61,7 @@ impl SelectionsCollection {
         self.buffer.read(cx).read(cx)
     }
 
-    pub fn set_state(&mut self, other: &SelectionsCollection) {
+    pub fn clone_state(&mut self, other: &SelectionsCollection) {
         self.next_selection_id = other.next_selection_id;
         self.line_mode = other.line_mode;
         self.disjoint = other.disjoint.clone();

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

@@ -1,6 +1,6 @@
 use std::sync::Arc;
 
-use editor::{display_map::ToDisplayPoint, Autoscroll, DisplayPoint, Editor};
+use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
 use gpui::{
     actions, elements::*, geometry::vector::Vector2F, AnyViewHandle, Axis, Entity,
     MutableAppContext, RenderContext, View, ViewContext, ViewHandle,

crates/gpui/src/app.rs πŸ”—

@@ -594,6 +594,9 @@ type ReleaseObservationCallback = Box<dyn FnOnce(&dyn Any, &mut MutableAppContex
 type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
 type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
 type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type KeystrokeCallback = Box<
+    dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
+>;
 type DeserializeActionCallback = fn(json: &str) -> anyhow::Result<Box<dyn Action>>;
 type WindowShouldCloseSubscriptionCallback = Box<dyn FnMut(&mut MutableAppContext) -> bool>;
 
@@ -619,6 +622,7 @@ pub struct MutableAppContext {
     observations: CallbackCollection<usize, ObservationCallback>,
     window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
     window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
+    keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
 
     release_observations: Arc<Mutex<HashMap<usize, BTreeMap<usize, ReleaseObservationCallback>>>>,
     action_dispatch_observations: Arc<Mutex<BTreeMap<usize, ActionObservationCallback>>>,
@@ -678,6 +682,7 @@ impl MutableAppContext {
             global_observations: Default::default(),
             window_activation_observations: Default::default(),
             window_fullscreen_observations: Default::default(),
+            keystroke_observations: Default::default(),
             action_dispatch_observations: Default::default(),
             presenters_and_platform_windows: Default::default(),
             foreground,
@@ -763,11 +768,11 @@ impl MutableAppContext {
             .with_context(|| format!("invalid data for action {}", name))
     }
 
-    pub fn add_action<A, V, F>(&mut self, handler: F)
+    pub fn add_action<A, V, F, R>(&mut self, handler: F)
     where
         A: Action,
         V: View,
-        F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
+        F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
     {
         self.add_action_internal(handler, false)
     }
@@ -781,11 +786,11 @@ impl MutableAppContext {
         self.add_action_internal(handler, true)
     }
 
-    fn add_action_internal<A, V, F>(&mut self, mut handler: F, capture: bool)
+    fn add_action_internal<A, V, F, R>(&mut self, mut handler: F, capture: bool)
     where
         A: Action,
         V: View,
-        F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>),
+        F: 'static + FnMut(&mut V, &A, &mut ViewContext<V>) -> R,
     {
         let handler = Box::new(
             move |view: &mut dyn AnyView,
@@ -1255,6 +1260,27 @@ impl MutableAppContext {
         }
     }
 
+    pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
+    where
+        F: 'static
+            + FnMut(
+                &Keystroke,
+                &MatchResult,
+                Option<&Box<dyn Action>>,
+                &mut MutableAppContext,
+            ) -> bool,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        self.keystroke_observations
+            .add_callback(window_id, subscription_id, Box::new(callback));
+
+        Subscription::KeystrokeObservation {
+            id: subscription_id,
+            window_id,
+            observations: Some(self.keystroke_observations.downgrade()),
+        }
+    }
+
     pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) {
         self.pending_effects.push_back(Effect::Deferred {
             callback: Box::new(callback),
@@ -1538,27 +1564,39 @@ impl MutableAppContext {
                 })
                 .collect();
 
-            match self
+            let match_result = self
                 .keystroke_matcher
-                .push_keystroke(keystroke.clone(), dispatch_path)
-            {
+                .push_keystroke(keystroke.clone(), dispatch_path);
+            let mut handled_by = None;
+
+            let keystroke_handled = match &match_result {
                 MatchResult::None => false,
                 MatchResult::Pending => true,
                 MatchResult::Matches(matches) => {
                     for (view_id, action) in matches {
                         if self.handle_dispatch_action_from_effect(
                             window_id,
-                            Some(view_id),
+                            Some(*view_id),
                             action.as_ref(),
                         ) {
                             self.keystroke_matcher.clear_pending();
-                            return true;
+                            handled_by = Some(action.boxed_clone());
+                            break;
                         }
                     }
-                    false
+                    handled_by.is_some()
                 }
-            }
+            };
+
+            self.keystroke(
+                window_id,
+                keystroke.clone(),
+                handled_by,
+                match_result.clone(),
+            );
+            keystroke_handled
         } else {
+            self.keystroke(window_id, keystroke.clone(), None, MatchResult::None);
             false
         }
     }
@@ -2110,6 +2148,12 @@ impl MutableAppContext {
                         } => {
                             self.handle_window_should_close_subscription_effect(window_id, callback)
                         }
+                        Effect::Keystroke {
+                            window_id,
+                            keystroke,
+                            handled_by,
+                            result,
+                        } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result),
                     }
                     self.pending_notifications.clear();
                     self.remove_dropped_entities();
@@ -2188,6 +2232,21 @@ impl MutableAppContext {
         });
     }
 
+    fn keystroke(
+        &mut self,
+        window_id: usize,
+        keystroke: Keystroke,
+        handled_by: Option<Box<dyn Action>>,
+        result: MatchResult,
+    ) {
+        self.pending_effects.push_back(Effect::Keystroke {
+            window_id,
+            keystroke,
+            handled_by,
+            result,
+        });
+    }
+
     pub fn refresh_windows(&mut self) {
         self.pending_effects.push_back(Effect::RefreshWindows);
     }
@@ -2299,6 +2358,21 @@ impl MutableAppContext {
         });
     }
 
+    fn handle_keystroke_effect(
+        &mut self,
+        window_id: usize,
+        keystroke: Keystroke,
+        handled_by: Option<Box<dyn Action>>,
+        result: MatchResult,
+    ) {
+        self.update(|this| {
+            let mut observations = this.keystroke_observations.clone();
+            observations.emit_and_cleanup(window_id, this, {
+                move |callback, this| callback(&keystroke, &result, handled_by.as_ref(), this)
+            });
+        });
+    }
+
     fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) {
         //Short circuit evaluation if we're already g2g
         if self
@@ -2852,6 +2926,12 @@ pub enum Effect {
         subscription_id: usize,
         callback: WindowFullscreenCallback,
     },
+    Keystroke {
+        window_id: usize,
+        keystroke: Keystroke,
+        handled_by: Option<Box<dyn Action>>,
+        result: MatchResult,
+    },
     RefreshWindows,
     DispatchActionFrom {
         window_id: usize,
@@ -2995,6 +3075,21 @@ impl Debug for Effect {
                 .debug_struct("Effect::WindowShouldCloseSubscription")
                 .field("window_id", window_id)
                 .finish(),
+            Effect::Keystroke {
+                window_id,
+                keystroke,
+                handled_by,
+                result,
+            } => f
+                .debug_struct("Effect::Keystroke")
+                .field("window_id", window_id)
+                .field("keystroke", keystroke)
+                .field(
+                    "keystroke",
+                    &handled_by.as_ref().map(|handled_by| handled_by.name()),
+                )
+                .field("result", result)
+                .finish(),
         }
     }
 }
@@ -3826,6 +3921,33 @@ impl<'a, T: View> ViewContext<'a, T> {
             })
     }
 
+    pub fn observe_keystroke<F>(&mut self, mut callback: F) -> Subscription
+    where
+        F: 'static
+            + FnMut(
+                &mut T,
+                &Keystroke,
+                Option<&Box<dyn Action>>,
+                &MatchResult,
+                &mut ViewContext<T>,
+            ) -> bool,
+    {
+        let observer = self.weak_handle();
+        self.app.observe_keystrokes(
+            self.window_id(),
+            move |keystroke, result, handled_by, cx| {
+                if let Some(observer) = observer.upgrade(cx) {
+                    observer.update(cx, |observer, cx| {
+                        callback(observer, keystroke, handled_by, result, cx);
+                    });
+                    true
+                } else {
+                    false
+                }
+            },
+        )
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -5018,6 +5140,11 @@ pub enum Subscription {
         window_id: usize,
         observations: Option<Weak<Mapping<usize, WindowFullscreenCallback>>>,
     },
+    KeystrokeObservation {
+        id: usize,
+        window_id: usize,
+        observations: Option<Weak<Mapping<usize, KeystrokeCallback>>>,
+    },
 
     ReleaseObservation {
         id: usize,
@@ -5056,6 +5183,9 @@ impl Subscription {
             Subscription::ActionObservation { observations, .. } => {
                 observations.take();
             }
+            Subscription::KeystrokeObservation { observations, .. } => {
+                observations.take();
+            }
             Subscription::WindowActivationObservation { observations, .. } => {
                 observations.take();
             }
@@ -5175,6 +5305,27 @@ impl Drop for Subscription {
                     observations.lock().remove(id);
                 }
             }
+            Subscription::KeystrokeObservation {
+                id,
+                window_id,
+                observations,
+            } => {
+                if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) {
+                    match observations
+                        .lock()
+                        .entry(*window_id)
+                        .or_default()
+                        .entry(*id)
+                    {
+                        btree_map::Entry::Vacant(entry) => {
+                            entry.insert(None);
+                        }
+                        btree_map::Entry::Occupied(entry) => {
+                            entry.remove();
+                        }
+                    }
+                }
+            }
             Subscription::WindowActivationObservation {
                 id,
                 window_id,

crates/gpui/src/keymap.rs πŸ”—

@@ -112,6 +112,21 @@ impl PartialEq for MatchResult {
 
 impl Eq for MatchResult {}
 
+impl Clone for MatchResult {
+    fn clone(&self) -> Self {
+        match self {
+            MatchResult::None => MatchResult::None,
+            MatchResult::Pending => MatchResult::Pending,
+            MatchResult::Matches(matches) => MatchResult::Matches(
+                matches
+                    .iter()
+                    .map(|(view_id, action)| (*view_id, Action::boxed_clone(action.as_ref())))
+                    .collect(),
+            ),
+        }
+    }
+}
+
 impl Matcher {
     pub fn new(keymap: Keymap) -> Self {
         Self {

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

@@ -1,5 +1,5 @@
 use chrono::{Datelike, Local, NaiveTime, Timelike};
-use editor::{Autoscroll, Editor};
+use editor::{scroll::autoscroll::Autoscroll, Editor};
 use gpui::{actions, MutableAppContext};
 use settings::{HourFormat, Settings};
 use std::{

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

@@ -1,6 +1,6 @@
 use editor::{
-    combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint, Anchor, AnchorRangeExt,
-    Autoscroll, DisplayPoint, Editor, ToPoint,
+    combine_syntax_and_fuzzy_match_highlights, display_map::ToDisplayPoint,
+    scroll::autoscroll::Autoscroll, Anchor, AnchorRangeExt, DisplayPoint, Editor, ToPoint,
 };
 use fuzzy::StringMatch;
 use gpui::{

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

@@ -1,5 +1,6 @@
 use editor::{
-    combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor,
+    combine_syntax_and_fuzzy_match_highlights, scroll::autoscroll::Autoscroll,
+    styled_runs_for_code_label, Bias, Editor,
 };
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{

crates/search/src/project_search.rs πŸ”—

@@ -4,8 +4,8 @@ use crate::{
 };
 use collections::HashMap;
 use editor::{
-    items::active_match_index, Anchor, Autoscroll, Editor, MultiBuffer, SelectAll,
-    MAX_TAB_TITLE_LEN,
+    items::active_match_index, scroll::autoscroll::Autoscroll, Anchor, Editor, MultiBuffer,
+    SelectAll, MAX_TAB_TITLE_LEN,
 };
 use gpui::{
     actions, elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox,

crates/util/src/lib.rs πŸ”—

@@ -216,6 +216,8 @@ pub fn unzip_option<T, U>(option: Option<(T, U)>) -> (Option<T>, Option<U>) {
     }
 }
 
+/// Immediately invoked function expression. Good for using the ? operator
+/// in functions which do not return an Option or Result
 #[macro_export]
 macro_rules! iife {
     ($block:block) => {
@@ -223,6 +225,8 @@ macro_rules! iife {
     };
 }
 
+/// Async lImmediately invoked function expression. Good for using the ? operator
+/// in functions which do not return an Option or Result. Async version of above
 #[macro_export]
 macro_rules! async_iife {
     ($block:block) => {

crates/vim/src/editor_events.rs πŸ”—

@@ -22,20 +22,9 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont
         vim.active_editor = Some(editor.downgrade());
         vim.selection_subscription = Some(cx.subscribe(editor, |editor, event, cx| {
             if editor.read(cx).leader_replica_id().is_none() {
-                match event {
-                    editor::Event::SelectionsChanged { local: true } => {
-                        let newest_empty =
-                            editor.read(cx).selections.newest::<usize>(cx).is_empty();
-                        editor_local_selections_changed(newest_empty, cx);
-                    }
-                    editor::Event::IgnoredInput => {
-                        Vim::update(cx, |vim, cx| {
-                            if vim.active_operator().is_some() {
-                                vim.clear_operator(cx);
-                            }
-                        });
-                    }
-                    _ => (),
+                if let editor::Event::SelectionsChanged { local: true } = event {
+                    let newest_empty = editor.read(cx).selections.newest::<usize>(cx).is_empty();
+                    editor_local_selections_changed(newest_empty, cx);
                 }
             }
         }));

crates/vim/src/insert.rs πŸ”—

@@ -1,5 +1,5 @@
 use crate::{state::Mode, Vim};
-use editor::{Autoscroll, Bias};
+use editor::{scroll::autoscroll::Autoscroll, Bias};
 use gpui::{actions, MutableAppContext, ViewContext};
 use language::SelectionGoal;
 use workspace::Workspace;

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

@@ -2,7 +2,7 @@ mod change;
 mod delete;
 mod yank;
 
-use std::borrow::Cow;
+use std::{borrow::Cow, cmp::Ordering};
 
 use crate::{
     motion::Motion,
@@ -12,10 +12,13 @@ use crate::{
 };
 use collections::{HashMap, HashSet};
 use editor::{
-    display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, ClipboardSelection, DisplayPoint,
+    display_map::ToDisplayPoint,
+    scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount},
+    Anchor, Bias, ClipboardSelection, DisplayPoint, Editor,
 };
-use gpui::{actions, MutableAppContext, ViewContext};
+use gpui::{actions, impl_actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, Point, SelectionGoal};
+use serde::Deserialize;
 use workspace::Workspace;
 
 use self::{
@@ -24,6 +27,9 @@ use self::{
     yank::{yank_motion, yank_object},
 };
 
+#[derive(Clone, PartialEq, Deserialize)]
+struct Scroll(ScrollAmount);
+
 actions!(
     vim,
     [
@@ -41,6 +47,8 @@ actions!(
     ]
 );
 
+impl_actions!(vim, [Scroll]);
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(insert_after);
     cx.add_action(insert_first_non_whitespace);
@@ -72,6 +80,13 @@ pub fn init(cx: &mut MutableAppContext) {
         })
     });
     cx.add_action(paste);
+    cx.add_action(|_: &mut Workspace, Scroll(amount): &Scroll, cx| {
+        Vim::update(cx, |vim, cx| {
+            vim.update_active_editor(cx, |editor, cx| {
+                scroll(editor, amount, cx);
+            })
+        })
+    });
 }
 
 pub fn normal_motion(
@@ -367,6 +382,46 @@ fn paste(_: &mut Workspace, _: &Paste, cx: &mut ViewContext<Workspace>) {
     });
 }
 
+fn scroll(editor: &mut Editor, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+    let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
+    editor.scroll_screen(amount, cx);
+    if should_move_cursor {
+        let selection_ordering = editor.newest_selection_on_screen(cx);
+        if selection_ordering.is_eq() {
+            return;
+        }
+
+        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
+            visible_rows as u32
+        } else {
+            return;
+        };
+
+        let scroll_margin_rows = editor.vertical_scroll_margin() as u32;
+        let top_anchor = editor.scroll_manager.anchor().top_anchor;
+
+        editor.change_selections(None, cx, |s| {
+            s.replace_cursors_with(|snapshot| {
+                let mut new_point = top_anchor.to_display_point(&snapshot);
+
+                match selection_ordering {
+                    Ordering::Less => {
+                        *new_point.row_mut() += scroll_margin_rows;
+                        new_point = snapshot.clip_point(new_point, Bias::Right);
+                    }
+                    Ordering::Greater => {
+                        *new_point.row_mut() += visible_rows - scroll_margin_rows as u32;
+                        new_point = snapshot.clip_point(new_point, Bias::Left);
+                    }
+                    Ordering::Equal => unreachable!(),
+                }
+
+                vec![new_point]
+            })
+        });
+    }
+}
+
 #[cfg(test)]
 mod test {
     use indoc::indoc;

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

@@ -1,6 +1,7 @@
 use crate::{motion::Motion, object::Object, state::Mode, utils::copy_selections_content, Vim};
 use editor::{
-    char_kind, display_map::DisplaySnapshot, movement, Autoscroll, CharKind, DisplayPoint,
+    char_kind, display_map::DisplaySnapshot, movement, scroll::autoscroll::Autoscroll, CharKind,
+    DisplayPoint,
 };
 use gpui::MutableAppContext;
 use language::Selection;
@@ -199,7 +200,6 @@ mod test {
                 Test test
                 Λ‡test"})
             .await;
-        println!("Marker");
         cx.assert(indoc! {"
                 Test test
                 Λ‡

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

@@ -1,6 +1,6 @@
 use crate::{motion::Motion, object::Object, utils::copy_selections_content, Vim};
 use collections::{HashMap, HashSet};
-use editor::{display_map::ToDisplayPoint, Autoscroll, Bias};
+use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias};
 use gpui::MutableAppContext;
 
 pub fn delete_motion(vim: &mut Vim, motion: Motion, times: usize, cx: &mut MutableAppContext) {

crates/vim/src/state.rs πŸ”—

@@ -18,6 +18,7 @@ impl Default for Mode {
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
 pub enum Namespace {
     G,
+    Z,
 }
 
 #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)]
@@ -95,6 +96,7 @@ impl Operator {
         let operator_context = match operator {
             Some(Operator::Number(_)) => "n",
             Some(Operator::Namespace(Namespace::G)) => "g",
+            Some(Operator::Namespace(Namespace::Z)) => "z",
             Some(Operator::Object { around: false }) => "i",
             Some(Operator::Object { around: true }) => "a",
             Some(Operator::Change) => "c",

crates/vim/src/test/vim_test_context.rs πŸ”—

@@ -51,8 +51,9 @@ impl<'a> VimTestContext<'a> {
             )
         });
 
-        // Setup search toolbars
+        // Setup search toolbars and keypress hook
         workspace.update(cx, |workspace, cx| {
+            observe_keypresses(window_id, cx);
             workspace.active_pane().update(cx, |pane, cx| {
                 pane.toolbar().update(cx, |toolbar, cx| {
                     let buffer_search_bar = cx.add_view(BufferSearchBar::new);

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

@@ -81,6 +81,25 @@ pub fn init(cx: &mut MutableAppContext) {
     .detach();
 }
 
+// Any keystrokes not mapped to vim should clear the active operator
+pub fn observe_keypresses(window_id: usize, cx: &mut MutableAppContext) {
+    cx.observe_keystrokes(window_id, |_keystroke, _result, handled_by, cx| {
+        if let Some(handled_by) = handled_by {
+            if handled_by.namespace() == "vim" {
+                return true;
+            }
+        }
+
+        Vim::update(cx, |vim, cx| {
+            if vim.active_operator().is_some() {
+                vim.clear_operator(cx);
+            }
+        });
+        true
+    })
+    .detach()
+}
+
 #[derive(Default)]
 pub struct Vim {
     editors: HashMap<usize, WeakViewHandle<Editor>>,

crates/vim/src/visual.rs πŸ”—

@@ -1,7 +1,9 @@
 use std::borrow::Cow;
 
 use collections::HashMap;
-use editor::{display_map::ToDisplayPoint, Autoscroll, Bias, ClipboardSelection};
+use editor::{
+    display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Bias, ClipboardSelection,
+};
 use gpui::{actions, MutableAppContext, ViewContext};
 use language::{AutoindentMode, SelectionGoal};
 use workspace::Workspace;

crates/workspace/src/dock.rs πŸ”—

@@ -175,21 +175,16 @@ impl Dock {
         new_position: DockPosition,
         cx: &mut ViewContext<Workspace>,
     ) {
-        dbg!("starting", &new_position);
         workspace.dock.position = new_position;
         // Tell the pane about the new anchor position
         workspace.dock.pane.update(cx, |pane, cx| {
-            dbg!("setting docked");
             pane.set_docked(Some(new_position.anchor()), cx)
         });
 
         if workspace.dock.position.is_visible() {
-            dbg!("dock is visible");
             // Close the right sidebar if the dock is on the right side and the right sidebar is open
             if workspace.dock.position.anchor() == DockAnchor::Right {
-                dbg!("dock anchor is right");
                 if workspace.right_sidebar().read(cx).is_open() {
-                    dbg!("Toggling right sidebar");
                     workspace.toggle_sidebar(SidebarSide::Right, cx);
                 }
             }
@@ -199,10 +194,8 @@ impl Dock {
             if pane.read(cx).items().next().is_none() {
                 let item_to_add = (workspace.dock.default_item_factory)(workspace, cx);
                 // Adding the item focuses the pane by default
-                dbg!("Adding item to dock");
                 Pane::add_item(workspace, &pane, item_to_add, true, true, None, cx);
             } else {
-                dbg!("just focusing dock");
                 cx.focus(pane);
             }
         } else if let Some(last_active_center_pane) = workspace
@@ -214,7 +207,6 @@ impl Dock {
         }
         cx.emit(crate::Event::DockAnchorChanged);
         workspace.serialize_workspace(cx);
-        dbg!("Serializing workspace after dock position changed");
         cx.notify();
     }
 

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

@@ -324,6 +324,9 @@ pub fn initialize_workspace(
 
     auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 
+    let window_id = cx.window_id();
+    vim::observe_keypresses(window_id, cx);
+
     cx.on_window_should_close(|workspace, cx| {
         if let Some(task) = workspace.close(&Default::default(), cx) {
             task.detach_and_log_err(cx);
@@ -613,7 +616,7 @@ fn schema_file_match(path: &Path) -> &Path {
 mod tests {
     use super::*;
     use assets::Assets;
-    use editor::{Autoscroll, DisplayPoint, Editor};
+    use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor};
     use gpui::{
         executor::Deterministic, AssetSource, MutableAppContext, TestAppContext, ViewHandle,
     };