14 Commits

Author SHA1 Message Date
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
Anthony Sottile
c184468843 v0.0.10 2020-05-24 18:31:48 -07:00
Anthony Sottile
c5653976c7 Merge pull request #68 from asottile/fix_macos_full_screen
fix fullscreen on macos in Terminal
2020-05-24 18:29:45 -07:00
Anthony Sottile
d81bb12ff7 fix fullscreen on macos in Terminal 2020-05-24 18:18:35 -07:00
Anthony Sottile
afe461372e Merge pull request #65 from YouTwitFace/add-reverse-sort
Add reverse sort
2020-05-20 18:04:10 -07:00
YouTwitFace
b486047e90 Add reverse sort 2020-05-20 17:07:40 -07:00
9 changed files with 109 additions and 26 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
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
<kbd>:q</kbd>!). babi also support syntax highlighting
<kbd>:q</kbd>!). babi also supports syntax highlighting
![](https://i.fluffy.cc/5WFZBJ4mWs7wtThD9strQnGlJqw4Z9KS.png)

View File

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

View File

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

View File

@@ -244,7 +244,7 @@ class File:
status.update('(new file)')
lines, self.nl, mixed, self.sha256 = get_lines(io.StringIO(''))
self.buf = Buf(lines)
self.buf = Buf(lines, self.buf.tab_size)
if mixed:
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()
for l_y in range(s_y, e_y + 1):
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:
self.buf.x += 4
self.buf.x += self.buf.tab_size
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)
@edit_action('insert tab', final=False)
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]
self.buf[self.buf.y] = line[:self.buf.x] + n * ' ' + line[self.buf.x:]
self.buf.x += n
@@ -544,9 +544,8 @@ class File:
else:
self._tab(margin)
@staticmethod
def _dedent_line(s: str) -> int:
bound = min(len(s), 4)
def _dedent_line(self, s: str) -> int:
bound = min(len(s), self.buf.tab_size)
i = 0
while i < bound and s[i] == ' ':
i += 1
@@ -641,9 +640,9 @@ class File:
self.buf[self.buf.y] += self.buf.pop(self.buf.y + 1)
self.buf.restore_eof_invariant()
def _sort(self, margin: Margin, s_y: int, e_y: int) -> None:
def _sort(self, margin: Margin, s_y: int, e_y: int, reverse: bool) -> None:
# self.buf intentionally does not support slicing so we use islice
lines = sorted(itertools.islice(self.buf, s_y, e_y))
lines = sorted(itertools.islice(self.buf, s_y, e_y), reverse=reverse)
for i, line in zip(range(s_y, e_y), lines):
self.buf[i] = line
@@ -652,17 +651,17 @@ class File:
self.buf.scroll_screen_if_needed(margin)
@edit_action('sort', final=True)
def sort(self, margin: Margin) -> None:
self._sort(margin, 0, len(self.buf) - 1)
def sort(self, margin: Margin, reverse: bool = False) -> None:
self._sort(margin, 0, len(self.buf) - 1, reverse=reverse)
@edit_action('sort selection', final=True)
@clear_selection
def sort_selection(self, margin: Margin) -> None:
def sort_selection(self, margin: Margin, reverse: bool = False) -> None:
(s_y, _), (e_y, _) = self.selection.get()
e_y = min(e_y + 1, len(self.buf) - 1)
if self.buf[e_y - 1] == '':
e_y -= 1
self._sort(margin, s_y, e_y)
self._sort(margin, s_y, e_y, reverse=reverse)
DISPATCH = {
# movement

View File

@@ -226,7 +226,10 @@ class Screen:
if self._buffered_input is not None:
wch, self._buffered_input = self._buffered_input, None
else:
wch = self.stdscr.get_wch()
try:
wch = self.stdscr.get_wch()
except curses.error: # pragma: no cover (macos bug?)
wch = self.stdscr.get_wch()
if isinstance(wch, str) and wch == '\x1b':
wch = self._get_sequence(wch)
if len(wch) == 2:
@@ -418,7 +421,9 @@ class Screen:
def command(self) -> Optional[EditResult]:
response = self.prompt('', history='command')
if response == ':q':
if response is PromptResult.CANCELLED:
pass
elif response == ':q':
return self.quit_save_modified()
elif response == ':q!':
return EditResult.EXIT
@@ -433,7 +438,26 @@ class Screen:
else:
self.file.sort(self.margin)
self.status.update('sorted!')
elif response is not PromptResult.CANCELLED:
elif response == ':sort!':
if self.file.selection.start:
self.file.sort_selection(self.margin, reverse=True)
else:
self.file.sort(self.margin, reverse=True)
self.status.update('sorted!')
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}')
return None

View File

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

View File

@@ -391,6 +391,7 @@ class DeferredRunner:
_curses_cbreak = _curses_endwin = _curses_noecho = _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

View File

@@ -21,6 +21,16 @@ def test_sort_entire_file(run, unsorted):
assert unsorted.read() == 'a\nb\nc\nd\n'
def test_reverse_sort_entire_file(run, unsorted):
with run(str(unsorted)) as h, and_exit(h):
trigger_command_mode(h)
h.press_and_enter(':sort!')
h.await_text('sorted!')
h.await_cursor_position(x=0, y=1)
h.press('^S')
assert unsorted.read() == 'd\nc\nb\na\n'
def test_sort_selection(run, unsorted):
with run(str(unsorted)) as h, and_exit(h):
h.press('S-Down')
@@ -32,6 +42,18 @@ def test_sort_selection(run, unsorted):
assert unsorted.read() == 'b\nd\nc\na\n'
def test_reverse_sort_selection(run, unsorted):
with run(str(unsorted)) as h, and_exit(h):
h.press('Down')
h.press('S-Down')
trigger_command_mode(h)
h.press_and_enter(':sort!')
h.await_text('sorted!')
h.await_cursor_position(x=0, y=2)
h.press('^S')
assert unsorted.read() == 'd\nc\nb\na\n'
def test_sort_selection_does_not_include_eof(run, unsorted):
with run(str(unsorted)) as h, and_exit(h):
for _ in range(5):

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)