markdown_preview: Add ScrollToTop and ScrollToBottom actions (#50460)

andrew j created

Add `gg`/`G` (vim), `cmd-up`/`cmd-down` (macOS), and
`ctrl-home`/`ctrl-end` (Linux/Windows) keybindings to scroll to the top
and bottom of the markdown preview.

The markdown preview already has page scroll (`ctrl-d`/`ctrl-u`), line
scroll (`ctrl-e`/`ctrl-y`), and item scroll (`alt-up`/`alt-down`) but
was missing top/bottom navigation. This adds two new actions —
`ScrollToTop` and `ScrollToBottom` — using the existing
`ListState::scroll_to()` infrastructure, following the same pattern as
the other scroll actions.

- [x] Done a self-review taking into account security and performance
aspects

Release Notes:

- Added scroll-to-top and scroll-to-bottom keybindings for markdown
preview (`gg`/`G` in vim mode, `cmd-up`/`cmd-down` on macOS,
`ctrl-home`/`ctrl-end` on Linux/Windows)

Change summary

assets/keymaps/default-linux.json                    |  2 
assets/keymaps/default-macos.json                    |  2 
assets/keymaps/default-windows.json                  |  2 
assets/keymaps/vim.json                              |  2 
crates/markdown_preview/src/markdown_preview.rs      |  4 +
crates/markdown_preview/src/markdown_preview_view.rs | 30 +++++++++++++
6 files changed, 40 insertions(+), 2 deletions(-)

Detailed changes

assets/keymaps/default-linux.json 🔗

@@ -1234,6 +1234,8 @@
       "down": "markdown::ScrollDown",
       "alt-up": "markdown::ScrollUpByItem",
       "alt-down": "markdown::ScrollDownByItem",
+      "ctrl-home": "markdown::ScrollToTop",
+      "ctrl-end": "markdown::ScrollToBottom",
     },
   },
   {

assets/keymaps/default-macos.json 🔗

@@ -1340,6 +1340,8 @@
       "down": "markdown::ScrollDown",
       "alt-up": "markdown::ScrollUpByItem",
       "alt-down": "markdown::ScrollDownByItem",
+      "cmd-up": "markdown::ScrollToTop",
+      "cmd-down": "markdown::ScrollToBottom",
     },
   },
   {

assets/keymaps/default-windows.json 🔗

@@ -1263,6 +1263,8 @@
       "down": "markdown::ScrollDown",
       "alt-up": "markdown::ScrollUpByItem",
       "alt-down": "markdown::ScrollDownByItem",
+      "ctrl-home": "markdown::ScrollToTop",
+      "ctrl-end": "markdown::ScrollToBottom",
     },
   },
   {

assets/keymaps/vim.json 🔗

@@ -1100,6 +1100,8 @@
       "ctrl-d": "markdown::ScrollPageDown",
       "ctrl-y": "markdown::ScrollUp",
       "ctrl-e": "markdown::ScrollDown",
+      "g g": "markdown::ScrollToTop",
+      "shift-g": "markdown::ScrollToBottom",
     },
   },
   {

crates/markdown_preview/src/markdown_preview.rs 🔗

@@ -26,6 +26,10 @@ actions!(
         ScrollUpByItem,
         /// Scrolls down by one markdown element in the markdown preview
         ScrollDownByItem,
+        /// Scrolls to the top of the markdown preview.
+        ScrollToTop,
+        /// Scrolls to the bottom of the markdown preview.
+        ScrollToBottom,
         /// Opens a following markdown preview that syncs with the editor.
         OpenFollowingPreview
     ]

crates/markdown_preview/src/markdown_preview_view.rs 🔗

@@ -8,7 +8,7 @@ use editor::scroll::Autoscroll;
 use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects};
 use gpui::{
     App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
-    IntoElement, IsZero, ListState, ParentElement, Render, RetainAllImageCache, Styled,
+    IntoElement, IsZero, ListOffset, ListState, ParentElement, Render, RetainAllImageCache, Styled,
     Subscription, Task, WeakEntity, Window, list,
 };
 use language::LanguageRegistry;
@@ -26,7 +26,7 @@ use crate::{
     markdown_parser::parse_markdown,
     markdown_renderer::{RenderContext, render_markdown_block},
 };
-use crate::{ScrollDown, ScrollDownByItem, ScrollUp, ScrollUpByItem};
+use crate::{ScrollDown, ScrollDownByItem, ScrollToBottom, ScrollToTop, ScrollUp, ScrollUpByItem};
 
 const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200);
 
@@ -511,6 +511,30 @@ impl MarkdownPreviewView {
         }
         cx.notify();
     }
+
+    fn scroll_to_top(&mut self, _: &ScrollToTop, _window: &mut Window, cx: &mut Context<Self>) {
+        self.list_state.scroll_to(ListOffset {
+            item_ix: 0,
+            offset_in_item: px(0.),
+        });
+        cx.notify();
+    }
+
+    fn scroll_to_bottom(
+        &mut self,
+        _: &ScrollToBottom,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let count = self.list_state.item_count();
+        if count > 0 {
+            self.list_state.scroll_to(ListOffset {
+                item_ix: count - 1,
+                offset_in_item: px(0.),
+            });
+        }
+        cx.notify();
+    }
 }
 
 impl Focusable for MarkdownPreviewView {
@@ -562,6 +586,8 @@ impl Render for MarkdownPreviewView {
             .on_action(cx.listener(MarkdownPreviewView::scroll_down))
             .on_action(cx.listener(MarkdownPreviewView::scroll_up_by_item))
             .on_action(cx.listener(MarkdownPreviewView::scroll_down_by_item))
+            .on_action(cx.listener(MarkdownPreviewView::scroll_to_top))
+            .on_action(cx.listener(MarkdownPreviewView::scroll_to_bottom))
             .size_full()
             .bg(cx.theme().colors().editor_background)
             .p_4()