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