From f000dfebd2aa35dfbc36de1b20ef4f10d2a4baaa Mon Sep 17 00:00:00 2001 From: Daniel Sauble Date: Wed, 2 Jul 2025 02:14:34 -0700 Subject: [PATCH] Add page up/down bindings to the Markdown preview (#33403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First time contributor here. 😊 I settled on markdown::MovePageUp and markdown::MovePageDown to match the names the editor uses for the same functionality. Closes #30246 Release Notes: - Support PgUp/PgDown in Markdown previews --- assets/keymaps/default-linux.json | 7 ++ assets/keymaps/default-macos.json | 7 ++ crates/gpui/src/elements/list.rs | 73 +++++++++++++++++++ .../markdown_preview/src/markdown_preview.rs | 8 +- .../src/markdown_preview_view.rs | 28 ++++++- 5 files changed, 119 insertions(+), 4 deletions(-) diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 9d1b040f56..0592dfca4e 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1068,6 +1068,13 @@ "ctrl-shift-tab": "pane::ActivatePreviousItem" } }, + { + "context": "MarkdownPreview", + "bindings": { + "pageup": "markdown::MovePageUp", + "pagedown": "markdown::MovePageDown" + } + }, { "context": "KeymapEditor", "use_key_equivalents": true, diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 3a4cbcfacc..48acf0c683 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1168,6 +1168,13 @@ "ctrl-shift-tab": "pane::ActivatePreviousItem" } }, + { + "context": "MarkdownPreview", + "bindings": { + "pageup": "markdown::MovePageUp", + "pagedown": "markdown::MovePageDown" + } + }, { "context": "KeymapEditor", "use_key_equivalents": true, diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index c9a08e968a..35a3b622b2 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -291,6 +291,31 @@ impl ListState { self.0.borrow().logical_scroll_top() } + /// Scroll the list by the given offset + pub fn scroll_by(&self, distance: Pixels) { + if distance == px(0.) { + return; + } + + let current_offset = self.logical_scroll_top(); + let state = &mut *self.0.borrow_mut(); + let mut cursor = state.items.cursor::(&()); + cursor.seek(&Count(current_offset.item_ix), Bias::Right, &()); + + let start_pixel_offset = cursor.start().height + current_offset.offset_in_item; + let new_pixel_offset = (start_pixel_offset + distance).max(px(0.)); + if new_pixel_offset > start_pixel_offset { + cursor.seek_forward(&Height(new_pixel_offset), Bias::Right, &()); + } else { + cursor.seek(&Height(new_pixel_offset), Bias::Right, &()); + } + + state.logical_scroll_top = Some(ListOffset { + item_ix: cursor.start().count, + offset_in_item: new_pixel_offset - cursor.start().height, + }); + } + /// Scroll the list to the given offset pub fn scroll_to(&self, mut scroll_top: ListOffset) { let state = &mut *self.0.borrow_mut(); @@ -1119,4 +1144,52 @@ mod test { assert_eq!(state.logical_scroll_top().item_ix, 0); assert_eq!(state.logical_scroll_top().offset_in_item, px(0.)); } + + #[gpui::test] + fn test_scroll_by_positive_and_negative_distance(cx: &mut TestAppContext) { + use crate::{ + AppContext, Context, Element, IntoElement, ListState, Render, Styled, Window, div, + list, point, px, size, + }; + + let cx = cx.add_empty_window(); + + let state = ListState::new(5, crate::ListAlignment::Top, px(10.), |_, _, _| { + div().h(px(20.)).w_full().into_any() + }); + + struct TestView(ListState); + impl Render for TestView { + fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { + list(self.0.clone()).w_full().h_full() + } + } + + // Paint + cx.draw(point(px(0.), px(0.)), size(px(100.), px(100.)), |_, cx| { + cx.new(|_| TestView(state.clone())) + }); + + // Test positive distance: start at item 1, move down 30px + state.scroll_by(px(30.)); + + // Should move to item 2 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 1); + assert_eq!(offset.offset_in_item, px(10.)); + + // Test negative distance: start at item 2, move up 30px + state.scroll_by(px(-30.)); + + // Should move back to item 1 + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + + // Test zero distance + state.scroll_by(px(0.)); + let offset = state.logical_scroll_top(); + assert_eq!(offset.item_ix, 0); + assert_eq!(offset.offset_in_item, px(0.)); + } } diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs index fad6355d8a..afc5b964b2 100644 --- a/crates/markdown_preview/src/markdown_preview.rs +++ b/crates/markdown_preview/src/markdown_preview.rs @@ -8,7 +8,13 @@ pub mod markdown_renderer; actions!( markdown, - [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview] + [ + MovePageUp, + MovePageDown, + OpenPreview, + OpenPreviewToTheSide, + OpenFollowingPreview + ] ); pub fn init(cx: &mut App) { diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index f22671d5df..03cfd7ee82 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -7,8 +7,8 @@ use editor::scroll::Autoscroll; use editor::{Editor, EditorEvent, SelectionEffects}; use gpui::{ App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, ListState, ParentElement, Render, RetainAllImageCache, Styled, Subscription, Task, - WeakEntity, Window, list, + IntoElement, IsZero, ListState, ParentElement, Render, RetainAllImageCache, Styled, + Subscription, Task, WeakEntity, Window, list, }; use language::LanguageRegistry; use settings::Settings; @@ -19,7 +19,7 @@ use workspace::{Pane, Workspace}; use crate::markdown_elements::ParsedMarkdownElement; use crate::{ - OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, + MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, markdown_renderer::{RenderContext, render_markdown_block}, @@ -530,6 +530,26 @@ impl MarkdownPreviewView { ) -> bool { !(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false)) } + + fn scroll_page_up(&mut self, _: &MovePageUp, _window: &mut Window, cx: &mut Context) { + let viewport_height = self.list_state.viewport_bounds().size.height; + if viewport_height.is_zero() { + return; + } + + self.list_state.scroll_by(-viewport_height); + cx.notify(); + } + + fn scroll_page_down(&mut self, _: &MovePageDown, _window: &mut Window, cx: &mut Context) { + let viewport_height = self.list_state.viewport_bounds().size.height; + if viewport_height.is_zero() { + return; + } + + self.list_state.scroll_by(viewport_height); + cx.notify(); + } } impl Focusable for MarkdownPreviewView { @@ -580,6 +600,8 @@ impl Render for MarkdownPreviewView { .id("MarkdownPreview") .key_context("MarkdownPreview") .track_focus(&self.focus_handle(cx)) + .on_action(cx.listener(MarkdownPreviewView::scroll_page_up)) + .on_action(cx.listener(MarkdownPreviewView::scroll_page_down)) .size_full() .bg(cx.theme().colors().editor_background) .p_4()