Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b55ebfd0e |
@@ -11,16 +11,16 @@ repos:
|
||||
- id: name-tests-test
|
||||
- id: requirements-txt-fixer
|
||||
- repo: https://gitlab.com/pycqa/flake8
|
||||
rev: 3.8.0
|
||||
rev: 3.7.9
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-typing-imports==1.7.0]
|
||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||
rev: v1.5.2
|
||||
rev: v1.5
|
||||
hooks:
|
||||
- id: autopep8
|
||||
- repo: https://github.com/asottile/reorder_python_imports
|
||||
rev: v2.3.0
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: [--py3-plus]
|
||||
@@ -30,12 +30,12 @@ repos:
|
||||
- id: add-trailing-comma
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/pyupgrade
|
||||
rev: v2.4.1
|
||||
rev: v2.1.0
|
||||
hooks:
|
||||
- id: pyupgrade
|
||||
args: [--py36-plus]
|
||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||
rev: v1.9.0
|
||||
rev: v1.7.0
|
||||
hooks:
|
||||
- id: setup-cfg-fmt
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
|
||||
@@ -45,7 +45,7 @@ these are all of the current key bindings in babi
|
||||
- <kbd>tab</kbd> / <kbd>shift-tab</kbd>: indent or dedent current line (or
|
||||
selection)
|
||||
- <kbd>^K</kbd> / <kbd>^U</kbd>: cut and uncut the current line (or selection)
|
||||
- <kbd>M-u</kbd> / <kbd>M-U</kbd> or <kbd>M-e</kbd>: undo / redo
|
||||
- <kbd>M-u</kbd> / <kbd>M-U</kbd>: undo / redo
|
||||
- <kbd>^W</kbd>: search
|
||||
- <kbd>^\\</kbd>: search and replace
|
||||
- <kbd>^C</kbd>: show the current position in the file
|
||||
|
||||
24
babi/buf.py
24
babi/buf.py
@@ -130,7 +130,7 @@ class Buf:
|
||||
return victim
|
||||
|
||||
def restore_eof_invariant(self) -> None:
|
||||
"""the file lines will always contain a blank empty string at the end'
|
||||
"""the file lines will always contain a blank empty string at the end
|
||||
to simplify rendering. call this whenever the last line may change
|
||||
"""
|
||||
if self[-1] != '':
|
||||
@@ -237,8 +237,10 @@ class Buf:
|
||||
# rendered lines
|
||||
|
||||
def rendered_line(self, idx: int, margin: Margin) -> str:
|
||||
x = self._cursor_x if idx == self.y else 0
|
||||
return scrolled_line(self._lines[idx].expandtabs(4), x, margin.cols)
|
||||
line = self._lines[idx]
|
||||
positions = self.line_positions(idx)
|
||||
cursor_x = self._cursor_x if idx == self.y else 0
|
||||
return scrolled_line(line, positions, cursor_x, margin.cols)
|
||||
|
||||
# movement
|
||||
|
||||
@@ -273,7 +275,7 @@ class Buf:
|
||||
if self.x >= len(self._lines[self.y]):
|
||||
if self.y < len(self._lines) - 1:
|
||||
self.down(margin)
|
||||
self.x = 0
|
||||
self.home()
|
||||
else:
|
||||
self.x += 1
|
||||
|
||||
@@ -281,10 +283,16 @@ class Buf:
|
||||
if self.x == 0:
|
||||
if self.y > 0:
|
||||
self.up(margin)
|
||||
self.x = len(self._lines[self.y])
|
||||
self.end()
|
||||
else:
|
||||
self.x -= 1
|
||||
|
||||
def home(self) -> None:
|
||||
self.x = 0
|
||||
|
||||
def end(self) -> None:
|
||||
self.x = len(self._lines[self.y])
|
||||
|
||||
# screen movement
|
||||
|
||||
def file_up(self, margin: Margin) -> None:
|
||||
@@ -298,3 +306,9 @@ class Buf:
|
||||
self.file_y += 1
|
||||
if self.y < self.file_y:
|
||||
self.down(margin)
|
||||
|
||||
# key input
|
||||
|
||||
def c(self, s: str) -> None:
|
||||
self[self.y] = self[self.y][:self.x] + s + self[self.y][self.x:]
|
||||
self.x += len(s)
|
||||
|
||||
21
babi/file.py
21
babi/file.py
@@ -198,12 +198,10 @@ 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'
|
||||
@@ -217,12 +215,7 @@ class File:
|
||||
self.selection = Selection()
|
||||
self._file_hls: Tuple[FileHL, ...] = ()
|
||||
|
||||
def ensure_loaded(
|
||||
self,
|
||||
status: Status,
|
||||
margin: Margin,
|
||||
stdin: str,
|
||||
) -> None:
|
||||
def ensure_loaded(self, status: Status, stdin: str) -> None:
|
||||
if self.buf:
|
||||
return
|
||||
|
||||
@@ -264,8 +257,6 @@ 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}>'
|
||||
|
||||
@@ -289,11 +280,11 @@ class File:
|
||||
|
||||
@action
|
||||
def home(self, margin: Margin) -> None:
|
||||
self.buf.x = 0
|
||||
self.buf.home()
|
||||
|
||||
@action
|
||||
def end(self, margin: Margin) -> None:
|
||||
self.buf.x = len(self.buf[self.buf.y])
|
||||
self.buf.end()
|
||||
|
||||
@action
|
||||
def ctrl_up(self, margin: Margin) -> None:
|
||||
@@ -707,10 +698,8 @@ class File:
|
||||
|
||||
@edit_action('text', final=False)
|
||||
@clear_selection
|
||||
def c(self, wch: str, margin: Margin) -> None:
|
||||
s = self.buf[self.buf.y]
|
||||
self.buf[self.buf.y] = s[:self.buf.x] + wch + s[self.buf.x:]
|
||||
self.buf.x += len(wch)
|
||||
def c(self, wch: str) -> None:
|
||||
self.buf.c(wch)
|
||||
self.buf.restore_eof_invariant()
|
||||
|
||||
def finalize_previous_action(self) -> None:
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import bisect
|
||||
import curses
|
||||
from typing import Tuple
|
||||
|
||||
from babi.cached_property import cached_property
|
||||
|
||||
@@ -18,18 +20,37 @@ def line_x(x: int, width: int) -> int:
|
||||
)
|
||||
|
||||
|
||||
def scrolled_line(s: str, x: int, width: int) -> str:
|
||||
l_x = line_x(x, width)
|
||||
def scrolled_line(
|
||||
s: str,
|
||||
positions: Tuple[int, ...],
|
||||
cursor_x: int,
|
||||
width: int,
|
||||
) -> str:
|
||||
l_x = line_x(cursor_x, width)
|
||||
if l_x:
|
||||
s = f'«{s[l_x + 1:]}'
|
||||
if len(s) > width:
|
||||
return f'{s[:width - 1]}»'
|
||||
l_x_min = l_x + 1
|
||||
start = bisect.bisect_left(positions, l_x_min)
|
||||
pad_left = '«' * (positions[start] - l_x)
|
||||
|
||||
l_x_max = l_x + width
|
||||
if positions[-1] > l_x_max:
|
||||
end_max = l_x_max - 1
|
||||
end = bisect.bisect_left(positions, end_max)
|
||||
if positions[end] > end_max:
|
||||
end -= 1
|
||||
pad_right = '»' * (l_x_max - positions[end])
|
||||
return f'{pad_left}{s[start:end].expandtabs(4)}{pad_right}'
|
||||
else:
|
||||
return s.ljust(width)
|
||||
elif len(s) > width:
|
||||
return f'{s[:width - 1]}»'
|
||||
return f'{pad_left}{s[start:]}'.ljust(width)
|
||||
elif positions[-1] > width:
|
||||
end_max = width - 1
|
||||
end = bisect.bisect_left(positions, end_max)
|
||||
if positions[end] > end_max:
|
||||
end -= 1
|
||||
pad_right = '»' * (width - positions[end])
|
||||
return f'{s[:end].expandtabs(4)}{pad_right}'
|
||||
else:
|
||||
return s.ljust(width)
|
||||
return s.expandtabs(4).ljust(width)
|
||||
|
||||
|
||||
class _CalcWidth:
|
||||
|
||||
95
babi/main.py
95
babi/main.py
@@ -1,12 +1,9 @@
|
||||
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
|
||||
@@ -17,11 +14,10 @@ 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, screen.margin, stdin)
|
||||
screen.file.ensure_loaded(screen.status, stdin)
|
||||
|
||||
while True:
|
||||
screen.status.tick(screen.margin)
|
||||
@@ -37,43 +33,42 @@ def _edit(screen: Screen, stdin: str) -> EditResult:
|
||||
return ret
|
||||
elif key.keyname == b'STRING':
|
||||
assert isinstance(key.wch, str), key.wch
|
||||
screen.file.c(key.wch, screen.margin)
|
||||
screen.file.c(key.wch)
|
||||
else:
|
||||
screen.status.update(f'unknown key: {key}')
|
||||
|
||||
|
||||
def c_main(
|
||||
stdscr: 'curses._CursesWindow',
|
||||
filenames: List[Optional[str]],
|
||||
positions: List[int],
|
||||
args: argparse.Namespace,
|
||||
stdin: str,
|
||||
perf: Perf,
|
||||
) -> int:
|
||||
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}')
|
||||
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}')
|
||||
return 0
|
||||
|
||||
|
||||
def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int:
|
||||
screen = Screen(stdscr, ['<<key debug>>'], [0], perf)
|
||||
def _key_debug(stdscr: 'curses._CursesWindow') -> int:
|
||||
screen = Screen(stdscr, ['<<key debug>>'], Perf())
|
||||
screen.file.buf = Buf([''])
|
||||
|
||||
while True:
|
||||
@@ -90,37 +85,6 @@ def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> 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='*')
|
||||
@@ -138,12 +102,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
else:
|
||||
stdin = ''
|
||||
|
||||
with perf_log(args.perf_log) as perf, make_stdscr() as stdscr:
|
||||
with make_stdscr() as stdscr:
|
||||
if args.key_debug:
|
||||
return _key_debug(stdscr, perf)
|
||||
return _key_debug(stdscr)
|
||||
else:
|
||||
filenames, positions = _filenames(args.filenames)
|
||||
return c_main(stdscr, filenames, positions, stdin, perf)
|
||||
return c_main(stdscr, args, stdin)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -6,8 +6,7 @@ from typing import Tuple
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
|
||||
from babi.horizontal_scrolling import line_x
|
||||
from babi.horizontal_scrolling import scrolled_line
|
||||
from babi.buf import Buf
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from babi.main import Screen # XXX: circular
|
||||
@@ -19,17 +18,25 @@ class Prompt:
|
||||
def __init__(self, screen: 'Screen', prompt: str, lst: List[str]) -> None:
|
||||
self._screen = screen
|
||||
self._prompt = prompt
|
||||
self._lst = lst
|
||||
self._y = len(lst) - 1
|
||||
self._buf = Buf(lst)
|
||||
self._buf.y = self._buf.file_y = len(lst) - 1
|
||||
self._x = len(self._s)
|
||||
|
||||
@property
|
||||
def _x(self) -> int:
|
||||
return self._buf.x
|
||||
|
||||
@_x.setter
|
||||
def _x(self, x: int) -> None:
|
||||
self._buf.x = x
|
||||
|
||||
@property
|
||||
def _s(self) -> str:
|
||||
return self._lst[self._y]
|
||||
return self._buf[self._buf.y]
|
||||
|
||||
@_s.setter
|
||||
def _s(self, s: str) -> None:
|
||||
self._lst[self._y] = s
|
||||
self._buf[self._buf.y] = s
|
||||
|
||||
def _render_prompt(self, *, base: Optional[str] = None) -> None:
|
||||
base = base or self._prompt
|
||||
@@ -39,24 +46,26 @@ class Prompt:
|
||||
prompt_s = f'{base[:self._screen.margin.cols - 7]}…: '
|
||||
else:
|
||||
prompt_s = f'{base}: '
|
||||
|
||||
width = self._screen.margin.cols - len(prompt_s)
|
||||
line = scrolled_line(self._s, self._x, width)
|
||||
cmd = f'{prompt_s}{line}'
|
||||
margin = self._screen.margin._replace(cols=width)
|
||||
cmd = f'{prompt_s}{self._buf.rendered_line(self._buf.y, margin)}'
|
||||
prompt_line = self._screen.margin.lines - 1
|
||||
self._screen.stdscr.insstr(prompt_line, 0, cmd, curses.A_REVERSE)
|
||||
x = len(prompt_s) + self._x - line_x(self._x, width)
|
||||
self._screen.stdscr.move(prompt_line, x)
|
||||
|
||||
_, x_off = self._buf.cursor_position(margin)
|
||||
self._screen.stdscr.move(prompt_line, len(prompt_s) + x_off)
|
||||
|
||||
def _up(self) -> None:
|
||||
self._y = max(0, self._y - 1)
|
||||
self._x = len(self._s)
|
||||
self._buf.up(self._screen.margin)
|
||||
self._x = len(self._buf[self._buf.y])
|
||||
|
||||
def _down(self) -> None:
|
||||
self._y = min(len(self._lst) - 1, self._y + 1)
|
||||
self._x = len(self._s)
|
||||
self._buf.down(self._screen.margin)
|
||||
self._x = len(self._buf[self._buf.y])
|
||||
|
||||
def _right(self) -> None:
|
||||
self._x = min(len(self._s), self._x + 1)
|
||||
self._x = min(len(self._buf[self._buf.y]), self._x + 1)
|
||||
|
||||
def _left(self) -> None:
|
||||
self._x = max(0, self._x - 1)
|
||||
@@ -65,11 +74,11 @@ class Prompt:
|
||||
self._x = 0
|
||||
|
||||
def _end(self) -> None:
|
||||
self._x = len(self._s)
|
||||
self._x = len(self._buf[self._buf.y])
|
||||
|
||||
def _ctrl_left(self) -> None:
|
||||
if self._x <= 1:
|
||||
self._x = 0
|
||||
self._buf.home()
|
||||
else:
|
||||
self._x -= 1
|
||||
tp = self._s[self._x - 1].isalnum()
|
||||
@@ -78,7 +87,7 @@ class Prompt:
|
||||
|
||||
def _ctrl_right(self) -> None:
|
||||
if self._x >= len(self._s) - 1:
|
||||
self._x = len(self._s)
|
||||
self._buf.end()
|
||||
else:
|
||||
self._x += 1
|
||||
tp = self._s[self._x].isalnum()
|
||||
@@ -103,9 +112,9 @@ class Prompt:
|
||||
def _check_failed(self, idx: int, s: str) -> Tuple[bool, int]:
|
||||
failed = False
|
||||
for search_idx in range(idx, -1, -1):
|
||||
if s in self._lst[search_idx]:
|
||||
idx = self._y = search_idx
|
||||
self._x = self._lst[search_idx].index(s)
|
||||
if s in self._buf[search_idx]:
|
||||
idx = self._buf.y = search_idx
|
||||
self._x = self._buf[search_idx].index(s)
|
||||
break
|
||||
else:
|
||||
failed = True
|
||||
@@ -113,7 +122,7 @@ class Prompt:
|
||||
|
||||
def _reverse_search(self) -> Union[None, str, PromptResult]:
|
||||
reverse_s = ''
|
||||
idx = self._y
|
||||
idx = self._buf.y
|
||||
while True:
|
||||
fail, idx = self._check_failed(idx, reverse_s)
|
||||
|
||||
@@ -174,8 +183,7 @@ class Prompt:
|
||||
}
|
||||
|
||||
def _c(self, c: str) -> None:
|
||||
self._s = self._s[:self._x] + c + self._s[self._x:]
|
||||
self._x += len(c)
|
||||
self._buf.c(c)
|
||||
|
||||
def run(self) -> Union[PromptResult, str]:
|
||||
while True:
|
||||
|
||||
@@ -81,11 +81,8 @@ KEYNAME_REWRITE = {
|
||||
b'CTL_DOWN': b'kDN5',
|
||||
b'CTL_RIGHT': b'kRIT5',
|
||||
b'CTL_LEFT': b'kLFT5',
|
||||
b'CTL_HOME': b'kHOM5',
|
||||
b'CTL_END': b'kEND5',
|
||||
b'ALT_RIGHT': b'kRIT3',
|
||||
b'ALT_LEFT': b'kLFT3',
|
||||
b'ALT_E': b'M-e',
|
||||
# windows-curses: idk why these are different
|
||||
b'KEY_SUP': b'KEY_SR',
|
||||
b'KEY_SDOWN': b'KEY_SF',
|
||||
@@ -93,7 +90,6 @@ KEYNAME_REWRITE = {
|
||||
b'^?': b'KEY_BACKSPACE',
|
||||
# linux, perhaps others
|
||||
b'^H': b'KEY_BACKSPACE', # ^Backspace on my keyboard
|
||||
b'PADENTER': b'^M', # Enter on numpad
|
||||
}
|
||||
|
||||
|
||||
@@ -107,15 +103,14 @@ 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, line, self.color_manager, self.hl_factories)
|
||||
for filename, line in zip(filenames, initial_lines)
|
||||
File(filename, self.color_manager, self.hl_factories)
|
||||
for filename in filenames
|
||||
]
|
||||
self.i = 0
|
||||
self.history = History()
|
||||
@@ -494,7 +489,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, 0, self.color_manager, self.hl_factories)
|
||||
opened = File(response, self.color_manager, self.hl_factories)
|
||||
self.files.append(opened)
|
||||
return EditResult.OPEN
|
||||
else:
|
||||
@@ -518,13 +513,10 @@ class Screen:
|
||||
return EditResult.EXIT
|
||||
|
||||
def background(self) -> None:
|
||||
if sys.platform == 'win32': # pragma: win32 cover
|
||||
self.status.update('cannot run babi in background on Windows')
|
||||
else: # pragma: win32 no cover
|
||||
curses.endwin()
|
||||
os.kill(os.getpid(), signal.SIGSTOP)
|
||||
self.stdscr = _init_screen()
|
||||
self.resize()
|
||||
curses.endwin()
|
||||
os.kill(os.getpid(), signal.SIGSTOP)
|
||||
self.stdscr = _init_screen()
|
||||
self.resize()
|
||||
|
||||
DISPATCH = {
|
||||
b'KEY_RESIZE': resize,
|
||||
@@ -534,7 +526,6 @@ class Screen:
|
||||
b'^U': uncut,
|
||||
b'M-u': undo,
|
||||
b'M-U': redo,
|
||||
b'M-e': redo,
|
||||
b'^W': search,
|
||||
b'^\\': replace,
|
||||
b'^[': command,
|
||||
|
||||
@@ -3,3 +3,4 @@ coverage
|
||||
git+https://github.com/asottile/hecate@875567f
|
||||
pytest
|
||||
remote-pdb
|
||||
wcwidth
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = babi
|
||||
version = 0.0.9
|
||||
version = 0.0.7
|
||||
description = a text editor
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import Union
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import wcwidth
|
||||
|
||||
from babi._types import Protocol
|
||||
from babi.main import main
|
||||
@@ -71,7 +72,7 @@ class Screen:
|
||||
self.attrs[y] = line_attr[:x] + new + line_attr[x + len(s):]
|
||||
|
||||
self.y = y
|
||||
self.x = x + len(s)
|
||||
self.x = x + wcwidth.wcswidth(s)
|
||||
|
||||
def insstr(self, y, x, s, attr):
|
||||
line = self.lines[y]
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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)
|
||||
@@ -415,10 +415,18 @@ def test_sequence_handling(run_only_fake):
|
||||
|
||||
def test_indentation_using_tabs(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write(f'123456789\n\t12\t{"x" * 20}\n')
|
||||
f.write(
|
||||
f'123456789\n'
|
||||
f'\t12\t{"x" * 20}\n'
|
||||
f'\tnot long\n',
|
||||
)
|
||||
|
||||
with run(str(f), width=20) as h, and_exit(h):
|
||||
h.await_text('123456789\n 12 xxxxxxxxxxx»\n')
|
||||
h.await_text(
|
||||
'123456789\n'
|
||||
' 12 xxxxxxxxxxx»\n'
|
||||
' not long\n',
|
||||
)
|
||||
|
||||
h.press('Down')
|
||||
h.await_cursor_position(x=0, y=2)
|
||||
@@ -438,3 +446,43 @@ def test_indentation_using_tabs(run, tmpdir):
|
||||
h.await_cursor_position(x=4, y=2)
|
||||
h.press('Up')
|
||||
h.await_cursor_position(x=4, y=1)
|
||||
|
||||
|
||||
def test_movement_with_wide_characters(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write(
|
||||
f'{"🙃" * 20}\n'
|
||||
f'a{"🙃" * 20}\n',
|
||||
)
|
||||
|
||||
with run(str(f), width=20) as h, and_exit(h):
|
||||
h.await_text(
|
||||
'🙃🙃🙃🙃🙃🙃🙃🙃🙃»»\n'
|
||||
'a🙃🙃🙃🙃🙃🙃🙃🙃🙃»\n',
|
||||
)
|
||||
|
||||
for _ in range(10):
|
||||
h.press('Right')
|
||||
h.await_text(
|
||||
'««🙃🙃🙃🙃🙃🙃🙃🙃»»\n'
|
||||
'a🙃🙃🙃🙃🙃🙃🙃🙃🙃»\n',
|
||||
)
|
||||
|
||||
for _ in range(6):
|
||||
h.press('Right')
|
||||
h.await_text(
|
||||
'««🙃🙃🙃🙃🙃🙃🙃\n'
|
||||
'a🙃🙃🙃🙃🙃🙃🙃🙃🙃»\n',
|
||||
)
|
||||
|
||||
h.press('Down')
|
||||
h.await_text(
|
||||
'🙃🙃🙃🙃🙃🙃🙃🙃🙃»»\n'
|
||||
'«🙃🙃🙃🙃🙃🙃🙃🙃\n',
|
||||
)
|
||||
|
||||
h.press('Left')
|
||||
h.await_text(
|
||||
'🙃🙃🙃🙃🙃🙃🙃🙃🙃»»\n'
|
||||
'«🙃🙃🙃🙃🙃🙃🙃🙃🙃»\n',
|
||||
)
|
||||
|
||||
@@ -133,7 +133,7 @@ def test_save_via_ctrl_o(run, tmpdir):
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
h.press('hello world')
|
||||
h.press('^O')
|
||||
h.await_text('enter filename: ')
|
||||
h.await_text(f'enter filename: ')
|
||||
h.press('Enter')
|
||||
h.await_text('saved! (1 line written)')
|
||||
assert f.read() == 'hello world\n'
|
||||
@@ -237,7 +237,7 @@ def test_vim_save_on_exit(run, tmpdir):
|
||||
h.press_and_enter(':q')
|
||||
h.await_text('file is modified - save [yes, no]?')
|
||||
h.press('y')
|
||||
h.await_text('enter filename: ')
|
||||
h.await_text(f'enter filename: ')
|
||||
h.press('Enter')
|
||||
h.await_exit()
|
||||
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import pytest
|
||||
|
||||
from testing.runner import and_exit
|
||||
|
||||
|
||||
@@ -11,8 +9,7 @@ def test_nothing_to_undo_redo(run):
|
||||
h.await_text('nothing to redo!')
|
||||
|
||||
|
||||
@pytest.mark.parametrize('r', ('M-U', 'M-e'))
|
||||
def test_undo_redo(run, r):
|
||||
def test_undo_redo(run):
|
||||
with run() as h, and_exit(h):
|
||||
h.press('hello')
|
||||
h.await_text('hello')
|
||||
@@ -20,7 +17,7 @@ def test_undo_redo(run, r):
|
||||
h.await_text('undo: text')
|
||||
h.await_text_missing('hello')
|
||||
h.await_text_missing(' *')
|
||||
h.press(r)
|
||||
h.press('M-U')
|
||||
h.await_text('redo: text')
|
||||
h.await_text('hello')
|
||||
h.await_text(' *')
|
||||
|
||||
@@ -8,7 +8,7 @@ from babi.file import get_lines
|
||||
|
||||
|
||||
def test_position_repr():
|
||||
ret = repr(File('f.txt', 0, ColorManager.make(), ()))
|
||||
ret = repr(File('f.txt', ColorManager.make(), ()))
|
||||
assert ret == "<File 'f.txt'>"
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
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