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