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