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