diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b18f734b50b58396056ff62456ef2d76bed88276..cb81da9c5e70fb7c3c681ce42571fb2a310a3504 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32693,6 +32693,80 @@ async fn test_no_duplicated_sticky_headers(cx: &mut TestAppContext) { assert_eq!(sticky_headers(5.0), vec![]); } +#[gpui::test] +async fn test_autoscroll_keeps_cursor_visible_below_sticky_headers(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + update_test_editor_settings(cx, &|settings| { + settings.vertical_scroll_margin = Some(0.0); + settings.scroll_beyond_last_line = Some(ScrollBeyondLastLine::OnePage); + settings.sticky_scroll = Some(settings::StickyScrollContent { + enabled: Some(true), + }); + }); + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + impl Foo { fn bar() { + let x = 1; + fn baz() { + let y = 2; + } + } } + ˇ + "}); + + let mut previous_cursor_row = cx.update_editor(|editor, window, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx)); + let cursor_row = editor + .selections + .newest_display(&editor.display_snapshot(cx)) + .head() + .row(); + editor.set_scroll_top_row(cursor_row, window, cx); + cursor_row + }); + + for _ in 0..6 { + cx.update_editor(|editor, window, cx| editor.move_up(&MoveUp, window, cx)); + cx.run_until_parked(); + + cx.update_editor(|editor, window, cx| { + let snapshot = editor.snapshot(window, cx); + let scroll_top = snapshot.scroll_position().y; + let sticky_header_count = EditorElement::sticky_headers(editor, &snapshot).len(); + let cursor_row = editor + .selections + .newest_display(&snapshot.display_snapshot) + .head() + .row(); + assert_eq!( + cursor_row, + previous_cursor_row + .previous_row() + .max(DisplayRow(scroll_top as u32) + DisplayRow(sticky_header_count as u32)) + ); + previous_cursor_row = cursor_row; + }); + + // The `ScrollCursorTop` action shouldn't change the scroll position, as the cursor is + // already as high up as the sticky headers allow. + let scroll_top_before = + cx.update_editor(|editor, window, cx| editor.snapshot(window, cx).scroll_position().y); + cx.update_editor(|editor, window, cx| { + editor.scroll_cursor_top(&ScrollCursorTop, window, cx) + }); + cx.run_until_parked(); + let scroll_top_after = + cx.update_editor(|editor, window, cx| editor.snapshot(window, cx).scroll_position().y); + assert_eq!(scroll_top_before, scroll_top_after); + } +} + #[gpui::test] fn test_relative_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 993641a4a7b77b08ec0c18469d003ede7092d93d..4162bc9c99dd80ac12b6770d0f9d77c1d2286393 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -10484,11 +10484,6 @@ impl Element for EditorElement { } else { None }; - self.editor.update(cx, |editor, _| { - editor.scroll_manager.set_sticky_header_line_count( - sticky_headers.as_ref().map_or(0, |h| h.lines.len()), - ); - }); let indent_guides = if scroll_pixel_position != preliminary_scroll_pixel_position { self.layout_indent_guides( diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index b14ba4bcdacf928378316dce8a259a79102dc1b2..143a73fd701ac900b3f597e5977a7b971f853b92 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -201,8 +201,6 @@ pub struct ScrollManager { /// Each side separately clamps the x component using its own scroll_max_x when reading from the SharedScrollAnchor. scroll_max_x: Option, ongoing: OngoingScroll, - /// Number of sticky header lines currently being rendered for the current scroll position. - sticky_header_line_count: usize, /// The second element indicates whether the autoscroll request is local /// (true) or remote (false). Local requests are initiated by user actions, /// while remote requests come from external sources. @@ -234,7 +232,6 @@ impl ScrollManager { anchor, scroll_max_x: None, ongoing: OngoingScroll::new(), - sticky_header_line_count: 0, autoscroll_request: None, show_scrollbars: true, hide_scrollbar_task: None, @@ -273,7 +270,6 @@ impl ScrollManager { this.display_map_id = Some(my_snapshot.display_map_id); }); self.ongoing = other.ongoing; - self.sticky_header_line_count = other.sticky_header_line_count; } pub fn offset(&self, cx: &App) -> gpui::Point { @@ -360,14 +356,6 @@ impl ScrollManager { pos } - pub fn sticky_header_line_count(&self) -> usize { - self.sticky_header_line_count - } - - pub fn set_sticky_header_line_count(&mut self, count: usize) { - self.sticky_header_line_count = count; - } - fn set_scroll_position( &mut self, scroll_position: gpui::Point, diff --git a/crates/editor/src/scroll/actions.rs b/crates/editor/src/scroll/actions.rs index 48438b6592a3a75c405fee496fbbd55091389a8f..4685b1003ee57de043b16cfd099a38eac8ce2069 100644 --- a/crates/editor/src/scroll/actions.rs +++ b/crates/editor/src/scroll/actions.rs @@ -1,12 +1,10 @@ use super::Axis; use crate::{ - Autoscroll, Editor, EditorMode, EditorSettings, NextScreen, NextScrollCursorCenterTopBottom, + Autoscroll, Editor, EditorMode, 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) { @@ -77,23 +75,9 @@ impl Editor { let scroll_margin_rows = self.vertical_scroll_margin() as u32; 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::>() - .len() - } else { - 0 - } as u32; + let sticky_headers_len = + self.visible_sticky_header_count_for_point(&display_snapshot, selection_head, cx) + as u32; let new_screen_top = selection_head.row().0; let header_offset = display_snapshot diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 832059f8c049be7fea164771eb13b8fb1b9334f2..c2b07ffa96aef43845d5e8b52502dd5e945786dd 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -1,12 +1,15 @@ use crate::{ - DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects, - display_map::ToDisplayPoint, + DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, LineWithInvisibles, RowExt, + SelectionEffects, + display_map::{DisplaySnapshot, ToDisplayPoint}, scroll::{ScrollOffset, WasScrolled}, }; -use gpui::{Bounds, Context, Pixels, Window}; +use gpui::{App, Bounds, Context, Pixels, Window}; use language::Point; use multi_buffer::Anchor; +use settings::Settings; use std::cmp; +use text::Bias; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { @@ -140,23 +143,24 @@ impl Editor { return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; + let mut target_point; let mut target_top; let mut target_bottom; if let Some(first_highlighted_row) = self.highlighted_display_row_for_autoscroll(&display_map) { - target_top = first_highlighted_row.as_f64(); + target_point = DisplayPoint::new(first_highlighted_row, 0); + target_top = target_point.row().as_f64(); target_bottom = target_top + 1.; } else { let selections = self.selections.all::(&display_map); - target_top = selections + target_point = selections .first() .unwrap() .head() - .to_display_point(&display_map) - .row() - .as_f64(); + .to_display_point(&display_map); + target_top = target_point.row().as_f64(); target_bottom = selections .last() .unwrap() @@ -173,21 +177,17 @@ impl Editor { ) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _)) && !selections_fit) { - let newest_selection_top = selections + target_point = selections .iter() .max_by_key(|s| s.id) .unwrap() .head() - .to_display_point(&display_map) - .row() - .as_f64(); - target_top = newest_selection_top; - target_bottom = newest_selection_top + 1.; + .to_display_point(&display_map); + target_top = target_point.row().as_f64(); + target_bottom = target_top + 1.; } } - let visible_sticky_headers = self.scroll_manager.sticky_header_line_count(); - let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { @@ -209,10 +209,14 @@ impl Editor { .unwrap_or_default(), }; if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll { - target_top = anchor.to_display_point(&display_map).row().as_f64(); + target_point = anchor.to_display_point(&display_map); + target_top = target_point.row().as_f64(); target_bottom = target_top + 1.; } + let visible_sticky_headers = + self.visible_sticky_header_count_for_point(&display_map, target_point, cx); + let was_autoscrolled = match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); @@ -274,6 +278,54 @@ impl Editor { (NeedsHorizontalAutoscroll(true), was_scrolled) } + pub(crate) fn visible_sticky_header_count_for_point( + &self, + display_map: &DisplaySnapshot, + target_point: DisplayPoint, + cx: &App, + ) -> usize { + let sticky_scroll = EditorSettings::get_global(cx).sticky_scroll; + if !sticky_scroll.enabled { + return 0; + } + + let Some(buffer_snapshot) = display_map.buffer_snapshot().as_singleton() else { + return 0; + }; + + let point = target_point.to_point(display_map); + let mut item_ranges = buffer_snapshot + .outline_ranges_containing(point..point) + .collect::>(); + item_ranges.sort_by_key(|item_range| item_range.start); + + let mut previous_sticky_row = None; + let mut num_visible_sticky_headers = 0; + + for item_range in item_ranges { + let sticky_row = display_map + .point_to_display_point(item_range.start, Bias::Left) + .row(); + if sticky_row >= target_point.row() { + break; + } + if previous_sticky_row.replace(sticky_row) == Some(sticky_row) { + continue; + } + + let end_row = display_map + .point_to_display_point(item_range.end, Bias::Left) + .row(); + if end_row <= target_point.row() { + continue; + } + + num_visible_sticky_headers += 1; + } + + num_visible_sticky_headers + } + pub(crate) fn autoscroll_horizontally( &mut self, start_row: DisplayRow, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 698efbfeed8363d38aa79f5afd93ba00b42e80b4..3770e4ccf13a53dc88c454f48c0fce3e386598fd 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -4251,7 +4251,10 @@ impl BufferSnapshot { items } - pub fn outline_range_containing(&self, range: Range) -> Option> { + pub fn outline_ranges_containing( + &self, + range: Range, + ) -> impl Iterator> + '_ { let range = range.to_offset(self); let mut matches = self.syntax.matches(range.clone(), &self.text, |grammar| { grammar.outline_config.as_ref().map(|c| &c.query) @@ -4262,35 +4265,41 @@ impl BufferSnapshot { .map(|g| g.outline_config.as_ref().unwrap()) .collect::>(); - while let Some(mat) = matches.peek() { - let config = &configs[mat.grammar_index]; - let containing_item_node = maybe!({ - let item_node = mat.captures.iter().find_map(|cap| { - if cap.index == config.item_capture_ix { - Some(cap.node) - } else { + std::iter::from_fn(move || { + while let Some(mat) = matches.peek() { + let config = &configs[mat.grammar_index]; + let containing_item_node = maybe!({ + let item_node = mat.captures.iter().find_map(|cap| { + if cap.index == config.item_capture_ix { + Some(cap.node) + } else { + None + } + })?; + + let item_byte_range = item_node.byte_range(); + if item_byte_range.end < range.start || item_byte_range.start > range.end { None + } else { + Some(item_node) } - })?; - - let item_byte_range = item_node.byte_range(); - if item_byte_range.end < range.start || item_byte_range.start > range.end { - None - } else { - Some(item_node) - } - }); + }); - if let Some(item_node) = containing_item_node { - return Some( + let range = containing_item_node.as_ref().map(|item_node| { Point::from_ts_point(item_node.start_position()) - ..Point::from_ts_point(item_node.end_position()), - ); + ..Point::from_ts_point(item_node.end_position()) + }); + matches.advance(); + if range.is_some() { + return range; + } } + None + }) + } - matches.advance(); - } - None + pub fn outline_range_containing(&self, range: Range) -> Option> { + self.outline_ranges_containing(range).next() } pub fn outline_items_containing(