Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b55ebfd0e | ||
|
|
7d1e61f734 | ||
|
|
3e7ca8e922 | ||
|
|
843f1b6ff1 | ||
|
|
f704505ee2 | ||
|
|
b595333fc6 | ||
|
|
486af96c12 | ||
|
|
8b71d289a3 | ||
|
|
759cadd868 |
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)
|
||||
|
||||
@@ -3,8 +3,10 @@ from typing import Iterable
|
||||
from typing import Mapping
|
||||
from typing import TypeVar
|
||||
|
||||
TKey = TypeVar('TKey')
|
||||
TValue = TypeVar('TValue')
|
||||
from babi._types import Protocol
|
||||
|
||||
TKey = TypeVar('TKey', contravariant=True)
|
||||
TValue = TypeVar('TValue', covariant=True)
|
||||
|
||||
|
||||
class FDict(Generic[TKey, TValue]):
|
||||
@@ -22,3 +24,21 @@ class FDict(Generic[TKey, TValue]):
|
||||
|
||||
def values(self) -> Iterable[TValue]:
|
||||
return self._dct.values()
|
||||
|
||||
|
||||
class Indexable(Generic[TKey, TValue], Protocol):
|
||||
def __getitem__(self, key: TKey) -> TValue: ...
|
||||
|
||||
|
||||
class FChainMap(Generic[TKey, TValue]):
|
||||
def __init__(self, *mappings: Indexable[TKey, TValue]) -> None:
|
||||
self._mappings = mappings
|
||||
|
||||
def __getitem__(self, key: TKey) -> TValue:
|
||||
for mapping in reversed(self._mappings):
|
||||
try:
|
||||
return mapping[key]
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
raise KeyError(key)
|
||||
|
||||
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:
|
||||
|
||||
@@ -14,7 +14,7 @@ from typing import TypeVar
|
||||
from identify.identify import tags_from_filename
|
||||
|
||||
from babi._types import Protocol
|
||||
from babi.fdict import FDict
|
||||
from babi.fdict import FChainMap
|
||||
from babi.reg import _Reg
|
||||
from babi.reg import _RegSet
|
||||
from babi.reg import ERR_REG
|
||||
@@ -67,6 +67,8 @@ class _Rule(Protocol):
|
||||
def include(self) -> Optional[str]: ...
|
||||
@property
|
||||
def patterns(self) -> 'Tuple[_Rule, ...]': ...
|
||||
@property
|
||||
def repository(self) -> 'FChainMap[str, _Rule]': ...
|
||||
|
||||
|
||||
@uniquely_constructed
|
||||
@@ -83,9 +85,24 @@ class Rule(NamedTuple):
|
||||
while_captures: Captures
|
||||
include: Optional[str]
|
||||
patterns: Tuple[_Rule, ...]
|
||||
repository: FChainMap[str, _Rule]
|
||||
|
||||
@classmethod
|
||||
def from_dct(cls, dct: Dict[str, Any]) -> _Rule:
|
||||
def make(
|
||||
cls,
|
||||
dct: Dict[str, Any],
|
||||
parent_repository: FChainMap[str, _Rule],
|
||||
) -> _Rule:
|
||||
if 'repository' in dct:
|
||||
# this looks odd, but it's so we can have a self-referential
|
||||
# immutable-after-construction chain map
|
||||
repository_dct: Dict[str, _Rule] = {}
|
||||
repository = FChainMap(parent_repository, repository_dct)
|
||||
for k, sub_dct in dct['repository'].items():
|
||||
repository_dct[k] = Rule.make(sub_dct, repository)
|
||||
else:
|
||||
repository = parent_repository
|
||||
|
||||
name = _split_name(dct.get('name'))
|
||||
match = dct.get('match')
|
||||
begin = dct.get('begin')
|
||||
@@ -95,7 +112,7 @@ class Rule(NamedTuple):
|
||||
|
||||
if 'captures' in dct:
|
||||
captures = tuple(
|
||||
(int(k), Rule.from_dct(v))
|
||||
(int(k), Rule.make(v, repository))
|
||||
for k, v in dct['captures'].items()
|
||||
)
|
||||
else:
|
||||
@@ -103,7 +120,7 @@ class Rule(NamedTuple):
|
||||
|
||||
if 'beginCaptures' in dct:
|
||||
begin_captures = tuple(
|
||||
(int(k), Rule.from_dct(v))
|
||||
(int(k), Rule.make(v, repository))
|
||||
for k, v in dct['beginCaptures'].items()
|
||||
)
|
||||
else:
|
||||
@@ -111,7 +128,7 @@ class Rule(NamedTuple):
|
||||
|
||||
if 'endCaptures' in dct:
|
||||
end_captures = tuple(
|
||||
(int(k), Rule.from_dct(v))
|
||||
(int(k), Rule.make(v, repository))
|
||||
for k, v in dct['endCaptures'].items()
|
||||
)
|
||||
else:
|
||||
@@ -119,7 +136,7 @@ class Rule(NamedTuple):
|
||||
|
||||
if 'whileCaptures' in dct:
|
||||
while_captures = tuple(
|
||||
(int(k), Rule.from_dct(v))
|
||||
(int(k), Rule.make(v, repository))
|
||||
for k, v in dct['whileCaptures'].items()
|
||||
)
|
||||
else:
|
||||
@@ -141,7 +158,7 @@ class Rule(NamedTuple):
|
||||
include = dct.get('include')
|
||||
|
||||
if 'patterns' in dct:
|
||||
patterns = tuple(Rule.from_dct(d) for d in dct['patterns'])
|
||||
patterns = tuple(Rule.make(d, repository) for d in dct['patterns'])
|
||||
else:
|
||||
patterns = ()
|
||||
|
||||
@@ -158,29 +175,33 @@ class Rule(NamedTuple):
|
||||
while_captures=while_captures,
|
||||
include=include,
|
||||
patterns=patterns,
|
||||
repository=repository,
|
||||
)
|
||||
|
||||
|
||||
@uniquely_constructed
|
||||
class Grammar(NamedTuple):
|
||||
scope_name: str
|
||||
repository: FChainMap[str, _Rule]
|
||||
patterns: Tuple[_Rule, ...]
|
||||
repository: FDict[str, _Rule]
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Dict[str, Any]) -> 'Grammar':
|
||||
def make(cls, data: Dict[str, Any]) -> 'Grammar':
|
||||
scope_name = data['scopeName']
|
||||
patterns = tuple(Rule.from_dct(dct) for dct in data['patterns'])
|
||||
if 'repository' in data:
|
||||
repository = FDict({
|
||||
k: Rule.from_dct(dct) for k, dct in data['repository'].items()
|
||||
})
|
||||
# this looks odd, but it's so we can have a self-referential
|
||||
# immutable-after-construction chain map
|
||||
repository_dct: Dict[str, _Rule] = {}
|
||||
repository = FChainMap(repository_dct)
|
||||
for k, dct in data['repository'].items():
|
||||
repository_dct[k] = Rule.make(dct, repository)
|
||||
else:
|
||||
repository = FDict({})
|
||||
repository = FChainMap()
|
||||
patterns = tuple(Rule.make(d, repository) for d in data['patterns'])
|
||||
return cls(
|
||||
scope_name=scope_name,
|
||||
patterns=patterns,
|
||||
repository=repository,
|
||||
patterns=patterns,
|
||||
)
|
||||
|
||||
|
||||
@@ -530,22 +551,23 @@ class Compiler:
|
||||
def _include(
|
||||
self,
|
||||
grammar: Grammar,
|
||||
repository: FChainMap[str, _Rule],
|
||||
s: str,
|
||||
) -> Tuple[List[str], Tuple[_Rule, ...]]:
|
||||
if s == '$self':
|
||||
return self._patterns(grammar, grammar.patterns)
|
||||
elif s == '$base':
|
||||
grammar = self._grammars.grammar_for_scope(self._root_scope)
|
||||
return self._include(grammar, '$self')
|
||||
return self._include(grammar, grammar.repository, '$self')
|
||||
elif s.startswith('#'):
|
||||
return self._patterns(grammar, (grammar.repository[s[1:]],))
|
||||
return self._patterns(grammar, (repository[s[1:]],))
|
||||
elif '#' not in s:
|
||||
grammar = self._grammars.grammar_for_scope(s)
|
||||
return self._include(grammar, '$self')
|
||||
return self._include(grammar, grammar.repository, '$self')
|
||||
else:
|
||||
scope, _, s = s.partition('#')
|
||||
grammar = self._grammars.grammar_for_scope(scope)
|
||||
return self._include(grammar, f'#{s}')
|
||||
return self._include(grammar, grammar.repository, f'#{s}')
|
||||
|
||||
@functools.lru_cache(maxsize=None)
|
||||
def _patterns(
|
||||
@@ -557,7 +579,9 @@ class Compiler:
|
||||
ret_rules: List[_Rule] = []
|
||||
for rule in rules:
|
||||
if rule.include is not None:
|
||||
tmp_regs, tmp_rules = self._include(grammar, rule.include)
|
||||
tmp_regs, tmp_rules = self._include(
|
||||
grammar, rule.repository, rule.include,
|
||||
)
|
||||
ret_regs.extend(tmp_regs)
|
||||
ret_rules.extend(tmp_rules)
|
||||
elif rule.match is None and rule.begin is None and rule.patterns:
|
||||
@@ -633,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')
|
||||
}
|
||||
|
||||
@@ -669,7 +693,7 @@ class Grammars:
|
||||
pass
|
||||
|
||||
raw = self._raw_for_scope(scope)
|
||||
ret = self._parsed[scope] = Grammar.from_data(raw)
|
||||
ret = self._parsed[scope] = Grammar.make(raw)
|
||||
return ret
|
||||
|
||||
def compiler_for_scope(self, scope: str) -> Compiler:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -69,6 +69,11 @@ KEYNAME_REWRITE = {
|
||||
b'KEY_C2': b'KEY_DOWN',
|
||||
b'KEY_B3': b'KEY_RIGHT',
|
||||
b'KEY_B1': b'KEY_LEFT',
|
||||
b'PADSTOP': b'KEY_DC',
|
||||
b'KEY_A3': b'KEY_PPAGE',
|
||||
b'KEY_C3': b'KEY_NPAGE',
|
||||
b'KEY_A1': b'KEY_HOME',
|
||||
b'KEY_C1': b'KEY_END',
|
||||
# windows-curses: map to our M- names
|
||||
b'ALT_U': b'M-u',
|
||||
# windows-curses: arguably these names are better than the xterm names
|
||||
|
||||
@@ -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.5
|
||||
version = 0.0.7
|
||||
description = a text editor
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from babi.fdict import FChainMap
|
||||
from babi.fdict import FDict
|
||||
|
||||
|
||||
@@ -5,3 +8,21 @@ def test_fdict_repr():
|
||||
# mostly because this shouldn't get hit elsewhere but is uesful for
|
||||
# debugging purposes
|
||||
assert repr(FDict({1: 2, 3: 4})) == 'FDict({1: 2, 3: 4})'
|
||||
|
||||
|
||||
def test_f_chain_map():
|
||||
chain_map = FChainMap({1: 2}, {3: 4}, FDict({1: 5}))
|
||||
assert chain_map[1] == 5
|
||||
assert chain_map[3] == 4
|
||||
|
||||
with pytest.raises(KeyError) as excinfo:
|
||||
chain_map[2]
|
||||
k, = excinfo.value.args
|
||||
assert k == 2
|
||||
|
||||
|
||||
def test_f_chain_map_extend():
|
||||
chain_map = FChainMap({1: 2})
|
||||
assert chain_map[1] == 2
|
||||
chain_map = FChainMap(chain_map, {1: 5})
|
||||
assert chain_map[1] == 5
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
@@ -441,6 +441,38 @@ def test_include_repository_rule(compiler_state):
|
||||
)
|
||||
|
||||
|
||||
def test_include_with_nested_repositories(compiler_state):
|
||||
compiler, state = compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [{
|
||||
'begin': '<', 'end': '>', 'name': 'b',
|
||||
'patterns': [
|
||||
{'include': '#rule1'},
|
||||
{'include': '#rule2'},
|
||||
{'include': '#rule3'},
|
||||
],
|
||||
'repository': {
|
||||
'rule2': {'match': '2', 'name': 'inner2'},
|
||||
'rule3': {'match': '3', 'name': 'inner3'},
|
||||
},
|
||||
}],
|
||||
'repository': {
|
||||
'rule1': {'match': '1', 'name': 'root1'},
|
||||
'rule2': {'match': '2', 'name': 'root2'},
|
||||
},
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, '<123>', first_line=True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 1, ('test', 'b')),
|
||||
Region(1, 2, ('test', 'b', 'root1')),
|
||||
Region(2, 3, ('test', 'b', 'inner2')),
|
||||
Region(3, 4, ('test', 'b', 'inner3')),
|
||||
Region(4, 5, ('test', 'b')),
|
||||
)
|
||||
|
||||
|
||||
def test_include_other_grammar(compiler_state):
|
||||
compiler, state = compiler_state(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user