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 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 /// The second element indicates whether the autoscroll request is local
205 /// (true) or remote (false). Local requests are initiated by user actions,
206 /// while remote requests come from external sources.
207 autoscroll_request: Option<(Autoscroll, bool)>,
208 last_autoscroll: Option<(
209 gpui::Point<ScrollOffset>,
210 ScrollOffset,
211 ScrollOffset,
212 AutoscrollStrategy,
213 )>,
214 show_scrollbars: bool,
215 hide_scrollbar_task: Option<Task<()>>,
216 active_scrollbar: Option<ActiveScrollbarState>,
217 visible_line_count: Option<f64>,
218 visible_column_count: Option<f64>,
219 forbid_vertical_scroll: bool,
220 minimap_thumb_state: Option<ScrollbarThumbState>,
221 _save_scroll_position_task: Task<()>,
222}
223
224impl ScrollManager {
225 pub fn new(cx: &mut Context<Editor>) -> Self {
226 let anchor = cx.new(|_| SharedScrollAnchor {
227 scroll_anchor: ScrollAnchor::new(),
228 display_map_id: None,
229 });
230 ScrollManager {
231 vertical_scroll_margin: EditorSettings::get_global(cx).vertical_scroll_margin,
232 anchor,
233 scroll_max_x: None,
234 ongoing: OngoingScroll::new(),
235 autoscroll_request: None,
236 show_scrollbars: true,
237 hide_scrollbar_task: None,
238 active_scrollbar: None,
239 last_autoscroll: None,
240 visible_line_count: None,
241 visible_column_count: None,
242 forbid_vertical_scroll: false,
243 minimap_thumb_state: None,
244 _save_scroll_position_task: Task::ready(()),
245 }
246 }
247
248 pub fn set_native_display_map_id(
249 &mut self,
250 display_map_id: EntityId,
251 cx: &mut Context<Editor>,
252 ) {
253 self.anchor.update(cx, |shared, _| {
254 if shared.display_map_id.is_none() {
255 shared.display_map_id = Some(display_map_id);
256 }
257 });
258 }
259
260 pub fn clone_state(
261 &mut self,
262 other: &Self,
263 other_snapshot: &DisplaySnapshot,
264 my_snapshot: &DisplaySnapshot,
265 cx: &mut Context<Editor>,
266 ) {
267 let native_anchor = other.native_anchor(other_snapshot, cx);
268 self.anchor.update(cx, |this, _| {
269 this.scroll_anchor = native_anchor;
270 this.display_map_id = Some(my_snapshot.display_map_id);
271 });
272 self.ongoing = other.ongoing;
273 }
274
275 pub fn offset(&self, cx: &App) -> gpui::Point<f64> {
276 let mut offset = self.anchor.read(cx).scroll_anchor.offset;
277 if let Some(max_x) = self.scroll_max_x {
278 offset.x = offset.x.min(max_x);
279 }
280 offset
281 }
282
283 /// Get a ScrollAnchor whose `anchor` field is guaranteed to point into the multibuffer for the provided snapshot.
284 ///
285 /// For normal editors, this just retrieves the internal ScrollAnchor and is lossless. When the editor is part of a split diff,
286 /// we may need to translate the anchor to point to the "native" multibuffer first. That translation is lossy,
287 /// so this method should be used sparingly---if you just need a scroll position or display point, call the appropriate helper method instead,
288 /// since they can losslessly handle the case where the ScrollAnchor was last set from the other side.
289 pub fn native_anchor(&self, snapshot: &DisplaySnapshot, cx: &App) -> ScrollAnchor {
290 let shared = self.anchor.read(cx);
291
292 let mut result = if let Some(display_map_id) = shared.display_map_id
293 && display_map_id != snapshot.display_map_id
294 {
295 let companion_snapshot = snapshot.companion_snapshot().unwrap();
296 assert_eq!(companion_snapshot.display_map_id, display_map_id);
297 let mut display_point = shared
298 .scroll_anchor
299 .anchor
300 .to_display_point(companion_snapshot);
301 *display_point.column_mut() = 0;
302 let buffer_point = snapshot.display_point_to_point(display_point, Bias::Left);
303 let anchor = snapshot.buffer_snapshot().anchor_before(buffer_point);
304 ScrollAnchor {
305 anchor,
306 offset: shared.scroll_anchor.offset,
307 }
308 } else {
309 shared.scroll_anchor
310 };
311
312 if let Some(max_x) = self.scroll_max_x {
313 result.offset.x = result.offset.x.min(max_x);
314 }
315 result
316 }
317
318 pub fn shared_scroll_anchor(&self, cx: &App) -> SharedScrollAnchor {
319 let mut shared = *self.anchor.read(cx);
320 if let Some(max_x) = self.scroll_max_x {
321 shared.scroll_anchor.offset.x = shared.scroll_anchor.offset.x.min(max_x);
322 }
323 shared
324 }
325
326 pub fn scroll_top_display_point(&self, snapshot: &DisplaySnapshot, cx: &App) -> DisplayPoint {
327 self.anchor.read(cx).scroll_top_display_point(snapshot)
328 }
329
330 pub fn scroll_anchor_entity(&self) -> Entity<SharedScrollAnchor> {
331 self.anchor.clone()
332 }
333
334 pub fn set_shared_scroll_anchor(&mut self, entity: Entity<SharedScrollAnchor>) {
335 self.anchor = entity;
336 }
337
338 pub fn ongoing_scroll(&self) -> OngoingScroll {
339 self.ongoing
340 }
341
342 pub fn update_ongoing_scroll(&mut self, axis: Option<Axis>) {
343 self.ongoing.last_event = Instant::now();
344 self.ongoing.axis = axis;
345 }
346
347 pub fn scroll_position(
348 &self,
349 snapshot: &DisplaySnapshot,
350 cx: &App,
351 ) -> gpui::Point<ScrollOffset> {
352 let mut pos = self.anchor.read(cx).scroll_position(snapshot);
353 if let Some(max_x) = self.scroll_max_x {
354 pos.x = pos.x.min(max_x);
355 }
356 pos
357 }
358
359 fn set_scroll_position(
360 &mut self,
361 scroll_position: gpui::Point<ScrollOffset>,
362 map: &DisplaySnapshot,
363 scroll_beyond_last_line: ScrollBeyondLastLine,
364 local: bool,
365 autoscroll: bool,
366 workspace_id: Option<WorkspaceId>,
367 window: &mut Window,
368 cx: &mut Context<Editor>,
369 ) -> WasScrolled {
370 let scroll_top = scroll_position.y.max(0.);
371 let scroll_top = match scroll_beyond_last_line {
372 ScrollBeyondLastLine::OnePage => scroll_top,
373 ScrollBeyondLastLine::Off => {
374 if let Some(height_in_lines) = self.visible_line_count {
375 let max_row = map.max_point().row().as_f64();
376 scroll_top.min(max_row - height_in_lines + 1.).max(0.)
377 } else {
378 scroll_top
379 }
380 }
381 ScrollBeyondLastLine::VerticalScrollMargin => {
382 if let Some(height_in_lines) = self.visible_line_count {
383 let max_row = map.max_point().row().as_f64();
384 scroll_top
385 .min(max_row - height_in_lines + 1. + self.vertical_scroll_margin)
386 .max(0.)
387 } else {
388 scroll_top
389 }
390 }
391 };
392 let scroll_top_row = DisplayRow(scroll_top as u32);
393 let scroll_top_buffer_point = map
394 .clip_point(
395 DisplayPoint::new(scroll_top_row, scroll_position.x as u32),
396 Bias::Left,
397 )
398 .to_point(map);
399 let top_anchor = map.buffer_snapshot().anchor_before(scroll_top_buffer_point);
400
401 self.set_anchor(
402 ScrollAnchor {
403 anchor: top_anchor,
404 offset: point(
405 scroll_position.x.max(0.),
406 scroll_top - top_anchor.to_display_point(map).row().as_f64(),
407 ),
408 },
409 map,
410 scroll_top_buffer_point.row,
411 local,
412 autoscroll,
413 workspace_id,
414 window,
415 cx,
416 )
417 }
418
419 fn set_anchor(
420 &mut self,
421 anchor: ScrollAnchor,
422 display_map: &DisplaySnapshot,
423 top_row: u32,
424 local: bool,
425 autoscroll: bool,
426 workspace_id: Option<WorkspaceId>,
427 window: &mut Window,
428 cx: &mut Context<Editor>,
429 ) -> WasScrolled {
430 let adjusted_anchor = if self.forbid_vertical_scroll {
431 let current = self.anchor.read(cx);
432 ScrollAnchor {
433 offset: gpui::Point::new(anchor.offset.x, current.scroll_anchor.offset.y),
434 anchor: current.scroll_anchor.anchor,
435 }
436 } else {
437 anchor
438 };
439
440 self.scroll_max_x.take();
441 self.autoscroll_request.take();
442
443 let current = self.anchor.read(cx);
444 if current.scroll_anchor == adjusted_anchor {
445 return WasScrolled(false);
446 }
447
448 self.anchor.update(cx, |shared, _| {
449 shared.scroll_anchor = adjusted_anchor;
450 shared.display_map_id = Some(display_map.display_map_id);
451 });
452 cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll });
453 self.show_scrollbars(window, cx);
454 if let Some(workspace_id) = workspace_id {
455 let item_id = cx.entity().entity_id().as_u64() as ItemId;
456 let executor = cx.background_executor().clone();
457
458 let db = EditorDb::global(cx);
459 self._save_scroll_position_task = cx.background_executor().spawn(async move {
460 executor.timer(Duration::from_millis(10)).await;
461 log::debug!(
462 "Saving scroll position for item {item_id:?} in workspace {workspace_id:?}"
463 );
464 db.save_scroll_position(
465 item_id,
466 workspace_id,
467 top_row,
468 anchor.offset.x,
469 anchor.offset.y,
470 )
471 .await
472 .log_err();
473 });
474 }
475 cx.notify();
476
477 WasScrolled(true)
478 }
479
480 pub fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context<Editor>) {
481 if !self.show_scrollbars {
482 self.show_scrollbars = true;
483 cx.notify();
484 }
485
486 if cx.default_global::<ScrollbarAutoHide>().should_hide() {
487 self.hide_scrollbar_task = Some(cx.spawn_in(window, async move |editor, cx| {
488 cx.background_executor()
489 .timer(SCROLLBAR_SHOW_INTERVAL)
490 .await;
491 editor
492 .update(cx, |editor, cx| {
493 editor.scroll_manager.show_scrollbars = false;
494 cx.notify();
495 })
496 .log_err();
497 }));
498 } else {
499 self.hide_scrollbar_task = None;
500 }
501 }
502
503 pub fn scrollbars_visible(&self) -> bool {
504 self.show_scrollbars
505 }
506
507 pub fn has_autoscroll_request(&self) -> bool {
508 self.autoscroll_request.is_some()
509 }
510
511 pub fn take_autoscroll_request(&mut self) -> Option<(Autoscroll, bool)> {
512 self.autoscroll_request.take()
513 }
514
515 pub fn active_scrollbar_state(&self) -> Option<&ActiveScrollbarState> {
516 self.active_scrollbar.as_ref()
517 }
518
519 pub fn dragging_scrollbar_axis(&self) -> Option<Axis> {
520 self.active_scrollbar
521 .as_ref()
522 .filter(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
523 .map(|scrollbar| scrollbar.axis)
524 }
525
526 pub fn any_scrollbar_dragged(&self) -> bool {
527 self.active_scrollbar
528 .as_ref()
529 .is_some_and(|scrollbar| scrollbar.thumb_state == ScrollbarThumbState::Dragging)
530 }
531
532 pub fn set_hovered_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
533 self.update_active_scrollbar_state(
534 Some(ActiveScrollbarState::new(
535 axis,
536 ScrollbarThumbState::Hovered,
537 )),
538 cx,
539 );
540 }
541
542 pub fn set_dragged_scroll_thumb_axis(&mut self, axis: Axis, cx: &mut Context<Editor>) {
543 self.update_active_scrollbar_state(
544 Some(ActiveScrollbarState::new(
545 axis,
546 ScrollbarThumbState::Dragging,
547 )),
548 cx,
549 );
550 }
551
552 pub fn reset_scrollbar_state(&mut self, cx: &mut Context<Editor>) {
553 self.update_active_scrollbar_state(None, cx);
554 }
555
556 fn update_active_scrollbar_state(
557 &mut self,
558 new_state: Option<ActiveScrollbarState>,
559 cx: &mut Context<Editor>,
560 ) {
561 if self.active_scrollbar != new_state {
562 self.active_scrollbar = new_state;
563 cx.notify();
564 }
565 }
566
567 pub fn set_is_hovering_minimap_thumb(&mut self, hovered: bool, cx: &mut Context<Editor>) {
568 self.update_minimap_thumb_state(
569 Some(if hovered {
570 ScrollbarThumbState::Hovered
571 } else {
572 ScrollbarThumbState::Idle
573 }),
574 cx,
575 );
576 }
577
578 pub fn set_is_dragging_minimap(&mut self, cx: &mut Context<Editor>) {
579 self.update_minimap_thumb_state(Some(ScrollbarThumbState::Dragging), cx);
580 }
581
582 pub fn hide_minimap_thumb(&mut self, cx: &mut Context<Editor>) {
583 self.update_minimap_thumb_state(None, cx);
584 }
585
586 pub fn is_dragging_minimap(&self) -> bool {
587 self.minimap_thumb_state
588 .is_some_and(|state| state == ScrollbarThumbState::Dragging)
589 }
590
591 fn update_minimap_thumb_state(
592 &mut self,
593 thumb_state: Option<ScrollbarThumbState>,
594 cx: &mut Context<Editor>,
595 ) {
596 if self.minimap_thumb_state != thumb_state {
597 self.minimap_thumb_state = thumb_state;
598 cx.notify();
599 }
600 }
601
602 pub fn minimap_thumb_state(&self) -> Option<ScrollbarThumbState> {
603 self.minimap_thumb_state
604 }
605
606 pub fn clamp_scroll_left(&mut self, max: f64, cx: &App) -> bool {
607 let current_x = self.anchor.read(cx).scroll_anchor.offset.x;
608 self.scroll_max_x = Some(max);
609 current_x > max
610 }
611
612 pub fn set_forbid_vertical_scroll(&mut self, forbid: bool) {
613 self.forbid_vertical_scroll = forbid;
614 }
615
616 pub fn forbid_vertical_scroll(&self) -> bool {
617 self.forbid_vertical_scroll
618 }
619}
620
621impl Editor {
622 pub fn has_autoscroll_request(&self) -> bool {
623 self.scroll_manager.has_autoscroll_request()
624 }
625
626 pub fn vertical_scroll_margin(&self) -> usize {
627 self.scroll_manager.vertical_scroll_margin as usize
628 }
629
630 pub(crate) fn scroll_beyond_last_line(&self, cx: &App) -> ScrollBeyondLastLine {
631 match self.mode {
632 EditorMode::Minimap { .. }
633 | EditorMode::Full {
634 sizing_behavior: SizingBehavior::Default,
635 ..
636 } => EditorSettings::get_global(cx).scroll_beyond_last_line,
637
638 EditorMode::Full { .. } | EditorMode::SingleLine | EditorMode::AutoHeight { .. } => {
639 ScrollBeyondLastLine::Off
640 }
641 }
642 }
643
644 pub fn set_vertical_scroll_margin(&mut self, margin_rows: usize, cx: &mut Context<Self>) {
645 self.scroll_manager.vertical_scroll_margin = margin_rows as f64;
646 cx.notify();
647 }
648
649 pub fn visible_line_count(&self) -> Option<f64> {
650 self.scroll_manager.visible_line_count
651 }
652
653 pub fn visible_row_count(&self) -> Option<u32> {
654 self.visible_line_count()
655 .map(|line_count| line_count as u32 - 1)
656 }
657
658 pub fn visible_column_count(&self) -> Option<f64> {
659 self.scroll_manager.visible_column_count
660 }
661
662 pub(crate) fn set_visible_line_count(
663 &mut self,
664 lines: f64,
665 window: &mut Window,
666 cx: &mut Context<Self>,
667 ) {
668 let opened_first_time = self.scroll_manager.visible_line_count.is_none();
669 self.scroll_manager.visible_line_count = Some(lines);
670 if opened_first_time {
671 self.update_data_on_scroll(false, window, cx);
672 }
673 }
674
675 pub(crate) fn set_visible_column_count(&mut self, columns: f64) {
676 self.scroll_manager.visible_column_count = Some(columns);
677 }
678
679 pub fn apply_scroll_delta(
680 &mut self,
681 scroll_delta: gpui::Point<f32>,
682 window: &mut Window,
683 cx: &mut Context<Self>,
684 ) {
685 let mut delta = scroll_delta;
686 if self.scroll_manager.forbid_vertical_scroll {
687 delta.y = 0.0;
688 }
689 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
690 let position = self.scroll_manager.scroll_position(&display_map, cx) + delta.map(f64::from);
691 self.set_scroll_position_taking_display_map(position, true, false, display_map, window, cx);
692 }
693
694 pub fn set_scroll_position(
695 &mut self,
696 scroll_position: gpui::Point<ScrollOffset>,
697 window: &mut Window,
698 cx: &mut Context<Self>,
699 ) -> WasScrolled {
700 let mut position = scroll_position;
701 if self.scroll_manager.forbid_vertical_scroll {
702 let current_position = self.scroll_position(cx);
703 position.y = current_position.y;
704 }
705 self.set_scroll_position_internal(position, true, false, window, cx)
706 }
707
708 /// Scrolls so that `row` is at the top of the editor view.
709 pub fn set_scroll_top_row(
710 &mut self,
711 row: DisplayRow,
712 window: &mut Window,
713 cx: &mut Context<Editor>,
714 ) {
715 let snapshot = self.snapshot(window, cx).display_snapshot;
716 let new_screen_top = DisplayPoint::new(row, 0);
717 let new_screen_top = new_screen_top.to_offset(&snapshot, Bias::Left);
718 let new_anchor = snapshot.buffer_snapshot().anchor_before(new_screen_top);
719
720 self.set_scroll_anchor(
721 ScrollAnchor {
722 anchor: new_anchor,
723 offset: Default::default(),
724 },
725 window,
726 cx,
727 );
728 }
729
730 pub(crate) fn set_scroll_position_internal(
731 &mut self,
732 scroll_position: gpui::Point<ScrollOffset>,
733 local: bool,
734 autoscroll: bool,
735 window: &mut Window,
736 cx: &mut Context<Self>,
737 ) -> WasScrolled {
738 let map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
739 let was_scrolled = self.set_scroll_position_taking_display_map(
740 scroll_position,
741 local,
742 autoscroll,
743 map,
744 window,
745 cx,
746 );
747
748 was_scrolled
749 }
750
751 fn set_scroll_position_taking_display_map(
752 &mut self,
753 scroll_position: gpui::Point<ScrollOffset>,
754 local: bool,
755 autoscroll: bool,
756 display_map: DisplaySnapshot,
757 window: &mut Window,
758 cx: &mut Context<Self>,
759 ) -> WasScrolled {
760 hide_hover(self, cx);
761 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
762
763 self.edit_prediction_preview
764 .set_previous_scroll_position(None);
765
766 let adjusted_position = if self.scroll_manager.forbid_vertical_scroll {
767 let current_position = self.scroll_manager.scroll_position(&display_map, cx);
768 gpui::Point::new(scroll_position.x, current_position.y)
769 } else {
770 scroll_position
771 };
772 let scroll_beyond_last_line = self.scroll_beyond_last_line(cx);
773 self.scroll_manager.set_scroll_position(
774 adjusted_position,
775 &display_map,
776 scroll_beyond_last_line,
777 local,
778 autoscroll,
779 workspace_id,
780 window,
781 cx,
782 )
783 }
784
785 pub fn scroll_position(&self, cx: &mut Context<Self>) -> gpui::Point<ScrollOffset> {
786 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
787 self.scroll_manager.scroll_position(&display_map, cx)
788 }
789
790 pub fn set_scroll_anchor(
791 &mut self,
792 scroll_anchor: ScrollAnchor,
793 window: &mut Window,
794 cx: &mut Context<Self>,
795 ) {
796 hide_hover(self, cx);
797 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
798 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
799 let top_row = scroll_anchor
800 .anchor
801 .to_point(&self.buffer().read(cx).snapshot(cx))
802 .row;
803 self.scroll_manager.set_anchor(
804 scroll_anchor,
805 &display_map,
806 top_row,
807 true,
808 false,
809 workspace_id,
810 window,
811 cx,
812 );
813 }
814
815 pub(crate) fn set_scroll_anchor_remote(
816 &mut self,
817 scroll_anchor: ScrollAnchor,
818 window: &mut Window,
819 cx: &mut Context<Self>,
820 ) {
821 hide_hover(self, cx);
822 let workspace_id = self.workspace.as_ref().and_then(|workspace| workspace.1);
823 let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
824 if !scroll_anchor.anchor.is_valid(&buffer_snapshot) {
825 log::warn!("Invalid scroll anchor: {:?}", scroll_anchor);
826 return;
827 }
828 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
829 let top_row = scroll_anchor.anchor.to_point(&buffer_snapshot).row;
830 self.scroll_manager.set_anchor(
831 scroll_anchor,
832 &display_map,
833 top_row,
834 false,
835 false,
836 workspace_id,
837 window,
838 cx,
839 );
840 }
841
842 pub fn scroll_screen(
843 &mut self,
844 amount: &ScrollAmount,
845 window: &mut Window,
846 cx: &mut Context<Self>,
847 ) {
848 if matches!(self.mode, EditorMode::SingleLine) {
849 cx.propagate();
850 return;
851 }
852
853 if self.take_rename(true, window, cx).is_some() {
854 return;
855 }
856
857 let mut current_position = self.scroll_position(cx);
858 let Some(visible_line_count) = self.visible_line_count() else {
859 return;
860 };
861 let Some(mut visible_column_count) = self.visible_column_count() else {
862 return;
863 };
864
865 // If the user has a preferred line length, and has the editor
866 // configured to wrap at the preferred line length, or bounded to it,
867 // use that value over the visible column count. This was mostly done so
868 // that tests could actually be written for vim's `z l`, `z h`, `z
869 // shift-l` and `z shift-h` commands, as there wasn't a good way to
870 // configure the editor to only display a certain number of columns. If
871 // that ever happens, this could probably be removed.
872 let settings = AllLanguageSettings::get_global(cx);
873 if matches!(settings.defaults.soft_wrap, SoftWrap::Bounded)
874 && (settings.defaults.preferred_line_length as f64) < visible_column_count
875 {
876 visible_column_count = settings.defaults.preferred_line_length as f64;
877 }
878
879 // If the scroll position is currently at the left edge of the document
880 // (x == 0.0) and the intent is to scroll right, the gutter's margin
881 // should first be added to the current position, otherwise the cursor
882 // will end at the column position minus the margin, which looks off.
883 if current_position.x == 0.0
884 && amount.columns(visible_column_count) > 0.
885 && let Some(last_position_map) = &self.last_position_map
886 {
887 current_position.x +=
888 f64::from(self.gutter_dimensions.margin / last_position_map.em_advance);
889 }
890 let new_position = current_position
891 + point(
892 amount.columns(visible_column_count),
893 amount.lines(visible_line_count),
894 );
895 self.set_scroll_position(new_position, window, cx);
896 }
897
898 /// Returns an ordering. The newest selection is:
899 /// Ordering::Equal => on screen
900 /// Ordering::Less => above or to the left of the screen
901 /// Ordering::Greater => below or to the right of the screen
902 pub fn newest_selection_on_screen(&self, cx: &mut App) -> Ordering {
903 let snapshot = self.display_map.update(cx, |map, cx| map.snapshot(cx));
904 let newest_head = self
905 .selections
906 .newest_anchor()
907 .head()
908 .to_display_point(&snapshot);
909 let screen_top = self.scroll_manager.scroll_top_display_point(&snapshot, cx);
910
911 if screen_top > newest_head {
912 return Ordering::Less;
913 }
914
915 if let (Some(visible_lines), Some(visible_columns)) =
916 (self.visible_line_count(), self.visible_column_count())
917 && newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32)
918 && newest_head.column() <= screen_top.column() + visible_columns as u32
919 {
920 return Ordering::Equal;
921 }
922
923 Ordering::Greater
924 }
925
926 pub fn read_scroll_position_from_db(
927 &mut self,
928 item_id: u64,
929 workspace_id: WorkspaceId,
930 window: &mut Window,
931 cx: &mut Context<Editor>,
932 ) {
933 let scroll_position = EditorDb::global(cx).get_scroll_position(item_id, workspace_id);
934 if let Ok(Some((top_row, x, y))) = scroll_position {
935 let top_anchor = self
936 .buffer()
937 .read(cx)
938 .snapshot(cx)
939 .anchor_before(Point::new(top_row, 0));
940 let scroll_anchor = ScrollAnchor {
941 offset: gpui::Point::new(x, y),
942 anchor: top_anchor,
943 };
944 self.set_scroll_anchor(scroll_anchor, window, cx);
945 }
946 }
947}