From 8441aa49b23b4c09dba32819d5271d001698f656 Mon Sep 17 00:00:00 2001 From: Dino Date: Fri, 3 Oct 2025 14:58:34 +0100 Subject: [PATCH] vim: Fix visual block handling of wrapped lines (#39355) These changes fix an issue with vim's visual block mode when soft wrapping is enabled. In this situation, if one was to move the cursor either up or down, the selection would be updated to include visual (wrapped) rows, instead of only the buffer rows. For example, take the following contents: ``` 1 | And here's a very long line that is wrapping at this exact point. 2 | And another very long line that is will also wrap at this exact point. ``` If one was to place the cursor at the start of the first line, character `A`, trigger visual block mode with `ctrl-v` and then move down one line with `j`, the selection would end up as (with [X] representing the selected characters): ``` 1 | [A]nd here's a very long line that is wrapping [a]t this exact point. 2 | [A]nd another very long line that is will also wrap at this exact point. ``` Instead of the expected: ``` 1 | [A]nd here's a very long line that is wrapping at this exact point. 2 | [A]nd another very long line that is will also wrap at this exact point. ``` With the changes in this commit, `Vim.visual_block_motion` will now leverage buffer rows in order to navigate to the next or previous row. Release Notes: - Fixed handling of soft wrapped lines in vim's visual block mode --- crates/vim/src/motion.rs | 5 ++ crates/vim/src/visual.rs | 73 +++++++++++++++++-- .../test_visual_block_wrapping_selection.json | 16 ++++ 3 files changed, 88 insertions(+), 6 deletions(-) create mode 100644 crates/vim/test_data/test_visual_block_wrapping_selection.json diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8c2d3630622537083e15942a788caac0bed53ae7..f72b352aa32b3c2f5131df7e28137073453e5c9e 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 ea45446d9e0b0ab63891939071eed6b24ef87947..2185635b7ae1fcab3d44769c27a799f28bd805f3 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 0000000000000000000000000000000000000000..bbe945cfef76737df06410e70f8a5215ec6f01c5 --- /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"}}