mod actions; pub(crate) mod autoscroll; pub(crate) mod scroll_amount; use crate::editor_settings::ScrollBeyondLastLine; use crate::{ Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings, InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint, display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, persistence::DB, }; pub use autoscroll::{Autoscroll, AutoscrollStrategy}; use core::fmt::Debug; use gpui::{Along, App, Axis, Context, Global, Pixels, Task, Window, point, px}; use language::language_settings::{AllLanguageSettings, SoftWrap}; use language::{Bias, Point}; pub use scroll_amount::ScrollAmount; use settings::Settings; use std::{ cmp::Ordering, time::{Duration, Instant}, }; use util::ResultExt; use workspace::{ItemId, WorkspaceId}; pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28); const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1); pub struct WasScrolled(pub(crate) bool); #[derive(Default)] pub struct ScrollbarAutoHide(pub bool); impl Global for ScrollbarAutoHide {} #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: gpui::Point, pub anchor: Anchor, } impl ScrollAnchor { pub(super) fn new() -> Self { Self { offset: gpui::Point::default(), anchor: Anchor::min(), } } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { self.offset.apply_along(Axis::Vertical, |offset| { if self.anchor == Anchor::min() { 0. } else { let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32(); (offset + scroll_top).max(0.) } }) } pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 { self.anchor.to_point(buffer).row } } #[derive(Clone, Copy, Debug)] pub struct OngoingScroll { last_event: Instant, axis: Option, } impl OngoingScroll { fn new() -> Self { Self { last_event: Instant::now() - SCROLL_EVENT_SEPARATION, axis: None, } } pub fn filter(&self, delta: &mut gpui::Point) -> Option { const UNLOCK_PERCENT: f32 = 1.9; const UNLOCK_LOWER_BOUND: Pixels = px(6.); let mut axis = self.axis; let x = delta.x.abs(); let y = delta.y.abs(); let duration = Instant::now().duration_since(self.last_event); if duration > SCROLL_EVENT_SEPARATION { //New ongoing scroll will start, determine axis axis = if x <= y { Some(Axis::Vertical) } else { Some(Axis::Horizontal) }; } else if x.max(y) >= UNLOCK_LOWER_BOUND { //Check if the current ongoing will need to unlock match axis { Some(Axis::Vertical) => { if x > y && x >= y * UNLOCK_PERCENT { axis = None; } } Some(Axis::Horizontal) => { if y > x && y >= x * UNLOCK_PERCENT { axis = None; } } None => {} } } match axis { Some(Axis::Vertical) => { *delta = point(px(0.), delta.y); } Some(Axis::Horizontal) => { *delta = point(delta.x, px(0.)); } None => {} } axis } } #[derive(Copy, Clone, Default, PartialEq, Eq)] pub enum ScrollbarThumbState { #[default] Idle, Hovered, Dragging, } #[derive(PartialEq, Eq)] pub struct ActiveScrollbarState { axis: Axis, thumb_state: ScrollbarThumbState, } impl ActiveScrollbarState { pub fn new(axis: Axis, thumb_state: ScrollbarThumbState) -> Self { ActiveScrollbarState { axis, thumb_state } } pub fn thumb_state_for_axis(&self, axis: Axis) -> Option { (self.axis == axis).then_some(self.thumb_state) } } pub struct ScrollManager { pub(crate) vertical_scroll_margin: f32, anchor: ScrollAnchor, ongoing: OngoingScroll, /// 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. autoscroll_request: Option<(Autoscroll, bool)>, last_autoscroll: Option<(gpui::Point, f32, f32, AutoscrollStrategy)>, show_scrollbars: bool, hide_scrollbar_task: Option>, active_scrollbar: Option, visible_line_count: Option, visible_column_count: Option, forbid_vertical_scroll: bool, minimap_thumb_state: Option, } impl ScrollManager { pub fn new(cx: &mut App) -> Self { ScrollManager { vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin, anchor: ScrollAnchor::new(), ongoing: OngoingScroll::new(), autoscroll_request: None, show_scrollbars: true, hide_scrollbar_task: None, active_scrollbar: None, last_autoscroll: None, visible_line_count: None, visible_column_count: None, forbid_vertical_scroll: false, minimap_thumb_state: None, } } pub fn clone_state(&mut self, other: &Self) { self.anchor = other.anchor; self.ongoing = other.ongoing; } pub fn anchor(&self) -> ScrollAnchor { self.anchor } pub fn ongoing_scroll(&self) -> OngoingScroll { self.ongoing } pub fn update_ongoing_scroll(&mut self, axis: Option) { self.ongoing.last_event = Instant::now(); self.ongoing.axis = axis; } pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point { self.anchor.scroll_position(snapshot) } fn set_scroll_position( &mut self, scroll_position: gpui::Point, map: &DisplaySnapshot, local: bool, autoscroll: bool, workspace_id: Option, window: &mut Window, cx: &mut Context, ) -> WasScrolled { let scroll_top = scroll_position.y.max(0.); let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line { ScrollBeyondLastLine::OnePage => scroll_top, ScrollBeyondLastLine::Off => { if let Some(height_in_lines) = self.visible_line_count { let max_row = map.max_point().row().0 as f32; scroll_top.min(max_row - height_in_lines + 1.).max(0.) } else { scroll_top } } ScrollBeyondLastLine::VerticalScrollMargin => { if let Some(height_in_lines) = self.visible_line_count { let max_row = map.max_point().row().0 as f32; scroll_top .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin) .max(0.) } else { scroll_top } } }; let scroll_top_row = DisplayRow(scroll_top as u32); let scroll_top_buffer_point = map .clip_point( DisplayPoint::new(scroll_top_row, scroll_position.x as u32), Bias::Left, ) .to_point(map); let top_anchor = map .buffer_snapshot .anchor_at(scroll_top_buffer_point, Bias::Right); self.set_anchor( ScrollAnchor { anchor: top_anchor, offset: point( scroll_position.x.max(0.), scroll_top - top_anchor.to_display_point(map).row().as_f32(), ), }, scroll_top_buffer_point.row, local, autoscroll, workspace_id, window, cx, ) } fn set_anchor( &mut self, anchor: ScrollAnchor, top_row: u32, local: bool, autoscroll: bool, workspace_id: Option, window: &mut Window, cx: &mut Context, ) -> WasScrolled { let adjusted_anchor = if self.forbid_vertical_scroll { ScrollAnchor { offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y), anchor: self.anchor.anchor, } } else { anchor }; self.autoscroll_request.take(); if self.anchor == adjusted_anchor { return WasScrolled(false); } self.anchor = adjusted_anchor; cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); self.show_scrollbars(window, cx); if let Some(workspace_id) = workspace_id { let item_id = cx.entity().entity_id().as_u64() as ItemId; cx.foreground_executor() .spawn(async move { log::debug!( "Saving scroll position for item {item_id:?} in workspace {workspace_id:?}" ); DB.save_scroll_position( item_id, workspace_id, top_row, anchor.offset.x, anchor.offset.y, ) .await .log_err() }) .detach() } cx.notify(); WasScrolled(true) } pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { if !self.show_scrollbars { self.show_scrollbars = true; cx.notify(); } if cx.default_global::().0 { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor() .timer(SCROLLBAR_SHOW_INTERVAL) .await; editor .update(cx, |editor, cx| { editor.scroll_manager.show_scrollbars = false; cx.notify(); }) .log_err(); })); } else { self.hide_scrollbar_task = None; } } pub fn scrollbars_visible(&self) -> bool { self.show_scrollbars } pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> { self.autoscroll_request.take() } pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> { self.active_scrollbar.as_ref() } pub fn dragging_scrollbar_axis(&self) -> Option { self.active_scrollbar .as_ref() .filter(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging) .map(|scrollbar| scrollbar.axis) } pub fn any_scrollbar_dragged(&self) -> bool { self.active_scrollbar .as_ref() .is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging) } pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context) { self.update_active_scrollbar_state( Some(ActiveScrollbarState::new( axis, ScrollbarThumbState::Hovered, )), cx, ); } pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context) { self.update_active_scrollbar_state( Some(ActiveScrollbarState::new( axis, ScrollbarThumbState::Dragging, )), cx, ); } pub fn reset_scrollbar_state(&mut self, cx: &mut Context) { self.update_active_scrollbar_state(None, cx); } fn update_active_scrollbar_state( &mut self, new_state: Option, cx: &mut Context, ) { if self.active_scrollbar != new_state { self.active_scrollbar = new_state; cx.notify(); } } pub fn set_is_hovering_minimap_thumb(&mut self, hovered: bool, cx: &mut Context) { self.update_minimap_thumb_state( Some(if hovered { ScrollbarThumbState::Hovered } else { ScrollbarThumbState::Idle }), cx, ); } pub fn set_is_dragging_minimap(&mut self, cx: &mut Context) { self.update_minimap_thumb_state(Some(ScrollbarThumbState::Dragging), cx); } pub fn hide_minimap_thumb(&mut self, cx: &mut Context) { self.update_minimap_thumb_state(None, cx); } pub fn is_dragging_minimap(&self) -> bool { self.minimap_thumb_state .is_some_and(|state| state == ScrollbarThumbState::Dragging) } fn update_minimap_thumb_state( &mut self, thumb_state: Option, cx: &mut Context, ) { if self.minimap_thumb_state != thumb_state { self.minimap_thumb_state = thumb_state; cx.notify(); } } pub fn minimap_thumb_state(&self) -> Option { self.minimap_thumb_state } pub fn clamp_scroll_left(&mut self, max: f32) -> bool { if max < self.anchor.offset.x { self.anchor.offset.x = max; true } else { false } } pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) { self.forbid_vertical_scroll = forbid; } pub fn forbid_vertical_scroll(&self) -> bool { self.forbid_vertical_scroll } } impl Editor { pub fn vertical_scroll_margin(&self) -> usize { self.scroll_manager.vertical_scroll_margin as usize } pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context) { self.scroll_manager.vertical_scroll_margin = margin_rows as f32; cx.notify(); } pub fn visible_line_count(&self) -> Option { self.scroll_manager.visible_line_count } pub fn visible_row_count(&self) -> Option { self.visible_line_count() .map(|line_count| line_count as u32 - 1) } pub fn visible_column_count(&self) -> Option { self.scroll_manager.visible_column_count } pub(crate) fn set_visible_line_count( &mut self, lines: f32, window: &mut Window, cx: &mut Context, ) { let opened_first_time = self.scroll_manager.visible_line_count.is_none(); self.scroll_manager.visible_line_count = Some(lines); if opened_first_time { cx.spawn_in(window, async move |editor, cx| { editor .update_in(cx, |editor, window, cx| { editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); editor.refresh_colors(false, None, window, cx); }) .ok() }) .detach() } } pub(crate) fn set_visible_column_count(&mut self, columns: f32) { self.scroll_manager.visible_column_count = Some(columns); } pub fn apply_scroll_delta( &mut self, scroll_delta: gpui::Point, window: &mut Window, cx: &mut Context, ) { let mut delta = scroll_delta; if self.scroll_manager.forbid_vertical_scroll { delta.y = 0.0; } let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let position = self.scroll_manager.anchor.scroll_position(&display_map) + delta; self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx); } pub fn set_scroll_position( &mut self, scroll_position: gpui::Point, window: &mut Window, cx: &mut Context, ) -> WasScrolled { let mut position = scroll_position; if self.scroll_manager.forbid_vertical_scroll { let current_position = self.scroll_position(cx); position.y = current_position.y; } self.set_scroll_position_internal(position, true, false, window, cx) } /// Scrolls so that `row` is at the top of the editor view. pub fn set_scroll_top_row( &mut self, row: DisplayRow, window: &mut Window, cx: &mut Context, ) { let snapshot = self.snapshot(window, cx).display_snapshot; let new_screen_top = DisplayPoint::new(row, 0); let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left); let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top); self.set_scroll_anchor( ScrollAnchor { anchor: new_anchor, offset: Default::default(), }, window, cx, ); } pub(crate) fn set_scroll_position_internal( &mut self, scroll_position: gpui::Point, local: bool, autoscroll: bool, window: &mut Window, cx: &mut Context, ) -> WasScrolled { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.set_scroll_position_taking_display_map( scroll_position, local, autoscroll, map, window, cx, ) } fn set_scroll_position_taking_display_map( &mut self, scroll_position: gpui::Point, local: bool, autoscroll: bool, display_map: DisplaySnapshot, window: &mut Window, cx: &mut Context, ) -> WasScrolled { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1); self.edit_prediction_preview .set_previous_scroll_position(None); let adjusted_position = if self.scroll_manager.forbid_vertical_scroll { let current_position = self.scroll_manager.anchor.scroll_position(&display_map); gpui::Point::new(scroll_position.x, current_position.y) } else { scroll_position }; let editor_was_scrolled = self.scroll_manager.set_scroll_position( adjusted_position, &display_map, local, autoscroll, workspace_id, window, cx, ); self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); self.refresh_colors(false, None, window, cx); editor_was_scrolled } pub fn scroll_position(&self, cx: &mut Context) -> gpui::Point { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); self.scroll_manager.anchor.scroll_position(&display_map) } pub fn set_scroll_anchor( &mut self, scroll_anchor: ScrollAnchor, window: &mut Window, cx: &mut Context, ) { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1); let top_row = scroll_anchor .anchor .to_point(&self.buffer().read(cx).snapshot(cx)) .row; self.scroll_manager.set_anchor( scroll_anchor, top_row, true, false, workspace_id, window, cx, ); } pub(crate) fn set_scroll_anchor_remote( &mut self, scroll_anchor: ScrollAnchor, window: &mut Window, cx: &mut Context, ) { hide_hover(self, cx); let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1); let snapshot = &self.buffer().read(cx).snapshot(cx); if !scroll_anchor.anchor.is_valid(snapshot) { log::warn!("Invalid scroll anchor: {:?}", scroll_anchor); return; } let top_row = scroll_anchor.anchor.to_point(snapshot).row; self.scroll_manager.set_anchor( scroll_anchor, top_row, false, false, workspace_id, window, cx, ); } pub fn scroll_screen( &mut self, amount: &ScrollAmount, window: &mut Window, cx: &mut Context, ) { if matches!(self.mode, EditorMode::SingleLine) { cx.propagate(); return; } if self.take_rename(true, window, cx).is_some() { return; } let mut current_position = self.scroll_position(cx); let Some(visible_line_count) = self.visible_line_count() else { return; }; let Some(mut visible_column_count) = self.visible_column_count() else { return; }; // If the user has a preferred line length, and has the editor // configured to wrap at the preferred line length, or bounded to it, // use that value over the visible column count. This was mostly done so // that tests could actually be written for vim's `z l`, `z h`, `z // shift-l` and `z shift-h` commands, as there wasn't a good way to // configure the editor to only display a certain number of columns. If // that ever happens, this could probably be removed. let settings = AllLanguageSettings::get_global(cx); if matches!( settings.defaults.soft_wrap, SoftWrap::PreferredLineLength | SoftWrap::Bounded ) && (settings.defaults.preferred_line_length as f32) < visible_column_count { visible_column_count = settings.defaults.preferred_line_length as f32; } // If the scroll position is currently at the left edge of the document // (x == 0.0) and the intent is to scroll right, the gutter's margin // should first be added to the current position, otherwise the cursor // will end at the column position minus the margin, which looks off. if current_position.x == 0.0 && amount.columns(visible_column_count) > 0. && let Some(last_position_map) = &self.last_position_map { current_position.x += self.gutter_dimensions.margin / last_position_map.em_advance; } let new_position = current_position + point( amount.columns(visible_column_count), amount.lines(visible_line_count), ); self.set_scroll_position(new_position, window, cx); } /// Returns an ordering. The newest selection is: /// Ordering::Equal => on screen /// Ordering::Less => above or to the left of the screen /// Ordering::Greater => below or to the right of the screen pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering { let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let newest_head = self .selections .newest_anchor() .head() .to_display_point(&snapshot); let screen_top = self .scroll_manager .anchor .anchor .to_display_point(&snapshot); if screen_top > newest_head { return Ordering::Less; } if let (Some(visible_lines), Some(visible_columns)) = (self.visible_line_count(), self.visible_column_count()) && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) && newest_head.column() <= screen_top.column() + visible_columns as u32 { return Ordering::Equal; } Ordering::Greater } pub fn read_scroll_position_from_db( &mut self, item_id: u64, workspace_id: WorkspaceId, window: &mut Window, cx: &mut Context, ) { let scroll_position = DB.get_scroll_position(item_id, workspace_id); if let Ok(Some((top_row, x, y))) = scroll_position { let top_anchor = self .buffer() .read(cx) .snapshot(cx) .anchor_at(Point::new(top_row, 0), Bias::Left); let scroll_anchor = ScrollAnchor { offset: gpui::Point::new(x, y), anchor: top_anchor, }; self.set_scroll_anchor(scroll_anchor, window, cx); } } }