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    dragging_minimap: bool,
161    show_minimap_thumb: bool,
162}
163
164impl ScrollManager {
165    pub fn new(cx: &mut App) -> Self {
166        ScrollManager {
167            vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
168            anchor: ScrollAnchor::new(),
169            ongoing: OngoingScroll::new(),
170            autoscroll_request: None,
171            show_scrollbars: true,
172            hide_scrollbar_task: None,
173            active_scrollbar: None,
174            last_autoscroll: None,
175            visible_line_count: None,
176            forbid_vertical_scroll: false,
177            dragging_minimap: false,
178            show_minimap_thumb: false,
179        }
180    }
181
182    pub fn clone_state(&mut self, other: &Self) {
183        self.anchor = other.anchor;
184        self.ongoing = other.ongoing;
185    }
186
187    pub fn anchor(&self) -> ScrollAnchor {
188        self.anchor
189    }
190
191    pub fn ongoing_scroll(&self) -> OngoingScroll {
192        self.ongoing
193    }
194
195    pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
196        self.ongoing.last_event = Instant::now();
197        self.ongoing.axis = axis;
198    }
199
200    pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<f32> {
201        self.anchor.scroll_position(snapshot)
202    }
203
204    fn set_scroll_position(
205        &mut self,
206        scroll_position: gpui::Point<f32>,
207        map: &DisplaySnapshot,
208        local: bool,
209        autoscroll: bool,
210        workspace_id: Option<WorkspaceId>,
211        window: &mut Window,
212        cx: &mut Context<Editor>,
213    ) {
214        let (new_anchor, top_row) = if scroll_position.y <= 0. {
215            (
216                ScrollAnchor {
217                    anchor: Anchor::min(),
218                    offset: scroll_position.max(&gpui::Point::default()),
219                },
220                0,
221            )
222        } else {
223            let scroll_top = scroll_position.y;
224            let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
225                ScrollBeyondLastLine::OnePage => scroll_top,
226                ScrollBeyondLastLine::Off => {
227                    if let Some(height_in_lines) = self.visible_line_count {
228                        let max_row = map.max_point().row().0 as f32;
229                        scroll_top.min(max_row - height_in_lines + 1.).max(0.)
230                    } else {
231                        scroll_top
232                    }
233                }
234                ScrollBeyondLastLine::VerticalScrollMargin => {
235                    if let Some(height_in_lines) = self.visible_line_count {
236                        let max_row = map.max_point().row().0 as f32;
237                        scroll_top
238                            .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
239                            .max(0.)
240                    } else {
241                        scroll_top
242                    }
243                }
244            };
245
246            let scroll_top_buffer_point =
247                DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
248            let top_anchor = map
249                .buffer_snapshot
250                .anchor_at(scroll_top_buffer_point, Bias::Right);
251
252            (
253                ScrollAnchor {
254                    anchor: top_anchor,
255                    offset: point(
256                        scroll_position.x.max(0.),
257                        scroll_top - top_anchor.to_display_point(map).row().as_f32(),
258                    ),
259                },
260                scroll_top_buffer_point.row,
261            )
262        };
263
264        self.set_anchor(
265            new_anchor,
266            top_row,
267            local,
268            autoscroll,
269            workspace_id,
270            window,
271            cx,
272        );
273    }
274
275    fn set_anchor(
276        &mut self,
277        anchor: ScrollAnchor,
278        top_row: u32,
279        local: bool,
280        autoscroll: bool,
281        workspace_id: Option<WorkspaceId>,
282        window: &mut Window,
283        cx: &mut Context<Editor>,
284    ) {
285        let adjusted_anchor = if self.forbid_vertical_scroll {
286            ScrollAnchor {
287                offset: gpui::Point::new(anchor.offset.x, self.anchor.offset.y),
288                anchor: self.anchor.anchor,
289            }
290        } else {
291            anchor
292        };
293
294        self.anchor = adjusted_anchor;
295        cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
296        self.show_scrollbars(window, cx);
297        self.autoscroll_request.take();
298        if let Some(workspace_id) = workspace_id {
299            let item_id = cx.entity().entity_id().as_u64() as ItemId;
300
301            cx.foreground_executor()
302                .spawn(async move {
303                    log::debug!(
304                        "Saving scroll position for item {item_id:?} in workspace {workspace_id:?}"
305                    );
306                    DB.save_scroll_position(
307                        item_id,
308                        workspace_id,
309                        top_row,
310                        anchor.offset.x,
311                        anchor.offset.y,
312                    )
313                    .await
314                    .log_err()
315                })
316                .detach()
317        }
318        cx.notify();
319    }
320
321    pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
322        if !self.show_scrollbars {
323            self.show_scrollbars = true;
324            cx.notify();
325        }
326
327        if cx.default_global::<ScrollbarAutoHide>().0 {
328            self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
329                cx.background_executor()
330                    .timer(SCROLLBAR_SHOW_INTERVAL)
331                    .await;
332                editor
333                    .update(cx, |editor, cx| {
334                        editor.scroll_manager.show_scrollbars = false;
335                        cx.notify();
336                    })
337                    .log_err();
338            }));
339        } else {
340            self.hide_scrollbar_task = None;
341        }
342    }
343
344    pub fn scrollbars_visible(&self) -> bool {
345        self.show_scrollbars
346    }
347
348    pub fn show_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
349        if !self.show_minimap_thumb {
350            self.show_minimap_thumb = true;
351            cx.notify();
352        }
353    }
354
355    pub fn hide_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
356        if self.show_minimap_thumb {
357            self.show_minimap_thumb = false;
358            cx.notify();
359        }
360    }
361
362    pub fn minimap_thumb_visible(&mut self) -> bool {
363        self.show_minimap_thumb
364    }
365
366    pub fn autoscroll_request(&self) -> Option<Autoscroll> {
367        self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
368    }
369
370    pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
371        self.active_scrollbar.as_ref()
372    }
373
374    pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
375        self.active_scrollbar
376            .as_ref()
377            .filter(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
378            .map(|scrollbar| scrollbar.axis)
379    }
380
381    pub fn any_scrollbar_dragged(&self) -> bool {
382        self.active_scrollbar
383            .as_ref()
384            .is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
385    }
386
387    pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
388        self.update_active_scrollbar_state(
389            Some(ActiveScrollbarState::new(
390                axis,
391                ScrollbarThumbState::Hovered,
392            )),
393            cx,
394        );
395    }
396
397    pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
398        self.update_active_scrollbar_state(
399            Some(ActiveScrollbarState::new(
400                axis,
401                ScrollbarThumbState::Dragging,
402            )),
403            cx,
404        );
405    }
406
407    pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
408        self.update_active_scrollbar_state(None, cx);
409    }
410
411    fn update_active_scrollbar_state(
412        &mut self,
413        new_state: Option<ActiveScrollbarState>,
414        cx: &mut Context<Editor>,
415    ) {
416        if self.active_scrollbar != new_state {
417            self.active_scrollbar = new_state;
418            cx.notify();
419        }
420    }
421
422    pub fn is_dragging_minimap(&self) -> bool {
423        self.dragging_minimap
424    }
425
426    pub fn set_is_dragging_minimap(&mut self, dragging: bool, cx: &mut Context<Editor>) {
427        self.dragging_minimap = dragging;
428        cx.notify();
429    }
430
431    pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
432        if max < self.anchor.offset.x {
433            self.anchor.offset.x = max;
434            true
435        } else {
436            false
437        }
438    }
439
440    pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
441        self.forbid_vertical_scroll = forbid;
442    }
443
444    pub fn forbid_vertical_scroll(&self) -> bool {
445        self.forbid_vertical_scroll
446    }
447}
448
449impl Editor {
450    pub fn vertical_scroll_margin(&self) -> usize {
451        self.scroll_manager.vertical_scroll_margin as usize
452    }
453
454    pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context<Self>) {
455        self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
456        cx.notify();
457    }
458
459    pub fn visible_line_count(&self) -> Option<f32> {
460        self.scroll_manager.visible_line_count
461    }
462
463    pub fn visible_row_count(&self) -> Option<u32> {
464        self.visible_line_count()
465            .map(|line_count| line_count as u32 - 1)
466    }
467
468    pub(crate) fn set_visible_line_count(
469        &mut self,
470        lines: f32,
471        window: &mut Window,
472        cx: &mut Context<Self>,
473    ) {
474        let opened_first_time = self.scroll_manager.visible_line_count.is_none();
475        self.scroll_manager.visible_line_count = Some(lines);
476        if opened_first_time {
477            cx.spawn_in(window, async move |editor, cx| {
478                editor
479                    .update(cx, |editor, cx| {
480                        editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
481                    })
482                    .ok()
483            })
484            .detach()
485        }
486    }
487
488    pub fn apply_scroll_delta(
489        &mut self,
490        scroll_delta: gpui::Point<f32>,
491        window: &mut Window,
492        cx: &mut Context<Self>,
493    ) {
494        let mut delta = scroll_delta;
495        if self.scroll_manager.forbid_vertical_scroll {
496            delta.y = 0.0;
497        }
498        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
499        let position = self.scroll_manager.anchor.scroll_position(&display_map) + delta;
500        self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
501    }
502
503    pub fn set_scroll_position(
504        &mut self,
505        scroll_position: gpui::Point<f32>,
506        window: &mut Window,
507        cx: &mut Context<Self>,
508    ) {
509        let mut position = scroll_position;
510        if self.scroll_manager.forbid_vertical_scroll {
511            let current_position = self.scroll_position(cx);
512            position.y = current_position.y;
513        }
514        self.set_scroll_position_internal(position, true, false, window, cx);
515    }
516
517    /// Scrolls so that `row` is at the top of the editor view.
518    pub fn set_scroll_top_row(
519        &mut self,
520        row: DisplayRow,
521        window: &mut Window,
522        cx: &mut Context<Editor>,
523    ) {
524        let snapshot = self.snapshot(window, cx).display_snapshot;
525        let new_screen_top = DisplayPoint::new(row, 0);
526        let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
527        let new_anchor = snapshot.buffer_snapshot.anchor_before(new_screen_top);
528
529        self.set_scroll_anchor(
530            ScrollAnchor {
531                anchor: new_anchor,
532                offset: Default::default(),
533            },
534            window,
535            cx,
536        );
537    }
538
539    pub(crate) fn set_scroll_position_internal(
540        &mut self,
541        scroll_position: gpui::Point<f32>,
542        local: bool,
543        autoscroll: bool,
544        window: &mut Window,
545        cx: &mut Context<Self>,
546    ) {
547        let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
548        self.set_scroll_position_taking_display_map(
549            scroll_position,
550            local,
551            autoscroll,
552            map,
553            window,
554            cx,
555        );
556    }
557
558    fn set_scroll_position_taking_display_map(
559        &mut self,
560        scroll_position: gpui::Point<f32>,
561        local: bool,
562        autoscroll: bool,
563        display_map: DisplaySnapshot,
564        window: &mut Window,
565        cx: &mut Context<Self>,
566    ) {
567        hide_hover(self, cx);
568        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
569
570        self.edit_prediction_preview
571            .set_previous_scroll_position(None);
572
573        let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
574            let current_position = self.scroll_manager.anchor.scroll_position(&display_map);
575            gpui::Point::new(scroll_position.x, current_position.y)
576        } else {
577            scroll_position
578        };
579
580        self.scroll_manager.set_scroll_position(
581            adjusted_position,
582            &display_map,
583            local,
584            autoscroll,
585            workspace_id,
586            window,
587            cx,
588        );
589
590        self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
591    }
592
593    pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
594        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
595        self.scroll_manager.anchor.scroll_position(&display_map)
596    }
597
598    pub fn set_scroll_anchor(
599        &mut self,
600        scroll_anchor: ScrollAnchor,
601        window: &mut Window,
602        cx: &mut Context<Self>,
603    ) {
604        hide_hover(self, cx);
605        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
606        let top_row = scroll_anchor
607            .anchor
608            .to_point(&self.buffer().read(cx).snapshot(cx))
609            .row;
610        self.scroll_manager.set_anchor(
611            scroll_anchor,
612            top_row,
613            true,
614            false,
615            workspace_id,
616            window,
617            cx,
618        );
619    }
620
621    pub(crate) fn set_scroll_anchor_remote(
622        &mut self,
623        scroll_anchor: ScrollAnchor,
624        window: &mut Window,
625        cx: &mut Context<Self>,
626    ) {
627        hide_hover(self, cx);
628        let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
629        let snapshot = &self.buffer().read(cx).snapshot(cx);
630        if !scroll_anchor.anchor.is_valid(snapshot) {
631            log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
632            return;
633        }
634        let top_row = scroll_anchor.anchor.to_point(snapshot).row;
635        self.scroll_manager.set_anchor(
636            scroll_anchor,
637            top_row,
638            false,
639            false,
640            workspace_id,
641            window,
642            cx,
643        );
644    }
645
646    pub fn scroll_screen(
647        &mut self,
648        amount: &ScrollAmount,
649        window: &mut Window,
650        cx: &mut Context<Self>,
651    ) {
652        if matches!(self.mode, EditorMode::SingleLine { .. }) {
653            cx.propagate();
654            return;
655        }
656
657        if self.take_rename(true, window, cx).is_some() {
658            return;
659        }
660
661        let cur_position = self.scroll_position(cx);
662        let Some(visible_line_count) = self.visible_line_count() else {
663            return;
664        };
665        let new_pos = cur_position + point(0., amount.lines(visible_line_count));
666        self.set_scroll_position(new_pos, window, cx);
667    }
668
669    /// Returns an ordering. The newest selection is:
670    ///     Ordering::Equal => on screen
671    ///     Ordering::Less => above the screen
672    ///     Ordering::Greater => below the screen
673    pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
674        let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
675        let newest_head = self
676            .selections
677            .newest_anchor()
678            .head()
679            .to_display_point(&snapshot);
680        let screen_top = self
681            .scroll_manager
682            .anchor
683            .anchor
684            .to_display_point(&snapshot);
685
686        if screen_top > newest_head {
687            return Ordering::Less;
688        }
689
690        if let Some(visible_lines) = self.visible_line_count() {
691            if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
692                return Ordering::Equal;
693            }
694        }
695
696        Ordering::Greater
697    }
698
699    pub fn read_scroll_position_from_db(
700        &mut self,
701        item_id: u64,
702        workspace_id: WorkspaceId,
703        window: &mut Window,
704        cx: &mut Context<Editor>,
705    ) {
706        let scroll_position = DB.get_scroll_position(item_id, workspace_id);
707        if let Ok(Some((top_row, x, y))) = scroll_position {
708            let top_anchor = self
709                .buffer()
710                .read(cx)
711                .snapshot(cx)
712                .anchor_at(Point::new(top_row, 0), Bias::Left);
713            let scroll_anchor = ScrollAnchor {
714                offset: gpui::Point::new(x, y),
715                anchor: top_anchor,
716            };
717            self.set_scroll_anchor(scroll_anchor, window, cx);
718        }
719    }
720}