diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 0bdf1aae32..26ccfa8920 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -341,6 +341,9 @@ "shift-s": "vim::SubstituteLine", "> >": "editor::Indent", "< <": "editor::Outdent", + "shift-h": "vim::WindowTop", + "shift-m": "vim::WindowMiddle", + "shift-l": "vim::WindowBottom", "ctrl-pagedown": "pane::ActivateNextItem", "ctrl-pageup": "pane::ActivatePrevItem" } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6d81f3e37c..787fb4590f 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -586,6 +586,8 @@ impl DisplaySnapshot { text_system, editor_style, rem_size, + anchor: _, + visible_rows: _, }: &TextLayoutDetails, ) -> Arc { let mut runs = Vec::new(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bc7061524e..6b7eee4c10 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3052,6 +3052,8 @@ impl Editor { text_system: cx.text_system().clone(), editor_style: self.style.clone().unwrap(), rem_size: cx.rem_size(), + anchor: self.scroll_manager.anchor().anchor, + visible_rows: self.visible_line_count(), } } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 71c2cceac1..f268e926c7 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -8,6 +8,8 @@ use language::Point; use std::{ops::Range, sync::Arc}; +use multi_buffer::Anchor; + /// Defines search strategy for items in `movement` module. /// `FindRange::SingeLine` only looks for a match on a single line at a time, whereas /// `FindRange::MultiLine` keeps going until the end of a string. @@ -23,6 +25,8 @@ pub struct TextLayoutDetails { pub(crate) text_system: Arc, pub(crate) editor_style: EditorStyle, pub(crate) rem_size: Pixels, + pub anchor: Anchor, + pub visible_rows: Option, } /// Returns a column to the left of the current point, wrapping diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d7f1456bd6..993e845e24 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -299,7 +299,7 @@ impl ScrollManager { } impl Editor { - pub fn vertical_scroll_margin(&mut self) -> usize { + pub fn vertical_scroll_margin(&self) -> usize { self.scroll_manager.vertical_scroll_margin as usize } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index d7a46888cc..b5c090683a 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -41,6 +41,9 @@ pub enum Motion { StartOfLineDownward, EndOfLineDownward, GoToColumn, + WindowTop, + WindowMiddle, + WindowBottom, } #[derive(Clone, Deserialize, PartialEq)] @@ -136,6 +139,9 @@ actions!( StartOfLineDownward, EndOfLineDownward, GoToColumn, + WindowTop, + WindowMiddle, + WindowBottom, ] ); @@ -231,6 +237,13 @@ pub fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|_: &mut Workspace, action: &RepeatFind, cx: _| { repeat_motion(action.backwards, cx) }); + workspace.register_action(|_: &mut Workspace, &WindowTop, cx: _| motion(Motion::WindowTop, cx)); + workspace.register_action(|_: &mut Workspace, &WindowMiddle, cx: _| { + motion(Motion::WindowMiddle, cx) + }); + workspace.register_action(|_: &mut Workspace, &WindowBottom, cx: _| { + motion(Motion::WindowBottom, cx) + }); } pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { @@ -295,6 +308,9 @@ impl Motion { | NextLineStart | StartOfLineDownward | StartOfParagraph + | WindowTop + | WindowMiddle + | WindowBottom | EndOfParagraph => true, EndOfLine { .. } | NextWordEnd { .. } @@ -336,6 +352,9 @@ impl Motion { | PreviousWordStart { .. } | FirstNonWhitespace { .. } | FindBackward { .. } + | WindowTop + | WindowMiddle + | WindowBottom | NextLineStart => false, } } @@ -353,6 +372,9 @@ impl Motion { | NextWordEnd { .. } | Matching | FindForward { .. } + | WindowTop + | WindowMiddle + | WindowBottom | NextLineStart => true, Left | Backspace @@ -449,6 +471,9 @@ impl Motion { StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), EndOfLineDownward => (next_line_end(map, point, times), SelectionGoal::None), GoToColumn => (go_to_column(map, point, times), SelectionGoal::None), + WindowTop => window_top(map, point, &text_layout_details), + WindowMiddle => window_middle(map, point, &text_layout_details), + WindowBottom => window_bottom(map, point, &text_layout_details), }; (new_point != point || infallible).then_some((new_point, goal)) @@ -955,6 +980,51 @@ pub(crate) fn next_line_end( end_of_line(map, false, point) } +fn window_top( + map: &DisplaySnapshot, + point: DisplayPoint, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + let first_visible_line = text_layout_details.anchor.to_display_point(map); + let new_col = point.column().min(map.line_len(first_visible_line.row())); + let new_point = DisplayPoint::new(first_visible_line.row(), new_col); + (map.clip_point(new_point, Bias::Left), SelectionGoal::None) +} + +fn window_middle( + map: &DisplaySnapshot, + point: DisplayPoint, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + if let Some(visible_rows) = text_layout_details.visible_rows { + let first_visible_line = text_layout_details.anchor.to_display_point(map); + let max_rows = (visible_rows as u32).min(map.max_buffer_row()); + let new_row = first_visible_line.row() + (max_rows.div_euclid(2)); + let new_col = point.column().min(map.line_len(new_row)); + let new_point = DisplayPoint::new(new_row, new_col); + (map.clip_point(new_point, Bias::Left), SelectionGoal::None) + } else { + (point, SelectionGoal::None) + } +} + +fn window_bottom( + map: &DisplaySnapshot, + point: DisplayPoint, + text_layout_details: &TextLayoutDetails, +) -> (DisplayPoint, SelectionGoal) { + if let Some(visible_rows) = text_layout_details.visible_rows { + let first_visible_line = text_layout_details.anchor.to_display_point(map); + let bottom_row = first_visible_line.row() + (visible_rows) as u32; + let bottom_row_capped = bottom_row.min(map.max_buffer_row()); + let new_col = point.column().min(map.line_len(bottom_row_capped)); + let new_point = DisplayPoint::new(bottom_row_capped, new_col); + (map.clip_point(new_point, Bias::Left), SelectionGoal::None) + } else { + (point, SelectionGoal::None) + } +} + #[cfg(test)] mod test { @@ -1107,4 +1177,218 @@ mod test { cx.simulate_shared_keystrokes(["enter"]).await; cx.assert_shared_state("one\n ˇtwo\nthree").await; } + + #[gpui::test] + async fn test_window_top(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + let initial_state = indoc! {r"abc + def + paragraph + the second + third ˇand + final"}; + + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["shift-h"]).await; + cx.assert_shared_state(indoc! {r"abˇc + def + paragraph + the second + third and + final"}) + .await; + + // clip point + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 ˇ9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-h"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 ˇ3 + 4 5 6 + 7 8 9 + "}) + .await; + + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + ˇ7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-h"]).await; + cx.assert_shared_state(indoc! {r" + ˇ1 2 3 + 4 5 6 + 7 8 9 + "}) + .await; + } + + #[gpui::test] + async fn test_window_middle(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + let initial_state = indoc! {r"abˇc + def + paragraph + the second + third and + final"}; + + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r"abc + def + paˇragraph + the second + third and + final"}) + .await; + + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 ˇ9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 ˇ6 + 7 8 9 + "}) + .await; + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + ˇ7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + ˇ4 5 6 + 7 8 9 + "}) + .await; + cx.set_shared_state(indoc! {r" + ˇ1 2 3 + 4 5 6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + ˇ4 5 6 + 7 8 9 + "}) + .await; + cx.set_shared_state(indoc! {r" + 1 2 3 + ˇ4 5 6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + ˇ4 5 6 + 7 8 9 + "}) + .await; + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 ˇ6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-m"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 ˇ6 + 7 8 9 + "}) + .await; + } + + #[gpui::test] + async fn test_window_bottom(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + let initial_state = indoc! {r"abc + deˇf + paragraph + the second + third and + final"}; + + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["shift-l"]).await; + cx.assert_shared_state(indoc! {r"abc + def + paragraph + the second + third and + fiˇnal"}) + .await; + + cx.set_shared_state(indoc! {r" + 1 2 3 + 4 5 ˇ6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-l"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 9 + ˇ"}) + .await; + + cx.set_shared_state(indoc! {r" + 1 2 3 + ˇ4 5 6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-l"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 9 + ˇ"}) + .await; + + cx.set_shared_state(indoc! {r" + 1 2 ˇ3 + 4 5 6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-l"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 9 + ˇ"}) + .await; + + cx.set_shared_state(indoc! {r" + ˇ1 2 3 + 4 5 6 + 7 8 9 + "}) + .await; + cx.simulate_shared_keystrokes(["shift-l"]).await; + cx.assert_shared_state(indoc! {r" + 1 2 3 + 4 5 6 + 7 8 9 + ˇ"}) + .await; + } } diff --git a/crates/vim/test_data/test_window_bottom.json b/crates/vim/test_data/test_window_bottom.json new file mode 100644 index 0000000000..50400883f2 --- /dev/null +++ b/crates/vim/test_data/test_window_bottom.json @@ -0,0 +1,15 @@ +{"Put":{"state":"abc\ndeˇf\nparagraph\nthe second\nthird and\nfinal"}} +{"Key":"shift-l"} +{"Get":{"state":"abc\ndef\nparagraph\nthe second\nthird and\nfiˇnal","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} +{"Key":"shift-l"} +{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} +{"Put":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n"}} +{"Key":"shift-l"} +{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} +{"Put":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n"}} +{"Key":"shift-l"} +{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} +{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} +{"Key":"shift-l"} +{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_middle.json b/crates/vim/test_data/test_window_middle.json new file mode 100644 index 0000000000..91154923e4 --- /dev/null +++ b/crates/vim/test_data/test_window_middle.json @@ -0,0 +1,17 @@ +{"Put":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal"}} +{"Key":"shift-m"} +{"Get":{"state":"abc\ndef\npaˇragraph\nthe second\nthird and\nfinal","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} +{"Key":"shift-m"} +{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} +{"Key":"shift-m"} +{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} +{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} +{"Key":"shift-m"} +{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} +{"Key":"shift-m"} +{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} +{"Key":"shift-m"} +{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_top.json b/crates/vim/test_data/test_window_top.json new file mode 100644 index 0000000000..607a26c110 --- /dev/null +++ b/crates/vim/test_data/test_window_top.json @@ -0,0 +1,9 @@ +{"Put":{"state":"abc\ndef\nparagraph\nthe second\nthird ˇand\nfinal"}} +{"Key":"shift-h"} +{"Get":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} +{"Key":"shift-h"} +{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}} +{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} +{"Key":"shift-h"} +{"Get":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n","mode":"Normal"}}