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