scroll.rs

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