Detailed changes
@@ -1068,6 +1068,13 @@
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
+ {
+ "context": "MarkdownPreview",
+ "bindings": {
+ "pageup": "markdown::MovePageUp",
+ "pagedown": "markdown::MovePageDown"
+ }
+ },
{
"context": "KeymapEditor",
"use_key_equivalents": true,
@@ -1168,6 +1168,13 @@
"ctrl-shift-tab": "pane::ActivatePreviousItem"
}
},
+ {
+ "context": "MarkdownPreview",
+ "bindings": {
+ "pageup": "markdown::MovePageUp",
+ "pagedown": "markdown::MovePageDown"
+ }
+ },
{
"context": "KeymapEditor",
"use_key_equivalents": true,
@@ -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::<ListItemSummary>(&());
+ 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<Self>) -> 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.));
+ }
}
@@ -8,7 +8,13 @@ pub mod markdown_renderer;
actions!(
markdown,
- [OpenPreview, OpenPreviewToTheSide, OpenFollowingPreview]
+ [
+ MovePageUp,
+ MovePageDown,
+ OpenPreview,
+ OpenPreviewToTheSide,
+ OpenFollowingPreview
+ ]
);
pub fn init(cx: &mut App) {
@@ -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<Self>) {
+ 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<Self>) {
+ 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()