editor: Fix vertical scroll margin not accounting for file header height (#43521)

Mayank Verma created

Closes #43178

Release Notes:

- Fixed vertical scroll margin not accounting for file header height

Here's the before/after:

With `{ "vertical_scroll_margin": 0 }` in `~/.config/zed/settings.json`


https://github.com/user-attachments/assets/418c6d7f-de0f-4da6-a038-69927b1b8b88

Change summary

crates/editor/src/editor_tests.rs   | 84 +++++++++++++++++++++++++++++++
crates/editor/src/scroll/actions.rs | 10 ++
2 files changed, 92 insertions(+), 2 deletions(-)

Detailed changes

crates/editor/src/editor_tests.rs 🔗

@@ -28198,3 +28198,87 @@ async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
         3
         "});
 }
+
+#[gpui::test]
+async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
+    init_test(cx, |_| {});
+
+    let (editor, cx) = cx.add_window_view(|window, cx| {
+        let multi_buffer = MultiBuffer::build_multi(
+            [
+                ("1\n2\n3\n", vec![Point::row_range(0..3)]),
+                ("1\n2\n3\n4\n5\n6\n7\n8\n9\n", vec![Point::row_range(0..9)]),
+            ],
+            cx,
+        );
+        Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
+    });
+
+    let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
+
+    cx.assert_excerpts_with_selections(indoc! {"
+        [EXCERPT]
+        ˇ1
+        2
+        3
+        [EXCERPT]
+        1
+        2
+        3
+        4
+        5
+        6
+        7
+        8
+        9
+        "});
+
+    cx.update_editor(|editor, window, cx| {
+        editor.change_selections(None.into(), window, cx, |s| {
+            s.select_ranges([MultiBufferOffset(19)..MultiBufferOffset(19)]);
+        });
+    });
+
+    cx.assert_excerpts_with_selections(indoc! {"
+        [EXCERPT]
+        1
+        2
+        3
+        [EXCERPT]
+        1
+        2
+        3
+        4
+        5
+        6
+        ˇ7
+        8
+        9
+        "});
+
+    cx.update_editor(|editor, _window, cx| {
+        editor.set_vertical_scroll_margin(0, cx);
+    });
+
+    cx.update_editor(|editor, window, cx| {
+        assert_eq!(editor.vertical_scroll_margin(), 0);
+        editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
+        assert_eq!(
+            editor.snapshot(window, cx).scroll_position(),
+            gpui::Point::new(0., 12.0)
+        );
+    });
+
+    cx.update_editor(|editor, _window, cx| {
+        editor.set_vertical_scroll_margin(3, cx);
+    });
+
+    cx.update_editor(|editor, window, cx| {
+        assert_eq!(editor.vertical_scroll_margin(), 3);
+        editor.scroll_cursor_top(&ScrollCursorTop, window, cx);
+        assert_eq!(
+            editor.snapshot(window, cx).scroll_position(),
+            gpui::Point::new(0., 9.0)
+        );
+    });
+}

crates/editor/src/scroll/actions.rs 🔗

@@ -71,14 +71,20 @@ impl Editor {
         window: &mut Window,
         cx: &mut Context<Editor>,
     ) {
+        let display_snapshot = self.display_snapshot(cx);
         let scroll_margin_rows = self.vertical_scroll_margin() as u32;
         let new_screen_top = self
             .selections
-            .newest_display(&self.display_snapshot(cx))
+            .newest_display(&display_snapshot)
             .head()
             .row()
             .0;
-        let new_screen_top = new_screen_top.saturating_sub(scroll_margin_rows);
+        let header_offset = display_snapshot
+            .buffer_snapshot()
+            .show_headers()
+            .then(|| display_snapshot.buffer_header_height())
+            .unwrap_or(0);
+        let new_screen_top = new_screen_top.saturating_sub(scroll_margin_rows + header_offset);
         self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx);
     }