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 fn set_scroll_position(
228 &mut self,
229 scroll_position: gpui::Point<f32>,
230 map: &DisplaySnapshot,
231 local: bool,
232 autoscroll: bool,
233 workspace_id: Option<WorkspaceId>,
234 window: &mut Window,
235 cx: &mut Context<Editor>,
236 ) {
237 if self.forbid_vertical_scroll {
238 return;
239 }
240 let (new_anchor, top_row) = if scroll_position.y <= 0. {
241 (
242 ScrollAnchor {
243 anchor: Anchor::min(),
244 offset: scroll_position.max(&gpui::Point::default()),
245 },
246 0,
247 )
248 } else {
249 let scroll_top = scroll_position.y;
250 let scroll_top = match EditorSettings::get_global(cx).scroll_beyond_last_line {
251 ScrollBeyondLastLine::OnePage => scroll_top,
252 ScrollBeyondLastLine::Off => {
253 if let Some(height_in_lines) = self.visible_line_count {
254 let max_row = map.max_point().row().0 as f32;
255 scroll_top.min(max_row - height_in_lines + 1.).max(0.)
256 } else {
257 scroll_top
258 }
259 }
260 ScrollBeyondLastLine::VerticalScrollMargin => {
261 if let Some(height_in_lines) = self.visible_line_count {
262 let max_row = map.max_point().row().0 as f32;
263 scroll_top
264 .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
265 .max(0.)
266 } else {
267 scroll_top
268 }
269 }
270 };
271
272 let scroll_top_buffer_point =
273 DisplayPoint::new(DisplayRow(scroll_top as u32), 0).to_point(map);
274 let top_anchor = map
275 .buffer_snapshot
276 .anchor_at(scroll_top_buffer_point, Bias::Right);
277
278 (
279 ScrollAnchor {
280 anchor: top_anchor,
281 offset: point(
282 scroll_position.x.max(0.),
283 scroll_top - top_anchor.to_display_point(map).row().as_f32(),
284 ),
285 },
286 scroll_top_buffer_point.row,
287 )
288 };
289
290 self.set_anchor(
291 new_anchor,
292 top_row,
293 local,
294 autoscroll,
295 workspace_id,
296 window,
297 cx,
298 );
299 }
300
301 fn set_anchor(
302 &mut self,
303 anchor: ScrollAnchor,
304 top_row: u32,
305 local: bool,
306 autoscroll: bool,
307 workspace_id: Option<WorkspaceId>,
308 window: &mut Window,
309 cx: &mut Context<Editor>,
310 ) {
311 if self.forbid_vertical_scroll {
312 return;
313 }
314 self.anchor = anchor;
315 cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
316 self.show_scrollbar(window, cx);
317 self.autoscroll_request.take();
318 if let Some(workspace_id) = workspace_id {
319 let item_id = cx.entity().entity_id().as_u64() as ItemId;
320
321 cx.foreground_executor()
322 .spawn(async move {
323 DB.save_scroll_position(
324 item_id,
325 workspace_id,
326 top_row,
327 anchor.offset.x,
328 anchor.offset.y,
329 )
330 .await
331 .log_err()
332 })
333 .detach()
334 }
335 cx.notify();
336 }
337
338 pub fn show_scrollbar(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
339 if !self.show_scrollbars {
340 self.show_scrollbars = true;
341 cx.notify();
342 }
343
344 if cx.default_global::<ScrollbarAutoHide>().0 {
345 self.hide_scrollbar_task = Some(cx.spawn_in(window, |editor, mut cx| async move {
346 cx.background_executor()
347 .timer(SCROLLBAR_SHOW_INTERVAL)
348 .await;
349 editor
350 .update(&mut cx, |editor, cx| {
351 editor.scroll_manager.show_scrollbars = false;
352 cx.notify();
353 })
354 .log_err();
355 }));
356 } else {
357 self.hide_scrollbar_task = None;
358 }
359 }
360
361 pub fn scrollbars_visible(&self) -> bool {
362 self.show_scrollbars
363 }
364
365 pub fn autoscroll_request(&self) -> Option<Autoscroll> {
366 self.autoscroll_request.map(|(autoscroll, _)| autoscroll)
367 }
368
369 pub fn is_dragging_scrollbar(&self, axis: Axis) -> bool {
370 self.dragging_scrollbar.along(axis)
371 }
372
373 pub fn set_is_dragging_scrollbar(
374 &mut self,
375 axis: Axis,
376 dragging: bool,
377 cx: &mut Context<Editor>,
378 ) {
379 self.dragging_scrollbar = self.dragging_scrollbar.apply_along(axis, |_| dragging);
380 cx.notify();
381 }
382
383 pub fn clamp_scroll_left(&mut self, max: f32) -> bool {
384 if max < self.anchor.offset.x {
385 self.anchor.offset.x = max;
386 true
387 } else {
388 false
389 }
390 }
391
392 pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
393 self.forbid_vertical_scroll = forbid;
394 }
395
396 pub fn forbid_vertical_scroll(&self) -> bool {
397 self.forbid_vertical_scroll
398 }
399}
400
401impl Editor {
402 pub fn vertical_scroll_margin(&self) -> usize {
403 self.scroll_manager.vertical_scroll_margin as usize
404 }
405
406 pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context<Self>) {
407 self.scroll_manager.vertical_scroll_margin = margin_rows as f32;
408 cx.notify();
409 }
410
411 pub fn visible_line_count(&self) -> Option<f32> {
412 self.scroll_manager.visible_line_count
413 }
414
415 pub fn visible_row_count(&self) -> Option<u32> {
416 self.visible_line_count()
417 .map(|line_count| line_count as u32 - 1)
418 }
419
420 pub(crate) fn set_visible_line_count(
421 &mut self,
422 lines: f32,
423 window: &mut Window,
424 cx: &mut Context<Self>,
425 ) {
426 let opened_first_time = self.scroll_manager.visible_line_count.is_none();
427 self.scroll_manager.visible_line_count = Some(lines);
428 if opened_first_time {
429 cx.spawn_in(window, |editor, mut cx| async move {
430 editor
431 .update(&mut cx, |editor, cx| {
432 editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx)
433 })
434 .ok()
435 })
436 .detach()
437 }
438 }
439
440 pub fn apply_scroll_delta(
441 &mut self,
442 scroll_delta: gpui::Point<f32>,
443 window: &mut Window,
444 cx: &mut Context<Self>,
445 ) {
446 if self.scroll_manager.forbid_vertical_scroll {
447 return;
448 }
449 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
450 let position = self.scroll_manager.anchor.scroll_position(&display_map) + scroll_delta;
451 self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
452 }
453
454 pub fn set_scroll_position(
455 &mut self,
456 scroll_position: gpui::Point<f32>,
457 window: &mut Window,
458 cx: &mut Context<Self>,
459 ) {
460 if self.scroll_manager.forbid_vertical_scroll {
461 return;
462 }
463 self.set_scroll_position_internal(scroll_position, true, false, window, cx);
464 }
465
466 pub(crate) fn set_scroll_position_internal(
467 &mut self,
468 scroll_position: gpui::Point<f32>,
469 local: bool,
470 autoscroll: bool,
471 window: &mut Window,
472 cx: &mut Context<Self>,
473 ) {
474 let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
475 self.set_scroll_position_taking_display_map(
476 scroll_position,
477 local,
478 autoscroll,
479 map,
480 window,
481 cx,
482 );
483 }
484
485 fn set_scroll_position_taking_display_map(
486 &mut self,
487 scroll_position: gpui::Point<f32>,
488 local: bool,
489 autoscroll: bool,
490 display_map: DisplaySnapshot,
491 window: &mut Window,
492 cx: &mut Context<Self>,
493 ) {
494 hide_hover(self, cx);
495 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
496
497 if let EditPredictionPreview::Active {
498 previous_scroll_position,
499 } = &mut self.edit_prediction_preview
500 {
501 if !autoscroll {
502 previous_scroll_position.take();
503 }
504 }
505
506 self.scroll_manager.set_scroll_position(
507 scroll_position,
508 &display_map,
509 local,
510 autoscroll,
511 workspace_id,
512 window,
513 cx,
514 );
515
516 self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
517 }
518
519 pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<f32> {
520 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
521 self.scroll_manager.anchor.scroll_position(&display_map)
522 }
523
524 pub fn set_scroll_anchor(
525 &mut self,
526 scroll_anchor: ScrollAnchor,
527 window: &mut Window,
528 cx: &mut Context<Self>,
529 ) {
530 hide_hover(self, cx);
531 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
532 let top_row = scroll_anchor
533 .anchor
534 .to_point(&self.buffer().read(cx).snapshot(cx))
535 .row;
536 self.scroll_manager.set_anchor(
537 scroll_anchor,
538 top_row,
539 true,
540 false,
541 workspace_id,
542 window,
543 cx,
544 );
545 }
546
547 pub(crate) fn set_scroll_anchor_remote(
548 &mut self,
549 scroll_anchor: ScrollAnchor,
550 window: &mut Window,
551 cx: &mut Context<Self>,
552 ) {
553 hide_hover(self, cx);
554 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
555 let snapshot = &self.buffer().read(cx).snapshot(cx);
556 if !scroll_anchor.anchor.is_valid(snapshot) {
557 log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
558 return;
559 }
560 let top_row = scroll_anchor.anchor.to_point(snapshot).row;
561 self.scroll_manager.set_anchor(
562 scroll_anchor,
563 top_row,
564 false,
565 false,
566 workspace_id,
567 window,
568 cx,
569 );
570 }
571
572 pub fn scroll_screen(
573 &mut self,
574 amount: &ScrollAmount,
575 window: &mut Window,
576 cx: &mut Context<Self>,
577 ) {
578 if matches!(self.mode, EditorMode::SingleLine { .. }) {
579 cx.propagate();
580 return;
581 }
582
583 if self.take_rename(true, window, cx).is_some() {
584 return;
585 }
586
587 let cur_position = self.scroll_position(cx);
588 let Some(visible_line_count) = self.visible_line_count() else {
589 return;
590 };
591 let new_pos = cur_position + point(0., amount.lines(visible_line_count));
592 self.set_scroll_position(new_pos, window, cx);
593 }
594
595 /// Returns an ordering. The newest selection is:
596 /// Ordering::Equal => on screen
597 /// Ordering::Less => above the screen
598 /// Ordering::Greater => below the screen
599 pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
600 let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
601 let newest_head = self
602 .selections
603 .newest_anchor()
604 .head()
605 .to_display_point(&snapshot);
606 let screen_top = self
607 .scroll_manager
608 .anchor
609 .anchor
610 .to_display_point(&snapshot);
611
612 if screen_top > newest_head {
613 return Ordering::Less;
614 }
615
616 if let Some(visible_lines) = self.visible_line_count() {
617 if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) {
618 return Ordering::Equal;
619 }
620 }
621
622 Ordering::Greater
623 }
624
625 pub fn read_scroll_position_from_db(
626 &mut self,
627 item_id: u64,
628 workspace_id: WorkspaceId,
629 window: &mut Window,
630 cx: &mut Context<Editor>,
631 ) {
632 let scroll_position = DB.get_scroll_position(item_id, workspace_id);
633 if let Ok(Some((top_row, x, y))) = scroll_position {
634 let top_anchor = self
635 .buffer()
636 .read(cx)
637 .snapshot(cx)
638 .anchor_at(Point::new(top_row, 0), Bias::Left);
639 let scroll_anchor = ScrollAnchor {
640 offset: gpui::Point::new(x, y),
641 anchor: top_anchor,
642 };
643 self.set_scroll_anchor(scroll_anchor, window, cx);
644 }
645 }
646}