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