From 07748b7baedc3f5dbd5867797545243a5ba583d4 Mon Sep 17 00:00:00 2001 From: KyleBarton Date: Thu, 11 Dec 2025 14:18:38 -0800 Subject: [PATCH] Add scrolling functionality to markdown preview mode (#44585) Closes #21324 Adds four new commands: - `markdown::MoveUp`, `markdown::MoveDown` - these scroll up and down in markdown preview mode, by no more than the height of a large headline. - `markdown::MoveUpByItem`, and `markdown::MoveDownByItem` - these scroll up and down by the height of the item at the top of the markdown preview window. So headlines and large codeblocks, for instance, scroll further than individual paragraph lines. Also attempts to create sensible defaults: `down` -> `markdown::ScrollDown` `up` -> `markdown::ScrollUp` `alt-down` -> `markdown::ScrollDownByItem` `alt-up` -> `markdown::ScrollUpByItem` And in Vim: `ctrl-u` -> `markdown::ScrollPageUp` `ctrl-d` -> `markdown::ScrollPageDown` `ctrl-e` -> `markdown::ScrollDown` `ctrl-y` -> `markdown::ScrollUp` Release Notes: - Added commands `markdown::ScrollUp`, `markdown::ScrollDown`, `markdown::ScrollUpByItem`, and `markdown::ScrollDownByItem` - Changed commands `markdown::MovePageUp` to `markdown::ScrollPageUp` and `markdown::MovePageDown` to `markdown::ScrollPageDown` --- Cargo.lock | 1 + assets/keymaps/default-linux.json | 8 ++- assets/keymaps/default-macos.json | 8 ++- assets/keymaps/default-windows.json | 8 ++- assets/keymaps/vim.json | 9 +++ .../markdown_preview/src/markdown_preview.rs | 14 +++- .../src/markdown_preview_view.rs | 67 ++++++++++++++++++- crates/vim/Cargo.toml | 1 + crates/vim/src/test/vim_test_context.rs | 1 + 9 files changed, 106 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ff1041695e..5de8cdd6a6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18097,6 +18097,7 @@ dependencies = [ "language", "log", "lsp", + "markdown_preview", "menu", "multi_buffer", "nvim-rs", diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json index 1fb02e6c1f..0bcbb455b5 100644 --- a/assets/keymaps/default-linux.json +++ b/assets/keymaps/default-linux.json @@ -1192,8 +1192,12 @@ { "context": "MarkdownPreview", "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" + "pageup": "markdown::ScrollPageUp", + "pagedown": "markdown::ScrollPageDown", + "up": "markdown::ScrollUp", + "down": "markdown::ScrollDown", + "alt-up": "markdown::ScrollUpByItem", + "alt-down": "markdown::ScrollDownByItem" } }, { diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json index 9edfaa03f8..65ac280ba7 100644 --- a/assets/keymaps/default-macos.json +++ b/assets/keymaps/default-macos.json @@ -1296,8 +1296,12 @@ { "context": "MarkdownPreview", "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" + "pageup": "markdown::ScrollPageUp", + "pagedown": "markdown::ScrollPageDown", + "up": "markdown::ScrollUp", + "down": "markdown::ScrollDown", + "alt-up": "markdown::ScrollUpByItem", + "alt-down": "markdown::ScrollDownByItem" } }, { diff --git a/assets/keymaps/default-windows.json b/assets/keymaps/default-windows.json index a4211e3c25..990902df14 100644 --- a/assets/keymaps/default-windows.json +++ b/assets/keymaps/default-windows.json @@ -1220,8 +1220,12 @@ "context": "MarkdownPreview", "use_key_equivalents": true, "bindings": { - "pageup": "markdown::MovePageUp", - "pagedown": "markdown::MovePageDown" + "pageup": "markdown::ScrollPageUp", + "pagedown": "markdown::ScrollPageDown", + "up": "markdown::ScrollUp", + "down": "markdown::ScrollDown", + "alt-up": "markdown::ScrollUpByItem", + "alt-down": "markdown::ScrollDownByItem" } }, { diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 7e5bc30c1f..533db14a5f 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -1046,5 +1046,14 @@ "g g": "settings_editor::FocusFirstNavEntry", "shift-g": "settings_editor::FocusLastNavEntry" } + }, + { + "context": "MarkdownPreview", + "bindings": { + "ctrl-u": "markdown::ScrollPageUp", + "ctrl-d": "markdown::ScrollPageDown", + "ctrl-y": "markdown::ScrollUp", + "ctrl-e": "markdown::ScrollDown" + } } ] diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs index 77bad89a62..61c99764ad 100644 --- a/crates/markdown_preview/src/markdown_preview.rs +++ b/crates/markdown_preview/src/markdown_preview.rs @@ -11,9 +11,19 @@ actions!( markdown, [ /// Scrolls up by one page in the markdown preview. - MovePageUp, + #[action(deprecated_aliases = ["markdown::MovePageUp"])] + ScrollPageUp, /// Scrolls down by one page in the markdown preview. - MovePageDown, + #[action(deprecated_aliases = ["markdown::MovePageDown"])] + ScrollPageDown, + /// Scrolls up by approximately one visual line. + ScrollUp, + /// Scrolls down by approximately one visual line. + ScrollDown, + /// Scrolls up by one markdown element in the markdown preview + ScrollUpByItem, + /// Scrolls down by one markdown element in the markdown preview + ScrollDownByItem, /// Opens a markdown preview for the current file. OpenPreview, /// Opens a markdown preview in a split pane. diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs index df8201dc7a..20613b112e 100644 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ b/crates/markdown_preview/src/markdown_preview_view.rs @@ -1,3 +1,4 @@ +use std::cmp::min; use std::sync::Arc; use std::time::Duration; use std::{ops::Range, path::PathBuf}; @@ -20,11 +21,12 @@ use workspace::{Pane, Workspace}; use crate::markdown_elements::ParsedMarkdownElement; use crate::markdown_renderer::CheckboxClickedEvent; use crate::{ - MovePageDown, MovePageUp, OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, + OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, ScrollPageDown, ScrollPageUp, markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, markdown_renderer::{RenderContext, render_markdown_block}, }; +use crate::{ScrollDown, ScrollDownByItem, ScrollUp, ScrollUpByItem}; const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); @@ -425,7 +427,7 @@ impl MarkdownPreviewView { !(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) { + fn scroll_page_up(&mut self, _: &ScrollPageUp, _window: &mut Window, cx: &mut Context) { let viewport_height = self.list_state.viewport_bounds().size.height; if viewport_height.is_zero() { return; @@ -435,7 +437,12 @@ impl MarkdownPreviewView { cx.notify(); } - fn scroll_page_down(&mut self, _: &MovePageDown, _window: &mut Window, cx: &mut Context) { + fn scroll_page_down( + &mut self, + _: &ScrollPageDown, + _window: &mut Window, + cx: &mut Context, + ) { let viewport_height = self.list_state.viewport_bounds().size.height; if viewport_height.is_zero() { return; @@ -444,6 +451,56 @@ impl MarkdownPreviewView { self.list_state.scroll_by(viewport_height); cx.notify(); } + + fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context) { + let scroll_top = self.list_state.logical_scroll_top(); + if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { + let item_height = bounds.size.height; + // Scroll no more than the rough equivalent of a large headline + let max_height = window.rem_size() * 2; + let scroll_height = min(item_height, max_height); + self.list_state.scroll_by(-scroll_height); + } + cx.notify(); + } + + fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context) { + let scroll_top = self.list_state.logical_scroll_top(); + if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { + let item_height = bounds.size.height; + // Scroll no more than the rough equivalent of a large headline + let max_height = window.rem_size() * 2; + let scroll_height = min(item_height, max_height); + self.list_state.scroll_by(scroll_height); + } + cx.notify(); + } + + fn scroll_up_by_item( + &mut self, + _: &ScrollUpByItem, + _window: &mut Window, + cx: &mut Context, + ) { + let scroll_top = self.list_state.logical_scroll_top(); + if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { + self.list_state.scroll_by(-bounds.size.height); + } + cx.notify(); + } + + fn scroll_down_by_item( + &mut self, + _: &ScrollDownByItem, + _window: &mut Window, + cx: &mut Context, + ) { + let scroll_top = self.list_state.logical_scroll_top(); + if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { + self.list_state.scroll_by(bounds.size.height); + } + cx.notify(); + } } impl Focusable for MarkdownPreviewView { @@ -496,6 +553,10 @@ impl Render for MarkdownPreviewView { .track_focus(&self.focus_handle(cx)) .on_action(cx.listener(MarkdownPreviewView::scroll_page_up)) .on_action(cx.listener(MarkdownPreviewView::scroll_page_down)) + .on_action(cx.listener(MarkdownPreviewView::scroll_up)) + .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)) .size_full() .bg(cx.theme().colors().editor_background) .p_4() diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index a5834f8550..74409a6c25 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -63,6 +63,7 @@ indoc.workspace = true language = { workspace = true, features = ["test-support"] } project = { workspace = true, features = ["test-support"] } lsp = { workspace = true, features = ["test-support"] } +markdown_preview.workspace = true parking_lot.workspace = true project_panel.workspace = true release_channel.workspace = true diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 80208fb23e..acd77839f2 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -28,6 +28,7 @@ impl VimTestContext { search::init(cx); theme::init(theme::LoadThemes::JustBase, cx); settings_ui::init(cx); + markdown_preview::init(cx); }); }