Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
144bbb9daf | ||
|
|
7c16cd966e | ||
|
|
dd19b26fa2 | ||
|
|
dca410dd44 | ||
|
|
ed51b6e6dc | ||
|
|
18b5e258f6 | ||
|
|
e7108f843b | ||
|
|
ff8d3f10fb | ||
|
|
8f603b8e14 | ||
|
|
c184468843 | ||
|
|
c5653976c7 | ||
|
|
d81bb12ff7 | ||
|
|
afe461372e | ||
|
|
b486047e90 |
@@ -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
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
17
babi/buf.py
17
babi/buf.py
@@ -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
|
||||
|
||||
|
||||
27
babi/file.py
27
babi/file.py
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
30
tests/features/tabsize_test.py
Normal file
30
tests/features/tabsize_test.py
Normal 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)
|
||||
Reference in New Issue
Block a user