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}