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