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