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