Detailed changes
@@ -270,6 +270,7 @@ gpui::actions!(
ScrollCursorBottom,
ScrollCursorCenter,
ScrollCursorTop,
+ ScrollCursorCenterTopBottom,
SelectAll,
SelectAllMatches,
SelectDown,
@@ -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<ElementId>,
@@ -561,6 +562,26 @@ pub struct Editor {
file_header_size: u32,
breadcrumb_header: Option<String>,
focused_block: Option<FocusedBlock>,
+ 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);
@@ -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<DisplayPoint> {
let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
point..point
@@ -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)
});
@@ -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<Self>,
+ ) {
+ 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<Editor>) {
let snapshot = self.snapshot(cx).display_snapshot;
let scroll_margin_rows = self.vertical_scroll_margin() as u32;