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