4 Commits
v0.0.6 ... wc

Author SHA1 Message Date
Anthony Sottile
9b55ebfd0e wip 2020-04-17 17:09:16 -07:00
Anthony Sottile
7d1e61f734 v0.0.7 2020-04-04 17:44:40 -07:00
Anthony Sottile
3e7ca8e922 make grammar loading more deterministic 2020-04-04 17:44:27 -07:00
Anthony Sottile
843f1b6ff1 remove stray print 2020-04-04 13:13:53 -07:00
11 changed files with 141 additions and 51 deletions

View File

@@ -130,7 +130,7 @@ class Buf:
return victim return victim
def restore_eof_invariant(self) -> None: 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 to simplify rendering. call this whenever the last line may change
""" """
if self[-1] != '': if self[-1] != '':
@@ -237,8 +237,10 @@ class Buf:
# rendered lines # rendered lines
def rendered_line(self, idx: int, margin: Margin) -> str: def rendered_line(self, idx: int, margin: Margin) -> str:
x = self._cursor_x if idx == self.y else 0 line = self._lines[idx]
return scrolled_line(self._lines[idx].expandtabs(4), x, margin.cols) 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 # movement
@@ -273,7 +275,7 @@ class Buf:
if self.x >= len(self._lines[self.y]): if self.x >= len(self._lines[self.y]):
if self.y < len(self._lines) - 1: if self.y < len(self._lines) - 1:
self.down(margin) self.down(margin)
self.x = 0 self.home()
else: else:
self.x += 1 self.x += 1
@@ -281,10 +283,16 @@ class Buf:
if self.x == 0: if self.x == 0:
if self.y > 0: if self.y > 0:
self.up(margin) self.up(margin)
self.x = len(self._lines[self.y]) self.end()
else: else:
self.x -= 1 self.x -= 1
def home(self) -> None:
self.x = 0
def end(self) -> None:
self.x = len(self._lines[self.y])
# screen movement # screen movement
def file_up(self, margin: Margin) -> None: def file_up(self, margin: Margin) -> None:
@@ -298,3 +306,9 @@ class Buf:
self.file_y += 1 self.file_y += 1
if self.y < self.file_y: if self.y < self.file_y:
self.down(margin) 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 @action
def home(self, margin: Margin) -> None: def home(self, margin: Margin) -> None:
self.buf.x = 0 self.buf.home()
@action @action
def end(self, margin: Margin) -> None: def end(self, margin: Margin) -> None:
self.buf.x = len(self.buf[self.buf.y]) self.buf.end()
@action @action
def ctrl_up(self, margin: Margin) -> None: def ctrl_up(self, margin: Margin) -> None:
@@ -698,10 +698,8 @@ class File:
@edit_action('text', final=False) @edit_action('text', final=False)
@clear_selection @clear_selection
def c(self, wch: str, margin: Margin) -> None: def c(self, wch: str) -> None:
s = self.buf[self.buf.y] self.buf.c(wch)
self.buf[self.buf.y] = s[:self.buf.x] + wch + s[self.buf.x:]
self.buf.x += len(wch)
self.buf.restore_eof_invariant() self.buf.restore_eof_invariant()
def finalize_previous_action(self) -> None: def finalize_previous_action(self) -> None:

View File

@@ -657,7 +657,7 @@ class Grammars:
os.path.splitext(filename)[0]: os.path.join(directory, filename) os.path.splitext(filename)[0]: os.path.join(directory, filename)
for directory in directories for directory in directories
if os.path.exists(directory) if os.path.exists(directory)
for filename in os.listdir(directory) for filename in sorted(os.listdir(directory))
if filename.endswith('.json') if filename.endswith('.json')
} }

View File

@@ -1,4 +1,6 @@
import bisect
import curses import curses
from typing import Tuple
from babi.cached_property import cached_property 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: def scrolled_line(
l_x = line_x(x, width) s: str,
positions: Tuple[int, ...],
cursor_x: int,
width: int,
) -> str:
l_x = line_x(cursor_x, width)
if l_x: if l_x:
s = f'«{s[l_x + 1:]}' l_x_min = l_x + 1
if len(s) > width: start = bisect.bisect_left(positions, l_x_min)
return f'{s[:width - 1]}»' 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: else:
return s.ljust(width) return f'{pad_left}{s[start:]}'.ljust(width)
elif len(s) > width: elif positions[-1] > width:
return f'{s[:width - 1]}»' 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: else:
return s.ljust(width) return s.expandtabs(4).ljust(width)
class _CalcWidth: class _CalcWidth:

View File

