Implement text insertion and backspace

This commit is contained in:
Anthony Sottile
2019-08-31 14:39:42 -07:00
parent f179b09209
commit 86fb2c7dab
3 changed files with 110 additions and 5 deletions

41
babi.py
View File

@@ -161,7 +161,7 @@ def _color_test(stdscr: '_curses._CursesWindow') -> None:
_write_header(stdscr, '<<color test>>', modified=False)
maxy, maxx = stdscr.getmaxyx()
if maxy < 19 or maxx < 68:
if maxy < 19 or maxx < 68: # pragma: no cover (will be deleted)
raise SystemExit('--color-test needs a window of at least 68 x 19')
y = 1
@@ -243,6 +243,15 @@ def _move_cursor(
stdscr.move(position.cursor_y(margin), position.cursor_x())
def _restore_lines_eof_invariant(lines: List[str]) -> None:
"""The file lines will always contain a blank empty string at the end to
simplify rendering. This should be called whenever the end of the file
might change.
"""
if not lines or lines[-1] != '':
lines.append('')
def _get_lines(sio: IO[str]) -> Tuple[List[str], str, bool]:
lines = []
newlines = collections.Counter({'\n': 0}) # default to `\n`
@@ -254,7 +263,7 @@ def _get_lines(sio: IO[str]) -> Tuple[List[str], str, bool]:
break
else:
lines.append(line)
lines.append('') # we use this as a padding line for display
_restore_lines_eof_invariant(lines)
(nl, _), = newlines.most_common(1)
mixed = len({k for k, v in newlines.items() if v}) > 1
return lines, nl, mixed
@@ -314,6 +323,34 @@ def c_main(stdscr: '_curses._CursesWindow', args: argparse.Namespace) -> None:
position.dispatch(key, margin, lines)
elif keyname == b'^X':
return
elif key == curses.KEY_BACKSPACE:
# backspace at the beginning of the file does nothing
if position.cursor_line == 0 and position.x == 0:
pass
# at the beginning of the line, we join the current line and
# the previous line
elif position.x == 0:
victim = lines.pop(position.cursor_line)
new_x = len(lines[position.cursor_line - 1])
lines[position.cursor_line - 1] += victim
position.up(margin, lines)
position.x = position.cursor_x_hint = new_x
# deleting the fake end-of-file doesn't cause modification
modified |= position.cursor_line < len(lines) - 1
_restore_lines_eof_invariant(lines)
else:
s = lines[position.cursor_line]
lines[position.cursor_line] = (
s[:position.x - 1] + s[position.x:]
)
position.left(margin, lines)
modified = True
elif isinstance(wch, str) and wch.isprintable():
s = lines[position.cursor_line]
lines[position.cursor_line] = s[:position.x] + wch + s[position.x:]
position.right(margin, lines)
modified = True
_restore_lines_eof_invariant(lines)
else:
_set_status(f'unknown key: {keyname} ({key})')

View File

@@ -58,9 +58,16 @@ class PrintsErrorRunner(Runner):
with self._onerror():
return super().await_exit(*args, **kwargs)
def await_text(self, *args, **kwargs):
def await_text(self, text, timeout=None):
"""copied from the base implementation but doesn't munge newlines"""
with self._onerror():
return super().await_text(*args, **kwargs)
for _ in self.poll_until_timeout(timeout):
screen = self.screenshot()
if text in screen:
return
raise AssertionError(
f'Timeout while waiting for text {text!r} to appear',
)
def await_text_missing(self, s):
"""largely based on await_text"""
@@ -395,3 +402,64 @@ def test_window_width_1(tmpdir):
h.press('Right')
h.await_text('hello')
assert h.get_cursor_position() == (3, 1)
def test_basic_text_editing(tmpdir):
with run() as h, and_exit(h):
h.press('hello world')
h.await_text('hello world')
h.press('Down')
h.press('bye!')
h.await_text('bye!')
assert h.screenshot().strip().endswith('world\nbye!')
def test_backspace_at_beginning_of_file():
with run() as h, and_exit(h):
h.press('Bspace')
h.await_text_missing('unknown key')
assert h.screenshot().strip().splitlines()[1:] == []
assert '*' not in h.screenshot()
def test_backspace_joins_lines(tmpdir):
f = tmpdir.join('f')
f.write('foo\nbar\nbaz\n')
with run(str(f)) as h, and_exit(h):
h.await_text('foo')
h.press('Down')
h.press('Bspace')
h.await_text('foobar')
h.await_text('f *')
assert h.get_cursor_position() == (3, 1)
# pressing down should retain the X position
h.press('Down')
assert h.get_cursor_position() == (3, 2)
def test_backspace_at_end_of_file_still_allows_scrolling_down(tmpdir):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Down')
h.press('Bspace')
h.press('Down')
assert h.get_cursor_position() == (0, 2)
assert '*' not in h.screenshot()
def test_backspace_deletes_text(tmpdir):
f = tmpdir.join('f')
f.write('ohai there')
with run(str(f)) as h, and_exit(h):
h.await_text('ohai there')
for _ in range(3):
h.press('Right')
h.press('Bspace')
h.await_text('ohi')
h.await_text('f *')
assert h.get_cursor_position() == (2, 1)

View File

@@ -7,7 +7,7 @@ commands =
coverage erase
coverage run -m pytest {posargs:tests}
coverage combine
coverage report
coverage report --fail-under 100
[testenv:pre-commit]
skip_install = true