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, SizingBehavior, ToPoint,
9 display_map::{DisplaySnapshot, ToDisplayPoint},
10 hover_popover::hide_hover,
11 persistence::EditorDb,
12};
13pub use autoscroll::{Autoscroll, AutoscrollStrategy};
14use core::fmt::Debug;
15use gpui::{
16 Along, App, AppContext as _, Axis, Context, Entity, EntityId, Pixels, Task, Window, point, px,
17};
18use language::language_settings::{AllLanguageSettings, SoftWrap};
19use language::{Bias, Point};
20pub use scroll_amount::ScrollAmount;
21use settings::Settings;
22use std::{
23 cmp::Ordering,
24 time::{Duration, Instant},
25};
26use ui::scrollbars::ScrollbarAutoHide;
27use util::ResultExt;
28use workspace::{ItemId, WorkspaceId};
29
30pub const SCROLL_EVENT_SEPARATION: Duration = Duration::from_millis(28);
31const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
32
33pub struct WasScrolled(pub(crate) bool);
34
35pub type ScrollOffset = f64;
36pub type ScrollPixelOffset = f64;
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct ScrollAnchor {
39 pub offset: gpui::Point<ScrollOffset>,
40 pub anchor: Anchor,
41}
42
43impl ScrollAnchor {
44 pub(super) fn new() -> Self {
45 Self {
46 offset: gpui::Point::default(),
47 anchor: Anchor::min(),
48 }
49 }
50
51 pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
52 self.offset.apply_along(Axis::Vertical, |offset| {
53 if self.anchor == Anchor::min() {
54 0.
55 } else {
56 let scroll_top = self.anchor.to_display_point(snapshot).row().as_f64();
57 (offset + scroll_top).max(0.)
58 }
59 })
60 }
61
62 pub fn top_row(&self, buffer: &MultiBufferSnapshot) -> u32 {
63 self.anchor.to_point(buffer).row
64 }
65}
66
67#[derive(Clone, Copy, Debug)]
68pub struct OngoingScroll {
69 last_event: Instant,
70 axis: Option<Axis>,
71}
72
73/// In the split diff view, the two sides share a ScrollAnchor using this struct.
74/// Either side can set a ScrollAnchor that points to its own multibuffer, and we store the ID of the display map
75/// that the last-written anchor came from so that we know how to resolve it to a DisplayPoint.
76///
77/// For normal editors, this just acts as a wrapper around a ScrollAnchor.
78#[derive(Clone, Copy, Debug)]
79pub struct SharedScrollAnchor {
80 pub scroll_anchor: ScrollAnchor,
81 pub display_map_id: Option<EntityId>,
82}
83
84impl SharedScrollAnchor {
85 pub fn scroll_position(&self, snapshot: &DisplaySnapshot) -> gpui::Point<ScrollOffset> {
86 let snapshot = if let Some(display_map_id) = self.display_map_id
87 && display_map_id != snapshot.display_map_id
88 {
89 let companion_snapshot = snapshot.companion_snapshot().unwrap();
90 assert_eq!(companion_snapshot.display_map_id, display_map_id);
91 companion_snapshot
92 } else {
93 snapshot
94 };
95
96 self.scroll_anchor.scroll_position(snapshot)
97 }
98
99 pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot) -> DisplayPoint {
100 let snapshot = if let Some(display_map_id) = self.display_map_id
101 && display_map_id != snapshot.display_map_id
102 {
103 let companion_snapshot = snapshot.companion_snapshot().unwrap();
104 assert_eq!(companion_snapshot.display_map_id, display_map_id);
105 companion_snapshot
106 } else {
107 snapshot
108 };
109
110 self.scroll_anchor.anchor.to_display_point(snapshot)
111 }
112}
113
114impl OngoingScroll {
115 fn new() -> Self {
116 Self {
117 last_event: Instant::now() - SCROLL_EVENT_SEPARATION,
118 axis: None,
119 }
120 }
121
122 pub fn filter(&self, delta: &mut gpui::Point<Pixels>) -> Option<Axis> {
123 const UNLOCK_PERCENT: f32 = 1.9;
124 const UNLOCK_LOWER_BOUND: Pixels = px(6.);
125 let mut axis = self.axis;
126
127 let x = delta.x.abs();
128 let y = delta.y.abs();
129 let duration = Instant::now().duration_since(self.last_event);
130 if duration > SCROLL_EVENT_SEPARATION {
131 //New ongoing scroll will start, determine axis
132 axis = if x <= y {
133 Some(Axis::Vertical)
134 } else {
135 Some(Axis::Horizontal)
136 };
137 } else if x.max(y) >= UNLOCK_LOWER_BOUND {
138 //Check if the current ongoing will need to unlock
139 match axis {
140 Some(Axis::Vertical) => {
141 if x > y && x >= y * UNLOCK_PERCENT {
142 axis = None;
143 }
144 }
145
146 Some(Axis::Horizontal) => {
147 if y > x && y >= x * UNLOCK_PERCENT {
148 axis = None;
149 }
150 }
151
152 None => {}
153 }
154 }
155
156 match axis {
157 Some(Axis::Vertical) => {
158 *delta = point(px(0.), delta.y);
159 }
160 Some(Axis::Horizontal) => {
161 *delta = point(delta.x, px(0.));
162 }
163 None => {}
164 }
165
166 axis
167 }
168}
169
170#[derive(Copy, Clone, Default, PartialEq, Eq)]
171pub enum ScrollbarThumbState {
172 #[default]
173 Idle,
174 Hovered,
175 Dragging,
176}
177
178#[derive(PartialEq, Eq)]
179pub struct ActiveScrollbarState {
180 axis: Axis,
181 thumb_state: ScrollbarThumbState,
182}
183
184impl ActiveScrollbarState {
185 pub fn new(axis: Axis, thumb_state: ScrollbarThumbState) -> Self {
186 ActiveScrollbarState { axis, thumb_state }
187 }
188
189 pub fn thumb_state_for_axis(&self, axis: Axis) -> Option<ScrollbarThumbState> {
190 (self.axis == axis).then_some(self.thumb_state)
191 }
192}
193
194pub struct ScrollManager {
195 pub(crate) vertical_scroll_margin: ScrollOffset,
196 anchor: Entity<SharedScrollAnchor>,
197 /// Value to be used for clamping the x component of the SharedScrollAnchor's offset.
198 ///
199 /// We store this outside the SharedScrollAnchor so that the two sides of a split diff can share
200 /// a horizontal scroll offset that may be out of range for one of the editors (when one side is wider than the other).
201 /// Each side separately clamps the x component using its own scroll_max_x when reading from the SharedScrollAnchor.
202 scroll_max_x: Option<f64>,
203 ongoing: OngoingScroll,
204 /// Number of sticky header lines currently being rendered for the current scroll position.
205 sticky_header_line_count: usize,
206 /// The second element indicates whether the autoscroll request is local
207 /// (true) or remote (false). Local requests are initiated by user actions,
208 /// while remote requests come from external sources.
209 autoscroll_request: Option<(Autoscroll, bool)>,
210 last_autoscroll: Option<(
211 gpui::Point<ScrollOffset>,
212 ScrollOffset,
213 ScrollOffset,
214 AutoscrollStrategy,
215 )>,
216 show_scrollbars: bool,
217 hide_scrollbar_task: Option<Task<()>>,
218 active_scrollbar: Option<ActiveScrollbarState>,
219 visible_line_count: Option<f64>,
220 visible_column_count: Option<f64>,
221 forbid_vertical_scroll: bool,
222 minimap_thumb_state: Option<ScrollbarThumbState>,
223 _save_scroll_position_task: Task<()>,
224}
225
226impl ScrollManager {
227 pub fn new(cx: &mut Context<Editor>) -> Self {
228 let anchor = cx.new(|_| SharedScrollAnchor {
229 scroll_anchor: ScrollAnchor::new(),
230 display_map_id: None,
231 });
232 ScrollManager {
233 vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
234 anchor,
235 scroll_max_x: None,
236 ongoing: OngoingScroll::new(),
237 sticky_header_line_count: 0,
238 autoscroll_request: None,
239 show_scrollbars: true,
240 hide_scrollbar_task: None,
241 active_scrollbar: None,
242 last_autoscroll: None,
243 visible_line_count: None,
244 visible_column_count: None,
245 forbid_vertical_scroll: false,
246 minimap_thumb_state: None,
247 _save_scroll_position_task: Task::ready(()),
248 }
249 }
250
251 pub fn set_native_display_map_id(
252 &mut self,
253 display_map_id: EntityId,
254 cx: &mut Context<Editor>,
255 ) {
256 self.anchor.update(cx, |shared, _| {
257 if shared.display_map_id.is_none() {
258 shared.display_map_id = Some(display_map_id);
259 }
260 });
261 }
262
263 pub fn clone_state(
264 &mut self,
265 other: &Self,
266 other_snapshot: &DisplaySnapshot,
267 my_snapshot: &DisplaySnapshot,
268 cx: &mut Context<Editor>,
269 ) {
270 let native_anchor = other.native_anchor(other_snapshot, cx);
271 self.anchor.update(cx, |this, _| {
272 this.scroll_anchor = native_anchor;
273 this.display_map_id = Some(my_snapshot.display_map_id);
274 });
275 self.ongoing = other.ongoing;
276 self.sticky_header_line_count = other.sticky_header_line_count;
277 }
278
279 pub fn offset(&self, cx: &App) -> gpui::Point<f64> {
280 let mut offset = self.anchor.read(cx).scroll_anchor.offset;
281 if let Some(max_x) = self.scroll_max_x {
282 offset.x = offset.x.min(max_x);
283 }
284 offset
285 }
286
287 /// Get a ScrollAnchor whose `anchor` field is guaranteed to point into the multibuffer for the provided snapshot.
288 ///
289 /// For normal editors, this just retrieves the internal ScrollAnchor and is lossless. When the editor is part of a split diff,
290 /// we may need to translate the anchor to point to the "native" multibuffer first. That translation is lossy,
291 /// so this method should be used sparingly---if you just need a scroll position or display point, call the appropriate helper method instead,
292 /// since they can losslessly handle the case where the ScrollAnchor was last set from the other side.
293 pub fn native_anchor(&self, snapshot: &DisplaySnapshot, cx: &App) -> ScrollAnchor {
294 let shared = self.anchor.read(cx);
295
296 let mut result = if let Some(display_map_id) = shared.display_map_id
297 && display_map_id != snapshot.display_map_id
298 {
299 let companion_snapshot = snapshot.companion_snapshot().unwrap();
300 assert_eq!(companion_snapshot.display_map_id, display_map_id);
301 let mut display_point = shared
302 .scroll_anchor
303 .anchor
304 .to_display_point(companion_snapshot);
305 *display_point.column_mut() = 0;
306 let buffer_point = snapshot.display_point_to_point(display_point, Bias::Left);
307 let anchor = snapshot.buffer_snapshot().anchor_before(buffer_point);
308 ScrollAnchor {
309 anchor,
310 offset: shared.scroll_anchor.offset,
311 }
312 } else {
313 shared.scroll_anchor
314 };
315
316 if let Some(max_x) = self.scroll_max_x {
317 result.offset.x = result.offset.x.min(max_x);
318 }
319 result
320 }
321
322 pub fn shared_scroll_anchor(&self, cx: &App) -> SharedScrollAnchor {
323 let mut shared = *self.anchor.read(cx);
324 if let Some(max_x) = self.scroll_max_x {
325 shared.scroll_anchor.offset.x = shared.scroll_anchor.offset.x.min(max_x);
326 }
327 shared
328 }
329
330 pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot, cx: &App) -> DisplayPoint {
331 self.anchor.read(cx).scroll_top_display_point(snapshot)
332 }
333
334 pub fn scroll_anchor_entity(&self) -> Entity<SharedScrollAnchor> {
335 self.anchor.clone()
336 }
337
338 pub fn set_shared_scroll_anchor(&mut self, entity: Entity<SharedScrollAnchor>) {
339 self.anchor = entity;
340 }
341
342 pub fn ongoing_scroll(&self) -> OngoingScroll {
343 self.ongoing
344 }
345
346 pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
347 self.ongoing.last_event = Instant::now();
348 self.ongoing.axis = axis;
349 }
350
351 pub fn scroll_position(
352 &self,
353 snapshot: &DisplaySnapshot,
354 cx: &App,
355 ) -> gpui::Point<ScrollOffset> {
356 let mut pos = self.anchor.read(cx).scroll_position(snapshot);
357 if let Some(max_x) = self.scroll_max_x {
358 pos.x = pos.x.min(max_x);
359 }
360 pos
361 }
362
363 pub fn sticky_header_line_count(&self) -> usize {
364 self.sticky_header_line_count
365 }
366
367 pub fn set_sticky_header_line_count(&mut self, count: usize) {
368 self.sticky_header_line_count = count;
369 }
370
371 fn set_scroll_position(
372 &mut self,
373 scroll_position: gpui::Point<ScrollOffset>,
374 map: &DisplaySnapshot,
375 scroll_beyond_last_line: ScrollBeyondLastLine,
376 local: bool,
377 autoscroll: bool,
378 workspace_id: Option<WorkspaceId>,
379 window: &mut Window,
380 cx: &mut Context<Editor>,
381 ) -> WasScrolled {
382 let scroll_top = scroll_position.y.max(0.);
383 let scroll_top = match scroll_beyond_last_line {
384 ScrollBeyondLastLine::OnePage => scroll_top,
385 ScrollBeyondLastLine::Off => {
386 if let Some(height_in_lines) = self.visible_line_count {
387 let max_row = map.max_point().row().as_f64();
388 scroll_top.min(max_row - height_in_lines + 1.).max(0.)
389 } else {
390 scroll_top
391 }
392 }
393 ScrollBeyondLastLine::VerticalScrollMargin => {
394 if let Some(height_in_lines) = self.visible_line_count {
395 let max_row = map.max_point().row().as_f64();
396 scroll_top
397 .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
398 .max(0.)
399 } else {
400 scroll_top
401 }
402 }
403 };
404 let scroll_top_row = DisplayRow(scroll_top as u32);
405 let scroll_top_buffer_point = map
406 .clip_point(
407 DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
408 Bias::Left,
409 )
410 .to_point(map);
411 let top_anchor = map.buffer_snapshot().anchor_before(scroll_top_buffer_point);
412
413 self.set_anchor(
414 ScrollAnchor {
415 anchor: top_anchor,
416 offset: point(
417 scroll_position.x.max(0.),
418 scroll_top - top_anchor.to_display_point(map).row().as_f64(),
419 ),
420 },
421 map,
422 scroll_top_buffer_point.row,
423 local,
424 autoscroll,
425 workspace_id,
426 window,
427 cx,
428 )
429 }
430
431 fn set_anchor(
432 &mut self,
433 anchor: ScrollAnchor,
434 display_map: &DisplaySnapshot,
435 top_row: u32,
436 local: bool,
437 autoscroll: bool,
438 workspace_id: Option<WorkspaceId>,
439 window: &mut Window,
440 cx: &mut Context<Editor>,
441 ) -> WasScrolled {
442 let adjusted_anchor = if self.forbid_vertical_scroll {
443 let current = self.anchor.read(cx);
444 ScrollAnchor {
445 offset: gpui::Point::new(anchor.offset.x, current.scroll_anchor.offset.y),
446 anchor: current.scroll_anchor.anchor,
447 }
448 } else {
449 anchor
450 };
451
452 self.scroll_max_x.take();
453 self.autoscroll_request.take();
454
455 let current = self.anchor.read(cx);
456 if current.scroll_anchor == adjusted_anchor {
457 return WasScrolled(false);
458 }
459
460 self.anchor.update(cx, |shared, _| {
461 shared.scroll_anchor = adjusted_anchor;
462 shared.display_map_id = Some(display_map.display_map_id);
463 });
464 cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
465 self.show_scrollbars(window, cx);
466 if let Some(workspace_id) = workspace_id {
467 let item_id = cx.entity().entity_id().as_u64() as ItemId;
468 let executor = cx.background_executor().clone();
469
470 let db = EditorDb::global(cx);
471 self._save_scroll_position_task = cx.background_executor().spawn(async move {
472 executor.timer(Duration::from_millis(10)).await;
473 log::debug!(
474 "Saving scroll position for item {item_id:?} in workspace {workspace_id:?}"
475 );
476 db.save_scroll_position(
477 item_id,
478 workspace_id,
479 top_row,
480 anchor.offset.x,
481 anchor.offset.y,
482 )
483 .await
484 .log_err();
485 });
486 }
487 cx.notify();
488
489 WasScrolled(true)
490 }
491
492 pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
493 if !self.show_scrollbars {
494 self.show_scrollbars = true;
495 cx.notify();
496 }
497
498 if cx.default_global::<ScrollbarAutoHide>().should_hide() {
499 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
500 cx.background_executor()
501 .timer(SCROLLBAR_SHOW_INTERVAL)
502 .await;
503 editor
504 .update(cx, |editor, cx| {
505 editor.scroll_manager.show_scrollbars = false;
506 cx.notify();
507 })
508 .log_err();
509 }));
510 } else {
511 self.hide_scrollbar_task = None;
512 }
513 }
514
515 pub fn scrollbars_visible(&self) -> bool {
516 self.show_scrollbars
517 }
518
519 pub fn has_autoscroll_request(&self) -> bool {
520 self.autoscroll_request.is_some()
521 }
522
523 pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
524 self.autoscroll_request.take()
525 }
526
527 pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
528 self.active_scrollbar.as_ref()
529 }
530
531 pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
532 self.active_scrollbar
533 .as_ref()
534 .filter(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
535 .map(|scrollbar| scrollbar.axis)
536 }
537
538 pub fn any_scrollbar_dragged(&self) -> bool {
539 self.active_scrollbar
540 .as_ref()
541 .is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
542 }
543
544 pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
545 self.update_active_scrollbar_state(
546 Some(ActiveScrollbarState::new(
547 axis,
548 ScrollbarThumbState::Hovered,
549 )),
550 cx,
551 );
552 }
553
554 pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
555 self.update_active_scrollbar_state(
556 Some(ActiveScrollbarState::new(
557 axis,
558 ScrollbarThumbState::Dragging,
559 )),
560 cx,
561 );
562 }
563
564 pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
565 self.update_active_scrollbar_state(None, cx);
566 }
567
568 fn update_active_scrollbar_state(
569 &mut self,
570 new_state: Option<ActiveScrollbarState>,
571 cx: &mut Context<Editor>,
572 ) {
573 if self.active_scrollbar != new_state {
574 self.active_scrollbar = new_state;
575 cx.notify();
576 }
577 }
578
579 pub fn set_is_hovering_minimap_thumb(&mut self, hovered: bool, cx: &mut Context<Editor>) {
580 self.update_minimap_thumb_state(
581 Some(if hovered {
582 ScrollbarThumbState::Hovered
583 } else {
584 ScrollbarThumbState::Idle
585 }),
586 cx,
587 );
588 }
589
590 pub fn set_is_dragging_minimap(&mut self, cx: &mut Context<Editor>) {
591 self.update_minimap_thumb_state(Some(ScrollbarThumbState::Dragging), cx);
592 }
593
594 pub fn hide_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
595 self.update_minimap_thumb_state(None, cx);
596 }
597
598 pub fn is_dragging_minimap(&self) -> bool {
599 self.minimap_thumb_state
600 .is_some_and(|state| state == ScrollbarThumbState::Dragging)
601 }
602
603 fn update_minimap_thumb_state(
604 &mut self,
605 thumb_state: Option<ScrollbarThumbState>,
606 cx: &mut Context<Editor>,
607 ) {
608 if self.minimap_thumb_state != thumb_state {
609 self.minimap_thumb_state = thumb_state;
610 cx.notify();
611 }
612 }
613
614 pub fn minimap_thumb_state(&self) -> Option<ScrollbarThumbState> {
615 self.minimap_thumb_state
616 }
617
618 pub fn clamp_scroll_left(&mut self, max: f64, cx: &App) -> bool {
619 let current_x = self.anchor.read(cx).scroll_anchor.offset.x;
620 self.scroll_max_x = Some(max);
621 current_x > max
622 }
623
624 pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
625 self.forbid_vertical_scroll = forbid;
626 }
627
628 pub fn forbid_vertical_scroll(&self) -> bool {
629 self.forbid_vertical_scroll
630 }
631}
632
633impl Editor {
634 pub fn has_autoscroll_request(&self) -> bool {
635 self.scroll_manager.has_autoscroll_request()
636 }
637
638 pub fn vertical_scroll_margin(&self) -> usize {
639 self.scroll_manager.vertical_scroll_margin as usize
640 }
641
642 pub(crate) fn scroll_beyond_last_line(&self, cx: &App) -> ScrollBeyondLastLine {
643 match self.mode {
644 EditorMode::Minimap { .. }
645 | EditorMode::Full {
646 sizing_behavior: SizingBehavior::Default,
647 ..
648 } => EditorSettings::get_global(cx).scroll_beyond_last_line,
649
650 EditorMode::Full { .. } | EditorMode::SingleLine | EditorMode::AutoHeight { .. } => {
651 ScrollBeyondLastLine::Off
652 }
653 }
654 }
655
656 pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context<Self>) {
657 self.scroll_manager.vertical_scroll_margin = margin_rows as f64;
658 cx.notify();
659 }
660
661 pub fn visible_line_count(&self) -> Option<f64> {
662 self.scroll_manager.visible_line_count
663 }
664
665 pub fn visible_row_count(&self) -> Option<u32> {
666 self.visible_line_count()
667 .map(|line_count| line_count as u32 - 1)
668 }
669
670 pub fn visible_column_count(&self) -> Option<f64> {
671 self.scroll_manager.visible_column_count
672 }
673
674 pub(crate) fn set_visible_line_count(
675 &mut self,
676 lines: f64,
677 window: &mut Window,
678 cx: &mut Context<Self>,
679 ) {
680 let opened_first_time = self.scroll_manager.visible_line_count.is_none();
681 self.scroll_manager.visible_line_count = Some(lines);
682 if opened_first_time {
683 self.post_scroll_update = cx.spawn_in(window, async move |editor, cx| {
684 editor
685 .update_in(cx, |editor, window, cx| {
686 editor.register_visible_buffers(cx);
687 editor.colorize_brackets(false, cx);
688 editor.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx);
689 editor.update_lsp_data(None, window, cx);
690 })
691 .ok();
692 });
693 }
694 }
695
696 pub(crate) fn set_visible_column_count(&mut self, columns: f64) {
697 self.scroll_manager.visible_column_count = Some(columns);
698 }
699
700 pub fn apply_scroll_delta(
701 &mut self,
702 scroll_delta: gpui::Point<f32>,
703 window: &mut Window,
704 cx: &mut Context<Self>,
705 ) {
706 let mut delta = scroll_delta;
707 if self.scroll_manager.forbid_vertical_scroll {
708 delta.y = 0.0;
709 }
710 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
711 let position = self.scroll_manager.scroll_position(&display_map, cx) + delta.map(f64::from);
712 self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
713 }
714
715 pub fn set_scroll_position(
716 &mut self,
717 scroll_position: gpui::Point<ScrollOffset>,
718 window: &mut Window,
719 cx: &mut Context<Self>,
720 ) -> WasScrolled {
721 let mut position = scroll_position;
722 if self.scroll_manager.forbid_vertical_scroll {
723 let current_position = self.scroll_position(cx);
724 position.y = current_position.y;
725 }
726 self.set_scroll_position_internal(position, true, false, window, cx)
727 }
728
729 /// Scrolls so that `row` is at the top of the editor view.
730 pub fn set_scroll_top_row(
731 &mut self,
732 row: DisplayRow,
733 window: &mut Window,
734 cx: &mut Context<Editor>,
735 ) {
736 let snapshot = self.snapshot(window, cx).display_snapshot;
737 let new_screen_top = DisplayPoint::new(row, 0);
738 let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
739 let new_anchor = snapshot.buffer_snapshot().anchor_before(new_screen_top);
740
741 self.set_scroll_anchor(
742 ScrollAnchor {
743 anchor: new_anchor,
744 offset: Default::default(),
745 },
746 window,
747 cx,
748 );
749 }
750
751 pub(crate) fn set_scroll_position_internal(
752 &mut self,
753 scroll_position: gpui::Point<ScrollOffset>,
754 local: bool,
755 autoscroll: bool,
756 window: &mut Window,
757 cx: &mut Context<Self>,
758 ) -> WasScrolled {
759 let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
760 let was_scrolled = self.set_scroll_position_taking_display_map(
761 scroll_position,
762 local,
763 autoscroll,
764 map,
765 window,
766 cx,
767 );
768
769 was_scrolled
770 }
771
772 fn set_scroll_position_taking_display_map(
773 &mut self,
774 scroll_position: gpui::Point<ScrollOffset>,
775 local: bool,
776 autoscroll: bool,
777 display_map: DisplaySnapshot,
778 window: &mut Window,
779 cx: &mut Context<Self>,
780 ) -> WasScrolled {
781 hide_hover(self, cx);
782 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
783
784 self.edit_prediction_preview
785 .set_previous_scroll_position(None);
786
787 let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
788 let current_position = self.scroll_manager.scroll_position(&display_map, cx);
789 gpui::Point::new(scroll_position.x, current_position.y)
790 } else {
791 scroll_position
792 };
793 let scroll_beyond_last_line = self.scroll_beyond_last_line(cx);
794 self.scroll_manager.set_scroll_position(
795 adjusted_position,
796 &display_map,
797 scroll_beyond_last_line,
798 local,
799 autoscroll,
800 workspace_id,
801 window,
802 cx,
803 )
804 }
805
806 pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
807 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
808 self.scroll_manager.scroll_position(&display_map, cx)
809 }
810
811 pub fn set_scroll_anchor(
812 &mut self,
813 scroll_anchor: ScrollAnchor,
814 window: &mut Window,
815 cx: &mut Context<Self>,
816 ) {
817 hide_hover(self, cx);
818 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
819 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
820 let top_row = scroll_anchor
821 .anchor
822 .to_point(&self.buffer().read(cx).snapshot(cx))
823 .row;
824 self.scroll_manager.set_anchor(
825 scroll_anchor,
826 &display_map,
827 top_row,
828 true,
829 false,
830 workspace_id,
831 window,
832 cx,
833 );
834 }
835
836 pub(crate) fn set_scroll_anchor_remote(
837 &mut self,
838 scroll_anchor: ScrollAnchor,
839 window: &mut Window,
840 cx: &mut Context<Self>,
841 ) {
842 hide_hover(self, cx);
843 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
844 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
845 if !scroll_anchor.anchor.is_valid(&buffer_snapshot) {
846 log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
847 return;
848 }
849 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
850 let top_row = scroll_anchor.anchor.to_point(&buffer_snapshot).row;
851 self.scroll_manager.set_anchor(
852 scroll_anchor,
853 &display_map,
854 top_row,
855 false,
856 false,
857 workspace_id,
858 window,
859 cx,
860 );
861 }
862
863 pub fn scroll_screen(
864 &mut self,
865 amount: &ScrollAmount,
866 window: &mut Window,
867 cx: &mut Context<Self>,
868 ) {
869 if matches!(self.mode, EditorMode::SingleLine) {
870 cx.propagate();
871 return;
872 }
873
874 if self.take_rename(true, window, cx).is_some() {
875 return;
876 }
877
878 let mut current_position = self.scroll_position(cx);
879 let Some(visible_line_count) = self.visible_line_count() else {
880 return;
881 };
882 let Some(mut visible_column_count) = self.visible_column_count() else {
883 return;
884 };
885
886 // If the user has a preferred line length, and has the editor
887 // configured to wrap at the preferred line length, or bounded to it,
888 // use that value over the visible column count. This was mostly done so
889 // that tests could actually be written for vim's `z l`, `z h`, `z
890 // shift-l` and `z shift-h` commands, as there wasn't a good way to
891 // configure the editor to only display a certain number of columns. If
892 // that ever happens, this could probably be removed.
893 let settings = AllLanguageSettings::get_global(cx);
894 if matches!(
895 settings.defaults.soft_wrap,
896 SoftWrap::PreferredLineLength | SoftWrap::Bounded
897 ) && (settings.defaults.preferred_line_length as f64) < visible_column_count
898 {
899 visible_column_count = settings.defaults.preferred_line_length as f64;
900 }
901
902 // If the scroll position is currently at the left edge of the document
903 // (x == 0.0) and the intent is to scroll right, the gutter's margin
904 // should first be added to the current position, otherwise the cursor
905 // will end at the column position minus the margin, which looks off.
906 if current_position.x == 0.0
907 && amount.columns(visible_column_count) > 0.
908 && let Some(last_position_map) = &self.last_position_map
909 {
910 current_position.x +=
911 f64::from(self.gutter_dimensions.margin / last_position_map.em_advance);
912 }
913 let new_position = current_position
914 + point(
915 amount.columns(visible_column_count),
916 amount.lines(visible_line_count),
917 );
918 self.set_scroll_position(new_position, window, cx);
919 }
920
921 /// Returns an ordering. The newest selection is:
922 /// Ordering::Equal => on screen
923 /// Ordering::Less => above or to the left of the screen
924 /// Ordering::Greater => below or to the right of the screen
925 pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
926 let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
927 let newest_head = self
928 .selections
929 .newest_anchor()
930 .head()
931 .to_display_point(&snapshot);
932 let screen_top = self.scroll_manager.scroll_top_display_point(&snapshot, cx);
933
934 if screen_top > newest_head {
935 return Ordering::Less;
936 }
937
938 if let (Some(visible_lines), Some(visible_columns)) =
939 (self.visible_line_count(), self.visible_column_count())
940 && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
941 && newest_head.column() <= screen_top.column() + visible_columns as u32
942 {
943 return Ordering::Equal;
944 }
945
946 Ordering::Greater
947 }
948
949 pub fn read_scroll_position_from_db(
950 &mut self,
951 item_id: u64,
952 workspace_id: WorkspaceId,
953 window: &mut Window,
954 cx: &mut Context<Editor>,
955 ) {
956 let scroll_position = EditorDb::global(cx).get_scroll_position(item_id, workspace_id);
957 if let Ok(Some((top_row, x, y))) = scroll_position {
958 let top_anchor = self
959 .buffer()
960 .read(cx)
961 .snapshot(cx)
962 .anchor_before(Point::new(top_row, 0));
963 let scroll_anchor = ScrollAnchor {
964 offset: gpui::Point::new(x, y),
965 anchor: top_anchor,
966 };
967 self.set_scroll_anchor(scroll_anchor, window, cx);
968 }
969 }
970}