From 4e9ffa3ee18d5ac2dff78be7e6d20b6d614f0b3f Mon Sep 17 00:00:00 2001 From: andrew j <61884728+dremnik@users.noreply.github.com> Date: Tue, 17 Mar 2026 02:55:48 -0700 Subject: [PATCH] markdown_preview: Add ScrollToTop and ScrollToBottom actions (#50460) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- assets/keymaps/default-linux.json | 2 ++ assets/keymaps/default-macos.json | 2 ++ assets/keymaps/default-windows.json | 2 ++ assets/keymaps/vim.json | 2 ++ .../markdown_preview/src/markdown_preview.rs | 4 +++ .../src/markdown_preview_view.rs | 30 +++++++++++++++++-- 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index a79384ad0139b804f0ba7721e6f42260733c8e0c..ab82c5d3f82262472a58aa35c72b9d478786fb31 100644 --- a/assets/keymaps/default-linux.json +++ b/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", }, }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 14804998a08de962b1849d7b1a728d1d9d6f9778..ae930523e371a0cc8416da30c3e3384566cb3a4c 100644 --- a/assets/keymaps/default-macos.json +++ b/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", }, }, { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index 66896f43984dac73d7c098bfb46fb1a19568c14a..b8e87c4a8687f2fc20ba1f3aadcbf8fc97eef61a 100644 --- a/assets/keymaps/default-windows.json +++ b/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", }, }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 66693ab0a153a73af1dccb101e0ed36259b774fa..dc69909a60102ba96f819107bde2c7653b0db1c7 100644 --- a/assets/keymaps/vim.json +++ b/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", }, }, { diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs index c7e8e9e9272e196da25be086640316129fb819bd..0a657d27bc1416995d3c4df7f6793c017356fa0d 100644 --- a/crates/markdown_preview/src/markdown_preview.rs +++ b/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 ] diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index 7cf4cd844548ba9a67cd660c3b296f48e11d2937..b5213504e72a8e99c6405df85001fa615257dc0e 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/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.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, + ) { + 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()