vim: Add }/{ for start/end of paragraph

Conrad Irwin created

Fixes: zed-industries/community#470

Change summary

assets/keymaps/vim.json       |   2 
crates/editor/src/editor.rs   |  14 +++-
crates/editor/src/movement.rs |  24 ++++++-
crates/vim/src/motion.rs      | 118 ++++++++++++++++++++++++++++++++++++
4 files changed, 149 insertions(+), 9 deletions(-)

Detailed changes

assets/keymaps/vim.json πŸ”—

@@ -37,6 +37,8 @@
       "$": "vim::EndOfLine",
       "shift-g": "vim::EndOfDocument",
       "w": "vim::NextWordStart",
+      "{": "vim::StartOfParagraph",
+      "}": "vim::EndOfParagraph",
       "shift-w": [
         "vim::NextWordStart",
         {

crates/editor/src/editor.rs πŸ”—

@@ -5120,7 +5120,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::start_of_paragraph(map, selection.head()),
+                    movement::start_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5140,7 +5140,7 @@ impl Editor {
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_with(|map, selection| {
                 selection.collapse_to(
-                    movement::end_of_paragraph(map, selection.head()),
+                    movement::end_of_paragraph(map, selection.head(), 1),
                     SelectionGoal::None,
                 )
             });
@@ -5159,7 +5159,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::start_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::start_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }
@@ -5176,7 +5179,10 @@ impl Editor {
 
         self.change_selections(Some(Autoscroll::fit()), cx, |s| {
             s.move_heads_with(|map, head, _| {
-                (movement::end_of_paragraph(map, head), SelectionGoal::None)
+                (
+                    movement::end_of_paragraph(map, head, 1),
+                    SelectionGoal::None,
+                )
             });
         })
     }

crates/editor/src/movement.rs πŸ”—

@@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo
     })
 }
 
-pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub 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 map.max_point();
@@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     for row in (0..point.row + 1).rev() {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;
@@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) ->
     DisplayPoint::zero()
 }
 
-pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint {
+pub fn end_of_paragraph(
+    map: &DisplaySnapshot,
+    display_point: DisplayPoint,
+    mut count: usize,
+) -> DisplayPoint {
     let point = display_point.to_point(map);
     if point.row == map.max_buffer_row() {
         return DisplayPoint::zero();
@@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D
     for row in point.row..map.max_buffer_row() + 1 {
         let blank = map.buffer_snapshot.is_line_blank(row);
         if found_non_blank_line && blank {
-            return Point::new(row, 0).to_display_point(map);
+            if count <= 1 {
+                return Point::new(row, 0).to_display_point(map);
+            }
+            count -= 1;
+            found_non_blank_line = false;
         }
 
         found_non_blank_line |= !blank;

crates/vim/src/motion.rs πŸ”—

@@ -31,6 +31,8 @@ pub enum Motion {
     CurrentLine,
     StartOfLine,
     EndOfLine,
+    StartOfParagraph,
+    EndOfParagraph,
     StartOfDocument,
     EndOfDocument,
     Matching,
@@ -72,6 +74,8 @@ actions!(
         StartOfLine,
         EndOfLine,
         CurrentLine,
+        StartOfParagraph,
+        EndOfParagraph,
         StartOfDocument,
         EndOfDocument,
         Matching,
@@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) {
     cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx));
     cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx));
+    cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| {
+        motion(Motion::StartOfParagraph, cx)
+    });
+    cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| {
+        motion(Motion::EndOfParagraph, cx)
+    });
     cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| {
         motion(Motion::StartOfDocument, cx)
     });
@@ -142,7 +152,8 @@ impl Motion {
     pub fn linewise(&self) -> bool {
         use Motion::*;
         match self {
-            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true,
+            Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart
+            | StartOfParagraph | EndOfParagraph => true,
             EndOfLine
             | NextWordEnd { .. }
             | Matching
@@ -172,6 +183,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -197,6 +210,8 @@ impl Motion {
             | Backspace
             | Right
             | StartOfLine
+            | StartOfParagraph
+            | EndOfParagraph
             | NextWordStart { .. }
             | PreviousWordStart { .. }
             | FirstNonWhitespace
@@ -235,6 +250,14 @@ impl Motion {
             FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None),
             StartOfLine => (start_of_line(map, point), SelectionGoal::None),
             EndOfLine => (end_of_line(map, point), SelectionGoal::None),
+            StartOfParagraph => (
+                movement::start_of_paragraph(map, point, times),
+                SelectionGoal::None,
+            ),
+            EndOfParagraph => (
+                movement::end_of_paragraph(map, point, times),
+                SelectionGoal::None,
+            ),
             CurrentLine => (end_of_line(map, point), SelectionGoal::None),
             StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None),
             EndOfDocument => (
@@ -590,3 +613,96 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) ->
     let new_row = (point.row() + times as u32).min(map.max_buffer_row());
     map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left)
 }
+
+#[cfg(test)]
+
+mod test {
+
+    use crate::{state::Mode, test::VimTestContext};
+    use indoc::indoc;
+
+    #[gpui::test]
+    async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) {
+        let mut cx = VimTestContext::new(cx, true).await;
+
+        let initial_state = indoc! {r"Λ‡abc
+            def
+
+            paragraph
+            the second
+
+
+
+            third and
+            final"};
+
+        // goes down once
+        cx.set_state(initial_state, Mode::Normal);
+        cx.simulate_keystrokes(["}"]);
+        cx.assert_state(
+            indoc! {r"abc
+            def
+            Λ‡
+            paragraph
+            the second
+
+
+
+            third and
+            final"},
+            Mode::Normal,
+        );
+
+        // goes up once
+        cx.simulate_keystrokes(["{"]);
+        cx.assert_state(initial_state, Mode::Normal);
+
+        // goes down twice
+        cx.simulate_keystrokes(["2", "}"]);
+        cx.assert_state(
+            indoc! {r"abc
+            def
+
+            paragraph
+            the second
+            Λ‡
+
+
+            third and
+            final"},
+            Mode::Normal,
+        );
+
+        // goes down over multiple blanks
+        cx.simulate_keystrokes(["}"]);
+        cx.assert_state(
+            indoc! {r"abc
+                def
+
+                paragraph
+                the second
+
+
+
+                third and
+                finalˇ"},
+            Mode::Normal,
+        );
+
+        // goes up twice
+        cx.simulate_keystrokes(["2", "{"]);
+        cx.assert_state(
+            indoc! {r"abc
+                def
+                Λ‡
+                paragraph
+                the second
+
+
+
+                third and
+                final"},
+            Mode::Normal,
+        )
+    }
+}