14 Commits

Author SHA1 Message Date
Anthony Sottile
33ff8d9726 v0.0.12 2020-07-13 13:33:59 -07:00
Anthony Sottile
f0b2af9a9f Merge pull request #77 from asottile/regex_flags
leverage new regex flags
2020-07-01 17:34:20 -07:00
Anthony Sottile
fc21a144aa leverage new regex flags 2020-07-01 17:07:32 -07:00
Anthony Sottile
973b4c3cf8 Merge pull request #76 from asottile/fix_background_on_close
fix race condition with ^Z on close
2020-06-29 13:37:14 -07:00
Anthony Sottile
bd60977438 fix race condition with ^Z on close 2020-06-29 13:13:14 -07:00
Anthony Sottile
144bbb9daf v0.0.11 2020-05-27 15:50:30 -07:00
Anthony Sottile
7c16cd966e Merge pull request #72 from asottile/pypy3_ci
re-enable pypy3 testing
2020-05-27 15:48:20 -07:00
Anthony Sottile
dd19b26fa2 re-enable pypy3 testing 2020-05-27 15:33:31 -07:00
Anthony Sottile
dca410dd44 Merge pull request #69 from YouTwitFace/add-tab-size
Add a vim style command to change the tab size
2020-05-27 15:31:06 -07:00
YouTwitFace
ed51b6e6dc Add :tabsize and :tabstop 2020-05-27 15:21:17 -07:00
Anthony Sottile
18b5e258f6 Merge pull request #71 from asottile/escdelay_tests
test py39
2020-05-26 11:41:15 -07:00
Anthony Sottile
e7108f843b test py39 2020-05-26 11:17:17 -07:00
Anthony Sottile
ff8d3f10fb Merge pull request #70 from asottile/asottile-patch-1
Fix typo in README
2020-05-26 11:09:22 -07:00
Anthony Sottile
8f603b8e14 Fix typo in README 2020-05-26 11:04:30 -07:00
11 changed files with 102 additions and 116 deletions

View File

