editor: Add scroll_beyond_last_line setting (#11155)

Evan Liu created

Add `scroll_beyond_last_line` setting with 3 options: 

- `one_page`: The default (current) behaviour of scrolling one more page
beyond the last line.
<img width="568" alt="SCR-20240429-sxry"
src="https://github.com/zed-industries/zed/assets/126383/1effbee9-759f-4858-9022-83bbb208ef82">

- `off`: No scrolling beyond the last line. 
<img width="568" alt="SCR-20240429-syhv"
src="https://github.com/zed-industries/zed/assets/126383/5391b1d7-918d-43f3-8a6f-7642ef32d174">

- `vertical_scroll_margin`: Scroll beyond the last line by the same
number of lines as `vertical_scroll_margin`. Matches the behaviour of
keyboard scrolling.
<img width="568" alt="SCR-20240429-sypc"
src="https://github.com/zed-industries/zed/assets/126383/bb9cc928-e515-4503-88f7-e434c45d742f">

Release Notes:

- Added `scroll_beyond_last_line` setting
([#4962](https://github.com/zed-industries/zed/issues/4962)).

Change summary

assets/settings/default.json         |  2 ++
crates/editor/src/editor_settings.rs | 21 +++++++++++++++++++++
crates/editor/src/element.rs         | 26 +++++++++++++++++++++++---
crates/editor/src/scroll.rs          | 17 +++++++++++++++--
4 files changed, 61 insertions(+), 5 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -255,6 +255,8 @@
     /// 2. "indent_aware"
     "background_coloring": "disabled"
   },
+  // Whether the editor will scroll beyond the last line.
+  "scroll_beyond_last_line": "one_page",
   // The number of lines to keep above/below the cursor when scrolling.
   "vertical_scroll_margin": 3,
   // Scroll sensitivity multiplier. This multiplier is applied

crates/editor/src/editor_settings.rs 🔗

@@ -15,6 +15,7 @@ pub struct EditorSettings {
     pub toolbar: Toolbar,
     pub scrollbar: Scrollbar,
     pub gutter: Gutter,
+    pub scroll_beyond_last_line: ScrollBeyondLastLine,
     pub vertical_scroll_margin: f32,
     pub scroll_sensitivity: f32,
     pub relative_line_numbers: bool,
@@ -116,6 +117,22 @@ pub enum MultiCursorModifier {
     CmdOrCtrl,
 }
 
+/// Whether the editor will scroll beyond the last line.
+///
+/// Default: one_page
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+pub enum ScrollBeyondLastLine {
+    /// The editor will not scroll beyond the last line.
+    Off,
+
+    /// The editor will scroll beyond the last line by one page.
+    OnePage,
+
+    /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin.
+    VerticalScrollMargin,
+}
+
 #[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
 pub struct EditorSettingsContent {
     /// Whether the cursor blinks in the editor.
@@ -158,6 +175,10 @@ pub struct EditorSettingsContent {
     pub scrollbar: Option<ScrollbarContent>,
     /// Gutter related settings
     pub gutter: Option<GutterContent>,
+    /// Whether the editor will scroll beyond the last line.
+    ///
+    /// Default: one_page
+    pub scroll_beyond_last_line: Option<ScrollBeyondLastLine>,
     /// The number of lines to keep above/below the cursor when auto-scrolling.
     ///
     /// Default: 3.

crates/editor/src/element.rs 🔗

@@ -1,3 +1,4 @@
+use crate::editor_settings::ScrollBeyondLastLine;
 use crate::{
     blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip},
     display_map::{
@@ -1089,11 +1090,17 @@ impl EditorElement {
             point(bounds.lower_right().x, bounds.lower_left().y),
         );
 
+        let settings = EditorSettings::get_global(cx);
+        let scroll_beyond_last_line: f32 = match settings.scroll_beyond_last_line {
+            ScrollBeyondLastLine::OnePage => rows_per_page,
+            ScrollBeyondLastLine::Off => 1.0,
+            ScrollBeyondLastLine::VerticalScrollMargin => 1.0 + settings.vertical_scroll_margin,
+        };
+        let total_rows = snapshot.max_point().row().as_f32() + scroll_beyond_last_line;
         let height = bounds.size.height;
-        let total_rows = snapshot.max_point().row().as_f32() + rows_per_page;
         let px_per_row = height / total_rows;
         let thumb_height = (rows_per_page * px_per_row).max(ScrollbarLayout::MIN_THUMB_HEIGHT);
-        let row_height = (height - thumb_height) / snapshot.max_point().row().as_f32();
+        let row_height = (height - thumb_height) / (total_rows - rows_per_page).max(0.0);
 
         Some(ScrollbarLayout {
             hitbox: cx.insert_hitbox(track_bounds, false),
@@ -4805,9 +4812,22 @@ impl Element for EditorElement {
                         cx,
                     );
 
+                    let settings = EditorSettings::get_global(cx);
+                    let scroll_max_row = max_row.as_f32();
+                    let scroll_max_row = match settings.scroll_beyond_last_line {
+                        ScrollBeyondLastLine::OnePage => scroll_max_row,
+                        ScrollBeyondLastLine::Off => {
+                            (scroll_max_row - height_in_lines + 1.0).max(0.0)
+                        }
+                        ScrollBeyondLastLine::VerticalScrollMargin => (scroll_max_row
+                            - height_in_lines
+                            + 1.0
+                            + settings.vertical_scroll_margin)
+                            .max(0.0),
+                    };
                     let scroll_max = point(
                         ((scroll_width - text_hitbox.size.width) / em_width).max(0.0),
-                        max_row.as_f32(),
+                        scroll_max_row,
                     );
 
                     self.editor.update(cx, |editor, cx| {

crates/editor/src/scroll.rs 🔗

@@ -2,6 +2,7 @@ mod actions;
 pub(crate) mod autoscroll;
 pub(crate) mod scroll_amount;
 
+use crate::editor_settings::ScrollBeyondLastLine;
 use crate::{
     display_map::{DisplaySnapshot, ToDisplayPoint},
     hover_popover::hide_hover,
@@ -199,8 +200,20 @@ impl ScrollManager {
                 0,
             )
         } else {
+            let scroll_top = scroll_position.y;
+            let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
+                ScrollBeyondLastLine::OnePage => scroll_top,
+                ScrollBeyondLastLine::Off => scroll_top
+                    .min((map.max_buffer_row().as_f32()) - self.visible_line_count.unwrap() + 1.0),
+                ScrollBeyondLastLine::VerticalScrollMargin => scroll_top.min(
+                    (map.max_buffer_row().as_f32()) - self.visible_line_count.unwrap()
+                        + 1.0
+                        + self.vertical_scroll_margin,
+                ),
+            };
+
             let scroll_top_buffer_point =
-                DisplayPoint::new(DisplayRow(scroll_position.y as u32), 0).to_point(&map);
+                DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(&map);
             let top_anchor = map
                 .buffer_snapshot
                 .anchor_at(scroll_top_buffer_point, Bias::Right);
@@ -210,7 +223,7 @@ impl ScrollManager {
                     anchor: top_anchor,
                     offset: point(
                         scroll_position.x.max(0.),
-                        scroll_position.y - top_anchor.to_display_point(&map).row().as_f32(),
+                        scroll_top - top_anchor.to_display_point(&map).row().as_f32(),
                     ),
                 },
                 scroll_top_buffer_point.row,