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