1 Commits
v0.0.17 ... wc

Author SHA1 Message Date
Anthony Sottile
9b55ebfd0e wip 2020-04-17 17:09:16 -07:00
8 changed files with 139 additions and 48 deletions

View File

@@ -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)

View File

@@ -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:

View File

@@ -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:

View File

@@ -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}')

View File

@@ -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:

View File

@@ -3,3 +3,4 @@ coverage
git+https://github.com/asottile/hecate@875567f
pytest
remote-pdb
wcwidth

View File

@@ -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]

View File

@@ -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',
)