diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index abd983b96b1251b0ab08db1950efdaeef834f690..498ea4e278cc6b07652af4da73b47e0c4b116dfe 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -523,7 +523,7 @@ fn is_subword_boundary_end(left: char, right: char, classifier: &CharClassifier) } /// Returns a position of the start of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines. +/// is defined as a run of non-empty lines. pub fn start_of_paragraph( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -534,25 +534,25 @@ pub fn start_of_paragraph( return DisplayPoint::zero(); } - let mut found_non_blank_line = false; + let mut found_non_empty_line = false; for row in (0..point.row + 1).rev() { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if found_non_blank_line && blank { + let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0; + if found_non_empty_line && empty { if count <= 1 { return Point::new(row, 0).to_display_point(map); } count -= 1; - found_non_blank_line = false; + found_non_empty_line = false; } - found_non_blank_line |= !blank; + found_non_empty_line |= !empty; } DisplayPoint::zero() } /// Returns a position of the end of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines. +/// is defined as a run of non-empty lines. pub fn end_of_paragraph( map: &DisplaySnapshot, display_point: DisplayPoint, @@ -563,18 +563,18 @@ pub fn end_of_paragraph( return map.max_point(); } - let mut found_non_blank_line = false; + let mut found_non_empty_line = false; for row in point.row..=map.buffer_snapshot().max_row().0 { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if found_non_blank_line && blank { + let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0; + if found_non_empty_line && empty { if count <= 1 { return Point::new(row, 0).to_display_point(map); } count -= 1; - found_non_blank_line = false; + found_non_empty_line = false; } - found_non_blank_line |= !blank; + found_non_empty_line |= !empty; } map.max_point() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 92ed6fee39ccafdbcc24a099eeffb4182e05b18d..837bca2ab11b36c2a48eda76350156c2f8a141fe 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -3292,6 +3292,29 @@ mod test { final"}); } + #[gpui::test] + async fn test_paragraph_motion_with_whitespace_lines(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + // Test that whitespace-only lines are NOT treated as paragraph boundaries + // Per vim's :help paragraph - only truly empty lines are boundaries + // Line 2 has 4 spaces (whitespace-only), line 4 is truly empty + cx.set_shared_state("ˇfirst\n \nstill first\n\nsecond") + .await; + cx.simulate_shared_keystrokes("}").await; + + // Should skip whitespace-only line and stop at truly empty line + let mut shared_state = cx.shared_state().await; + shared_state.assert_eq("first\n \nstill first\nˇ\nsecond"); + shared_state.assert_matches(); + + // Should go back to original position + cx.simulate_shared_keystrokes("{").await; + let mut shared_state = cx.shared_state().await; + shared_state.assert_eq("ˇfirst\n \nstill first\n\nsecond"); + shared_state.assert_matches(); + } + #[gpui::test] async fn test_matching(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/test_data/test_paragraph_motion_with_whitespace_lines.json b/crates/vim/test_data/test_paragraph_motion_with_whitespace_lines.json new file mode 100644 index 0000000000000000000000000000000000000000..638d413343ef998e3855a6740a9427e89f0833cd --- /dev/null +++ b/crates/vim/test_data/test_paragraph_motion_with_whitespace_lines.json @@ -0,0 +1,5 @@ +{"Put":{"state":"ˇfirst\n \nstill first\n\nsecond"}} +{"Key":"}"} +{"Get":{"state":"first\n \nstill first\nˇ\nsecond","mode":"Normal"}} +{"Key":"{"} +{"Get":{"state":"ˇfirst\n \nstill first\n\nsecond","mode":"Normal"}}