Detailed changes
@@ -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, |_| {});
@@ -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(
@@ -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<f64>,
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<f64> {
@@ -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<ScrollOffset>,
@@ -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<Editor>) {
@@ -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::<Vec<_>>()
- .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
@@ -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::<Point>(&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::<Vec<_>>();
+ 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,
@@ -4251,7 +4251,10 @@ impl BufferSnapshot {
items
}
- pub fn outline_range_containing<T: ToOffset>(&self, range: Range<T>) -> Option<Range<Point>> {
+ pub fn outline_ranges_containing<T: ToOffset>(
+ &self,
+ range: Range<T>,
+ ) -> impl Iterator<Item = Range<Point>> + '_ {
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::<Vec<_>>();
- 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<T: ToOffset>(&self, range: Range<T>) -> Option<Range<Point>> {
+ self.outline_ranges_containing(range).next()
}
pub fn outline_items_containing<T: ToOffset>(