scroll.rs

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