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