vim: Apply strict paragraph motion only in vim mode (#48024)

Reverts the editor's paragraph navigation behavior that was changed in
#47734. Whitespace-only lines are now treated as paragraph boundaries
again for non-vim mode users.

Vim mode retains its own implementation where only truly empty lines
are paragraph boundaries.

Release Notes:

- Fixed editor paragraph navigation to treat whitespace-only lines as
paragraph boundaries again

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: dino <dinojoaocosta@gmail.com>
This commit is contained in:
authored and GitHub committed 2026-02-17 11:40:50 +00:00
1 parent 060715704d
commit 4eb6eebf3a
3 files changed
+86 -93

No files matched your search

+10 -76
View File
@@ -2243,107 +2243,41 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext)
});
cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
cx.set_state(
&r#"ˇone
two
three
fourˇ
five
six"#
.unindent(),
);
// The third line only contains a single space so we can later assert that the
// editor's paragraph movement considers a non-blank line as a paragraph
// boundary.
cx.set_state(&"ˇone\ntwo\n \nthree\nfourˇ\nfive\n\nsix");
cx.update_editor(|editor, window, cx| {
editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
ˇ
six"#
.unindent(),
);
cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\nˇ\nsix");
cx.update_editor(|editor, window, cx| {
editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
sixˇ"#
.unindent(),
);
cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsixˇ");
cx.update_editor(|editor, window, cx| {
editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"one
two
three
four
five
sixˇ"#
.unindent(),
);
cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\n\nsixˇ");
cx.update_editor(|editor, window, cx| {
editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"one
two
three
four
five
ˇ
six"#
.unindent(),
);
cx.assert_editor_state(&"one\ntwo\n \nthree\nfour\nfive\nˇ\nsix");
cx.update_editor(|editor, window, cx| {
editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"one
two
ˇ
three
four
five
six"#
.unindent(),
);
cx.assert_editor_state(&"one\ntwo\nˇ \nthree\nfour\nfive\n\nsix");
cx.update_editor(|editor, window, cx| {
editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
});
cx.assert_editor_state(
&r#"ˇone
two
three
four
five
six"#
.unindent(),
);
cx.assert_editor_state(&"ˇone\ntwo\n \nthree\nfour\nfive\n\nsix");
}
#[gpui::test]
+12 -12
View File
@@ -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-empty lines.
/// is defined as a run of non-blank 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_empty_line = false;
let mut found_non_blank_line = false;
for row in (0..point.row + 1).rev() {
let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
if found_non_empty_line && empty {
let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_empty_line = false;
found_non_blank_line = false;
}
found_non_empty_line |= !empty;
found_non_blank_line |= !blank;
}
DisplayPoint::zero()
}
/// Returns a position of the end of the current paragraph, where a paragraph
/// is defined as a run of non-empty lines.
/// is defined as a run of non-blank 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_empty_line = false;
let mut found_non_blank_line = false;
for row in point.row..=map.buffer_snapshot().max_row().0 {
let empty = map.buffer_snapshot().line_len(MultiBufferRow(row)) == 0;
if found_non_empty_line && empty {
let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row));
if found_non_blank_line && blank {
if count <= 1 {
return Point::new(row, 0).to_display_point(map);
}
count -= 1;
found_non_empty_line = false;
found_non_blank_line = false;
}
found_non_empty_line |= !empty;
found_non_blank_line |= !blank;
}
map.max_point()
+64 -5
View File
@@ -1038,12 +1038,9 @@ impl Motion {
),
SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None),
SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None),
StartOfParagraph => (
movement::start_of_paragraph(map, point, times),
SelectionGoal::None,
),
StartOfParagraph => (start_of_paragraph(map, point, times), SelectionGoal::None),
EndOfParagraph => (
map.clip_at_line_end(movement::end_of_paragraph(map, point, times)),
map.clip_at_line_end(end_of_paragraph(map, point, times)),
SelectionGoal::None,
),
CurrentLine => (next_line_end(map, point, times), SelectionGoal::None),
@@ -2238,6 +2235,68 @@ pub(crate) fn sentence_forwards(
map.max_point()
}
/// Returns a position of the start of the current paragraph for vim motions,
/// where a paragraph is defined as a run of non-empty lines. Lines containing
/// only whitespace are not considered empty and do not act as paragraph
/// boundaries.
pub(crate) fn start_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == 0 {
return DisplayPoint::zero();
}
let mut found_non_empty_line = false;
for row in (0..point.row + 1).rev() {
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_empty_line = false;
}
found_non_empty_line |= !empty;
}
DisplayPoint::zero()
}
/// Returns a position of the end of the current paragraph for vim motions,
/// where a paragraph is defined as a run of non-empty lines. Lines containing
/// only whitespace are not considered empty and do not act as paragraph
/// boundaries.
pub(crate) fn end_of_paragraph(
map: &DisplaySnapshot,
display_point: DisplayPoint,
mut count: usize,
) -> DisplayPoint {
let point = display_point.to_point(map);
if point.row == map.buffer_snapshot().max_row().0 {
return map.max_point();
}
let mut found_non_empty_line = false;
for row in point.row..=map.buffer_snapshot().max_row().0 {
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_empty_line = false;
}
found_non_empty_line |= !empty;
}
map.max_point()
}
fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset {
for (c, o) in map.buffer_chars_at(start) {
if c == '\n' || !c.is_whitespace() {