diff --git a/babi.py b/babi.py index f79b761..ea4bd48 100644 --- a/babi.py +++ b/babi.py @@ -24,6 +24,13 @@ class Margin(NamedTuple): def body_lines(self) -> int: return curses.LINES - self.header - self.footer + @property + def page_size(self) -> int: + if self.body_lines <= 2: + return 1 + else: + return self.body_lines - 2 + @classmethod def from_screen(cls, screen: '_curses._CursesWindow') -> 'Margin': if curses.LINES == 1: @@ -95,6 +102,22 @@ class Position: def end(self, margin: Margin, lines: List[str]) -> None: self.x = self.x_hint = len(lines[self.cursor_line]) + def page_up(self, margin: Margin, lines: List[str]) -> None: + if self.cursor_line < margin.body_lines: + self.cursor_line = self.file_line = 0 + else: + pos = self.file_line - margin.page_size + self.cursor_line = self.file_line = pos + self._set_x_after_vertical_movement(lines) + + def page_down(self, margin: Margin, lines: List[str]) -> None: + if self.file_line + margin.body_lines >= len(lines): + self.cursor_line = len(lines) - 1 + else: + pos = self.file_line + margin.page_size + self.cursor_line = self.file_line = pos + self._set_x_after_vertical_movement(lines) + DISPATCH = { curses.KEY_DOWN: down, curses.KEY_UP: up, @@ -102,6 +125,8 @@ class Position: curses.KEY_RIGHT: right, curses.KEY_HOME: home, curses.KEY_END: end, + curses.KEY_PPAGE: page_up, + curses.KEY_NPAGE: page_down, } def dispatch(self, key: int, margin: Margin, lines: List[str]) -> None: @@ -345,6 +370,10 @@ def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None: pos.home(margin, lines) elif keyname == b'^E': pos.end(margin, lines) + elif keyname == b'^Y': + pos.page_up(margin, lines) + elif keyname == b'^V': + pos.page_down(margin, lines) elif keyname == b'^X': return elif keyname == b'^Z': diff --git a/tests/babi_test.py b/tests/babi_test.py index d1a9078..b878d74 100644 --- a/tests/babi_test.py +++ b/tests/babi_test.py @@ -118,6 +118,13 @@ class PrintsErrorRunner(Runner): f'Last cursor position: {pos}', ) + def get_screen_line(self, n): + return self.screenshot().splitlines()[n] + + def get_cursor_line(self): + _, y = self._get_cursor_position() + return self.get_screen_line(y) + @contextlib.contextmanager def resize(self, width, height): current_w, current_h = self.get_pane_size() @@ -234,7 +241,7 @@ def test_status_clearing_behaviour(): def test_reacts_to_resize(): with run() as h, and_exit(h): - first_line = h.screenshot().splitlines()[0] + first_line = h.get_screen_line(0) with h.resize(40, 20): # the first line should be different after resize h.await_text_missing(first_line) @@ -304,6 +311,56 @@ def test_arrow_key_movement(tmpdir): h.await_cursor_position(x=19, y=3) +@pytest.mark.parametrize( + ('page_up', 'page_down'), + (('PageUp', 'PageDown'), ('^Y', '^V')), +) +def test_page_up_and_page_down(tmpdir, page_up, page_down): + f = tmpdir.join('f') + f.write('\n'.join(f'line_{i}' for i in range(10))) + + with run(str(f), height=10) as h, and_exit(h): + h.press('Down') + h.press('Down') + h.press(page_up) + h.await_cursor_position(x=0, y=1) + + h.press(page_down) + h.await_text('line_8') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_6' + + h.press(page_up) + h.await_text_missing('line_8') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' + + h.press(page_down) + h.press(page_down) + h.await_cursor_position(x=0, y=5) + assert h.get_cursor_line() == '' + h.press('Up') + h.await_cursor_position(x=0, y=4) + assert h.get_cursor_line() == 'line_9' + + +def test_page_up_page_down_size_small_window(tmpdir): + f = tmpdir.join('f') + f.write('\n'.join(f'line_{i}' for i in range(10))) + + with run(str(f), height=4) as h, and_exit(h): + h.press('PageDown') + h.await_text('line_2') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_1' + + h.press('Down') + h.press('PageUp') + h.await_text_missing('line_2') + h.await_cursor_position(x=0, y=1) + assert h.get_cursor_line() == 'line_0' + + def test_scrolling_arrow_key_movement(tmpdir): f = tmpdir.join('f') f.write('\n'.join(f'line_{i}' for i in range(10))) @@ -319,7 +376,7 @@ def test_scrolling_arrow_key_movement(tmpdir): h.press('Down') h.await_text('line_8') h.await_cursor_position(x=0, y=4) - assert h.screenshot().splitlines()[4] == 'line_8' + assert h.get_cursor_line() == 'line_8' # we should not have scrolled after 3 up presses for _ in range(3): h.press('Up') @@ -382,7 +439,7 @@ def test_resize_scrolls_up(tmpdir): h.await_text_missing('line_0') h.await_cursor_position(x=0, y=3) # make sure we're still on the same line - assert h.screenshot().splitlines()[3] == 'line_7' + assert h.get_cursor_line() == 'line_7' def test_resize_scroll_does_not_go_negative(tmpdir): @@ -401,7 +458,7 @@ def test_resize_scroll_does_not_go_negative(tmpdir): for _ in range(2): h.press('Up') - assert h.screenshot().splitlines()[1] == 'line_0' + assert h.get_screen_line(1) == 'line_0' def test_very_narrow_window_status():