Implement text insertion and backspace
This commit is contained in:
41
babi.py
41
babi.py
@@ -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})')
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user