Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:37:44.602Z 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

autoscroll.rs

466 lines · 17.5 KB · rust
1use crate::{
2    DisplayPoint, DisplayRow, Editor, EditorMode, EditorSettings, LineWithInvisibles, RowExt,
3    SelectionEffects,
4    display_map::{DisplaySnapshot, ToDisplayPoint},
5    editor_settings::GoToDefinitionScrollStrategy,
6    scroll::{ScrollOffset, WasScrolled},
7};
8use gpui::{App, Bounds, Context, Pixels, Window};
9use language::Point;
10use multi_buffer::Anchor;
11use settings::Settings;
12use std::cmp;
13use text::Bias;
14
15#[derive(Debug, PartialEq, Eq, Clone, Copy)]
16pub enum Autoscroll {
17    Next,
18    Strategy(AutoscrollStrategy, Option<Anchor>),
19}
20
21impl Autoscroll {
22    /// scrolls the minimal amount to (try) and fit all cursors onscreen
23    pub fn fit() -> Self {
24        Self::Strategy(AutoscrollStrategy::Fit, None)
25    }
26
27    /// scrolls the minimal amount to fit the newest cursor
28    pub fn newest() -> Self {
29        Self::Strategy(AutoscrollStrategy::Newest, None)
30    }
31
32    /// scrolls so the newest cursor is vertically centered
33    pub fn center() -> Self {
34        Self::Strategy(AutoscrollStrategy::Center, None)
35    }
36
37    /// Returns the autoscroll strategy configured for navigation to definitions
38    /// and references, based on `go_to_definition_scroll_strategy`.
39    pub fn for_go_to_definition(offset: Option<ScrollOffset>, cx: &App) -> Self {
40        match EditorSettings::get_global(cx).go_to_definition_scroll_strategy {
41            GoToDefinitionScrollStrategy::Center => Self::center(),
42            GoToDefinitionScrollStrategy::Minimum => Self::fit(),
43            GoToDefinitionScrollStrategy::Top => Self::focused(),
44            GoToDefinitionScrollStrategy::Preserve => {
45                offset.map(Self::top_relative).unwrap_or_else(Self::center)
46            }
47        }
48    }
49
50    /// scrolls so the newest cursor is near the top
51    /// (offset by vertical_scroll_margin)
52    pub fn focused() -> Self {
53        Self::Strategy(AutoscrollStrategy::Focused, None)
54    }
55
56    /// Scrolls so that the newest cursor is the given offset (in display rows)
57    /// from the top of the viewport.
58    pub fn top_relative(offset: ScrollOffset) -> Self {
59        Self::Strategy(AutoscrollStrategy::TopRelative(offset), None)
60    }
61
62    /// Scrolls so that the newest cursor is at the top.
63    pub fn top() -> Self {
64        Self::Strategy(AutoscrollStrategy::Top, None)
65    }
66
67    /// Scrolls so that the newest cursor is the given offset (in display rows)
68    /// from the bottom of the viewport.
69    pub fn bottom_relative(offset: ScrollOffset) -> Self {
70        Self::Strategy(AutoscrollStrategy::BottomRelative(offset), None)
71    }
72
73    /// Scrolls so that the newest cursor is at the bottom.
74    pub fn bottom() -> Self {
75        Self::Strategy(AutoscrollStrategy::Bottom, None)
76    }
77
78    /// Applies a given auto-scroll strategy to a given anchor instead of a cursor.
79    /// E.G: Autoscroll::center().for_anchor(...) results in the anchor being at the center of the screen.
80    pub fn for_anchor(self, anchor: Anchor) -> Self {
81        match self {
82            Autoscroll::Next => self,
83            Autoscroll::Strategy(autoscroll_strategy, _) => {
84                Self::Strategy(autoscroll_strategy, Some(anchor))
85            }
86        }
87    }
88}
89
90impl Into<SelectionEffects> for Option<Autoscroll> {
91    fn into(self) -> SelectionEffects {
92        match self {
93            Some(autoscroll) => SelectionEffects::scroll(autoscroll),
94            None => SelectionEffects::no_scroll(),
95        }
96    }
97}
98
99#[derive(Debug, PartialEq, Default, Clone, Copy)]
100pub enum AutoscrollStrategy {
101    Fit,
102    Newest,
103    #[default]
104    Center,
105    Focused,
106    Top,
107    Bottom,
108    TopRelative(ScrollOffset),
109    BottomRelative(ScrollOffset),
110}
111
112impl Eq for AutoscrollStrategy {}
113
114impl AutoscrollStrategy {
115    fn next(&self) -> Self {
116        match self {
117            AutoscrollStrategy::Center => AutoscrollStrategy::Top,
118            AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
119            _ => AutoscrollStrategy::Center,
120        }
121    }
122}
123
124pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
125
126impl Editor {
127    pub(crate) fn autoscroll_vertically(
128        &mut self,
129        bounds: Bounds<Pixels>,
130        line_height: Pixels,
131        max_scroll_top: ScrollOffset,
132        autoscroll_request: Option<(Autoscroll, bool)>,
133        window: &mut Window,
134        cx: &mut Context<Editor>,
135    ) -> (NeedsHorizontalAutoscroll, WasScrolled) {
136        let viewport_height = bounds.size.height;
137        let visible_lines = ScrollOffset::from(viewport_height / line_height);
138        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
139        let mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx);
140        let original_y = scroll_position.y;
141        if let Some(last_bounds) = self.expect_bounds_change.take()
142            && scroll_position.y != 0.
143        {
144            scroll_position.y +=
145                ScrollOffset::from((bounds.top() - last_bounds.top()) / line_height);
146            if scroll_position.y < 0. {
147                scroll_position.y = 0.;
148            }
149        }
150        if scroll_position.y > max_scroll_top {
151            scroll_position.y = max_scroll_top;
152        }
153
154        let editor_was_scrolled = if original_y != scroll_position.y {
155            self.set_scroll_position(scroll_position, window, cx)
156        } else {
157            WasScrolled(false)
158        };
159
160        let Some((autoscroll, local)) = autoscroll_request else {
161            return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
162        };
163
164        let mut target_point;
165        let mut target_top;
166        let mut target_bottom;
167        if let Some(first_highlighted_row) =
168            self.highlighted_display_row_for_autoscroll(&display_map)
169        {
170            target_point = DisplayPoint::new(first_highlighted_row, 0);
171            target_top = target_point.row().as_f64();
172            target_bottom = target_top + 1.;
173        } else {
174            // Autoscroll only needs the first, last, and newest selections.
175            target_point = self
176                .selections
177                .first::<Point>(&display_map)
178                .head()
179                .to_display_point(&display_map);
180            target_top = target_point.row().as_f64();
181            target_bottom = self
182                .selections
183                .last::<Point>(&display_map)
184                .head()
185                .to_display_point(&display_map)
186                .row()
187                .next_row()
188                .as_f64();
189
190            let selections_fit = target_bottom - target_top <= visible_lines;
191            if matches!(
192                autoscroll,
193                Autoscroll::Strategy(AutoscrollStrategy::Newest, _)
194            ) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _))
195                && !selections_fit)
196            {
197                target_point = self
198                    .selections
199                    .newest::<Point>(&display_map)
200                    .head()
201                    .to_display_point(&display_map);
202                target_top = target_point.row().as_f64();
203                target_bottom = target_top + 1.;
204            }
205        }
206
207        let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
208            0.
209        } else {
210            ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
211        };
212
213        let strategy = match autoscroll {
214            Autoscroll::Strategy(strategy, _) => strategy,
215            Autoscroll::Next => self
216                .scroll_manager
217                .last_autoscroll
218                .as_ref()
219                .filter(|(offset, last_target_top, last_target_bottom, _)| {
220                    self.scroll_manager.offset(cx) == *offset
221                        && target_top == *last_target_top
222                        && target_bottom == *last_target_bottom
223                })
224                .map(|(_, _, _, strategy)| strategy.next())
225                .unwrap_or_default(),
226        };
227        if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll {
228            target_point = anchor.to_display_point(&display_map);
229            target_top = target_point.row().as_f64();
230            target_bottom = target_top + 1.;
231        }
232
233        let visible_sticky_headers =
234            self.visible_sticky_header_count_for_point(&display_map, target_point, cx);
235
236        let was_autoscrolled = match strategy {
237            AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
238                let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
239                let target_top = (target_top - margin - visible_sticky_headers as f64).max(0.0);
240                let target_bottom = target_bottom + margin;
241                let start_row = scroll_position.y;
242                let end_row = start_row + visible_lines;
243
244                let needs_scroll_up = target_top < start_row;
245                let needs_scroll_down = target_bottom >= end_row;
246
247                if needs_scroll_up && !needs_scroll_down {
248                    scroll_position.y = target_top;
249                } else if !needs_scroll_up && needs_scroll_down {
250                    scroll_position.y = target_bottom - visible_lines;
251                }
252
253                if needs_scroll_up ^ needs_scroll_down {
254                    self.set_scroll_position_internal(scroll_position, local, true, window, cx)
255                } else {
256                    WasScrolled(false)
257                }
258            }
259            AutoscrollStrategy::Center => {
260                scroll_position.y = (target_top - margin).max(0.0);
261                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
262            }
263            AutoscrollStrategy::Focused => {
264                let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
265                scroll_position.y = (target_top - margin).max(0.0);
266                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
267            }
268            AutoscrollStrategy::Top => {
269                scroll_position.y = (target_top).max(0.0);
270                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
271            }
272            AutoscrollStrategy::Bottom => {
273                scroll_position.y = (target_bottom - visible_lines).max(0.0);
274                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
275            }
276            AutoscrollStrategy::TopRelative(lines) => {
277                scroll_position.y = target_top - lines as ScrollOffset;
278                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
279            }
280            AutoscrollStrategy::BottomRelative(lines) => {
281                scroll_position.y = target_bottom + lines as ScrollOffset;
282                self.set_scroll_position_internal(scroll_position, local, true, window, cx)
283            }
284        };
285
286        self.scroll_manager.last_autoscroll = Some((
287            self.scroll_manager.offset(cx),
288            target_top,
289            target_bottom,
290            strategy,
291        ));
292
293        let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
294        (NeedsHorizontalAutoscroll(true), was_scrolled)
295    }
296
297    pub(crate) fn visible_sticky_header_count_for_point(
298        &self,
299        display_map: &DisplaySnapshot,
300        target_point: DisplayPoint,
301        cx: &App,
302    ) -> usize {
303        let sticky_scroll = EditorSettings::get_global(cx).sticky_scroll;
304        if !sticky_scroll.enabled {
305            return 0;
306        }
307
308        let Some(buffer_snapshot) = display_map.buffer_snapshot().as_singleton() else {
309            return 0;
310        };
311
312        let point = target_point.to_point(display_map);
313        let mut item_ranges = buffer_snapshot
314            .outline_ranges_containing(point..point)
315            .collect::<Vec<_>>();
316        item_ranges.sort_by_key(|item_range| item_range.start);
317
318        let mut previous_sticky_row = None;
319        let mut num_visible_sticky_headers = 0;
320
321        for item_range in item_ranges {
322            let sticky_row = display_map
323                .point_to_display_point(item_range.start, Bias::Left)
324                .row();
325            if sticky_row >= target_point.row() {
326                break;
327            }
328            if previous_sticky_row.replace(sticky_row) == Some(sticky_row) {
329                continue;
330            }
331
332            let end_row = display_map
333                .point_to_display_point(item_range.end, Bias::Left)
334                .row();
335            if end_row <= target_point.row() {
336                continue;
337            }
338
339            num_visible_sticky_headers += 1;
340        }
341
342        num_visible_sticky_headers
343    }
344
345    pub(crate) fn autoscroll_horizontally(
346        &mut self,
347        start_row: DisplayRow,
348        viewport_width: Pixels,
349        scroll_width: Pixels,
350        em_advance: Pixels,
351        layouts: &[LineWithInvisibles],
352        autoscroll_request: Option<(Autoscroll, bool)>,
353        window: &mut Window,
354        cx: &mut Context<Self>,
355    ) -> Option<gpui::Point<ScrollOffset>> {
356        let (_, local) = autoscroll_request?;
357        let em_advance = ScrollOffset::from(em_advance);
358        let viewport_width = ScrollOffset::from(viewport_width);
359        let scroll_width = ScrollOffset::from(scroll_width);
360
361        let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
362        // Horizontal autoscroll only needs selections whose head is visible.
363        let visible_end_row = DisplayRow(start_row.0 + layouts.len() as u32);
364        let visible_range = display_map
365            .buffer_snapshot()
366            .anchor_before(DisplayPoint::new(start_row, 0).to_offset(&display_map, Bias::Left))
367            ..display_map.buffer_snapshot().anchor_after(
368                DisplayPoint::new(visible_end_row, 0).to_offset(&display_map, Bias::Right),
369            );
370        let mut selections = self
371            .selections
372            .disjoint_in_range::<Point>(visible_range, &display_map);
373        selections.extend(self.selections.pending::<Point>(&display_map));
374        let mut scroll_position = self.scroll_manager.scroll_position(&display_map, cx);
375
376        let mut target_left;
377        let mut target_right: f64;
378
379        if self
380            .highlighted_display_row_for_autoscroll(&display_map)
381            .is_none()
382        {
383            target_left = f64::INFINITY;
384            target_right = 0.;
385            for selection in selections {
386                let head = selection.head().to_display_point(&display_map);
387                if head.row() >= start_row
388                    && head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
389                {
390                    let row_line_len = display_map.line_len(head.row());
391                    let start_dp = selection.start.to_display_point(&display_map);
392                    let end_dp = selection.end.to_display_point(&display_map);
393
394                    let start_column = if start_dp.row() == head.row() {
395                        start_dp.column()
396                    } else {
397                        0
398                    };
399                    let end_column = cmp::min(
400                        row_line_len,
401                        if end_dp.row() == head.row() {
402                            end_dp.column()
403                        } else {
404                            row_line_len
405                        },
406                    );
407
408                    target_left = target_left.min(ScrollOffset::from(
409                        layouts[head.row().minus(start_row) as usize]
410                            .x_for_index(start_column as usize)
411                            + self.gutter_dimensions.margin,
412                    ));
413                    target_right = target_right.max(
414                        ScrollOffset::from(
415                            layouts[head.row().minus(start_row) as usize]
416                                .x_for_index(end_column as usize),
417                        ) + em_advance,
418                    );
419                }
420            }
421        } else {
422            target_left = 0.;
423            target_right = 0.;
424        }
425
426        target_right = target_right.min(scroll_width);
427
428        if target_right - target_left > viewport_width {
429            return None;
430        }
431
432        let scroll_left = self.scroll_manager.offset(cx).x * em_advance;
433        let scroll_right = scroll_left + viewport_width;
434
435        let was_scrolled = if target_left < scroll_left {
436            scroll_position.x = target_left / em_advance;
437            self.set_scroll_position_internal(scroll_position, local, true, window, cx)
438        } else if target_right > scroll_right {
439            scroll_position.x = (target_right - viewport_width) / em_advance;
440            self.set_scroll_position_internal(scroll_position, local, true, window, cx)
441        } else {
442            WasScrolled(false)
443        };
444
445        if was_scrolled.0 {
446            Some(scroll_position)
447        } else {
448            None
449        }
450    }
451
452    pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut Context<Self>) {
453        self.scroll_manager.autoscroll_request = Some((autoscroll, true));
454        cx.notify();
455    }
456
457    pub(crate) fn request_autoscroll_remotely(
458        &mut self,
459        autoscroll: Autoscroll,
460        cx: &mut Context<Self>,
461    ) {
462        self.scroll_manager.autoscroll_request = Some((autoscroll, false));
463        cx.notify();
464    }
465}
466
Served at tenant.openagents/omega Member data and write actions are omitted.