diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8c2d363062..f72b352aa3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1525,6 +1525,11 @@ fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayP } } +/// Given a point, returns the start of the buffer row that is a given number of +/// buffer rows away from the current position. +/// +/// This moves by buffer rows instead of display rows, a distinction that is +/// important when soft wrapping is enabled. pub(crate) fn start_of_relative_buffer_row( map: &DisplaySnapshot, point: DisplayPoint, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index ea45446d9e..2185635b7a 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -15,7 +15,10 @@ use workspace::searchable::Direction; use crate::{ Vim, - motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line}, + motion::{ + Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line, + start_of_relative_buffer_row, + }, object::Object, state::{Mark, Mode, Operator}, }; @@ -399,12 +402,13 @@ impl Vim { if row == head.row() { break; } - if tail.row() > head.row() { - row.0 -= 1 - } else { - row.0 += 1 - } + + // Move to the next or previous buffer row, ensuring that + // wrapped lines are handled correctly. + let direction = if tail.row() > head.row() { -1 } else { 1 }; + row = start_of_relative_buffer_row(map, DisplayPoint::new(row, 0), direction).row(); } + s.select(selections); }) } @@ -1533,6 +1537,63 @@ mod test { }); } + #[gpui::test] + async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // Ensure that the editor is wrapping lines at 12 columns so that each + // of the lines ends up being wrapped. + cx.set_shared_wrap(12).await; + cx.set_shared_state(indoc! { + "ˇ12345678901234567890 + 12345678901234567890 + 12345678901234567890 + " + }) + .await; + cx.simulate_shared_keystrokes("ctrl-v j").await; + cx.shared_state().await.assert_eq(indoc! { + "«1ˇ»2345678901234567890 + «1ˇ»2345678901234567890 + 12345678901234567890 + " + }); + + // Test with lines taking up different amounts of display rows to ensure + // that, even in that case, only the buffer rows are taken into account. + cx.set_shared_state(indoc! { + "ˇ123456789012345678901234567890123456789012345678901234567890 + 1234567890123456789012345678901234567890 + 12345678901234567890 + " + }) + .await; + cx.simulate_shared_keystrokes("ctrl-v 2 j").await; + cx.shared_state().await.assert_eq(indoc! { + "«1ˇ»23456789012345678901234567890123456789012345678901234567890 + «1ˇ»234567890123456789012345678901234567890 + «1ˇ»2345678901234567890 + " + }); + + // Same scenario as above, but using the up motion to ensure that the + // result is the same. + cx.set_shared_state(indoc! { + "123456789012345678901234567890123456789012345678901234567890 + 1234567890123456789012345678901234567890 + ˇ12345678901234567890 + " + }) + .await; + cx.simulate_shared_keystrokes("ctrl-v 2 k").await; + cx.shared_state().await.assert_eq(indoc! { + "«1ˇ»23456789012345678901234567890123456789012345678901234567890 + «1ˇ»234567890123456789012345678901234567890 + «1ˇ»2345678901234567890 + " + }); + } + #[gpui::test] async fn test_visual_object(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_visual_block_wrapping_selection.json b/crates/vim/test_data/test_visual_block_wrapping_selection.json new file mode 100644 index 0000000000..bbe945cfef --- /dev/null +++ b/crates/vim/test_data/test_visual_block_wrapping_selection.json @@ -0,0 +1,16 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"ˇ12345678901234567890\n12345678901234567890\n12345678901234567890\n"}} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Get":{"state":"«1ˇ»2345678901234567890\n«1ˇ»2345678901234567890\n12345678901234567890\n","mode":"VisualBlock"}} +{"Put":{"state":"ˇ123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\n12345678901234567890\n"}} +{"Key":"ctrl-v"} +{"Key":"2"} +{"Key":"j"} +{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}} +{"Put":{"state":"123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\nˇ12345678901234567890\n"}} +{"Key":"ctrl-v"} +{"Key":"2"} +{"Key":"k"} +{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}}