Repository: asottile/babi

Issue:
Jump to paragraph - feature request
Hello. Is there a possibility that you can add a **jump to paragraph** feature, like in nano?
This is what I mean:
![nano presentation](https://i.imgur.com/as6lXWT.gif)
I achieve this by pressing <kbd>Ctrl</kbd> + <kbd>Arrow Up</kbd>/<kbd>Arrow Down</kbd> and I don't see this feature anywhere in babi.

Also, I got used to pressing <kbd>Ctrl</kbd> + <kbd>Backspace</kbd> in order to delete a whole word, just like in other text editors (not in nano). This feature is very handy, so would you mind adding this as well?

Discussion:

Patch:
diff --git a/babi/file.py b/babi/file.py
index 0f3e9f2..9f618a3 100644
--- a/babi/file.py
+++ b/babi/file.py
@@ -465,6 +465,45 @@ class File:
             self.buf.y = self.buf.file_y = pos
         self.buf.x = 0
 
+    @action
+    def alt_up(self, margin: Margin) -> None:
+        if self.buf.y > 0:
+            offset = 1
+            while self.buf[self.buf.y - offset] == '':
+                offset += 1
+            if offset >= 2:
+                self.buf.y -= offset
+                self.buf.scroll_screen_if_needed(margin)
+                self.buf.x = 0
+                return
+            while (
+                self.buf[self.buf.y - offset - 1] != '' and
+                self.buf.y - offset - 1 >= 0
+            ):
+                offset += 1
+            self.buf.y -= offset
+            self.buf.scroll_screen_if_needed(margin)
+            self.buf.x = 0
+
+    @action
+    def alt_down(self, margin: Margin) -> None:
+        if self.buf.y < len(self.buf) - 1:
+            offset = 0
+            while self.buf[self.buf.y + offset] != '':
+                offset += 1
+            if offset > 1:
+                self.buf.y += offset - 1
+                self.buf.scroll_screen_if_needed(margin)
+                self.buf.x = 0
+                return
+            while self.buf[self.buf.y + offset] == '':
+                if self.buf.y + offset >= len(self.buf) - 1:
+                    break
+                offset += 1
+            self.buf.y += offset
+            self.buf.scroll_screen_if_needed(margin)
+            self.buf.x = 0
+
     # editing
 
     @edit_action('backspace text', final=False)
@@ -757,6 +796,8 @@ class File:
         b'kLFT5': ctrl_left,
         b'kHOM5': ctrl_home,
         b'kEND5': ctrl_end,
+        b'kUP3': alt_up,
+        b'kDN3': alt_down,
         # editing
         b'KEY_BACKSPACE': backspace,
         b'KEY_DC': delete,
@@ -776,6 +817,8 @@ class File:
         b'kLFT6': keep_selection(ctrl_left),
         b'kHOM6': keep_selection(ctrl_home),
         b'kEND6': keep_selection(ctrl_end),
+        b'kUP4': keep_selection(alt_up),
+        b'kDN4': keep_selection(alt_down),
     }
 
     @edit_action('text', final=False)


Test patch:
diff --git a/tests/features/conftest.py b/tests/features/conftest.py
index 78b41c3..87ac27c 100644
--- a/tests/features/conftest.py
+++ b/tests/features/conftest.py
@@ -262,6 +262,8 @@ KEYS = [
     Key('^Left', b'kLFT5', 545),
     Key('^Home', b'kHOM5', 535),
     Key('^End', b'kEND5', 530),
+    Key('M-Up', b'kUP3', 564),
+    Key('M-Down', b'kDN3', 523),
     Key('M-Right', b'kRIT3', 558),
     Key('M-Left', b'kLFT3', 543),
     Key('S-Up', b'KEY_SR', curses.KEY_SR),
diff --git a/tests/features/movement_test.py b/tests/features/movement_test.py
index 083c802..0050cdb 100644
--- a/tests/features/movement_test.py
+++ b/tests/features/movement_test.py
@@ -440,3 +440,92 @@ def test_indentation_using_tabs(run, tmpdir):
         h.await_cursor_position(x=4, y=2)
         h.press('Up')
         h.await_cursor_position(x=4, y=1)
+
+
+def test_goto_previous_paragraph(run, tmpdir):
+    src = '''\
+class A:
+    def __init__(self):
+        self._test = 1
+
+
+def another_test():
+    pass
+
+yet_another_test = None
+
+# comment
+'''
+    f = tmpdir.join('f')
+    f.write(src)
+
+    with run(str(f)) as h, and_exit(h):
+        h.press('^End')
+        h.await_cursor_position(x=0, y=12)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=11)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=9)
+
+        h.press('End')
+        h.await_cursor_position(x=23, y=9)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=7)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=6)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=3)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=1)
+
+        h.press('M-Up')
+        h.await_cursor_position(x=0, y=1)
+
+
+def test_goto_next_paragraph(run, tmpdir):
+    src = '''\
+class A:
+    def __init__(self):
+        self._test = 1
+
+
+def another_test():
+    pass
+
+yet_another_test = None
+
+# comment
+'''
+    f = tmpdir.join('f')
+    f.write(src)
+
+    with run(str(f)) as h, and_exit(h):
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=3)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=6)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=7)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=9)
+
+        h.press('End')
+        h.await_cursor_position(x=23, y=9)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=11)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=12)
+
+        h.press('M-Down')
+        h.await_cursor_position(x=0, y=12)