use crate::{ DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects, display_map::ToDisplayPoint, scroll::{ScrollOffset, WasScrolled}, }; use gpui::{Bounds, Context, Pixels, Window}; use language::Point; use multi_buffer::Anchor; use std::cmp; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Autoscroll { Next, Strategy(AutoscrollStrategy, Option), } impl Autoscroll { /// scrolls the minimal amount to (try) and fit all cursors onscreen pub fn fit() -> Self { Self::Strategy(AutoscrollStrategy::Fit, None) } /// scrolls the minimal amount to fit the newest cursor pub fn newest() -> Self { Self::Strategy(AutoscrollStrategy::Newest, None) } /// scrolls so the newest cursor is vertically centered pub fn center() -> Self { Self::Strategy(AutoscrollStrategy::Center, None) } /// scrolls so the newest cursor is near the top /// (offset by vertical_scroll_margin) pub fn focused() -> Self { Self::Strategy(AutoscrollStrategy::Focused, None) } /// Scrolls so that the newest cursor is roughly an n-th line from the top. pub fn top_relative(n: usize) -> Self { Self::Strategy(AutoscrollStrategy::TopRelative(n), None) } /// Scrolls so that the newest cursor is at the top. pub fn top() -> Self { Self::Strategy(AutoscrollStrategy::Top, None) } /// Scrolls so that the newest cursor is roughly an n-th line from the bottom. pub fn bottom_relative(n: usize) -> Self { Self::Strategy(AutoscrollStrategy::BottomRelative(n), None) } /// Scrolls so that the newest cursor is at the bottom. pub fn bottom() -> Self { Self::Strategy(AutoscrollStrategy::Bottom, None) } /// Applies a given auto-scroll strategy to a given anchor instead of a cursor. /// E.G: Autoscroll::center().for_anchor(...) results in the anchor being at the center of the screen. pub fn for_anchor(self, anchor: Anchor) -> Self { match self { Autoscroll::Next => self, Autoscroll::Strategy(autoscroll_strategy, _) => { Self::Strategy(autoscroll_strategy, Some(anchor)) } } } } impl Into for Option { fn into(self) -> SelectionEffects { match self { Some(autoscroll) => SelectionEffects::scroll(autoscroll), None => SelectionEffects::no_scroll(), } } } #[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] pub enum AutoscrollStrategy { Fit, Newest, #[default] Center, Focused, Top, Bottom, TopRelative(usize), BottomRelative(usize), } impl AutoscrollStrategy { fn next(&self) -> Self { match self { AutoscrollStrategy::Center => AutoscrollStrategy::Top, AutoscrollStrategy::Top => AutoscrollStrategy::Bottom, _ => AutoscrollStrategy::Center, } } } pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool); impl Editor { pub(crate) fn autoscroll_vertically( &mut self, bounds: Bounds, line_height: Pixels, max_scroll_top: ScrollOffset, autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> (NeedsHorizontalAutoscroll, WasScrolled) { let viewport_height = bounds.size.height; let visible_lines = ScrollOffset::from(viewport_height / line_height); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx); let original_y = scroll_position.y; if let Some(last_bounds) = self.expect_bounds_change.take() && scroll_position.y != 0. { scroll_position.y += ScrollOffset::from((bounds.top() - last_bounds.top()) / line_height); if scroll_position.y < 0. { scroll_position.y = 0.; } } if scroll_position.y > max_scroll_top { scroll_position.y = max_scroll_top; } let editor_was_scrolled = if original_y != scroll_position.y { self.set_scroll_position(scroll_position, window, cx) } else { WasScrolled(false) }; let Some((autoscroll, local)) = autoscroll_request else { return (NeedsHorizontalAutoscroll(false), editor_was_scrolled); }; 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_bottom = target_top + 1.; } else { let selections = self.selections.all::(&display_map); target_top = selections .first() .unwrap() .head() .to_display_point(&display_map) .row() .as_f64(); target_bottom = selections .last() .unwrap() .head() .to_display_point(&display_map) .row() .next_row() .as_f64(); let selections_fit = target_bottom - target_top <= visible_lines; if matches!( autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Newest, _) ) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _)) && !selections_fit) { let newest_selection_top = 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.; } } let visible_sticky_headers = self.scroll_manager.sticky_header_line_count(); let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { ((visible_lines - (target_bottom - target_top)) / 2.0).floor() }; let strategy = match autoscroll { Autoscroll::Strategy(strategy, _) => strategy, Autoscroll::Next => self .scroll_manager .last_autoscroll .as_ref() .filter(|(offset, last_target_top, last_target_bottom, _)| { self.scroll_manager.offset(cx) == *offset && target_top == *last_target_top && target_bottom == *last_target_bottom }) .map(|(_, _, _, strategy)| strategy.next()) .unwrap_or_default(), }; if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll { target_top = anchor.to_display_point(&display_map).row().as_f64(); target_bottom = target_top + 1.; } let was_autoscrolled = match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); let target_top = (target_top - margin - visible_sticky_headers as f64).max(0.0); let target_bottom = target_bottom + margin; let start_row = scroll_position.y; let end_row = start_row + visible_lines; let needs_scroll_up = target_top < start_row; let needs_scroll_down = target_bottom >= end_row; if needs_scroll_up && !needs_scroll_down { scroll_position.y = target_top; } else if !needs_scroll_up && needs_scroll_down { scroll_position.y = target_bottom - visible_lines; } if needs_scroll_up ^ needs_scroll_down { self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else { WasScrolled(false) } } AutoscrollStrategy::Center => { scroll_position.y = (target_top - margin).max(0.0); self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Focused => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); scroll_position.y = (target_top - margin).max(0.0); self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Top => { scroll_position.y = (target_top).max(0.0); self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::Bottom => { scroll_position.y = (target_bottom - visible_lines).max(0.0); self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::TopRelative(lines) => { scroll_position.y = target_top - lines as ScrollOffset; self.set_scroll_position_internal(scroll_position, local, true, window, cx) } AutoscrollStrategy::BottomRelative(lines) => { scroll_position.y = target_bottom + lines as ScrollOffset; self.set_scroll_position_internal(scroll_position, local, true, window, cx) } }; self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.offset(cx), target_top, target_bottom, strategy, )); let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0); (NeedsHorizontalAutoscroll(true), was_scrolled) } pub(crate) fn autoscroll_horizontally( &mut self, start_row: DisplayRow, viewport_width: Pixels, scroll_width: Pixels, em_advance: Pixels, layouts: &[LineWithInvisibles], autoscroll_request: Option<(Autoscroll, bool)>, window: &mut Window, cx: &mut Context, ) -> Option> { let (_, local) = autoscroll_request?; let em_advance = ScrollOffset::from(em_advance); let viewport_width = ScrollOffset::from(viewport_width); let scroll_width = ScrollOffset::from(scroll_width); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let selections = self.selections.all::(&display_map); let mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx); let mut target_left; let mut target_right: f64; if self .highlighted_display_row_for_autoscroll(&display_map) .is_none() { target_left = f64::INFINITY; target_right = 0.; for selection in selections { let head = selection.head().to_display_point(&display_map); if head.row() >= start_row && head.row() < DisplayRow(start_row.0 + layouts.len() as u32) { let start_column = head.column(); let end_column = cmp::min(display_map.line_len(head.row()), head.column()); target_left = target_left.min(ScrollOffset::from( layouts[head.row().minus(start_row) as usize] .x_for_index(start_column as usize) + self.gutter_dimensions.margin, )); target_right = target_right.max( ScrollOffset::from( layouts[head.row().minus(start_row) as usize] .x_for_index(end_column as usize), ) + em_advance, ); } } } else { target_left = 0.; target_right = 0.; } target_right = target_right.min(scroll_width); if target_right - target_left > viewport_width { return None; } let scroll_left = self.scroll_manager.offset(cx).x * em_advance; let scroll_right = scroll_left + viewport_width; let was_scrolled = if target_left < scroll_left { scroll_position.x = target_left / em_advance; self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else if target_right > scroll_right { scroll_position.x = (target_right - viewport_width) / em_advance; self.set_scroll_position_internal(scroll_position, local, true, window, cx) } else { WasScrolled(false) }; if was_scrolled.0 { Some(scroll_position) } else { None } } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut Context) { self.scroll_manager.autoscroll_request = Some((autoscroll, true)); cx.notify(); } pub(crate) fn request_autoscroll_remotely( &mut self, autoscroll: Autoscroll, cx: &mut Context, ) { self.scroll_manager.autoscroll_request = Some((autoscroll, false)); cx.notify(); } }