diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index db6dfb0ace..a3cfc5a287 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -59,7 +59,7 @@ use convert_case::{Case, Casing}; use debounced_delay::DebouncedDelay; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; -pub use editor_settings::{CurrentLineHighlight, EditorSettings}; +pub use editor_settings::{CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine}; pub use editor_settings_controls::*; use element::LineWithInvisibles; pub use element::{ diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index fb13cc70ad..f47c131011 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -505,7 +505,7 @@ impl Editor { } if let Some(visible_lines) = self.visible_line_count() { - if newest_head.row() < DisplayRow(screen_top.row().0 + visible_lines as u32) { + if newest_head.row() <= DisplayRow(screen_top.row().0 + visible_lines as u32) { return Ordering::Equal; } } diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs index 6aceb06425..f89faa3748 100644 --- a/crates/vim/src/normal/scroll.rs +++ b/crates/vim/src/normal/scroll.rs @@ -91,6 +91,7 @@ fn scroll_editor( s.move_with(|map, selection| { let mut head = selection.head(); let top = top_anchor.to_display_point(map); + let starting_column = head.column(); let vertical_scroll_margin = (vertical_scroll_margin as u32).min(visible_line_count as u32 / 2); @@ -99,7 +100,7 @@ fn scroll_editor( let old_top = old_top_anchor.to_display_point(map); let new_row = if old_top.row() == top.row() { DisplayRow( - top.row() + head.row() .0 .saturating_add_signed(amount.lines(visible_line_count) as i32), ) @@ -108,25 +109,25 @@ fn scroll_editor( }; head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left) } + let min_row = if top.row().0 == 0 { DisplayRow(0) } else { DisplayRow(top.row().0 + vertical_scroll_margin) }; - let max_row = DisplayRow( - top.row().0 - + (visible_line_count as u32) - .saturating_sub(vertical_scroll_margin) - .saturating_sub(1), - ); + let max_row = DisplayRow(map.max_point().row().0.max(top.row().0.saturating_add( + (visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin), + ))); - let new_head = if head.row() < min_row { - map.clip_point(DisplayPoint::new(min_row, head.column()), Bias::Left) + let new_row = if head.row() < min_row { + min_row } else if head.row() > max_row { - map.clip_point(DisplayPoint::new(max_row, head.column()), Bias::Left) + max_row } else { - head + head.row() }; + let new_head = map.clip_point(DisplayPoint::new(new_row, starting_column), Bias::Left); + if selection.is_empty() { selection.collapse_to(new_head, selection.goal) } else { @@ -142,9 +143,24 @@ mod test { state::Mode, test::{NeovimBackedTestContext, VimTestContext}, }; + use editor::{EditorSettings, ScrollBeyondLastLine}; use gpui::{point, px, size, Context}; use indoc::indoc; use language::Point; + use settings::SettingsStore; + + pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { + let mut text = String::new(); + for row in 0..rows { + let c: char = (start_char as u32 + row as u32) as u8 as char; + let mut line = c.to_string().repeat(cols); + if row < rows - 1 { + line.push('\n'); + } + text += &line; + } + text + } #[gpui::test] async fn test_scroll(cx: &mut gpui::TestAppContext) { @@ -241,18 +257,6 @@ mod test { cx.set_scroll_height(10).await; - pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = (start_char as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text - } let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); cx.set_shared_state(&content).await; @@ -277,4 +281,33 @@ mod test { .await; cx.shared_state().await.assert_matches(); } + + #[gpui::test] + async fn test_scroll_beyond_last_line(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_scroll_height(10).await; + cx.neovim.set_option(&format!("scrolloff={}", 0)).await; + + let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); + cx.set_shared_state(&content).await; + + cx.update_global(|store: &mut SettingsStore, cx| { + store.update_user_settings::(cx, |s| { + s.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off) + }); + }); + + // ctrl-d can reach the end and the cursor stays in the first column + cx.simulate_shared_keystrokes("shift-g k").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("ctrl-d").await; + cx.shared_state().await.assert_matches(); + + // ctrl-u from the last line + cx.simulate_shared_keystrokes("shift-g").await; + cx.shared_state().await.assert_matches(); + cx.simulate_shared_keystrokes("ctrl-u").await; + cx.shared_state().await.assert_matches(); + } } diff --git a/crates/vim/test_data/test_scroll_beyond_last_line.json b/crates/vim/test_data/test_scroll_beyond_last_line.json new file mode 100644 index 0000000000..0f95059a52 --- /dev/null +++ b/crates/vim/test_data/test_scroll_beyond_last_line.json @@ -0,0 +1,13 @@ +{"SetOption":{"value":"scrolloff=3"}} +{"SetOption":{"value":"lines=12"}} +{"SetOption":{"value":"scrolloff=0"}} +{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} +{"Key":"shift-g"} +{"Key":"k"} +{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nˇyy\nzz","mode":"Normal"}} +{"Key":"ctrl-d"} +{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} +{"Key":"shift-g"} +{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} +{"Key":"ctrl-u"} +{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nˇuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/docs/src/vim.md b/docs/src/vim.md index 15a70a4dc1..519c0d4a26 100644 --- a/docs/src/vim.md +++ b/docs/src/vim.md @@ -258,6 +258,8 @@ There are also a few Zed settings that you may also enjoy if you use vim mode: "relative_line_numbers": true, // hide the scroll bar "scrollbar": { "show": "never" }, + // prevent the buffer from scrolling beyond the last line + "scroll_beyond_last_line": "off", // allow cursor to reach edges of screen "vertical_scroll_margin": 0, "gutter": {