vim: Ensure paragraph motions use empty and not blank lines (#47734)
lex00
and
dino
created
The `}` and `{` paragraph motions now correctly treat only truly empty
lines (zero characters) as paragraph boundaries, matching vim's
documented behavior. Whitespace-only lines are no longer treated as
boundaries.
Changed `start_of_paragraph()` and `end_of_paragraph()` in
`editor/src/movement.rs` to check `line_len() == 0` instead of
`is_line_blank()`.
Note: This change does NOT affect the `ap`/`ip` text objects. Per vim's
`:help ap`, those DO treat whitespace-only lines as boundaries, which is
the existing (correct) behavior in `vim/src/object.rs`.
Closes #36171
Release Notes:
- Fixed vim mode paragraph motions (`}` and `{`) to correctly ignore
whitespace-only lines
---------
Co-authored-by: dino <dinojoaocosta@gmail.com>
@@ -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()
@@ -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;