Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f259403fe | ||
|
|
4b27a18c0f | ||
|
|
58bc4780ca | ||
|
|
4812daf300 |
11
babi/file.py
11
babi/file.py
@@ -198,10 +198,12 @@ class File:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
filename: Optional[str],
|
filename: Optional[str],
|
||||||
|
initial_line: int,
|
||||||
color_manager: ColorManager,
|
color_manager: ColorManager,
|
||||||
hl_factories: Tuple[HLFactory, ...],
|
hl_factories: Tuple[HLFactory, ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
|
self.initial_line = initial_line
|
||||||
self.modified = False
|
self.modified = False
|
||||||
self.buf = Buf([])
|
self.buf = Buf([])
|
||||||
self.nl = '\n'
|
self.nl = '\n'
|
||||||
@@ -215,7 +217,12 @@ class File:
|
|||||||
self.selection = Selection()
|
self.selection = Selection()
|
||||||
self._file_hls: Tuple[FileHL, ...] = ()
|
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:
|
if self.buf:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -257,6 +264,8 @@ class File:
|
|||||||
for file_hl in self._file_hls:
|
for file_hl in self._file_hls:
|
||||||
file_hl.register_callbacks(self.buf)
|
file_hl.register_callbacks(self.buf)
|
||||||
|
|
||||||
|
self.go_to_line(self.initial_line, margin)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f'<{type(self).__name__} {self.filename!r}>'
|
return f'<{type(self).__name__} {self.filename!r}>'
|
||||||
|
|
||||||
|
|||||||
93
babi/main.py
93
babi/main.py
@@ -1,9 +1,12 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import curses
|
import curses
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from typing import List
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.file import File
|
from babi.file import File
|
||||||
@@ -14,10 +17,11 @@ from babi.screen import make_stdscr
|
|||||||
from babi.screen import Screen
|
from babi.screen import Screen
|
||||||
|
|
||||||
CONSOLE = 'CONIN$' if sys.platform == 'win32' else '/dev/tty'
|
CONSOLE = 'CONIN$' if sys.platform == 'win32' else '/dev/tty'
|
||||||
|
POSITION_RE = re.compile(r'^\+-?\d+$')
|
||||||
|
|
||||||
|
|
||||||
def _edit(screen: Screen, stdin: str) -> EditResult:
|
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:
|
while True:
|
||||||
screen.status.tick(screen.margin)
|
screen.status.tick(screen.margin)
|
||||||
@@ -40,35 +44,36 @@ def _edit(screen: Screen, stdin: str) -> EditResult:
|
|||||||
|
|
||||||
def c_main(
|
def c_main(
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: 'curses._CursesWindow',
|
||||||
args: argparse.Namespace,
|
filenames: List[Optional[str]],
|
||||||
|
positions: List[int],
|
||||||
stdin: str,
|
stdin: str,
|
||||||
|
perf: Perf,
|
||||||
) -> int:
|
) -> int:
|
||||||
with perf_log(args.perf_log) as perf:
|
screen = Screen(stdscr, filenames, positions, perf)
|
||||||
screen = Screen(stdscr, args.filenames or [None], perf)
|
with screen.history.save():
|
||||||
with screen.history.save():
|
while screen.files:
|
||||||
while screen.files:
|
screen.i = screen.i % len(screen.files)
|
||||||
screen.i = screen.i % len(screen.files)
|
res = _edit(screen, stdin)
|
||||||
res = _edit(screen, stdin)
|
if res == EditResult.EXIT:
|
||||||
if res == EditResult.EXIT:
|
del screen.files[screen.i]
|
||||||
del screen.files[screen.i]
|
# always go to the next file except at the end
|
||||||
# always go to the next file except at the end
|
screen.i = min(screen.i, len(screen.files) - 1)
|
||||||
screen.i = min(screen.i, len(screen.files) - 1)
|
screen.status.clear()
|
||||||
screen.status.clear()
|
elif res == EditResult.NEXT:
|
||||||
elif res == EditResult.NEXT:
|
screen.i += 1
|
||||||
screen.i += 1
|
screen.status.clear()
|
||||||
screen.status.clear()
|
elif res == EditResult.PREV:
|
||||||
elif res == EditResult.PREV:
|
screen.i -= 1
|
||||||
screen.i -= 1
|
screen.status.clear()
|
||||||
screen.status.clear()
|
elif res == EditResult.OPEN:
|
||||||
elif res == EditResult.OPEN:
|
screen.i = len(screen.files) - 1
|
||||||
screen.i = len(screen.files) - 1
|
else:
|
||||||
else:
|
raise AssertionError(f'unreachable {res}')
|
||||||
raise AssertionError(f'unreachable {res}')
|
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _key_debug(stdscr: 'curses._CursesWindow') -> int:
|
def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int:
|
||||||
screen = Screen(stdscr, ['<<key debug>>'], Perf())
|
screen = Screen(stdscr, ['<<key debug>>'], [0], perf)
|
||||||
screen.file.buf = Buf([''])
|
screen.file.buf = Buf([''])
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
@@ -85,6 +90,37 @@ def _key_debug(stdscr: 'curses._CursesWindow') -> int:
|
|||||||
return 0
|
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:
|
def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filenames', metavar='filename', nargs='*')
|
parser.add_argument('filenames', metavar='filename', nargs='*')
|
||||||
@@ -102,11 +138,12 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
else:
|
else:
|
||||||
stdin = ''
|
stdin = ''
|
||||||
|
|
||||||
with make_stdscr() as stdscr:
|
with perf_log(args.perf_log) as perf, make_stdscr() as stdscr:
|
||||||
if args.key_debug:
|
if args.key_debug:
|
||||||
return _key_debug(stdscr)
|
return _key_debug(stdscr, perf)
|
||||||
else:
|
else:
|
||||||
return c_main(stdscr, args, stdin)
|
filenames, positions = _filenames(args.filenames)
|
||||||
|
return c_main(stdscr, filenames, positions, stdin, perf)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ KEYNAME_REWRITE = {
|
|||||||
b'CTL_DOWN': b'kDN5',
|
b'CTL_DOWN': b'kDN5',
|
||||||
b'CTL_RIGHT': b'kRIT5',
|
b'CTL_RIGHT': b'kRIT5',
|
||||||
b'CTL_LEFT': b'kLFT5',
|
b'CTL_LEFT': b'kLFT5',
|
||||||
|
b'CTL_HOME': b'kHOM5',
|
||||||
|
b'CTL_END': b'kEND5',
|
||||||
b'ALT_RIGHT': b'kRIT3',
|
b'ALT_RIGHT': b'kRIT3',
|
||||||
b'ALT_LEFT': b'kLFT3',
|
b'ALT_LEFT': b'kLFT3',
|
||||||
# windows-curses: idk why these are different
|
# windows-curses: idk why these are different
|
||||||
@@ -103,14 +105,15 @@ class Screen:
|
|||||||
self,
|
self,
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: 'curses._CursesWindow',
|
||||||
filenames: List[Optional[str]],
|
filenames: List[Optional[str]],
|
||||||
|
initial_lines: List[int],
|
||||||
perf: Perf,
|
perf: Perf,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.stdscr = stdscr
|
self.stdscr = stdscr
|
||||||
self.color_manager = ColorManager.make()
|
self.color_manager = ColorManager.make()
|
||||||
self.hl_factories = (Syntax.from_screen(stdscr, self.color_manager),)
|
self.hl_factories = (Syntax.from_screen(stdscr, self.color_manager),)
|
||||||
self.files = [
|
self.files = [
|
||||||
File(filename, self.color_manager, self.hl_factories)
|
File(filename, line, self.color_manager, self.hl_factories)
|
||||||
for filename in filenames
|
for filename, line in zip(filenames, initial_lines)
|
||||||
]
|
]
|
||||||
self.i = 0
|
self.i = 0
|
||||||
self.history = History()
|
self.history = History()
|
||||||
@@ -489,7 +492,7 @@ class Screen:
|
|||||||
def open_file(self) -> Optional[EditResult]:
|
def open_file(self) -> Optional[EditResult]:
|
||||||
response = self.prompt('enter filename', history='open')
|
response = self.prompt('enter filename', history='open')
|
||||||
if response is not PromptResult.CANCELLED:
|
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)
|
self.files.append(opened)
|
||||||
return EditResult.OPEN
|
return EditResult.OPEN
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = babi
|
name = babi
|
||||||
version = 0.0.7
|
version = 0.0.8
|
||||||
description = a text editor
|
description = a text editor
|
||||||
long_description = file: README.md
|
long_description = file: README.md
|
||||||
long_description_content_type = text/markdown
|
long_description_content_type = text/markdown
|
||||||
|
|||||||
28
tests/features/initial_position_test.py
Normal file
28
tests/features/initial_position_test.py
Normal file
@@ -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)
|
||||||
@@ -8,7 +8,7 @@ from babi.file import get_lines
|
|||||||
|
|
||||||
|
|
||||||
def test_position_repr():
|
def test_position_repr():
|
||||||
ret = repr(File('f.txt', ColorManager.make(), ()))
|
ret = repr(File('f.txt', 0, ColorManager.make(), ()))
|
||||||
assert ret == "<File 'f.txt'>"
|
assert ret == "<File 'f.txt'>"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
19
tests/main_test.py
Normal file
19
tests/main_test.py
Normal file
@@ -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)
|
||||||
Reference in New Issue
Block a user