From 4812daf3009421480f105d210c2eb935e2cada5e Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 17 Apr 2020 19:30:13 -0700 Subject: [PATCH] Implement open-with-offset Resolves #60 --- babi/file.py | 11 ++- babi/main.py | 93 +++++++++++++++++-------- babi/screen.py | 7 +- tests/features/initial_position_test.py | 28 ++++++++ tests/file_test.py | 2 +- tests/main_test.py | 19 +++++ 6 files changed, 127 insertions(+), 33 deletions(-) create mode 100644 tests/features/initial_position_test.py create mode 100644 tests/main_test.py diff --git a/babi/file.py b/babi/file.py index 9497035..6d46f58 100644 --- a/babi/file.py +++ b/babi/file.py @@ -198,10 +198,12 @@ class File: def __init__( self, filename: Optional[str], + initial_line: int, color_manager: ColorManager, hl_factories: Tuple[HLFactory, ...], ) -> None: self.filename = filename + self.initial_line = initial_line self.modified = False self.buf = Buf([]) self.nl = '\n' @@ -215,7 +217,12 @@ class File: self.selection = Selection() self._file_hls: Tuple[FileHL, ...] = () - def ensure_loaded(self, status: Status, stdin: str) -> None: + def ensure_loaded( + self, + status: Status, + margin: Margin, + stdin: str, + ) -> None: if self.buf: return @@ -257,6 +264,8 @@ class File: for file_hl in self._file_hls: file_hl.register_callbacks(self.buf) + self.go_to_line(self.initial_line, margin) + def __repr__(self) -> str: return f'<{type(self).__name__} {self.filename!r}>' diff --git a/babi/main.py b/babi/main.py index 49002e0..e5c1687 100644 --- a/babi/main.py +++ b/babi/main.py @@ -1,9 +1,12 @@ import argparse import curses import os +import re import sys +from typing import List from typing import Optional from typing import Sequence +from typing import Tuple from babi.buf import Buf from babi.file import File @@ -14,10 +17,11 @@ from babi.screen import make_stdscr from babi.screen import Screen CONSOLE = 'CONIN$' if sys.platform == 'win32' else '/dev/tty' +POSITION_RE = re.compile(r'^\+-?\d+$') def _edit(screen: Screen, stdin: str) -> EditResult: - screen.file.ensure_loaded(screen.status, stdin) + screen.file.ensure_loaded(screen.status, screen.margin, stdin) while True: screen.status.tick(screen.margin) @@ -40,35 +44,36 @@ def _edit(screen: Screen, stdin: str) -> EditResult: def c_main( stdscr: 'curses._CursesWindow', - args: argparse.Namespace, + filenames: List[Optional[str]], + positions: List[int], stdin: str, + perf: Perf, ) -> int: - with perf_log(args.perf_log) as perf: - screen = Screen(stdscr, args.filenames or [None], perf) - with screen.history.save(): - while screen.files: - screen.i = screen.i % len(screen.files) - res = _edit(screen, stdin) - if res == EditResult.EXIT: - del screen.files[screen.i] - # always go to the next file except at the end - screen.i = min(screen.i, len(screen.files) - 1) - screen.status.clear() - elif res == EditResult.NEXT: - screen.i += 1 - screen.status.clear() - elif res == EditResult.PREV: - screen.i -= 1 - screen.status.clear() - elif res == EditResult.OPEN: - screen.i = len(screen.files) - 1 - else: - raise AssertionError(f'unreachable {res}') + screen = Screen(stdscr, filenames, positions, perf) + with screen.history.save(): + while screen.files: + screen.i = screen.i % len(screen.files) + res = _edit(screen, stdin) + if res == EditResult.EXIT: + del screen.files[screen.i] + # always go to the next file except at the end + screen.i = min(screen.i, len(screen.files) - 1) + screen.status.clear() + elif res == EditResult.NEXT: + screen.i += 1 + screen.status.clear() + elif res == EditResult.PREV: + screen.i -= 1 + screen.status.clear() + elif res == EditResult.OPEN: + screen.i = len(screen.files) - 1 + else: + raise AssertionError(f'unreachable {res}') return 0 -def _key_debug(stdscr: 'curses._CursesWindow') -> int: - screen = Screen(stdscr, ['<>'], Perf()) +def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int: + screen = Screen(stdscr, ['<>'], [0], perf) screen.file.buf = Buf(['']) while True: @@ -85,6 +90,37 @@ def _key_debug(stdscr: 'curses._CursesWindow') -> int: return 0 +def _filenames(filenames: List[str]) -> Tuple[List[Optional[str]], List[int]]: + if not filenames: + return [None], [0] + + ret_filenames: List[Optional[str]] = [] + ret_positions = [] + + filenames_iter = iter(filenames) + for filename in filenames_iter: + if POSITION_RE.match(filename): + # in the success case we get: + # + # position_s = +... + # filename = (the next thing) + # + # in the error case we only need to reset `position_s` as + # `filename` is already correct + position_s = filename + try: + filename = next(filenames_iter) + except StopIteration: + position_s = '+0' + ret_positions.append(int(position_s[1:])) + ret_filenames.append(filename) + else: + ret_positions.append(0) + ret_filenames.append(filename) + + return ret_filenames, ret_positions + + def main(argv: Optional[Sequence[str]] = None) -> int: parser = argparse.ArgumentParser() parser.add_argument('filenames', metavar='filename', nargs='*') @@ -102,11 +138,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int: else: stdin = '' - with make_stdscr() as stdscr: + with perf_log(args.perf_log) as perf, make_stdscr() as stdscr: if args.key_debug: - return _key_debug(stdscr) + return _key_debug(stdscr, perf) else: - return c_main(stdscr, args, stdin) + filenames, positions = _filenames(args.filenames) + return c_main(stdscr, filenames, positions, stdin, perf) if __name__ == '__main__': diff --git a/babi/screen.py b/babi/screen.py index d3bf9d2..d0cf59f 100644 --- a/babi/screen.py +++ b/babi/screen.py @@ -103,14 +103,15 @@ class Screen: self, stdscr: 'curses._CursesWindow', filenames: List[Optional[str]], + initial_lines: List[int], perf: Perf, ) -> None: self.stdscr = stdscr self.color_manager = ColorManager.make() self.hl_factories = (Syntax.from_screen(stdscr, self.color_manager),) self.files = [ - File(filename, self.color_manager, self.hl_factories) - for filename in filenames + File(filename, line, self.color_manager, self.hl_factories) + for filename, line in zip(filenames, initial_lines) ] self.i = 0 self.history = History() @@ -489,7 +490,7 @@ class Screen: def open_file(self) -> Optional[EditResult]: response = self.prompt('enter filename', history='open') if response is not PromptResult.CANCELLED: - opened = File(response, self.color_manager, self.hl_factories) + opened = File(response, 0, self.color_manager, self.hl_factories) self.files.append(opened) return EditResult.OPEN else: diff --git a/tests/features/initial_position_test.py b/tests/features/initial_position_test.py new file mode 100644 index 0000000..130ea3a --- /dev/null +++ b/tests/features/initial_position_test.py @@ -0,0 +1,28 @@ +from testing.runner import and_exit + + +def test_open_file_named_plus_something(run): + with run('+3') as h, and_exit(h): + h.await_text(' +3') + + +def test_initial_position_one_file(run, tmpdir): + f = tmpdir.join('f') + f.write('hello\nworld\n') + + with run('+2', str(f)) as h, and_exit(h): + h.await_cursor_position(x=0, y=2) + + +def test_initial_position_multiple_files(run, tmpdir): + f = tmpdir.join('f') + f.write('1\n2\n3\n4\n') + g = tmpdir.join('g') + g.write('5\n6\n7\n8\n') + + with run('+2', str(f), '+3', str(g)) as h, and_exit(h): + h.await_cursor_position(x=0, y=2) + + h.press('^X') + + h.await_cursor_position(x=0, y=3) diff --git a/tests/file_test.py b/tests/file_test.py index df1cb8f..0affbac 100644 --- a/tests/file_test.py +++ b/tests/file_test.py @@ -8,7 +8,7 @@ from babi.file import get_lines def test_position_repr(): - ret = repr(File('f.txt', ColorManager.make(), ())) + ret = repr(File('f.txt', 0, ColorManager.make(), ())) assert ret == "" diff --git a/tests/main_test.py b/tests/main_test.py new file mode 100644 index 0000000..14ea172 --- /dev/null +++ b/tests/main_test.py @@ -0,0 +1,19 @@ +import pytest + +from babi import main + + +@pytest.mark.parametrize( + ('in_filenames', 'expected_filenames', 'expected_positions'), + ( + ([], [None], [0]), + (['+3'], ['+3'], [0]), + (['f'], ['f'], [0]), + (['+3', 'f'], ['f'], [3]), + (['+-3', 'f'], ['f'], [-3]), + (['+3', '+3'], ['+3'], [3]), + (['+2', 'f', '+5', 'g'], ['f', 'g'], [2, 5]), + ), +) +def test_filenames(in_filenames, expected_filenames, expected_positions): + filenames, positions = main._filenames(in_filenames)