@@ -87,7 +87,7 @@ this opens the file, displays it, and can be edited and can save! unknown keys
are displayed as errors in the status bar. babi will scroll if the cursor are displayed as errors in the status bar. babi will scroll if the cursor
goes off screen either from resize events or from movement. babi can edit goes off screen either from resize events or from movement. babi can edit
multiple files. babi has a command mode (so you can quit it like vim multiple files. babi has a command mode (so you can quit it like vim
<kbd>:q</kbd>!). babi also support syntax highlighting <kbd>:q</kbd>!). babi also supports syntax highlighting
![](https://i.fluffy.cc/5WFZBJ4mWs7wtThD9strQnGlJqw4Z9KS.png) ![](https://i.fluffy.cc/5WFZBJ4mWs7wtThD9strQnGlJqw4Z9KS.png)

View File

@@ -10,11 +10,11 @@ resources:
type: github type: github
endpoint: github endpoint: github
name: asottile/azure-pipeline-templates name: asottile/azure-pipeline-templates
ref: refs/tags/v1.0.0 ref: refs/tags/v2.0.0
jobs: jobs:
- template: job--pre-commit.yml@asottile - template: job--pre-commit.yml@asottile
- template: job--python-tox.yml@asottile - template: job--python-tox.yml@asottile
parameters: parameters:
toxenvs: [py36, py37, py38] toxenvs: [pypy3, py36, py37, py38, py39]
os: linux os: linux

View File

@@ -19,11 +19,11 @@ DelCallback = Callable[['Buf', int, str], None]
InsCallback = Callable[['Buf', int], None] InsCallback = Callable[['Buf', int], None]
def _offsets(s: str) -> Tuple[int, ...]: def _offsets(s: str, tab_size: int) -> Tuple[int, ...]:
ret = [0] ret = [0]
for c in s: for c in s:
if c == '\t': if c == '\t':
ret.append(ret[-1] + (4 - ret[-1] % 4)) ret.append(ret[-1] + (tab_size - ret[-1] % tab_size))
else: else:
ret.append(ret[-1] + wcwidth(c)) ret.append(ret[-1] + wcwidth(c))
return tuple(ret) return tuple(ret)
@@ -57,8 +57,9 @@ class DelModification(NamedTuple):
class Buf: class Buf:
def __init__(self, lines: List[str]) -> None: def __init__(self, lines: List[str], tab_size: int = 4) -> None:
self._lines = lines self._lines = lines
self.tab_size = tab_size
self.file_y = self.y = self._x = self._x_hint = 0 self.file_y = self.y = self._x = self._x_hint = 0
self._set_callbacks: List[SetCallback] = [self._set_cb] self._set_callbacks: List[SetCallback] = [self._set_cb]
@@ -136,6 +137,10 @@ class Buf:
if self[-1] != '': if self[-1] != '':
self.append('') self.append('')
def set_tab_size(self, tab_size: int) -> None:
self.tab_size = tab_size
self._positions = [None]
# event handling # event handling
def add_set_callback(self, cb: SetCallback) -> None: def add_set_callback(self, cb: SetCallback) -> None:
@@ -219,7 +224,8 @@ class Buf:
self._extend_positions(idx) self._extend_positions(idx)
value = self._positions[idx] value = self._positions[idx]
if value is None: if value is None:
value = self._positions[idx] = _offsets(self._lines[idx]) value = _offsets(self._lines[idx], self.tab_size)
self._positions[idx] = value
return value return value
def line_x(self, margin: Margin) -> int: def line_x(self, margin: Margin) -> int:
@@ -238,7 +244,8 @@ class Buf:
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 x = self._cursor_x if idx == self.y else 0
return scrolled_line(self._lines[idx].expandtabs(4), x, margin.cols) expanded = self._lines[idx].expandtabs(self.tab_size)
return scrolled_line(expanded, x, margin.cols)
# movement # movement

View File

@@ -244,7 +244,7 @@ class File:
status.update('(new file)') status.update('(new file)')
lines, self.nl, mixed, self.sha256 = get_lines(io.StringIO('')) lines, self.nl, mixed, self.sha256 = get_lines(io.StringIO(''))
self.buf = Buf(lines) self.buf = Buf(lines, self.buf.tab_size)
if mixed: if mixed:
status.update(f'mixed newlines will be converted to {self.nl!r}') status.update(f'mixed newlines will be converted to {self.nl!r}')
@@ -523,16 +523,16 @@ class File:
(s_y, _), (e_y, _) = self.selection.get() (s_y, _), (e_y, _) = self.selection.get()
for l_y in range(s_y, e_y + 1): for l_y in range(s_y, e_y + 1):
if self.buf[l_y]: if self.buf[l_y]:
self.buf[l_y] = ' ' * 4 + self.buf[l_y] self.buf[l_y] = ' ' * self.buf.tab_size + self.buf[l_y]
if l_y == self.buf.y: if l_y == self.buf.y:
self.buf.x += 4 self.buf.x += self.buf.tab_size
if l_y == sel_y and sel_x != 0: if l_y == sel_y and sel_x != 0:
sel_x += 4 sel_x += self.buf.tab_size
self.selection.set(sel_y, sel_x, self.buf.y, self.buf.x) self.selection.set(sel_y, sel_x, self.buf.y, self.buf.x)
@edit_action('insert tab', final=False) @edit_action('insert tab', final=False)
def _tab(self, margin: Margin) -> None: def _tab(self, margin: Margin) -> None:
n = 4 - self.buf.x % 4 n = self.buf.tab_size - self.buf.x % self.buf.tab_size
line = self.buf[self.buf.y] line = self.buf[self.buf.y]
self.buf[self.buf.y] = line[:self.buf.x] + n * ' ' + line[self.buf.x:] self.buf[self.buf.y] = line[:self.buf.x] + n * ' ' + line[self.buf.x:]
self.buf.x += n self.buf.x += n
@@ -544,9 +544,8 @@ class File:
else: else:
self._tab(margin) self._tab(margin)
@staticmethod def _dedent_line(self, s: str) -> int:
def _dedent_line(s: str) -> int: bound = min(len(s), self.buf.tab_size)
bound = min(len(s), 4)
i = 0 i = 0
while i < bound and s[i] == ' ': while i < bound and s[i] == ' ':
i += 1 i += 1

View File

@@ -2,6 +2,7 @@ import argparse
import curses import curses
import os import os
import re import re
import signal
import sys import sys
from typing import List from typing import List
from typing import Optional from typing import Optional
@@ -138,6 +139,11 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
else: else:
stdin = '' stdin = ''
# ignore backgrounding signals, we'll handle those in curses
# fixes a problem with ^Z on termination which would break the terminal
if sys.platform != 'win32': # pragma: win32 no cover # pragma: no branch
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
with perf_log(args.perf_log) as perf, make_stdscr() as stdscr: with perf_log(args.perf_log) as perf, make_stdscr() as stdscr:
if args.key_debug: if args.key_debug:
return _key_debug(stdscr, perf) return _key_debug(stdscr, perf)

View File

@@ -6,80 +6,36 @@ from typing import Tuple
import onigurumacffi import onigurumacffi
from babi.cached_property import cached_property
_BACKREF_RE = re.compile(r'((?<!\\)(?:\\\\)*)\\([0-9]+)') _BACKREF_RE = re.compile(r'((?<!\\)(?:\\\\)*)\\([0-9]+)')
def _replace_esc(s: str, chars: str) -> str: _FLAGS = {
"""replace the given escape sequences of `chars` with \\uffff""" # (first_line, boundary)
for c in chars: (False, False): (
if f'\\{c}' in s: onigurumacffi.OnigSearchOption.NOT_END_STRING |
break onigurumacffi.OnigSearchOption.NOT_BEGIN_STRING |
else: onigurumacffi.OnigSearchOption.NOT_BEGIN_POSITION
return s ),
(False, True): (
b = [] onigurumacffi.OnigSearchOption.NOT_END_STRING |
i = 0 onigurumacffi.OnigSearchOption.NOT_BEGIN_STRING
length = len(s) ),
while i < length: (True, False): (
try: onigurumacffi.OnigSearchOption.NOT_END_STRING |
sbi = s.index('\\', i) onigurumacffi.OnigSearchOption.NOT_BEGIN_POSITION
except ValueError: ),
b.append(s[i:]) (True, True): onigurumacffi.OnigSearchOption.NOT_END_STRING,
break }
if sbi > i:
b.append(s[i:sbi])
b.append('\\')
i = sbi + 1
if i < length:
if s[i] in chars:
b.append('\uffff')
else:
b.append(s[i])
i += 1
return ''.join(b)
class _Reg: class _Reg:
def __init__(self, s: str) -> None: def __init__(self, s: str) -> None:
self._pattern = s self._pattern = s
self._reg = onigurumacffi.compile(self._pattern)
def __repr__(self) -> str: def __repr__(self) -> str:
return f'{type(self).__name__}({self._pattern!r})' return f'{type(self).__name__}({self._pattern!r})'
@cached_property
def _reg(self) -> onigurumacffi._Pattern:
return onigurumacffi.compile(_replace_esc(self._pattern, 'z'))
@cached_property
def _reg_no_A(self) -> onigurumacffi._Pattern:
return onigurumacffi.compile(_replace_esc(self._pattern, 'Az'))
@cached_property
def _reg_no_G(self) -> onigurumacffi._Pattern:
return onigurumacffi.compile(_replace_esc(self._pattern, 'Gz'))
@cached_property
def _reg_no_A_no_G(self) -> onigurumacffi._Pattern:
return onigurumacffi.compile(_replace_esc(self._pattern, 'AGz'))
def _get_reg(
self,
first_line: bool,
boundary: bool,
) -> onigurumacffi._Pattern:
if boundary:
if first_line:
return self._reg
else:
return self._reg_no_A
else:
if first_line:
return self._reg_no_G
else:
return self._reg_no_A_no_G
def search( def search(
self, self,
line: str, line: str,
@@ -87,7 +43,7 @@ class _Reg:
first_line: bool, first_line: bool,
boundary: bool, boundary: bool,
) -> Optional[Match[str]]: ) -> Optional[Match[str]]:
return self._get_reg(first_line, boundary).search(line, pos) return self._reg.search(line, pos, flags=_FLAGS[first_line, boundary])
def match( def match(
self, self,
@@ -96,36 +52,18 @@ class _Reg:
first_line: bool, first_line: bool,
boundary: bool, boundary: bool,
) -> Optional[Match[str]]: ) -> Optional[Match[str]]:
return self._get_reg(first_line, boundary).match(line, pos) return self._reg.match(line, pos, flags=_FLAGS[first_line, boundary])
class _RegSet: class _RegSet:
def __init__(self, *s: str) -> None: def __init__(self, *s: str) -> None:
self._patterns = s self._patterns = s
self._set = onigurumacffi.compile_regset(*self._patterns)
def __repr__(self) -> str: def __repr__(self) -> str:
args = ', '.join(repr(s) for s in self._patterns) args = ', '.join(repr(s) for s in self._patterns)
return f'{type(self).__name__}({args})' return f'{type(self).__name__}({args})'
@cached_property
def _set(self) -> onigurumacffi._RegSet:
return onigurumacffi.compile_regset(*self._patterns)
@cached_property
def _set_no_A(self) -> onigurumacffi._RegSet:
patterns = (_replace_esc(p, 'A') for p in self._patterns)
return onigurumacffi.compile_regset(*patterns)
@cached_property
def _set_no_G(self) -> onigurumacffi._RegSet:
patterns = (_replace_esc(p, 'G') for p in self._patterns)
return onigurumacffi.compile_regset(*patterns)
@cached_property
def _set_no_A_no_G(self) -> onigurumacffi._RegSet:
patterns = (_replace_esc(p, 'AG') for p in self._patterns)
return onigurumacffi.compile_regset(*patterns)
def search( def search(
self, self,
line: str, line: str,
@@ -133,16 +71,7 @@ class _RegSet:
first_line: bool, first_line: bool,
boundary: bool, boundary: bool,
) -> Tuple[int, Optional[Match[str]]]: ) -> Tuple[int, Optional[Match[str]]]:
if boundary: return self._set.search(line, pos, flags=_FLAGS[first_line, boundary])
if first_line:
return self._set.search(line, pos)
else:
return self._set_no_A.search(line, pos)
else:
if first_line:
return self._set_no_G.search(line, pos)
else:
return self._set_no_A_no_G.search(line, pos)
def expand_escaped(match: Match[str], s: str) -> str: def expand_escaped(match: Match[str], s: str) -> str:
@@ -151,4 +80,4 @@ def expand_escaped(match: Match[str], s: str) -> str:
make_reg = functools.lru_cache(maxsize=None)(_Reg) make_reg = functools.lru_cache(maxsize=None)(_Reg)
make_regset = functools.lru_cache(maxsize=None)(_RegSet) make_regset = functools.lru_cache(maxsize=None)(_RegSet)
ERR_REG = make_reg(')this pattern always triggers an error when used(') ERR_REG = make_reg('$ ^')

View File

@@ -421,7 +421,9 @@ class Screen:
def command(self) -> Optional[EditResult]: def command(self) -> Optional[EditResult]:
response = self.prompt('', history='command') response = self.prompt('', history='command')
if response == ':q': if response is PromptResult.CANCELLED:
pass
elif response == ':q':
return self.quit_save_modified() return self.quit_save_modified()
elif response == ':q!': elif response == ':q!':
return EditResult.EXIT return EditResult.EXIT
@@ -442,7 +444,20 @@ class Screen:
else: else:
self.file.sort(self.margin, reverse=True) self.file.sort(self.margin, reverse=True)
self.status.update('sorted!') self.status.update('sorted!')
elif response is not PromptResult.CANCELLED: elif response.startswith((':tabstop ', ':tabsize ')):
_, _, tab_size = response.partition(' ')
try:
parsed_tab_size = int(tab_size)
except ValueError:
self.status.update(f'invalid size: {tab_size}')
else:
if parsed_tab_size <= 0:
self.status.update(f'invalid size: {parsed_tab_size}')
else:
for file in self.files:
file.buf.set_tab_size(parsed_tab_size)
self.status.update('updated!')
else:
self.status.update(f'invalid command: {response}') self.status.update(f'invalid command: {response}')
return None return None

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = babi name = babi
version = 0.0.10 version = 0.0.12
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
@@ -24,7 +24,7 @@ packages = find:
install_requires = install_requires =
babi-grammars babi-grammars
identify identify
onigurumacffi>=0.0.10 onigurumacffi>=0.0.18
importlib_metadata>=1;python_version<"3.8" importlib_metadata>=1;python_version<"3.8"
windows-curses;sys_platform=="win32" windows-curses;sys_platform=="win32"
python_requires = >=3.6.1 python_requires = >=3.6.1

View File

@@ -391,6 +391,7 @@ class DeferredRunner:
_curses_cbreak = _curses_endwin = _curses_noecho = _curses__noop _curses_cbreak = _curses_endwin = _curses_noecho = _curses__noop
_curses_nonl = _curses_raw = _curses_use_default_colors = _curses__noop _curses_nonl = _curses_raw = _curses_use_default_colors = _curses__noop
_curses_set_escdelay = _curses__noop
_curses_error = curses.error # so we don't mock the exception _curses_error = curses.error # so we don't mock the exception

View File

@@ -0,0 +1,30 @@
import pytest
from testing.runner import and_exit
from testing.runner import trigger_command_mode
@pytest.mark.parametrize('setting', ('tabsize', 'tabstop'))
def test_set_tabstop(run, setting):
with run() as h, and_exit(h):
h.press('a')
h.press('Left')
trigger_command_mode(h)
h.press_and_enter(f':{setting} 2')
h.await_text('updated!')
h.press('Tab')
h.await_text('\n a')
h.await_cursor_position(x=2, y=1)
@pytest.mark.parametrize('tabsize', ('-1', '0', 'wat'))
def test_set_invalid_tabstop(run, tabsize):
with run() as h, and_exit(h):
h.press('a')
h.press('Left')
trigger_command_mode(h)
h.press_and_enter(f':tabstop {tabsize}')
h.await_text(f'invalid size: {tabsize}')
h.press('Tab')
h.await_text(' a')
h.await_cursor_position(x=4, y=1)

View File

@@ -35,9 +35,8 @@ def test_reg_other_escapes_left_untouched():
def test_reg_not_out_of_bounds_at_end(): def test_reg_not_out_of_bounds_at_end():
# the only way this is triggerable is with an illegal regex, we'd rather # the only way this is triggerable is with an illegal regex, we'd rather
# produce an error about the regex being wrong than an IndexError # produce an error about the regex being wrong than an IndexError
reg = _Reg('\\A\\')
with pytest.raises(onigurumacffi.OnigError) as excinfo: with pytest.raises(onigurumacffi.OnigError) as excinfo:
reg.search('\\', 0, first_line=False, boundary=False) _Reg('\\A\\')
msg, = excinfo.value.args msg, = excinfo.value.args
assert msg == 'end pattern at escape' assert msg == 'end pattern at escape'