Skip to repository content466 lines · 17.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T02:36:39.718Z 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
autoscroll.rs
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