9 Commits
v0.0.5 ... 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
Anthony Sottile
f704505ee2 v0.0.6 2020-04-04 13:04:34 -07:00
Anthony Sottile
b595333fc6 Fix grammars where rules have local repositorys
for example: ruby
2020-04-04 13:03:33 -07:00
Anthony Sottile
486af96c12 Merge pull request #53 from brynphillips/PS-key-fix
Ps key fix
2020-04-03 10:36:12 -07:00
Bryn Phillips
8b71d289a3 Fixed PgDn 2020-04-03 10:28:40 -07:00
Bryn Phillips
759cadd868 Fixes for Win PS keys 2020-04-03 10:26:17 -07:00
15 changed files with 266 additions and 74 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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -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(
{