Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b55ebfd0e | ||
|
|
7d1e61f734 | ||
|
|
3e7ca8e922 | ||
|
|
843f1b6ff1 |
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)
|
||||
|
||||
10
babi/file.py
10
babi/file.py
@@ -280,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:
|
||||
@@ -698,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:
|
||||
|
||||
@@ -657,7 +657,7 @@ class Grammars:
|
||||
os.path.splitext(filename)[0]: os.path.join(directory, filename)
|
||||
for directory in directories
|
||||
if os.path.exists(directory)
|
||||
for filename in os.listdir(directory)
|
||||
for filename in sorted(os.listdir(directory))
|
||||
if filename.endswith('.json')
|
||||
}
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -33,7 +33,7 @@ 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}')
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -39,7 +39,6 @@ def json_with_comments(s: bytes) -> Any:
|
||||
idx = match.end()
|
||||
match = TOKEN.search(s, idx)
|
||||
|
||||
print(bio.getvalue())
|
||||
bio.seek(0)
|
||||
return json.load(bio)
|
||||
|
||||
|
||||
@@ -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.6
|
||||
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]
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user