scroll.rs

  1mod actions;
  2pub(crate) mod autoscroll;
  3pub(crate) mod scroll_amount;
  4
  5use crate::editor_settings::ScrollBeyondLastLine;
  6use crate::{
  7    Anchor, DisplayPoint, DisplayRow, Editor, EditorEvent, EditorMode, EditorSettings,
  8    InlayHintRefreshReason, MultiBufferSnapshot, RowExt, ToPoint,
  9    display_map::{DisplaySnapshot, ToDisplayPoint},
 10    hover_popover::hide_hover,
 11    persistence::DB,
 12};
 13pub use autoscroll::{Autoscroll, AutoscrollStrategy};
 14use core::fmt::Debug;
 15use gpui::{App, Axis, Context, Global, Pixels, Task, Window, point, px};
 16use language::{Bias, Point};
 17pub use scroll_amount::ScrollAmount;
 18use settings::Settings;
 19use std::{
 20    cmp::Ordering,
 21    time::{Duration, Instant},
 22};
 23use util::ResultExt;
 24use workspace::{ItemId, WorkspaceId};
 25
 26pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
 27const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 28
 29#[derive(Default)]
 30pub struct ScrollbarAutoHide(pub bool);
 31
 32impl Global for ScrollbarAutoHide {}
 33
 34#[derive(Clone, Copy, Debug, PartialEq)]
 35pub struct ScrollAnchor {
 36    pub offset: gpui::Point<f32>,
 37    pub anchor: Anchor,
 38}
 39
 40impl ScrollAnchor {
 41    pub(super) fn new() -> Self {
 42        Self {
 43            offset: gpui::Point::default(),
 44            anchor: Anchor::min(),
 45        }
 46    }
 47
 48    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
 49        let mut scroll_position = self.offset;
 50        if self.anchor == Anchor::min() {
 51            scroll_position.y = 0.;
 52        } else {
 53            let scroll_top = self.anchor.to_display_point(snapshot).row().as_f32();
 54            scroll_position.y += scroll_top;
 55        }
 56        scroll_position
 57    }
 58
 59    pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
 60        self.anchor.to_point(buffer).row
 61    }
 62}
 63
 64#[derive(Clone, Copy, Debug)]
 65pub struct OngoingScroll {
 66    last_event: Instant,
 67    axis: Option<Axis>,
 68}
 69
 70impl OngoingScroll {
 71    fn new() -> Self {
 72        Self {
 73            last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
 74            axis: None,
 75        }
 76    }
 77
 78    pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
 79        const UNLOCK_PERCENT: f32 = 1.9;
 80        const UNLOCK_LOWER_BOUND: Pixels = px(6.);
 81        let mut axis = self.axis;
 82
 83        let x = delta.x.abs();
 84        let y = delta.y.abs();
 85        let duration = Instant::now().duration_since(self.last_event);
 86        if duration > SCROLL_EVENT_SEPARATION {
 87            //New ongoing scroll will start, determine axis
 88            axis = if x <= y {
 89                Some(Axis::Vertical)
 90            } else {
 91                Some(Axis::Horizontal)
 92            };
 93        } else if x.max(y) >= UNLOCK_LOWER_BOUND {
 94            //Check if the current ongoing will need to unlock
 95            match axis {
 96                Some(Axis::Vertical) => {
 97                    if x > y && x >= y * UNLOCK_PERCENT {
 98                        axis = None;
 99                    }
100                }
101
102                Some(Axis::Horizontal) => {
103                    if y > x && y >= x * UNLOCK_PERCENT {
104                        axis = None;
105                    }
106                }
107
108                None => {}
109            }
110        }
111
112        match axis {
113            Some(Axis::Vertical) => {
114                *delta = point(px(0.), delta.y);
115            }
116            Some(Axis::Horizontal) => {
117                *delta = point(delta.x, px(0.));
118            }
119            None => {}
120        }
121
122        axis
123    }
124}
125
126#[derive(Copy, Clone, PartialEq, Eq)]
127pub enum ScrollbarThumbState {
128    Idle,
129    Hovered,
130    Dragging,
131}
132
133#[derive(PartialEq, Eq)]
134pub struct ActiveScrollbarState {
135    axis: Axis,
136    thumb_state: ScrollbarThumbState,
137}
138
139impl ActiveScrollbarState {
140    pub fn new(axis: Axis, thumb_state: ScrollbarThumbState) -> Self {
141        ActiveScrollbarState { axis, thumb_state }
142    }
143
144    pub fn thumb_state_for_axis(&self, axis: Axis) -> Option<ScrollbarThumbState> {
145        (self.axis == axis).then_some(self.thumb_state)
146    }
147}
148
149pub struct ScrollManager {
150    pub(crate) vertical_scroll_margin: f32,
151    anchor: ScrollAnchor,
152    ongoing: OngoingScroll,
153    autoscroll_request: Option<(Autoscroll, bool)>,
154    last_autoscroll: Option<(gpui::Point<f32>, f32, f32, AutoscrollStrategy)>,
155    show_scrollbars: bool,
156    hide_scrollbar_task: Option<Task<()>>,
157    active_scrollbar: Option<ActiveScrollbarState>,
158    visible_line_count: Option<f32>,
159    forbid_vertical_scroll: bool,
160}
161
162impl ScrollManager {
163    pub fn new(cx: &mut App) -> Self {
164        ScrollManager {
165            vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
166            anchor: ScrollAnchor::new(),
167            ongoing: OngoingScroll::new(),
168            autoscroll_request: None,
169            show_scrollbars: true,
170            hide_scrollbar_task: None,
171            active_scrollbar: None,
172            last_autoscroll: None,
173            visible_line_count: None,
174            forbid_vertical_scroll: false,
175        }
176    }
177
178    pub fn clone_state(&mut self, other: &Self) {
179        self.anchor = other.anchor;
180        self.ongoing = other.ongoing;
181    }
182
183    pub fn anchor(&self) -> ScrollAnchor {
184        self.anchor
185    }
186
187    pub fn ongoing_scroll(&self) -> OngoingScroll {
188        self.ongoing
189    }
190
191    pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
192        self.ongoing.last_event = Instant::now();
193        self.ongoing.axis = axis;
194    }
195
196    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
197        self.anchor.scroll_position(snapshot)
198    }
199
200    fn set_scroll_position(
201        &mut self,
202        scroll_position: gpui::Point<f32>,
203        map: &DisplaySnapshot,
204        local: bool,
205        autoscroll: bool,
206        workspace_id: Option<WorkspaceId>,
207        window: &mut Window,
208        cx: &mut Context<Editor>,
209    ) {
210        let (new_anchor, top_row) = if scroll_position.y <= 0. {
211            (
212                ScrollAnchor {
213                    anchor: Anchor::min(),
214                    offset: scroll_position.max(&gpui::Point::default()),
215                },
216                0,
217            )
218        } else {
219            let scroll_top = scroll_position.y;
220            let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
221                ScrollBeyondLastLine::OnePage => scroll_top,
222                ScrollBeyondLastLine::Off => {
223                    if let Some(height_in_lines) = self.visible_line_count {
224                        let max_row = map.max_point().row().0 as f32;
225                        scroll_top.min(max_row - height_in_lines + 1.).max(0.)
226                    } else {
227                        scroll_top
228                    }
229                }
230                ScrollBeyondLastLine::VerticalScrollMargin => {
231                    if let Some(height_in_lines) = self.visible_line_count {
232                        let max_row = map.max_point().row().0 as f32;
233                        scroll_top
234                            .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
235                            .max(0.)
236                    } else {
237                        scroll_top
238                    }
239                }
240            };
241
242            let scroll_top_buffer_point =
243                DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
244            let top_anchor = map
245                .buffer_snapshot
246                .anchor_at(scroll_top_buffer_point, Bias::Right);
247
248            (
249                ScrollAnchor {
250                    anchor: top_anchor,
251                    offset: point(
252                        scroll_position.x.max(0.),
253                        scroll_top - top_anchor.to_display_point(map).row().as_f32(),
254                    ),
255                },
256                scroll_top_buffer_point.row,
257            )
258        };
259
260        self.set_anchor(
261            new_anchor,
262            top_row,
263            local,
264            autoscroll,
265            workspace_id,
266            window,
267            cx,
268        );
269    }
270
271    fn set_anchor(
272        &mut self,
273        anchor: ScrollAnchor,
274        top_row: u32,
275        local: bool,
276        autoscroll: bool,
277        workspace_id: Option<WorkspaceId>,
278        window: &mut Window,
279        cx: &mut Context<Editor>,
280    ) {
281        let adjusted_anchor = if self.forbid_vertical_scroll {
282            ScrollAnchor {
283                offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
284                anchor: self.anchor.anchor,
285            }
286        } else {
287            anchor
288        };
289
290        self.anchor = adjusted_anchor;
291        cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
292        self.show_scrollbars(window, cx);
293        self.autoscroll_request.take();
294        if let Some(workspace_id) = workspace_id {
295            let item_id = cx.entity().entity_id().as_u64() as ItemId;
296
297            cx.foreground_executor()
298                .spawn(async move {
299                    log::debug!(
300                        "Saving scroll position for item {item_id:?} in workspace {workspace_id:?}"
301                    );
302                    DB.save_scroll_position(
303                        item_id,
304                        workspace_id,
305                        top_row,
306                        anchor.offset.x,
307                        anchor.offset.y,
308                    )
309                    .await
310                    .log_err()
311                })
312                .detach()
313        }
314        cx.notify();
315    }
316
317    pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
318        if !self.show_scrollbars {
319            self.show_scrollbars = true;
320            cx.notify();
321        }
322
323        if cx.default_global::<ScrollbarAutoHide>().0 {
324            self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
325                cx.background_executor()
326                    .timer(SCROLLBAR_SHOW_INTERVAL)
327                    .await;
328                editor
329                    .update(cx, |editor, cx| {
330                        editor.scroll_manager.show_scrollbars = false;
331                        cx.notify();
332                    })
333                    .log_err();
334            }));
335        } else {
336            self.hide_scrollbar_task = None;
337        }
338    }
339
340    pub fn scrollbars_visible(&self) -> bool {
341        self.show_scrollbars
342    }
343
344    pub fn autoscroll_request(&self) -> Option<Autoscroll> {
345        self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
346    }
347
348    pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
349        self.active_scrollbar.as_ref()
350    }
351
352    pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
353        self.active_scrollbar
354            .as_ref()
355            .map(|scrollbar| scrollbar.axis)
356    }
357
358    pub fn any_scrollbar_dragged(&self) -> bool {
359        self.active_scrollbar
360            .as_ref()
361            .is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
362    }
363
364    pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
365        self.update_active_scrollbar_state(
366            Some(ActiveScrollbarState::new(
367                axis,
368                ScrollbarThumbState::Hovered,
369            )),
370            cx,
371        );
372    }
373
374    pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
375        self.update_active_scrollbar_state(
376            Some(ActiveScrollbarState::new(
377                axis,
378                ScrollbarThumbState::Dragging,
379            )),
380            cx,
381        );
382    }
383
384    pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
385        self.update_active_scrollbar_state(None, cx);
386    }
387
388    fn update_active_scrollbar_state(
389        &mut self,
390        new_state: Option<ActiveScrollbarState>,
391        cx: &mut Context<Editor>,
392    ) {
393        if self.active_scrollbar != new_state {
394            self.active_scrollbar = new_state;
395            cx.notify();
396        }
397    }
398
399    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
400        if max < self.anchor.offset.x {
401            self.anchor.offset.x = max;
402            true
403        } else {
404            false
405        }
406    }
407
408    pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
409        self.forbid_vertical_scroll = forbid;
410    }
411
412    pub fn forbid_vertical_scroll(&self) -> bool {
413        self.forbid_vertical_scroll
414    }
415}
416
417impl Editor {
418    pub fn vertical_scroll_margin(&self) -> usize {
419        self.scroll_manager.vertical_scroll_margin as usize
420    }
421
422    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context<Self>) {
423        self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
424        cx.notify();
425    }
426
427    pub fn visible_line_count(&self) -> Option<f32> {
428        self.scroll_manager.visible_line_count
429    }
430
431    pub fn visible_row_count(&self) -> Option<u32> {
432        self.visible_line_count()
433            .map(|line_count| line_count as u32 - 1)
434    }
435
436    pub(crate) fn set_visible_line_count(
437        &mut self,
438        lines: f32,
439        window: &mut Window,
440        cx: &mut Context<Self>,
441    ) {
442        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
443        self.scroll_manager.visible_line_count = Some(lines);
444        if opened_first_time {
445            cx.spawn_in(window, async move |editor, cx| {
446                editor
447                    .update(cx, |editor, cx| {
448                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
449                    })
450                    .ok()
451            })
452            .detach()
453        }
454    }
455
456    pub fn apply_scroll_delta(
457        &mut self,
458        scroll_delta: gpui::Point<f32>,
459        window: &mut Window,
460        cx: &mut Context<Self>,
461    ) {
462        let mut delta = scroll_delta;
463        if self.scroll_manager.forbid_vertical_scroll {
464            delta.y = 0.0;
465        }
466        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
467        let position = self.scroll_manager.anchor.scroll_position(&display_map) + delta;
468        self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
469    }
470
471    pub fn set_scroll_position(
472        &mut self,
473        scroll_position: gpui::Point<f32>,
474        window: &mut Window,
475        cx: &mut Context<Self>,
476    ) {
477        let mut position = scroll_position;
478        if self.scroll_manager.forbid_vertical_scroll {
479            let current_position = self.scroll_position(cx);
480            position.y = current_position.y;
481        }
482        self.set_scroll_position_internal(position, true, false, window, cx);
483    }
484
485    /// Scrolls so that `row` is at the top of the editor view.
486    pub fn set_scroll_top_row(
487        &mut self,
488        row: DisplayRow,
489        window: &mut Window,
490        cx: &mut Context<Editor>,
491    ) {
492        let snapshot = self.snapshot(window, cx).display_snapshot;
493        let new_screen_top = DisplayPoint::new(row, 0);
494        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
495        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
496
497        self.set_scroll_anchor(
498            ScrollAnchor {
499                anchor: new_anchor,
500                offset: Default::default(),
501            },
502            window,
503            cx,
504        );
505    }
506
507    pub(crate) fn set_scroll_position_internal(
508        &mut self,
509        scroll_position: gpui::Point<f32>,
510        local: bool,
511        autoscroll: bool,
512        window: &mut Window,
513        cx: &mut Context<Self>,
514    ) {
515        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
516        self.set_scroll_position_taking_display_map(
517            scroll_position,
518            local,
519            autoscroll,
520            map,
521            window,
522            cx,
523        );
524    }
525
526    fn set_scroll_position_taking_display_map(
527        &mut self,
528        scroll_position: gpui::Point<f32>,
529        local: bool,
530        autoscroll: bool,
531        display_map: DisplaySnapshot,
532        window: &mut Window,
533        cx: &mut Context<Self>,
534    ) {
535        hide_hover(self, cx);
536        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
537
538        self.edit_prediction_preview
539            .set_previous_scroll_position(None);
540
541        let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
542            let current_position = self.scroll_manager.anchor.scroll_position(&display_map);
543            gpui::Point::new(scroll_position.x, current_position.y)
544        } else {
545            scroll_position
546        };
547
548        self.scroll_manager.set_scroll_position(
549            adjusted_position,
550            &display_map,
551            local,
552            autoscroll,
553            workspace_id,
554            window,
555            cx,
556        );
557
558        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
559    }
560
561    pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
562        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
563        self.scroll_manager.anchor.scroll_position(&display_map)
564    }
565
566    pub fn set_scroll_anchor(
567        &mut self,
568        scroll_anchor: ScrollAnchor,
569        window: &mut Window,
570        cx: &mut Context<Self>,
571    ) {
572        hide_hover(self, cx);
573        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
574        let top_row = scroll_anchor
575            .anchor
576            .to_point(&self.buffer().read(cx).snapshot(cx))
577            .row;
578        self.scroll_manager.set_anchor(
579            scroll_anchor,
580            top_row,
581            true,
582            false,
583            workspace_id,
584            window,
585            cx,
586        );
587    }
588
589    pub(crate) fn set_scroll_anchor_remote(
590        &mut self,
591        scroll_anchor: ScrollAnchor,
592        window: &mut Window,
593        cx: &mut Context<Self>,
594    ) {
595        hide_hover(self, cx);
596        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
597        let snapshot = &self.buffer().read(cx).snapshot(cx);
598        if !scroll_anchor.anchor.is_valid(snapshot) {
599            log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
600            return;
601        }
602        let top_row = scroll_anchor.anchor.to_point(snapshot).row;
603        self.scroll_manager.set_anchor(
604            scroll_anchor,
605            top_row,
606            false,
607            false,
608            workspace_id,
609            window,
610            cx,
611        );
612    }
613
614    pub fn scroll_screen(
615        &mut self,
616        amount: &ScrollAmount,
617        window: &mut Window,
618        cx: &mut Context<Self>,
619    ) {
620        if matches!(self.mode, EditorMode::SingleLine { .. }) {
621            cx.propagate();
622            return;
623        }
624
625        if self.take_rename(true, window, cx).is_some() {
626            return;
627        }
628
629        let cur_position = self.scroll_position(cx);
630        let Some(visible_line_count) = self.visible_line_count() else {
631            return;
632        };
633        let new_pos = cur_position + point(0., amount.lines(visible_line_count));
634        self.set_scroll_position(new_pos, window, cx);
635    }
636
637    /// Returns an ordering. The newest selection is:
638    ///     Ordering::Equal => on screen
639    ///     Ordering::Less => above the screen
640    ///     Ordering::Greater => below the screen
641    pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
642        let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
643        let newest_head = self
644            .selections
645            .newest_anchor()
646            .head()
647            .to_display_point(&snapshot);
648        let screen_top = self
649            .scroll_manager
650            .anchor
651            .anchor
652            .to_display_point(&snapshot);
653
654        if screen_top > newest_head {
655            return Ordering::Less;
656        }
657
658        if let Some(visible_lines) = self.visible_line_count() {
659            if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
660                return Ordering::Equal;
661            }
662        }
663
664        Ordering::Greater
665    }
666
667    pub fn read_scroll_position_from_db(
668        &mut self,
669        item_id: u64,
670        workspace_id: WorkspaceId,
671        window: &mut Window,
672        cx: &mut Context<Editor>,
673    ) {
674        let scroll_position = DB.get_scroll_position(item_id, workspace_id);
675        if let Ok(Some((top_row, x, y))) = scroll_position {
676            let top_anchor = self
677                .buffer()
678                .read(cx)
679                .snapshot(cx)
680                .anchor_at(Point::new(top_row, 0), Bias::Left);
681            let scroll_anchor = ScrollAnchor {
682                offset: gpui::Point::new(x, y),
683                anchor: top_anchor,
684            };
685            self.set_scroll_anchor(scroll_anchor, window, cx);
686        }
687    }
688}