Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7622f38c6 | ||
|
|
e474396790 | ||
|
|
e6a0353650 | ||
|
|
e0a59e3f9c | ||
|
|
787dc0d18f | ||
|
|
fd9393c8b1 | ||
|
|
eb26d93e03 | ||
|
|
055d738142 | ||
|
|
29062628f9 | ||
|
|
1fab2a4b71 | ||
|
|
9f5e8c02cb | ||
|
|
31624856d2 | ||
|
|
97b3b4deef |
@@ -1,6 +1,8 @@
|
|||||||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
||||||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
babi
|
babi
|
||||||
====
|
====
|
||||||
|
|
||||||
|
|||||||
@@ -33,14 +33,17 @@ class ColorManager(NamedTuple):
|
|||||||
return self.raw_color_pair(fg_i, bg_i)
|
return self.raw_color_pair(fg_i, bg_i)
|
||||||
|
|
||||||
def raw_color_pair(self, fg: int, bg: int) -> int:
|
def raw_color_pair(self, fg: int, bg: int) -> int:
|
||||||
try:
|
if curses.COLORS > 0:
|
||||||
return self.raw_pairs[(fg, bg)]
|
try:
|
||||||
except KeyError:
|
return self.raw_pairs[(fg, bg)]
|
||||||
pass
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
|
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
|
||||||
curses.init_pair(n, fg, bg)
|
curses.init_pair(n, fg, bg)
|
||||||
return n
|
return n
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls) -> 'ColorManager':
|
def make(cls) -> 'ColorManager':
|
||||||
|
|||||||
57
babi/file.py
57
babi/file.py
@@ -6,6 +6,7 @@ import hashlib
|
|||||||
import io
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import os.path
|
import os.path
|
||||||
|
import re
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import cast
|
from typing import cast
|
||||||
@@ -38,6 +39,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
TCallable = TypeVar('TCallable', bound=Callable[..., Any])
|
TCallable = TypeVar('TCallable', bound=Callable[..., Any])
|
||||||
|
|
||||||
|
WS_RE = re.compile(r'^\s*')
|
||||||
|
|
||||||
|
|
||||||
def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]:
|
def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]:
|
||||||
sha256 = hashlib.sha256()
|
sha256 = hashlib.sha256()
|
||||||
@@ -650,6 +653,13 @@ class File:
|
|||||||
self.buf.x = 0
|
self.buf.x = 0
|
||||||
self.buf.scroll_screen_if_needed(margin)
|
self.buf.scroll_screen_if_needed(margin)
|
||||||
|
|
||||||
|
def _selection_lines(self) -> Tuple[int, int]:
|
||||||
|
(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
|
||||||
|
return s_y, e_y
|
||||||
|
|
||||||
@edit_action('sort', final=True)
|
@edit_action('sort', final=True)
|
||||||
def sort(self, margin: Margin, reverse: bool = False) -> None:
|
def sort(self, margin: Margin, reverse: bool = False) -> None:
|
||||||
self._sort(margin, 0, len(self.buf) - 1, reverse=reverse)
|
self._sort(margin, 0, len(self.buf) - 1, reverse=reverse)
|
||||||
@@ -657,12 +667,51 @@ class File:
|
|||||||
@edit_action('sort selection', final=True)
|
@edit_action('sort selection', final=True)
|
||||||
@clear_selection
|
@clear_selection
|
||||||
def sort_selection(self, margin: Margin, reverse: bool = False) -> None:
|
def sort_selection(self, margin: Margin, reverse: bool = False) -> None:
|
||||||
(s_y, _), (e_y, _) = self.selection.get()
|
s_y, e_y = self._selection_lines()
|
||||||
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, reverse=reverse)
|
self._sort(margin, s_y, e_y, reverse=reverse)
|
||||||
|
|
||||||
|
def _is_commented(self, lineno: int, prefix: str) -> bool:
|
||||||
|
return self.buf[lineno].lstrip().startswith(prefix)
|
||||||
|
|
||||||
|
def _comment_remove(self, lineno: int, prefix: str) -> None:
|
||||||
|
line = self.buf[lineno]
|
||||||
|
ws_match = WS_RE.match(line)
|
||||||
|
assert ws_match is not None
|
||||||
|
ws_len = len(ws_match[0])
|
||||||
|
rest_offset = ws_len + len(prefix)
|
||||||
|
if line.startswith(prefix, ws_len):
|
||||||
|
self.buf[lineno] = f'{ws_match[0]}{line[rest_offset:].lstrip()}'
|
||||||
|
if self.buf.y == lineno and self.buf.x > ws_len:
|
||||||
|
self.buf.x -= len(line) - len(self.buf[lineno])
|
||||||
|
|
||||||
|
def _comment_add(self, lineno: int, prefix: str) -> None:
|
||||||
|
prefix = f'{prefix} '
|
||||||
|
line = self.buf[lineno]
|
||||||
|
ws_match = WS_RE.match(line)
|
||||||
|
assert ws_match is not None
|
||||||
|
ws_len = len(ws_match[0])
|
||||||
|
self.buf[lineno] = f'{ws_match[0]}{prefix}{line[ws_len:]}'
|
||||||
|
if lineno == self.buf.y and self.buf.x > ws_len:
|
||||||
|
self.buf.x += len(prefix)
|
||||||
|
|
||||||
|
@edit_action('comment', final=True)
|
||||||
|
def toggle_comment(self, prefix: str) -> None:
|
||||||
|
if self._is_commented(self.buf.y, prefix):
|
||||||
|
self._comment_remove(self.buf.y, prefix)
|
||||||
|
else:
|
||||||
|
self._comment_add(self.buf.y, prefix)
|
||||||
|
|
||||||
|
@edit_action('comment selection', final=True)
|
||||||
|
@clear_selection
|
||||||
|
def toggle_comment_selection(self, prefix: str) -> None:
|
||||||
|
s_y, e_y = self._selection_lines()
|
||||||
|
commented = self._is_commented(s_y, prefix)
|
||||||
|
for lineno in range(s_y, e_y):
|
||||||
|
if commented:
|
||||||
|
self._comment_remove(lineno, prefix)
|
||||||
|
else:
|
||||||
|
self._comment_add(lineno, prefix)
|
||||||
|
|
||||||
DISPATCH = {
|
DISPATCH = {
|
||||||
# movement
|
# movement
|
||||||
b'KEY_UP': up,
|
b'KEY_UP': up,
|
||||||
|
|||||||
@@ -457,6 +457,13 @@ class Screen:
|
|||||||
for file in self.files:
|
for file in self.files:
|
||||||
file.buf.set_tab_size(parsed_tab_size)
|
file.buf.set_tab_size(parsed_tab_size)
|
||||||
self.status.update('updated!')
|
self.status.update('updated!')
|
||||||
|
elif response == ':comment' or response.startswith(':comment '):
|
||||||
|
_, _, comment = response.partition(' ')
|
||||||
|
comment = (comment or '#').strip()
|
||||||
|
if self.file.selection.start:
|
||||||
|
self.file.toggle_comment_selection(comment)
|
||||||
|
else:
|
||||||
|
self.file.toggle_comment(comment)
|
||||||
else:
|
else:
|
||||||
self.status.update(f'invalid command: {response}')
|
self.status.update(f'invalid command: {response}')
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = babi
|
name = babi
|
||||||
version = 0.0.13
|
version = 0.0.16
|
||||||
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
|
||||||
|
|||||||
114
tests/features/comment_test.py
Normal file
114
tests/features/comment_test.py
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
from testing.runner import and_exit
|
||||||
|
from testing.runner import trigger_command_mode
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_some_code(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('S-Down')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('# line_0\n# line_1\nline_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_some_code_with_alternate_comment_character(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('S-Down')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment //')
|
||||||
|
|
||||||
|
h.await_text('// line_0\n// line_1\nline_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_partially_commented(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('#')
|
||||||
|
h.press('S-Down')
|
||||||
|
h.await_text('#line_0\nline_1\nline_2')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\nline_1\nline_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_partially_uncommented(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('Down')
|
||||||
|
h.press('#')
|
||||||
|
h.press('Up')
|
||||||
|
h.press('S-Down')
|
||||||
|
h.await_text('line_0\n#line_1\nline_2')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('# line_0\n# #line_1\nline_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_single_line(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('# line_0\nline_1\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_uncomment_single_line(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('#')
|
||||||
|
h.await_text('#line_0\nline_1\n')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\nline_1\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_with_trailing_whitespace(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment // ')
|
||||||
|
|
||||||
|
h.await_text('// line_0\nline_1\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_cursor_at_end_of_line(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('# ')
|
||||||
|
h.press('End')
|
||||||
|
h.await_cursor_position(x=8, y=1)
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_cursor_position(x=6, y=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_comment_moves_cursor(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('End')
|
||||||
|
|
||||||
|
h.await_cursor_position(x=6, y=1)
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_cursor_position(x=8, y=1)
|
||||||
|
|
||||||
|
|
||||||
|
def test_do_not_move_if_cursor_before_comment(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('\t\tfoo')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.press('Right')
|
||||||
|
|
||||||
|
h.await_cursor_position(x=4, y=1)
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_cursor_position(x=4, y=1)
|
||||||
@@ -303,6 +303,7 @@ class DeferredRunner:
|
|||||||
self.color_pairs = {0: (7, 0)}
|
self.color_pairs = {0: (7, 0)}
|
||||||
self.screen = Screen(width, height)
|
self.screen = Screen(width, height)
|
||||||
self._n_colors, self._can_change_color = {
|
self._n_colors, self._can_change_color = {
|
||||||
|
'xterm-mono': (0, False),
|
||||||
'screen': (8, False),
|
'screen': (8, False),
|
||||||
'screen-256color': (256, False),
|
'screen-256color': (256, False),
|
||||||
'xterm-256color': (256, True),
|
'xterm-256color': (256, True),
|
||||||
|
|||||||
@@ -153,3 +153,8 @@ def test_syntax_highlighting_tabs_after_line_creation(run, tmpdir):
|
|||||||
h.press('Enter')
|
h.press('Enter')
|
||||||
|
|
||||||
h.await_text('foo\n x\nx\ny\n')
|
h.await_text('foo\n x\nx\ny\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_does_not_crash_with_no_color_support(run):
|
||||||
|
with run(term='xterm-mono') as h, and_exit(h):
|
||||||
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user