diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index ed8c1033a7e1af7dd690e22a27a89f93223f02bf..5e0b872695794d349a6e15cdf2c1f850fa3fb2ae 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -270,6 +270,7 @@ gpui::actions!( ScrollCursorBottom, ScrollCursorCenter, ScrollCursorTop, + ScrollCursorCenterTopBottom, SelectAll, SelectAllMatches, SelectDown, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8df833120aa87daec60132b98d1064151b54504a..9ba802426a964ad51ab89182f47972af908b1cdf 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -175,6 +175,7 @@ pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); +pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); pub fn render_parsed_markdown( element_id: impl Into, @@ -561,6 +562,26 @@ pub struct Editor { file_header_size: u32, breadcrumb_header: Option, focused_block: Option, + next_scroll_position: NextScrollCursorCenterTopBottom, + _scroll_cursor_center_top_bottom_task: Task<()>, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] +enum NextScrollCursorCenterTopBottom { + #[default] + Center, + Top, + Bottom, +} + +impl NextScrollCursorCenterTopBottom { + fn next(&self) -> Self { + match self { + Self::Center => Self::Top, + Self::Top => Self::Bottom, + Self::Bottom => Self::Center, + } + } } #[derive(Clone)] @@ -1895,6 +1916,8 @@ impl Editor { previous_search_ranges: None, breadcrumb_header: None, focused_block: None, + next_scroll_position: NextScrollCursorCenterTopBottom::default(), + _scroll_cursor_center_top_bottom_task: Task::ready(()), }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 77b22674f3e221ece3a56c461b0916540253ea42..7e194cac214dbdeb29481e8e5e9ec086df6123f6 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -13149,6 +13149,77 @@ async fn test_input_text(cx: &mut gpui::TestAppContext) { ); } +#[gpui::test] +async fn test_scroll_cursor_center_top_bottom(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#"let foo = 1; +let foo = 2; +let foo = 3; +let fooˇ = 4; +let foo = 5; +let foo = 6; +let foo = 7; +let foo = 8; +let foo = 9; +let foo = 10; +let foo = 11; +let foo = 12; +let foo = 13; +let foo = 14; +let foo = 15;"#, + ); + + cx.update_editor(|e, cx| { + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Center, + "Default next scroll direction is center", + ); + + e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, cx); + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Top, + "After center, next scroll direction should be top", + ); + + e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, cx); + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Bottom, + "After top, next scroll direction should be bottom", + ); + + e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, cx); + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Center, + "After bottom, scrolling should start over", + ); + + e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, cx); + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Top, + "Scrolling continues if retriggered fast enough" + ); + }); + + cx.executor() + .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200)); + cx.executor().run_until_parked(); + cx.update_editor(|e, _| { + assert_eq!( + e.next_scroll_position, + NextScrollCursorCenterTopBottom::Center, + "If scrolling is not triggered fast enough, it should reset" + ); + }); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(DisplayRow(row as u32), column as u32); point..point diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 46a7d9d17b62da83cd84ebdf5ca0d66ccc1c9eac..4f8382a6e98b55cf9c274585a33f341d89c54b7c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -222,6 +222,7 @@ impl EditorElement { register_action(view, cx, Editor::scroll_cursor_top); register_action(view, cx, Editor::scroll_cursor_center); register_action(view, cx, Editor::scroll_cursor_bottom); + register_action(view, cx, Editor::scroll_cursor_center_top_bottom); register_action(view, cx, |editor, _: &LineDown, cx| { editor.scroll_screen(&ScrollAmount::Line(1.), cx) }); diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index fabdf82d045a91e7b28f5eec7bd52b3271c7a5da..eb49ac62190938f8312cf1ffbeef9f9803873448 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -1,7 +1,8 @@ use super::Axis; use crate::{ - Autoscroll, Bias, Editor, EditorMode, NextScreen, ScrollAnchor, ScrollCursorBottom, - ScrollCursorCenter, ScrollCursorTop, + Autoscroll, Bias, Editor, EditorMode, NextScreen, NextScrollCursorCenterTopBottom, + ScrollAnchor, ScrollCursorBottom, ScrollCursorCenter, ScrollCursorCenterTopBottom, + ScrollCursorTop, SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT, }; use gpui::{Point, ViewContext}; @@ -32,6 +33,60 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } + pub fn scroll_cursor_center_top_bottom( + &mut self, + _: &ScrollCursorCenterTopBottom, + cx: &mut ViewContext, + ) { + let snapshot = self.snapshot(cx).display_snapshot; + let visible_rows = if let Some(visible_rows) = self.visible_line_count() { + visible_rows as u32 + } else { + return; + }; + + let scroll_margin_rows = self.vertical_scroll_margin() as u32; + let mut new_screen_top = self.selections.newest_display(cx).head(); + *new_screen_top.column_mut() = 0; + match self.next_scroll_position { + NextScrollCursorCenterTopBottom::Center => { + *new_screen_top.row_mut() = new_screen_top.row().0.saturating_sub(visible_rows / 2); + } + NextScrollCursorCenterTopBottom::Top => { + *new_screen_top.row_mut() = + new_screen_top.row().0.saturating_sub(scroll_margin_rows); + } + NextScrollCursorCenterTopBottom::Bottom => { + *new_screen_top.row_mut() = new_screen_top + .row() + .0 + .saturating_sub(visible_rows.saturating_sub(scroll_margin_rows)); + } + } + self.set_scroll_anchor( + ScrollAnchor { + anchor: snapshot + .buffer_snapshot + .anchor_before(new_screen_top.to_offset(&snapshot, Bias::Left)), + offset: Default::default(), + }, + cx, + ); + + self.next_scroll_position = self.next_scroll_position.next(); + self._scroll_cursor_center_top_bottom_task = + cx.spawn(|editor, mut cx: gpui::AsyncWindowContext| async move { + cx.background_executor() + .timer(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT) + .await; + editor + .update(&mut cx, |editor, _| { + editor.next_scroll_position = NextScrollCursorCenterTopBottom::default(); + }) + .ok(); + }); + } + pub fn scroll_cursor_top(&mut self, _: &ScrollCursorTop, cx: &mut ViewContext) { let snapshot = self.snapshot(cx).display_snapshot; let scroll_margin_rows = self.vertical_scroll_margin() as u32;