1use crate::{
2 DisplayRow, Editor, EditorMode, LineWithInvisibles, RowExt, SelectionEffects,
3 display_map::ToDisplayPoint,
4 scroll::{ScrollOffset, WasScrolled},
5};
6use gpui::{Bounds, Context, Pixels, Window};
7use language::Point;
8use multi_buffer::{Anchor, ToPoint};
9use std::cmp;
10
11#[derive(Debug, PartialEq, Eq, Clone, Copy)]
12pub enum Autoscroll {
13 Next,
14 Strategy(AutoscrollStrategy, Option<Anchor>),
15}
16
17impl Autoscroll {
18 /// scrolls the minimal amount to (try) and fit all cursors onscreen
19 pub fn fit() -> Self {
20 Self::Strategy(AutoscrollStrategy::Fit, None)
21 }
22
23 /// scrolls the minimal amount to fit the newest cursor
24 pub fn newest() -> Self {
25 Self::Strategy(AutoscrollStrategy::Newest, None)
26 }
27
28 /// scrolls so the newest cursor is vertically centered
29 pub fn center() -> Self {
30 Self::Strategy(AutoscrollStrategy::Center, None)
31 }
32
33 /// scrolls so the newest cursor is near the top
34 /// (offset by vertical_scroll_margin)
35 pub fn focused() -> Self {
36 Self::Strategy(AutoscrollStrategy::Focused, None)
37 }
38
39 /// Scrolls so that the newest cursor is roughly an n-th line from the top.
40 pub fn top_relative(n: usize) -> Self {
41 Self::Strategy(AutoscrollStrategy::TopRelative(n), None)
42 }
43
44 /// Scrolls so that the newest cursor is at the top.
45 pub fn top() -> Self {
46 Self::Strategy(AutoscrollStrategy::Top, None)
47 }
48
49 /// Scrolls so that the newest cursor is roughly an n-th line from the bottom.
50 pub fn bottom_relative(n: usize) -> Self {
51 Self::Strategy(AutoscrollStrategy::BottomRelative(n), None)
52 }
53
54 /// Scrolls so that the newest cursor is at the bottom.
55 pub fn bottom() -> Self {
56 Self::Strategy(AutoscrollStrategy::Bottom, None)
57 }
58
59 /// Applies a given auto-scroll strategy to a given anchor instead of a cursor.
60 /// E.G: Autoscroll::center().for_anchor(...) results in the anchor being at the center of the screen.
61 pub fn for_anchor(self, anchor: Anchor) -> Self {
62 match self {
63 Autoscroll::Next => self,
64 Autoscroll::Strategy(autoscroll_strategy, _) => {
65 Self::Strategy(autoscroll_strategy, Some(anchor))
66 }
67 }
68 }
69}
70
71impl Into<SelectionEffects> for Option<Autoscroll> {
72 fn into(self) -> SelectionEffects {
73 match self {
74 Some(autoscroll) => SelectionEffects::scroll(autoscroll),
75 None => SelectionEffects::no_scroll(),
76 }
77 }
78}
79
80#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)]
81pub enum AutoscrollStrategy {
82 Fit,
83 Newest,
84 #[default]
85 Center,
86 Focused,
87 Top,
88 Bottom,
89 TopRelative(usize),
90 BottomRelative(usize),
91}
92
93impl AutoscrollStrategy {
94 fn next(&self) -> Self {
95 match self {
96 AutoscrollStrategy::Center => AutoscrollStrategy::Top,
97 AutoscrollStrategy::Top => AutoscrollStrategy::Bottom,
98 _ => AutoscrollStrategy::Center,
99 }
100 }
101}
102
103pub(crate) struct NeedsHorizontalAutoscroll(pub(crate) bool);
104
105impl Editor {
106 pub(crate) fn autoscroll_vertically(
107 &mut self,
108 bounds: Bounds<Pixels>,
109 line_height: Pixels,
110 max_scroll_top: ScrollOffset,
111 autoscroll_request: Option<(Autoscroll, bool)>,
112 window: &mut Window,
113 cx: &mut Context<Editor>,
114 ) -> (NeedsHorizontalAutoscroll, WasScrolled) {
115 let viewport_height = bounds.size.height;
116 let visible_lines = ScrollOffset::from(viewport_height / line_height);
117 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
118 let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
119 let original_y = scroll_position.y;
120 if let Some(last_bounds) = self.expect_bounds_change.take()
121 && scroll_position.y != 0.
122 {
123 scroll_position.y +=
124 ScrollOffset::from((bounds.top() - last_bounds.top()) / line_height);
125 if scroll_position.y < 0. {
126 scroll_position.y = 0.;
127 }
128 }
129 if scroll_position.y > max_scroll_top {
130 scroll_position.y = max_scroll_top;
131 }
132
133 let editor_was_scrolled = if original_y != scroll_position.y {
134 self.set_scroll_position(scroll_position, window, cx)
135 } else {
136 WasScrolled(false)
137 };
138
139 let Some((autoscroll, local)) = autoscroll_request else {
140 return (NeedsHorizontalAutoscroll(false), editor_was_scrolled);
141 };
142
143 let mut target_top;
144 let mut target_bottom;
145 if let Some(first_highlighted_row) =
146 self.highlighted_display_row_for_autoscroll(&display_map)
147 {
148 target_top = first_highlighted_row.as_f64();
149 target_bottom = target_top + 1.;
150 } else {
151 let selections = self.selections.all::<Point>(&display_map);
152
153 target_top = selections
154 .first()
155 .unwrap()
156 .head()
157 .to_display_point(&display_map)
158 .row()
159 .as_f64();
160 target_bottom = selections
161 .last()
162 .unwrap()
163 .head()
164 .to_display_point(&display_map)
165 .row()
166 .next_row()
167 .as_f64();
168
169 let selections_fit = target_bottom - target_top <= visible_lines;
170 if matches!(
171 autoscroll,
172 Autoscroll::Strategy(AutoscrollStrategy::Newest, _)
173 ) || (matches!(autoscroll, Autoscroll::Strategy(AutoscrollStrategy::Fit, _))
174 && !selections_fit)
175 {
176 let newest_selection_top = selections
177 .iter()
178 .max_by_key(|s| s.id)
179 .unwrap()
180 .head()
181 .to_display_point(&display_map)
182 .row()
183 .as_f64();
184 target_top = newest_selection_top;
185 target_bottom = newest_selection_top + 1.;
186 }
187 }
188
189 let style = self.style(cx).clone();
190 let sticky_headers = self.sticky_headers(&style, cx).unwrap_or_default();
191 let visible_sticky_headers = sticky_headers
192 .iter()
193 .filter(|h| {
194 let buffer_snapshot = display_map.buffer_snapshot();
195 let buffer_range =
196 h.range.start.to_point(buffer_snapshot)..h.range.end.to_point(buffer_snapshot);
197
198 buffer_range.contains(&Point::new(target_top as u32, 0))
199 })
200 .count();
201
202 let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) {
203 0.
204 } else {
205 ((visible_lines - (target_bottom - target_top)) / 2.0).floor()
206 };
207
208 let strategy = match autoscroll {
209 Autoscroll::Strategy(strategy, _) => strategy,
210 Autoscroll::Next => {
211 let last_autoscroll = &self.scroll_manager.last_autoscroll;
212 if let Some(last_autoscroll) = last_autoscroll {
213 if self.scroll_manager.anchor.offset == last_autoscroll.0
214 && target_top == last_autoscroll.1
215 && target_bottom == last_autoscroll.2
216 {
217 last_autoscroll.3.next()
218 } else {
219 AutoscrollStrategy::default()
220 }
221 } else {
222 AutoscrollStrategy::default()
223 }
224 }
225 };
226 if let Autoscroll::Strategy(_, Some(anchor)) = autoscroll {
227 target_top = anchor.to_display_point(&display_map).row().as_f64();
228 target_bottom = target_top + 1.;
229 }
230
231 let was_autoscrolled = match strategy {
232 AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => {
233 let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
234 let target_top = (target_top - margin - visible_sticky_headers as f64).max(0.0);
235 let target_bottom = target_bottom + margin;
236 let start_row = scroll_position.y;
237 let end_row = start_row + visible_lines;
238
239 let needs_scroll_up = target_top < start_row;
240 let needs_scroll_down = target_bottom >= end_row;
241
242 if needs_scroll_up && !needs_scroll_down {
243 scroll_position.y = target_top;
244 } else if !needs_scroll_up && needs_scroll_down {
245 scroll_position.y = target_bottom - visible_lines;
246 }
247
248 if needs_scroll_up ^ needs_scroll_down {
249 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
250 } else {
251 WasScrolled(false)
252 }
253 }
254 AutoscrollStrategy::Center => {
255 scroll_position.y = (target_top - margin).max(0.0);
256 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
257 }
258 AutoscrollStrategy::Focused => {
259 let margin = margin.min(self.scroll_manager.vertical_scroll_margin);
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::Top => {
264 scroll_position.y = (target_top).max(0.0);
265 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
266 }
267 AutoscrollStrategy::Bottom => {
268 scroll_position.y = (target_bottom - visible_lines).max(0.0);
269 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
270 }
271 AutoscrollStrategy::TopRelative(lines) => {
272 scroll_position.y = target_top - lines as ScrollOffset;
273 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
274 }
275 AutoscrollStrategy::BottomRelative(lines) => {
276 scroll_position.y = target_bottom + lines as ScrollOffset;
277 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
278 }
279 };
280
281 self.scroll_manager.last_autoscroll = Some((
282 self.scroll_manager.anchor.offset,
283 target_top,
284 target_bottom,
285 strategy,
286 ));
287
288 let was_scrolled = WasScrolled(editor_was_scrolled.0 || was_autoscrolled.0);
289 (NeedsHorizontalAutoscroll(true), was_scrolled)
290 }
291
292 pub(crate) fn autoscroll_horizontally(
293 &mut self,
294 start_row: DisplayRow,
295 viewport_width: Pixels,
296 scroll_width: Pixels,
297 em_advance: Pixels,
298 layouts: &[LineWithInvisibles],
299 autoscroll_request: Option<(Autoscroll, bool)>,
300 window: &mut Window,
301 cx: &mut Context<Self>,
302 ) -> Option<gpui::Point<ScrollOffset>> {
303 let (_, local) = autoscroll_request?;
304 let em_advance = ScrollOffset::from(em_advance);
305 let viewport_width = ScrollOffset::from(viewport_width);
306 let scroll_width = ScrollOffset::from(scroll_width);
307
308 let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx));
309 let selections = self.selections.all::<Point>(&display_map);
310 let mut scroll_position = self.scroll_manager.scroll_position(&display_map);
311
312 let mut target_left;
313 let mut target_right: f64;
314
315 if self
316 .highlighted_display_row_for_autoscroll(&display_map)
317 .is_none()
318 {
319 target_left = f64::INFINITY;
320 target_right = 0.;
321 for selection in selections {
322 let head = selection.head().to_display_point(&display_map);
323 if head.row() >= start_row
324 && head.row() < DisplayRow(start_row.0 + layouts.len() as u32)
325 {
326 let start_column = head.column();
327 let end_column = cmp::min(display_map.line_len(head.row()), head.column());
328 target_left = target_left.min(ScrollOffset::from(
329 layouts[head.row().minus(start_row) as usize]
330 .x_for_index(start_column as usize)
331 + self.gutter_dimensions.margin,
332 ));
333 target_right = target_right.max(
334 ScrollOffset::from(
335 layouts[head.row().minus(start_row) as usize]
336 .x_for_index(end_column as usize),
337 ) + em_advance,
338 );
339 }
340 }
341 } else {
342 target_left = 0.;
343 target_right = 0.;
344 }
345
346 target_right = target_right.min(scroll_width);
347
348 if target_right - target_left > viewport_width {
349 return None;
350 }
351
352 let scroll_left = self.scroll_manager.anchor.offset.x * em_advance;
353 let scroll_right = scroll_left + viewport_width;
354
355 let was_scrolled = if target_left < scroll_left {
356 scroll_position.x = target_left / em_advance;
357 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
358 } else if target_right > scroll_right {
359 scroll_position.x = (target_right - viewport_width) / em_advance;
360 self.set_scroll_position_internal(scroll_position, local, true, window, cx)
361 } else {
362 WasScrolled(false)
363 };
364
365 if was_scrolled.0 {
366 Some(scroll_position)
367 } else {
368 None
369 }
370 }
371
372 pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut Context<Self>) {
373 self.scroll_manager.autoscroll_request = Some((autoscroll, true));
374 cx.notify();
375 }
376
377 pub(crate) fn request_autoscroll_remotely(
378 &mut self,
379 autoscroll: Autoscroll,
380 cx: &mut Context<Self>,
381 ) {
382 self.scroll_manager.autoscroll_request = Some((autoscroll, false));
383 cx.notify();
384 }
385}