diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index fe0613eac3..4e82311165 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -258,7 +258,7 @@ "u": "vim::ConvertToLowerCase", "shift-u": "vim::ConvertToUpperCase", "shift-o": "vim::OtherEnd", - "o": "vim::OtherEnd", + "o": "vim::OtherEndRowAware", "d": "vim::VisualDelete", "x": "vim::VisualDelete", "shift-d": "vim::VisualDeleteLine", diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index 7896322e1b..9a2d800b3a 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -655,6 +655,25 @@ impl<'a> MutableSelectionsCollection<'a> { .collect(); self.select(selections); } + pub fn reverse_selections(&mut self) { + let map = &self.display_map(); + let mut new_selections: Vec> = Vec::new(); + let disjoint = self.disjoint.clone(); + for selection in disjoint + .iter() + .sorted_by(|first, second| Ord::cmp(&second.id, &first.id)) + .collect::>>() + { + new_selections.push(Selection { + id: self.new_selection_id(), + start: selection.start.to_display_point(map).to_point(map), + end: selection.end.to_display_point(map).to_point(map), + reversed: selection.reversed, + goal: selection.goal, + }); + } + self.select(new_selections); + } pub fn move_with( &mut self, diff --git a/crates/vim/README.md b/crates/vim/README.md index 67e97a4a0b..28f1375c62 100644 --- a/crates/vim/README.md +++ b/crates/vim/README.md @@ -28,7 +28,7 @@ but while developing this test you'll need to run it with the neovim flag enable cargo test -p vim --features neovim test_visual_star_hash ``` -This will run your keystrokes against a headless neovim and cache the results in the test_data directory. +This will run your keystrokes against a headless neovim and cache the results in the test_data directory. Note that neovim must be installed and reachable on your $PATH in order to run the feature. ## Testing zed-only behavior diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 5385d8a76a..d7719960e0 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -32,6 +32,7 @@ actions!( VisualYank, VisualYankLine, OtherEnd, + OtherEndRowAware, SelectNext, SelectPrevious, SelectNextMatch, @@ -55,6 +56,7 @@ pub fn register(editor: &mut Editor, cx: &mut Context) { vim.toggle_mode(Mode::VisualBlock, window, cx) }); Vim::action(editor, cx, Vim::other_end); + Vim::action(editor, cx, Vim::other_end_row_aware); Vim::action(editor, cx, Vim::visual_insert_end_of_line); Vim::action(editor, cx, Vim::visual_insert_first_non_white_space); Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| { @@ -265,7 +267,16 @@ impl Vim { head = movement::saturating_left(map, head); } - let Some((new_head, _)) = move_selection(map, head, goal) else { + let reverse_aware_goal = if was_reversed { + SelectionGoal::HorizontalRange { + start: end, + end: start, + } + } else { + goal + }; + + let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else { return; }; head = new_head; @@ -321,7 +332,9 @@ impl Vim { id: s.new_selection_id(), start: start.to_point(map), end: end.to_point(map), - reversed: is_reversed, + reversed: is_reversed && + // For neovim parity: cursor is not reversed when column is a single character + end.column() - start.column() > 1, goal, }; @@ -336,7 +349,6 @@ impl Vim { row.0 += 1 } } - s.select(selections); }) } @@ -462,7 +474,26 @@ impl Vim { editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { s.move_with(|_, selection| { selection.reversed = !selection.reversed; - }) + }); + }) + }); + } + + pub fn other_end_row_aware( + &mut self, + _: &OtherEndRowAware, + window: &mut Window, + cx: &mut Context, + ) { + let mode = self.mode; + self.update_editor(window, cx, |_, editor, window, cx| { + editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { + s.move_with(|_, selection| { + selection.reversed = !selection.reversed; + }); + if mode == Mode::VisualBlock { + s.reverse_selections(); + } }) }); } @@ -1214,6 +1245,75 @@ mod test { " }); } + #[gpui::test] + async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await; + cx.shared_state().await.assert_eq(indoc! {" + The «quick ˇ»brown + fox «jumps ˇ»over + the lazy dog"}); + } + + #[gpui::test] + async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + The quick brown + fox jumpsˇ over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await; + cx.shared_state().await.assert_eq(indoc! {" + The «ˇquick »brown + fox «ˇjumps »over + the lazy dog"}); + } + + #[gpui::test] + async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + The quick brown + fox jˇumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; + cx.shared_state().await.assert_eq(indoc! {" + The quick brown + fox j«umps ˇ»over + the l«azy dˇ»og"}); + cx.simulate_shared_keystrokes("o k").await; + cx.shared_state().await.assert_eq(indoc! {" + The q«ˇuick »brown + fox j«ˇumps »over + the l«ˇazy d»og"}); + } + + #[gpui::test] + async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_shared_state(indoc! {" + The quick brown + fox jˇumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; + cx.shared_state().await.assert_eq(indoc! {" + The quick brown + fox j«umps ˇ»over + the l«azy dˇ»og"}); + cx.simulate_shared_keystrokes("shift-o k").await; + cx.shared_state().await.assert_eq(indoc! {" + The quick brown + fox j«ˇumps »over + the lazy dog"}); + } #[gpui::test] async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { diff --git a/crates/vim/test_data/test_visual_block_mode_down_right.json b/crates/vim/test_data/test_visual_block_mode_down_right.json new file mode 100644 index 0000000000..fbb0558a89 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode_down_right.json @@ -0,0 +1,9 @@ +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"j"} +{"Get":{"state":"The «quick ˇ»brown\nfox «jumps ˇ»over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_other_end.json b/crates/vim/test_data/test_visual_block_mode_other_end.json new file mode 100644 index 0000000000..bf7956f0e8 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode_other_end.json @@ -0,0 +1,11 @@ +{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"j"} +{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} +{"Key":"o"} +{"Key":"k"} +{"Get":{"state":"The q«ˇuick »brown\nfox j«ˇumps »over\nthe l«ˇazy d»og","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_shift_other_end.json b/crates/vim/test_data/test_visual_block_mode_shift_other_end.json new file mode 100644 index 0000000000..03882167b4 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode_shift_other_end.json @@ -0,0 +1,11 @@ +{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"l"} +{"Key":"j"} +{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} +{"Key":"shift-o"} +{"Key":"k"} +{"Get":{"state":"The quick brown\nfox j«ˇumps »over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_up_left.json b/crates/vim/test_data/test_visual_block_mode_up_left.json new file mode 100644 index 0000000000..00bbd577c3 --- /dev/null +++ b/crates/vim/test_data/test_visual_block_mode_up_left.json @@ -0,0 +1,9 @@ +{"Put":{"state":"The quick brown\nfox jumpsˇ over\nthe lazy dog"}} +{"Key":"ctrl-v"} +{"Key":"h"} +{"Key":"h"} +{"Key":"h"} +{"Key":"h"} +{"Key":"h"} +{"Key":"k"} +{"Get":{"state":"The «ˇquick »brown\nfox «ˇjumps »over\nthe lazy dog","mode":"VisualBlock"}}