Add ability to scroll popovers with vim (#12650)

Conrad Irwin created

Co-Authored-By: ahmadraheel@gmail.com



Release Notes:

- vim: allow scrolling the currently open information overlay using
`ctrl-{u,d,e,y}`etc. (#11883)

Change summary

crates/editor/src/hover_links.rs          | 22 ++++++++++++++++
crates/editor/src/hover_popover.rs        | 32 +++++++++++++++++-------
crates/editor/src/scroll/scroll_amount.rs | 10 +++++++
crates/gpui/src/elements/div.rs           | 12 +++++++-
crates/vim/src/normal/scroll.rs           |  4 +++
5 files changed, 67 insertions(+), 13 deletions(-)

Detailed changes

crates/editor/src/hover_links.rs 🔗

@@ -1,5 +1,6 @@
 use crate::{
     hover_popover::{self, InlayHover},
+    scroll::ScrollAmount,
     Anchor, Editor, EditorSnapshot, FindAllReferences, GoToDefinition, GoToTypeDefinition, InlayId,
     PointForPosition, SelectPhase,
 };
@@ -38,7 +39,11 @@ impl RangeInEditor {
         }
     }
 
-    fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool {
+    pub fn point_within_range(
+        &self,
+        trigger_point: &TriggerPoint,
+        snapshot: &EditorSnapshot,
+    ) -> bool {
         match (self, trigger_point) {
             (Self::Text(range), TriggerPoint::Text(point)) => {
                 let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le();
@@ -169,6 +174,21 @@ impl Editor {
         .detach();
     }
 
+    pub fn scroll_hover(&mut self, amount: &ScrollAmount, cx: &mut ViewContext<Self>) -> bool {
+        let selection = self.selections.newest_anchor().head();
+        let snapshot = self.snapshot(cx);
+
+        let Some(popover) = self.hover_state.info_popovers.iter().find(|popover| {
+            popover
+                .symbol_range
+                .point_within_range(&TriggerPoint::Text(selection), &snapshot)
+        }) else {
+            return false;
+        };
+        popover.scroll(amount, cx);
+        true
+    }
+
     fn cmd_click_reveal_task(
         &mut self,
         point: PointForPosition,

crates/editor/src/hover_popover.rs 🔗

@@ -1,14 +1,15 @@
 use crate::{
     display_map::{InlayOffset, ToDisplayPoint},
     hover_links::{InlayHighlight, RangeInEditor},
+    scroll::ScrollAmount,
     Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
     EditorStyle, ExcerptId, Hover, RangeToAnchorExt,
 };
 use futures::{stream::FuturesUnordered, FutureExt};
 use gpui::{
     div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
-    ParentElement, Pixels, SharedString, Size, StatefulInteractiveElement, Styled, Task,
-    ViewContext, WeakView,
+    ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
+    Task, ViewContext, WeakView,
 };
 use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
 
@@ -118,6 +119,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
                 let hover_popover = InfoPopover {
                     symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
                     parsed_content,
+                    scroll_handle: ScrollHandle::new(),
                 };
 
                 this.update(&mut cx, |this, cx| {
@@ -317,6 +319,7 @@ fn show_hover(
                         InfoPopover {
                             symbol_range: RangeInEditor::Text(range),
                             parsed_content,
+                            scroll_handle: ScrollHandle::new(),
                         },
                     )
                 })
@@ -423,7 +426,7 @@ async fn parse_blocks(
     }
 }
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 pub struct HoverState {
     pub info_popovers: Vec<InfoPopover>,
     pub diagnostic_popover: Option<DiagnosticPopover>,
@@ -487,10 +490,11 @@ impl HoverState {
     }
 }
 
-#[derive(Debug, Clone)]
+#[derive(Clone, Debug)]
 pub struct InfoPopover {
-    symbol_range: RangeInEditor,
-    parsed_content: ParsedMarkdown,
+    pub symbol_range: RangeInEditor,
+    pub parsed_content: ParsedMarkdown,
+    pub scroll_handle: ScrollHandle,
 }
 
 impl InfoPopover {
@@ -504,23 +508,33 @@ impl InfoPopover {
         div()
             .id("info_popover")
             .elevation_2(cx)
-            .p_2()
             .overflow_y_scroll()
+            .track_scroll(&self.scroll_handle)
             .max_w(max_size.width)
             .max_h(max_size.height)
             // Prevent a mouse down/move on the popover from being propagated to the editor,
             // because that would dismiss the popover.
             .on_mouse_move(|_, cx| cx.stop_propagation())
             .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
-            .child(crate::render_parsed_markdown(
+            .child(div().p_2().child(crate::render_parsed_markdown(
                 "content",
                 &self.parsed_content,
                 style,
                 workspace,
                 cx,
-            ))
+            )))
             .into_any_element()
     }
+
+    pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
+        let mut current = self.scroll_handle.offset();
+        current.y -= amount.pixels(
+            cx.line_height(),
+            self.scroll_handle.bounds().size.height - px(16.),
+        ) / 2.0;
+        cx.notify();
+        self.scroll_handle.set_offset(current);
+    }
 }
 
 #[derive(Debug, Clone)]

crates/editor/src/scroll/scroll_amount.rs 🔗

@@ -1,7 +1,8 @@
 use crate::Editor;
 use serde::Deserialize;
+use ui::{px, Pixels};
 
-#[derive(Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Deserialize)]
 pub enum ScrollAmount {
     // Scroll N lines (positive is towards the end of the document)
     Line(f32),
@@ -25,4 +26,11 @@ impl ScrollAmount {
                 .unwrap_or(0.),
         }
     }
+
+    pub fn pixels(&self, line_height: Pixels, height: Pixels) -> Pixels {
+        match self {
+            ScrollAmount::Line(x) => px(line_height.0 * x),
+            ScrollAmount::Page(x) => px(height.0 * x),
+        }
+    }
 }

crates/gpui/src/elements/div.rs 🔗

@@ -2437,7 +2437,7 @@ where
     }
 }
 
-#[derive(Default)]
+#[derive(Default, Debug)]
 struct ScrollHandleState {
     offset: Rc<RefCell<Point<Pixels>>>,
     bounds: Bounds<Pixels>,
@@ -2449,7 +2449,7 @@ struct ScrollHandleState {
 /// A handle to the scrollable aspects of an element.
 /// Used for accessing scroll state, like the current scroll offset,
 /// and for mutating the scroll state, like scrolling to a specific child.
-#[derive(Clone)]
+#[derive(Clone, Debug)]
 pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
 
 impl Default for ScrollHandle {
@@ -2526,6 +2526,14 @@ impl ScrollHandle {
         }
     }
 
+    /// Set the offset explicitly. The offset is the distance from the top left of the
+    /// parent container to the top left of the first child.
+    /// As you scroll further down the offset becomes more negative.
+    pub fn set_offset(&self, mut position: Point<Pixels>) {
+        let state = self.0.borrow();
+        *state.offset.borrow_mut() = position;
+    }
+
     /// Get the logical scroll top, based on a child index and a pixel offset.
     pub fn logical_scroll_top(&self) -> (usize, Pixels) {
         let ix = self.top_item();

crates/vim/src/normal/scroll.rs 🔗

@@ -69,6 +69,10 @@ fn scroll_editor(
     let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
     let old_top_anchor = editor.scroll_manager.anchor().anchor;
 
+    if editor.scroll_hover(amount, cx) {
+        return;
+    }
+
     editor.scroll_screen(amount, cx);
     if should_move_cursor {
         let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {