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