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