Adjust scroll top by number of sticky headers (#50359)

KyleBarton created

In cases where sticky headers are enabled, count the number of sticky
headers that would be present when performing `editor:scroll cursor
top`. Take the maximum of that number and `verical_scroll_margin` so
that we don't inadvertently bury the cursor behind the sticky headers.



https://github.com/user-attachments/assets/6d49fe3a-2017-4c76-bd92-c4ec9794f898



Closes #48864

Before you mark this PR as ready for review, make sure that you have:
- [x] Added a solid test coverage and/or screenshots from doing manual
testing
- [x] Done a self-review taking into account security and performance
aspects
- [x] Aligned any UI changes with the [UI
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)

Release Notes:

- Fixed scroll top behavior when there are more sticky headers than
vertical_scroll_margin

Change summary

crates/editor/src/scroll/actions.rs | 37 ++++++++++++++++++++++++------
1 file changed, 29 insertions(+), 8 deletions(-)

Detailed changes

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

@@ -1,10 +1,12 @@
 use super::Axis;
 use crate::{
-    Autoscroll, Editor, EditorMode, NextScreen, NextScrollCursorCenterTopBottom,
+    Autoscroll, Editor, EditorMode, EditorSettings, NextScreen, NextScrollCursorCenterTopBottom,
     SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT, ScrollCursorBottom, ScrollCursorCenter,
     ScrollCursorCenterTopBottom, ScrollCursorTop, display_map::DisplayRow, scroll::ScrollOffset,
 };
 use gpui::{Context, Point, Window};
+use settings::Settings;
+use text::ToOffset;
 
 impl Editor {
     pub fn next_screen(&mut self, _: &NextScreen, window: &mut Window, cx: &mut Context<Editor>) {
@@ -73,18 +75,37 @@ impl 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(&display_snapshot)
-            .head()
-            .row()
-            .0;
+        let selection_head = self.selections.newest_display(&display_snapshot).head();
+
+        let sticky_headers_len = if EditorSettings::get_global(cx).sticky_scroll.enabled
+            && let Some((_, _, buffer_snapshot)) = display_snapshot.buffer_snapshot().as_singleton()
+        {
+            let select_head_point =
+                rope::Point::new(selection_head.to_point(&display_snapshot).row, 0);
+            buffer_snapshot
+                .outline_items_containing(select_head_point..select_head_point, false, None)
+                .iter()
+                .filter(|outline| {
+                    outline.range.start.offset
+                        < select_head_point.to_offset(&buffer_snapshot) as u32
+                })
+                .collect::<Vec<_>>()
+                .len()
+        } else {
+            0
+        } as u32;
+
+        let new_screen_top = selection_head.row().0;
         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);
+
+        // If the number of sticky headers exceeds the vertical_scroll_margin,
+        // we need to adjust the scroll top a bit further
+        let adjustment = scroll_margin_rows.max(sticky_headers_len) + header_offset;
+        let new_screen_top = new_screen_top.saturating_sub(adjustment);
         self.set_scroll_top_row(DisplayRow(new_screen_top), window, cx);
     }