From f8f489271ef975e37e6b1d822a51a2f1b846d323 Mon Sep 17 00:00:00 2001 From: lex00 <121451605+lex00@users.noreply.github.com> Date: Tue, 17 Feb 2026 04:40:50 -0700 Subject: [PATCH] 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 Co-authored-by: dino --- crates/editor/src/editor_tests.rs | 86 ++++--------------------------- crates/editor/src/movement.rs | 24 ++++----- crates/vim/src/motion.rs | 69 +++++++++++++++++++++++-- 3 files changed, 86 insertions(+), 93 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 71fd78a680341bcb89b00fe60f9e2d0036004ba4..654770350785c7a0a88c12d8a6b13fee7d77063d 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/editor/src/movement.rs b/crates/editor/src/movement.rs index de84ffafa363bbdfe475a7d2ad646f118268be2b..75765b1816d1ccc8bff2eba39dcd0c966f0bfa8d 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 b33d85bc5a5398b912ea999865729c362dced995..84d8dba77237fbc44295f07019a5cf3923c8fe49 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 => (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() {