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