@@ -33,7 +33,7 @@ def _edit(screen: Screen, stdin: str) -> EditResult:
return ret return ret
elif key.keyname == b'STRING': elif key.keyname == b'STRING':
assert isinstance(key.wch, str), key.wch assert isinstance(key.wch, str), key.wch
screen.file.c(key.wch, screen.margin) screen.file.c(key.wch)
else: else:
screen.status.update(f'unknown key: {key}') 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 TYPE_CHECKING
from typing import Union from typing import Union
from babi.horizontal_scrolling import line_x from babi.buf import Buf
from babi.horizontal_scrolling import scrolled_line
if TYPE_CHECKING: if TYPE_CHECKING:
from babi.main import Screen # XXX: circular from babi.main import Screen # XXX: circular
@@ -19,17 +18,25 @@ class Prompt:
def __init__(self, screen: 'Screen', prompt: str, lst: List[str]) -> None: def __init__(self, screen: 'Screen', prompt: str, lst: List[str]) -> None:
self._screen = screen self._screen = screen
self._prompt = prompt self._prompt = prompt
self._lst = lst self._buf = Buf(lst)
self._y = len(lst) - 1 self._buf.y = self._buf.file_y = len(lst) - 1
self._x = len(self._s) 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 @property
def _s(self) -> str: def _s(self) -> str:
return self._lst[self._y] return self._buf[self._buf.y]
@_s.setter @_s.setter
def _s(self, s: str) -> None: 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: def _render_prompt(self, *, base: Optional[str] = None) -> None:
base = base or self._prompt base = base or self._prompt
@@ -39,24 +46,26 @@ class Prompt:
prompt_s = f'{base[:self._screen.margin.cols - 7]}…: ' prompt_s = f'{base[:self._screen.margin.cols - 7]}…: '
else: else:
prompt_s = f'{base}: ' prompt_s = f'{base}: '
width = self._screen.margin.cols - len(prompt_s) width = self._screen.margin.cols - len(prompt_s)
line = scrolled_line(self._s, self._x, width) margin = self._screen.margin._replace(cols=width)
cmd = f'{prompt_s}{line}' cmd = f'{prompt_s}{self._buf.rendered_line(self._buf.y, margin)}'
prompt_line = self._screen.margin.lines - 1 prompt_line = self._screen.margin.lines - 1
self._screen.stdscr.insstr(prompt_line, 0, cmd, curses.A_REVERSE) 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: def _up(self) -> None:
self._y = max(0, self._y - 1) self._buf.up(self._screen.margin)
self._x = len(self._s) self._x = len(self._buf[self._buf.y])
def _down(self) -> None: def _down(self) -> None:
self._y = min(len(self._lst) - 1, self._y + 1) self._buf.down(self._screen.margin)
self._x = len(self._s) self._x = len(self._buf[self._buf.y])
def _right(self) -> None: 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: def _left(self) -> None:
self._x = max(0, self._x - 1) self._x = max(0, self._x - 1)
@@ -65,11 +74,11 @@ class Prompt:
self._x = 0 self._x = 0
def _end(self) -> None: def _end(self) -> None:
self._x = len(self._s) self._x = len(self._buf[self._buf.y])
def _ctrl_left(self) -> None: def _ctrl_left(self) -> None:
if self._x <= 1: if self._x <= 1:
self._x = 0 self._buf.home()
else: else:
self._x -= 1 self._x -= 1
tp = self._s[self._x - 1].isalnum() tp = self._s[self._x - 1].isalnum()
@@ -78,7 +87,7 @@ class Prompt:
def _ctrl_right(self) -> None: def _ctrl_right(self) -> None:
if self._x >= len(self._s) - 1: if self._x >= len(self._s) - 1:
self._x = len(self._s) self._buf.end()
else: else:
self._x += 1 self._x += 1
tp = self._s[self._x].isalnum() tp = self._s[self._x].isalnum()
@@ -103,9 +112,9 @@ class Prompt:
def _check_failed(self, idx: int, s: str) -> Tuple[bool, int]: def _check_failed(self, idx: int, s: str) -> Tuple[bool, int]:
failed = False failed = False
for search_idx in range(idx, -1, -1): for search_idx in range(idx, -1, -1):
if s in self._lst[search_idx]: if s in self._buf[search_idx]:
idx = self._y = search_idx idx = self._buf.y = search_idx
self._x = self._lst[search_idx].index(s) self._x = self._buf[search_idx].index(s)
break break
else: else:
failed = True failed = True
@@ -113,7 +122,7 @@ class Prompt:
def _reverse_search(self) -> Union[None, str, PromptResult]: def _reverse_search(self) -> Union[None, str, PromptResult]:
reverse_s = '' reverse_s = ''
idx = self._y idx = self._buf.y
while True: while True:
fail, idx = self._check_failed(idx, reverse_s) fail, idx = self._check_failed(idx, reverse_s)
@@ -174,8 +183,7 @@ class Prompt:
} }
def _c(self, c: str) -> None: def _c(self, c: str) -> None:
self._s = self._s[:self._x] + c + self._s[self._x:] self._buf.c(c)
self._x += len(c)
def run(self) -> Union[PromptResult, str]: def run(self) -> Union[PromptResult, str]:
while True: while True:

View File

@@ -39,7 +39,6 @@ def json_with_comments(s: bytes) -> Any:
idx = match.end() idx = match.end()
match = TOKEN.search(s, idx) match = TOKEN.search(s, idx)
print(bio.getvalue())
bio.seek(0) bio.seek(0)
return json.load(bio) return json.load(bio)

View File

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

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = babi name = babi
version = 0.0.6 version = 0.0.7
description = a text editor description = a text editor
long_description = file: README.md long_description = file: README.md
long_description_content_type = text/markdown long_description_content_type = text/markdown

View File

@@ -9,6 +9,7 @@ from typing import Union
from unittest import mock from unittest import mock
import pytest import pytest
import wcwidth
from babi._types import Protocol from babi._types import Protocol
from babi.main import main from babi.main import main
@@ -71,7 +72,7 @@ class Screen:
self.attrs[y] = line_attr[:x] + new + line_attr[x + len(s):] self.attrs[y] = line_attr[:x] + new + line_attr[x + len(s):]
self.y = y self.y = y
self.x = x + len(s) self.x = x + wcwidth.wcswidth(s)
def insstr(self, y, x, s, attr): def insstr(self, y, x, s, attr):
line = self.lines[y] line = self.lines[y]

View File

@@ -415,10 +415,18 @@ def test_sequence_handling(run_only_fake):
def test_indentation_using_tabs(run, tmpdir): def test_indentation_using_tabs(run, tmpdir):
f = tmpdir.join('f') 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): 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.press('Down')
h.await_cursor_position(x=0, y=2) 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.await_cursor_position(x=4, y=2)
h.press('Up') h.press('Up')
h.await_cursor_position(x=4, y=1) 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',
)