954 lines · 31.4 KB · Rust
RawDownload
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.