Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:32:08.371Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

scroll.rs

1055 lines · 36.1 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.