From 7ee39fbe19b0c025c571c7b6b2a6d2a9de24fa0a Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Fri, 30 Jan 2026 07:51:31 -0700 Subject: [PATCH 1/2] vim: Apply strict paragraph motion only in vim mode Addresses feedback from #47734: the previous change affected all users, but the stricter "empty line only" paragraph behavior (per vim's :help paragraph) should only apply in vim mode. - Revert editor's paragraph functions to use is_line_blank() (whitespace lines count as boundaries for non-vim users) - Add vim-specific paragraph_backwards/paragraph_forwards that use line_len() == 0 (only truly empty lines are boundaries) Co-Authored-By: Claude Opus 4.5 --- crates/editor/src/movement.rs | 24 ++++++------- crates/vim/src/motion.rs | 67 ++++++++++++++++++++++++++++++++--- 2 files changed, 74 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index de84ffafa363bb..75765b1816d1cc 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-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() diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index b33d85bc5a5398..7e74fdc563a2cf 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -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 => (paragraph_backwards(map, point, times), SelectionGoal::None), EndOfParagraph => ( - map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), + map.clip_at_line_end(paragraph_forwards(map, point, times)), SelectionGoal::None, ), CurrentLine => (next_line_end(map, point, times), SelectionGoal::None), @@ -2238,6 +2235,66 @@ 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 (per vim's :help paragraph). +/// This differs from the editor's paragraph function which uses blank (whitespace-only) lines. +pub(crate) fn paragraph_backwards( + 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 (per vim's :help paragraph). +/// This differs from the editor's paragraph function which uses blank (whitespace-only) lines. +pub(crate) fn paragraph_forwards( + 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() { From 23d506d7c2348750096a407f553bf1a1992f6fcf Mon Sep 17 00:00:00 2001 From: dino Date: Mon, 16 Feb 2026 15:52:22 +0000 Subject: [PATCH 2/2] refactor: update editor test and rename function * Rename `vim::motion::paragraph_backwards` to `vim::motion::start_of_paragraph` and `vim::motion::paragraph_forwards` to `vim::motion::end_of_paragraph` to align it with the action and command name * Update the existing editor test for paragraph start and end to ensure that non-blank lines, i.e., lines with only whitespace characters, are also considered paragraph boundaries, contrary to what vim mode does --- crates/editor/src/editor_tests.rs | 86 ++++--------------------------- crates/vim/src/motion.rs | 18 ++++--- 2 files changed, 20 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index f0e1d601d0454c..c3e4345644abf0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -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] diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 7e74fdc563a2cf..84d8dba77237fb 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1038,9 +1038,9 @@ impl Motion { ), SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None), SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None), - StartOfParagraph => (paragraph_backwards(map, point, times), SelectionGoal::None), + StartOfParagraph => (start_of_paragraph(map, point, times), SelectionGoal::None), EndOfParagraph => ( - map.clip_at_line_end(paragraph_forwards(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), @@ -2236,9 +2236,10 @@ pub(crate) fn sentence_forwards( } /// 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 (per vim's :help paragraph). -/// This differs from the editor's paragraph function which uses blank (whitespace-only) lines. -pub(crate) fn paragraph_backwards( +/// 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, @@ -2266,9 +2267,10 @@ pub(crate) fn paragraph_backwards( } /// 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 (per vim's :help paragraph). -/// This differs from the editor's paragraph function which uses blank (whitespace-only) lines. -pub(crate) fn paragraph_forwards( +/// 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,