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, AppContext as _, Axis, Context, Entity, EntityId, 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 ui::scrollbars::ScrollbarAutoHide;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); pub type ScrollOffset = f64;pub type ScrollPixelOffset = f64;#[derive(Clone, Copy, Debug, PartialEq)]pub struct ScrollAnchor { pub offset: gpui::Point<ScrollOffset>, 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<ScrollOffset> { 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_f64(); (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<Axis>,} /// In the split diff view, the two sides share a ScrollAnchor using this struct./// Either side can set a ScrollAnchor that points to its own multibuffer, and we store the ID of the display map/// that the last-written anchor came from so that we know how to resolve it to a DisplayPoint.////// For normal editors, this just acts as a wrapper around a ScrollAnchor.#[derive(Clone, Copy, Debug)]pub struct SharedScrollAnchor { pub scroll_anchor: ScrollAnchor, pub display_map_id: Option<EntityId>,} impl SharedScrollAnchor { pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> { let snapshot = if let Some(display_map_id) = self.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); companion_snapshot } else { snapshot }; self.scroll_anchor.scroll_position(snapshot) } pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot) -> DisplayPoint { let snapshot = if let Some(display_map_id) = self.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); companion_snapshot } else { snapshot }; self.scroll_anchor.anchor.to_display_point(snapshot) }} impl OngoingScroll { fn new() -> Self { Self { last_event: Instant::now() - SCROLL_EVENT_SEPARATION, axis: None, } } pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> { 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<ScrollbarThumbState> { (self.axis == axis).then_some(self.thumb_state) }} pub struct ScrollManager { pub(crate) vertical_scroll_margin: ScrollOffset, anchor: Entity<SharedScrollAnchor>, /// Value to be used for clamping the x component of the SharedScrollAnchor's offset. /// /// We store this outside the SharedScrollAnchor so that the two sides of a split diff can share /// a horizontal scroll offset that may be out of range for one of the editors (when one side is wider than the other). /// 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. autoscroll_request: Option<(Autoscroll, bool)>, last_autoscroll: Option<( gpui::Point<ScrollOffset>, ScrollOffset, ScrollOffset, AutoscrollStrategy, )>, show_scrollbars: bool, hide_scrollbar_task: Option<Task<()>>, active_scrollbar: Option<ActiveScrollbarState>, visible_line_count: Option<f64>, visible_column_count: Option<f64>, forbid_vertical_scroll: bool, minimap_thumb_state: Option<ScrollbarThumbState>, _save_scroll_position_task: Task<()>,} impl ScrollManager { pub fn new(cx: &mut Context<Editor>) -> Self { let anchor = cx.new(|_| SharedScrollAnchor { scroll_anchor: ScrollAnchor::new(), display_map_id: None, }); ScrollManager { vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin, anchor, scroll_max_x: None, ongoing: OngoingScroll::new(), sticky_header_line_count: 0, 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, _save_scroll_position_task: Task::ready(()), } } pub fn set_native_display_map_id( &mut self, display_map_id: EntityId, cx: &mut Context<Editor>, ) { self.anchor.update(cx, |shared, _| { if shared.display_map_id.is_none() { shared.display_map_id = Some(display_map_id); } }); } pub fn clone_state( &mut self, other: &Self, other_snapshot: &DisplaySnapshot, my_snapshot: &DisplaySnapshot, cx: &mut Context<Editor>, ) { let native_anchor = other.native_anchor(other_snapshot, cx); self.anchor.update(cx, |this, _| { this.scroll_anchor = native_anchor; 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> { let mut offset = self.anchor.read(cx).scroll_anchor.offset; if let Some(max_x) = self.scroll_max_x { offset.x = offset.x.min(max_x); } offset } /// Get a ScrollAnchor whose `anchor` field is guaranteed to point into the multibuffer for the provided snapshot. /// /// For normal editors, this just retrieves the internal ScrollAnchor and is lossless. When the editor is part of a split diff, /// we may need to translate the anchor to point to the "native" multibuffer first. That translation is lossy, /// so this method should be used sparingly---if you just need a scroll position or display point, call the appropriate helper method instead, /// since they can losslessly handle the case where the ScrollAnchor was last set from the other side. pub fn native_anchor(&self, snapshot: &DisplaySnapshot, cx: &App) -> ScrollAnchor { let shared = self.anchor.read(cx); let mut result = if let Some(display_map_id) = shared.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); let mut display_point = shared .scroll_anchor .anchor .to_display_point(companion_snapshot); *display_point.column_mut() = 0; let buffer_point = snapshot.display_point_to_point(display_point, Bias::Left); let anchor = snapshot.buffer_snapshot().anchor_before(buffer_point); ScrollAnchor { anchor, offset: shared.scroll_anchor.offset, } } else { shared.scroll_anchor }; if let Some(max_x) = self.scroll_max_x { result.offset.x = result.offset.x.min(max_x); } result } pub fn shared_scroll_anchor(&self, cx: &App) -> SharedScrollAnchor { let mut shared = *self.anchor.read(cx); if let Some(max_x) = self.scroll_max_x { shared.scroll_anchor.offset.x = shared.scroll_anchor.offset.x.min(max_x); } shared } pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot, cx: &App) -> DisplayPoint { self.anchor.read(cx).scroll_top_display_point(snapshot) } pub fn scroll_anchor_entity(&self) -> Entity<SharedScrollAnchor> { self.anchor.clone() } pub fn set_shared_scroll_anchor(&mut self, entity: Entity<SharedScrollAnchor>) { self.anchor = entity; } pub fn ongoing_scroll(&self) -> OngoingScroll { self.ongoing } pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) { self.ongoing.last_event = Instant::now(); self.ongoing.axis = axis; } pub fn scroll_position( &self, snapshot: &DisplaySnapshot, cx: &App, ) -> gpui::Point<ScrollOffset> { let mut pos = self.anchor.read(cx).scroll_position(snapshot); if let Some(max_x) = self.scroll_max_x { pos.x = pos.x.min(max_x); } 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>, map: &DisplaySnapshot, local: bool, autoscroll: bool, workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, ) -> 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().as_f64(); 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().as_f64(); 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_before(scroll_top_buffer_point); self.set_anchor( ScrollAnchor { anchor: top_anchor, offset: point( scroll_position.x.max(0.), scroll_top - top_anchor.to_display_point(map).row().as_f64(), ), }, map, scroll_top_buffer_point.row, local, autoscroll, workspace_id, window, cx, ) } fn set_anchor( &mut self, anchor: ScrollAnchor, display_map: &DisplaySnapshot, top_row: u32, local: bool, autoscroll: bool, workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, ) -> WasScrolled { let adjusted_anchor = if self.forbid_vertical_scroll { let current = self.anchor.read(cx); ScrollAnchor { offset: gpui::Point::new(anchor.offset.x, current.scroll_anchor.offset.y), anchor: current.scroll_anchor.anchor, } } else { anchor }; self.scroll_max_x.take(); self.autoscroll_request.take(); let current = self.anchor.read(cx); if current.scroll_anchor == adjusted_anchor { return WasScrolled(false); } self.anchor.update(cx, |shared, _| { shared.scroll_anchor = adjusted_anchor; shared.display_map_id = Some(display_map.display_map_id); }); 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; let executor = cx.background_executor().clone(); self._save_scroll_position_task = cx.background_executor().spawn(async move { executor.timer(Duration::from_millis(10)).await; 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(); }); } cx.notify(); WasScrolled(true) } pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) { if !self.show_scrollbars { self.show_scrollbars = true; cx.notify(); } if cx.default_global::<ScrollbarAutoHide>().should_hide() { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor() .timer(SCROLLBAR_SHOW_INTERVAL)Lines 1–500 of 954. Load more
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, AppContext as _, Axis, Context, Entity, EntityId, 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 ui::scrollbars::ScrollbarAutoHide; 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); pub type ScrollOffset = f64; pub type ScrollPixelOffset = f64; #[derive(Clone, Copy, Debug, PartialEq)] pub struct ScrollAnchor { pub offset: gpui::Point<ScrollOffset>, 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<ScrollOffset> { 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_f64(); (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<Axis>, } /// In the split diff view, the two sides share a ScrollAnchor using this struct. /// Either side can set a ScrollAnchor that points to its own multibuffer, and we store the ID of the display map /// that the last-written anchor came from so that we know how to resolve it to a DisplayPoint. /// /// For normal editors, this just acts as a wrapper around a ScrollAnchor. #[derive(Clone, Copy, Debug)] pub struct SharedScrollAnchor { pub scroll_anchor: ScrollAnchor, pub display_map_id: Option<EntityId>, } impl SharedScrollAnchor { pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> { let snapshot = if let Some(display_map_id) = self.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); companion_snapshot } else { snapshot }; self.scroll_anchor.scroll_position(snapshot) } pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot) -> DisplayPoint { let snapshot = if let Some(display_map_id) = self.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); companion_snapshot } else { snapshot }; self.scroll_anchor.anchor.to_display_point(snapshot) } } impl OngoingScroll { fn new() -> Self { Self { last_event: Instant::now() - SCROLL_EVENT_SEPARATION, axis: None, } } pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> { 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<ScrollbarThumbState> { (self.axis == axis).then_some(self.thumb_state) } } pub struct ScrollManager { pub(crate) vertical_scroll_margin: ScrollOffset, anchor: Entity<SharedScrollAnchor>, /// Value to be used for clamping the x component of the SharedScrollAnchor's offset. /// /// We store this outside the SharedScrollAnchor so that the two sides of a split diff can share /// a horizontal scroll offset that may be out of range for one of the editors (when one side is wider than the other). /// 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. autoscroll_request: Option<(Autoscroll, bool)>, last_autoscroll: Option<( gpui::Point<ScrollOffset>, ScrollOffset, ScrollOffset, AutoscrollStrategy, )>, show_scrollbars: bool, hide_scrollbar_task: Option<Task<()>>, active_scrollbar: Option<ActiveScrollbarState>, visible_line_count: Option<f64>, visible_column_count: Option<f64>, forbid_vertical_scroll: bool, minimap_thumb_state: Option<ScrollbarThumbState>, _save_scroll_position_task: Task<()>, } impl ScrollManager { pub fn new(cx: &mut Context<Editor>) -> Self { let anchor = cx.new(|_| SharedScrollAnchor { scroll_anchor: ScrollAnchor::new(), display_map_id: None, }); ScrollManager { vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin, anchor, scroll_max_x: None, ongoing: OngoingScroll::new(), sticky_header_line_count: 0, 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, _save_scroll_position_task: Task::ready(()), } } pub fn set_native_display_map_id( &mut self, display_map_id: EntityId, cx: &mut Context<Editor>, ) { self.anchor.update(cx, |shared, _| { if shared.display_map_id.is_none() { shared.display_map_id = Some(display_map_id); } }); } pub fn clone_state( &mut self, other: &Self, other_snapshot: &DisplaySnapshot, my_snapshot: &DisplaySnapshot, cx: &mut Context<Editor>, ) { let native_anchor = other.native_anchor(other_snapshot, cx); self.anchor.update(cx, |this, _| { this.scroll_anchor = native_anchor; 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> { let mut offset = self.anchor.read(cx).scroll_anchor.offset; if let Some(max_x) = self.scroll_max_x { offset.x = offset.x.min(max_x); } offset } /// Get a ScrollAnchor whose `anchor` field is guaranteed to point into the multibuffer for the provided snapshot. /// /// For normal editors, this just retrieves the internal ScrollAnchor and is lossless. When the editor is part of a split diff, /// we may need to translate the anchor to point to the "native" multibuffer first. That translation is lossy, /// so this method should be used sparingly---if you just need a scroll position or display point, call the appropriate helper method instead, /// since they can losslessly handle the case where the ScrollAnchor was last set from the other side. pub fn native_anchor(&self, snapshot: &DisplaySnapshot, cx: &App) -> ScrollAnchor { let shared = self.anchor.read(cx); let mut result = if let Some(display_map_id) = shared.display_map_id && display_map_id != snapshot.display_map_id { let companion_snapshot = snapshot.companion_snapshot().unwrap(); assert_eq!(companion_snapshot.display_map_id, display_map_id); let mut display_point = shared .scroll_anchor .anchor .to_display_point(companion_snapshot); *display_point.column_mut() = 0; let buffer_point = snapshot.display_point_to_point(display_point, Bias::Left); let anchor = snapshot.buffer_snapshot().anchor_before(buffer_point); ScrollAnchor { anchor, offset: shared.scroll_anchor.offset, } } else { shared.scroll_anchor }; if let Some(max_x) = self.scroll_max_x { result.offset.x = result.offset.x.min(max_x); } result } pub fn shared_scroll_anchor(&self, cx: &App) -> SharedScrollAnchor { let mut shared = *self.anchor.read(cx); if let Some(max_x) = self.scroll_max_x { shared.scroll_anchor.offset.x = shared.scroll_anchor.offset.x.min(max_x); } shared } pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot, cx: &App) -> DisplayPoint { self.anchor.read(cx).scroll_top_display_point(snapshot) } pub fn scroll_anchor_entity(&self) -> Entity<SharedScrollAnchor> { self.anchor.clone() } pub fn set_shared_scroll_anchor(&mut self, entity: Entity<SharedScrollAnchor>) { self.anchor = entity; } pub fn ongoing_scroll(&self) -> OngoingScroll { self.ongoing } pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) { self.ongoing.last_event = Instant::now(); self.ongoing.axis = axis; } pub fn scroll_position( &self, snapshot: &DisplaySnapshot, cx: &App, ) -> gpui::Point<ScrollOffset> { let mut pos = self.anchor.read(cx).scroll_position(snapshot); if let Some(max_x) = self.scroll_max_x { pos.x = pos.x.min(max_x); } 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>, map: &DisplaySnapshot, local: bool, autoscroll: bool, workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, ) -> 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().as_f64(); 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().as_f64(); 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_before(scroll_top_buffer_point); self.set_anchor( ScrollAnchor { anchor: top_anchor, offset: point( scroll_position.x.max(0.), scroll_top - top_anchor.to_display_point(map).row().as_f64(), ), }, map, scroll_top_buffer_point.row, local, autoscroll, workspace_id, window, cx, ) } fn set_anchor( &mut self, anchor: ScrollAnchor, display_map: &DisplaySnapshot, top_row: u32, local: bool, autoscroll: bool, workspace_id: Option<WorkspaceId>, window: &mut Window, cx: &mut Context<Editor>, ) -> WasScrolled { let adjusted_anchor = if self.forbid_vertical_scroll { let current = self.anchor.read(cx); ScrollAnchor { offset: gpui::Point::new(anchor.offset.x, current.scroll_anchor.offset.y), anchor: current.scroll_anchor.anchor, } } else { anchor }; self.scroll_max_x.take(); self.autoscroll_request.take(); let current = self.anchor.read(cx); if current.scroll_anchor == adjusted_anchor { return WasScrolled(false); } self.anchor.update(cx, |shared, _| { shared.scroll_anchor = adjusted_anchor; shared.display_map_id = Some(display_map.display_map_id); }); 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; let executor = cx.background_executor().clone(); self._save_scroll_position_task = cx.background_executor().spawn(async move { executor.timer(Duration::from_millis(10)).await; 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(); }); } cx.notify(); WasScrolled(true) } pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) { if !self.show_scrollbars { self.show_scrollbars = true; cx.notify(); } if cx.default_global::<ScrollbarAutoHide>().should_hide() { self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| { cx.background_executor() .timer(SCROLLBAR_SHOW_INTERVAL)