scroll.rs

  1use crate::Vim;
  2use editor::{
  3    display_map::{DisplayRow, ToDisplayPoint},
  4    scroll::ScrollAmount,
  5    DisplayPoint, Editor, EditorSettings,
  6};
  7use gpui::{actions, ViewContext};
  8use language::Bias;
  9use settings::Settings;
 10use workspace::Workspace;
 11
 12actions!(
 13    vim,
 14    [LineUp, LineDown, ScrollUp, ScrollDown, PageUp, PageDown]
 15);
 16
 17pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
 18    workspace.register_action(|_: &mut Workspace, _: &LineDown, cx| {
 19        scroll(cx, false, |c| ScrollAmount::Line(c.unwrap_or(1.)))
 20    });
 21    workspace.register_action(|_: &mut Workspace, _: &LineUp, cx| {
 22        scroll(cx, false, |c| ScrollAmount::Line(-c.unwrap_or(1.)))
 23    });
 24    workspace.register_action(|_: &mut Workspace, _: &PageDown, cx| {
 25        scroll(cx, false, |c| ScrollAmount::Page(c.unwrap_or(1.)))
 26    });
 27    workspace.register_action(|_: &mut Workspace, _: &PageUp, cx| {
 28        scroll(cx, false, |c| ScrollAmount::Page(-c.unwrap_or(1.)))
 29    });
 30    workspace.register_action(|_: &mut Workspace, _: &ScrollDown, cx| {
 31        scroll(cx, true, |c| {
 32            if let Some(c) = c {
 33                ScrollAmount::Line(c)
 34            } else {
 35                ScrollAmount::Page(0.5)
 36            }
 37        })
 38    });
 39    workspace.register_action(|_: &mut Workspace, _: &ScrollUp, cx| {
 40        scroll(cx, true, |c| {
 41            if let Some(c) = c {
 42                ScrollAmount::Line(-c)
 43            } else {
 44                ScrollAmount::Page(-0.5)
 45            }
 46        })
 47    });
 48}
 49
 50fn scroll(
 51    cx: &mut ViewContext<Workspace>,
 52    move_cursor: bool,
 53    by: fn(c: Option<f32>) -> ScrollAmount,
 54) {
 55    Vim::update(cx, |vim, cx| {
 56        let amount = by(vim.take_count(cx).map(|c| c as f32));
 57        vim.update_active_editor(cx, |_, editor, cx| {
 58            scroll_editor(editor, move_cursor, &amount, cx)
 59        });
 60    })
 61}
 62
 63fn scroll_editor(
 64    editor: &mut Editor,
 65    preserve_cursor_position: bool,
 66    amount: &ScrollAmount,
 67    cx: &mut ViewContext<Editor>,
 68) {
 69    let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq();
 70    let old_top_anchor = editor.scroll_manager.anchor().anchor;
 71
 72    if editor.scroll_hover(amount, cx) {
 73        return;
 74    }
 75
 76    editor.scroll_screen(amount, cx);
 77    if should_move_cursor {
 78        let visible_rows = if let Some(visible_rows) = editor.visible_line_count() {
 79            visible_rows as u32
 80        } else {
 81            return;
 82        };
 83
 84        let top_anchor = editor.scroll_manager.anchor().anchor;
 85        let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin;
 86
 87        editor.change_selections(None, cx, |s| {
 88            s.move_with(|map, selection| {
 89                let mut head = selection.head();
 90                let top = top_anchor.to_display_point(map);
 91
 92                if preserve_cursor_position {
 93                    let old_top = old_top_anchor.to_display_point(map);
 94                    let new_row =
 95                        DisplayRow(top.row().0 + selection.head().row().0 - old_top.row().0);
 96                    head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left)
 97                }
 98                let min_row = DisplayRow(top.row().0 + vertical_scroll_margin as u32);
 99                let max_row =
100                    DisplayRow(top.row().0 + visible_rows - vertical_scroll_margin as u32 - 1);
101
102                let new_head = if head.row() < min_row {
103                    map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left)
104                } else if head.row() > max_row {
105                    map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left)
106                } else {
107                    head
108                };
109                if selection.is_empty() {
110                    selection.collapse_to(new_head, selection.goal)
111                } else {
112                    selection.set_head(new_head, selection.goal)
113                };
114            })
115        });
116    }
117}
118
119#[cfg(test)]
120mod test {
121    use crate::{
122        state::Mode,
123        test::{NeovimBackedTestContext, VimTestContext},
124    };
125    use gpui::{point, px, size, Context};
126    use indoc::indoc;
127    use language::Point;
128
129    #[gpui::test]
130    async fn test_scroll(cx: &mut gpui::TestAppContext) {
131        let mut cx = VimTestContext::new(cx, true).await;
132
133        let (line_height, visible_line_count) = cx.editor(|editor, cx| {
134            (
135                editor
136                    .style()
137                    .unwrap()
138                    .text
139                    .line_height_in_pixels(cx.rem_size()),
140                editor.visible_line_count().unwrap(),
141            )
142        });
143
144        let window = cx.window;
145        let margin = cx
146            .update_window(window, |_, cx| {
147                cx.viewport_size().height - line_height * visible_line_count
148            })
149            .unwrap();
150        cx.simulate_window_resize(
151            cx.window,
152            size(px(1000.), margin + 8. * line_height - px(1.0)),
153        );
154
155        cx.set_state(
156            indoc!(
157                "Λ‡one
158                two
159                three
160                four
161                five
162                six
163                seven
164                eight
165                nine
166                ten
167                eleven
168                twelve
169            "
170            ),
171            Mode::Normal,
172        );
173
174        cx.update_editor(|editor, cx| {
175            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
176        });
177        cx.simulate_keystrokes("ctrl-e");
178        cx.update_editor(|editor, cx| {
179            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 1.))
180        });
181        cx.simulate_keystrokes("2 ctrl-e");
182        cx.update_editor(|editor, cx| {
183            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.))
184        });
185        cx.simulate_keystrokes("ctrl-y");
186        cx.update_editor(|editor, cx| {
187            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 2.))
188        });
189
190        // does not select in normal mode
191        cx.simulate_keystrokes("g g");
192        cx.update_editor(|editor, cx| {
193            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
194        });
195        cx.simulate_keystrokes("ctrl-d");
196        cx.update_editor(|editor, cx| {
197            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
198            assert_eq!(
199                editor.selections.newest(cx).range(),
200                Point::new(6, 0)..Point::new(6, 0)
201            )
202        });
203
204        // does select in visual mode
205        cx.simulate_keystrokes("g g");
206        cx.update_editor(|editor, cx| {
207            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 0.))
208        });
209        cx.simulate_keystrokes("v ctrl-d");
210        cx.update_editor(|editor, cx| {
211            assert_eq!(editor.snapshot(cx).scroll_position(), point(0., 3.0));
212            assert_eq!(
213                editor.selections.newest(cx).range(),
214                Point::new(0, 0)..Point::new(6, 1)
215            )
216        });
217    }
218    #[gpui::test]
219    async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) {
220        let mut cx = NeovimBackedTestContext::new(cx).await;
221
222        cx.set_scroll_height(10).await;
223
224        pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String {
225            let mut text = String::new();
226            for row in 0..rows {
227                let c: char = (start_char as u32 + row as u32) as u8 as char;
228                let mut line = c.to_string().repeat(cols);
229                if row < rows - 1 {
230                    line.push('\n');
231                }
232                text += &line;
233            }
234            text
235        }
236        let content = "Λ‡".to_owned() + &sample_text(26, 2, 'a');
237        cx.set_shared_state(&content).await;
238
239        // skip over the scrolloff at the top
240        // test ctrl-d
241        cx.simulate_shared_keystrokes("4 j ctrl-d").await;
242        cx.shared_state().await.assert_matches();
243        cx.simulate_shared_keystrokes("ctrl-d").await;
244        cx.shared_state().await.assert_matches();
245        cx.simulate_shared_keystrokes("g g ctrl-d").await;
246        cx.shared_state().await.assert_matches();
247
248        // test ctrl-u
249        cx.simulate_shared_keystrokes("ctrl-u").await;
250        cx.shared_state().await.assert_matches();
251        cx.simulate_shared_keystrokes("ctrl-d ctrl-d 4 j ctrl-u ctrl-u")
252            .await;
253        cx.shared_state().await.assert_matches();
254    }
255}