Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
604942306f | ||
|
|
00570f8eda | ||
|
|
51a7b10192 | ||
|
|
4d1101daf9 | ||
|
|
08ec1874d1 | ||
|
|
4881953763 | ||
|
|
8f91c12a45 | ||
|
|
5df223f81e | ||
|
|
57bae10448 | ||
|
|
a2afbfa07b | ||
|
|
229ec77f4f | ||
|
|
5a25901cdb | ||
|
|
9c5f28d475 | ||
|
|
a87497cbe2 | ||
|
|
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
|
||||||
====
|
====
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class DelModification(NamedTuple):
|
|||||||
class Buf:
|
class Buf:
|
||||||
def __init__(self, lines: List[str], tab_size: int = 4) -> None:
|
def __init__(self, lines: List[str], tab_size: int = 4) -> None:
|
||||||
self._lines = lines
|
self._lines = lines
|
||||||
|
self.expandtabs = True
|
||||||
self.tab_size = tab_size
|
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
|
||||||
|
|
||||||
@@ -242,6 +243,13 @@ class Buf:
|
|||||||
|
|
||||||
# rendered lines
|
# rendered lines
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tab_string(self) -> str:
|
||||||
|
if self.expandtabs:
|
||||||
|
return ' ' * self.tab_size
|
||||||
|
else:
|
||||||
|
return '\t'
|
||||||
|
|
||||||
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
|
||||||
expanded = self._lines[idx].expandtabs(self.tab_size)
|
expanded = self._lines[idx].expandtabs(self.tab_size)
|
||||||
|
|||||||
@@ -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':
|
||||||
|
|||||||
96
babi/file.py
96
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()
|
||||||
@@ -233,7 +236,7 @@ class File:
|
|||||||
sio = io.StringIO(stdin)
|
sio = io.StringIO(stdin)
|
||||||
lines, self.nl, mixed, self.sha256 = get_lines(sio)
|
lines, self.nl, mixed, self.sha256 = get_lines(sio)
|
||||||
elif self.filename is not None and os.path.isfile(self.filename):
|
elif self.filename is not None and os.path.isfile(self.filename):
|
||||||
with open(self.filename, newline='') as f:
|
with open(self.filename, encoding='UTF-8', newline='') as f:
|
||||||
lines, self.nl, mixed, self.sha256 = get_lines(f)
|
lines, self.nl, mixed, self.sha256 = get_lines(f)
|
||||||
else:
|
else:
|
||||||
if self.filename is not None:
|
if self.filename is not None:
|
||||||
@@ -521,20 +524,29 @@ class File:
|
|||||||
assert self.selection.start is not None
|
assert self.selection.start is not None
|
||||||
sel_y, sel_x = self.selection.start
|
sel_y, sel_x = self.selection.start
|
||||||
(s_y, _), (e_y, _) = self.selection.get()
|
(s_y, _), (e_y, _) = self.selection.get()
|
||||||
|
tab_string = self.buf.tab_string
|
||||||
|
tab_size = len(tab_string)
|
||||||
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] = ' ' * self.buf.tab_size + self.buf[l_y]
|
self.buf[l_y] = tab_string + self.buf[l_y]
|
||||||
if l_y == self.buf.y:
|
if l_y == self.buf.y:
|
||||||
self.buf.x += self.buf.tab_size
|
self.buf.x += tab_size
|
||||||
if l_y == sel_y and sel_x != 0:
|
if l_y == sel_y and sel_x != 0:
|
||||||
sel_x += self.buf.tab_size
|
sel_x += 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 = self.buf.tab_size - self.buf.x % self.buf.tab_size
|
tab_string = self.buf.tab_string
|
||||||
|
if tab_string == '\t':
|
||||||
|
n = 1
|
||||||
|
else:
|
||||||
|
n = self.buf.tab_size - self.buf.x % self.buf.tab_size
|
||||||
|
tab_string = tab_string[:n]
|
||||||
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] + tab_string + line[self.buf.x:]
|
||||||
|
)
|
||||||
self.buf.x += n
|
self.buf.x += n
|
||||||
self.buf.restore_eof_invariant()
|
self.buf.restore_eof_invariant()
|
||||||
|
|
||||||
@@ -545,9 +557,9 @@ class File:
|
|||||||
self._tab(margin)
|
self._tab(margin)
|
||||||
|
|
||||||
def _dedent_line(self, s: str) -> int:
|
def _dedent_line(self, s: str) -> int:
|
||||||
bound = min(len(s), self.buf.tab_size)
|
bound = min(len(s), len(self.buf.tab_string))
|
||||||
i = 0
|
i = 0
|
||||||
while i < bound and s[i] == ' ':
|
while i < bound and s[i] in (' ', '\t'):
|
||||||
i += 1
|
i += 1
|
||||||
return i
|
return i
|
||||||
|
|
||||||
@@ -650,6 +662,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 +676,65 @@ 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 _indent(self, lineno: int) -> str:
|
||||||
|
ws_match = WS_RE.match(self.buf[lineno])
|
||||||
|
assert ws_match is not None
|
||||||
|
return ws_match[0]
|
||||||
|
|
||||||
|
def _minimum_indent_for_selection(self) -> int:
|
||||||
|
s_y, e_y = self._selection_lines()
|
||||||
|
return min(len(self._indent(lineno)) for lineno in range(s_y, e_y))
|
||||||
|
|
||||||
|
def _comment_remove(self, lineno: int, prefix: str) -> None:
|
||||||
|
line = self.buf[lineno]
|
||||||
|
indent = self._indent(lineno)
|
||||||
|
ws_len = len(indent)
|
||||||
|
|
||||||
|
if line.startswith(f'{prefix} ', ws_len):
|
||||||
|
self.buf[lineno] = f'{indent}{line[ws_len + len(prefix) + 1:]}'
|
||||||
|
elif line.startswith(prefix, ws_len):
|
||||||
|
self.buf[lineno] = f'{indent}{line[ws_len + len(prefix):]}'
|
||||||
|
|
||||||
|
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, s_offset: int) -> None:
|
||||||
|
line = self.buf[lineno]
|
||||||
|
|
||||||
|
if not line:
|
||||||
|
self.buf[lineno] = f'{prefix}'
|
||||||
|
else:
|
||||||
|
self.buf[lineno] = f'{line[:s_offset]}{prefix} {line[s_offset:]}'
|
||||||
|
|
||||||
|
if lineno == self.buf.y and self.buf.x > s_offset:
|
||||||
|
self.buf.x += len(self.buf[lineno]) - len(line)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
ws_len = len(self._indent(self.buf.y))
|
||||||
|
self._comment_add(self.buf.y, prefix, ws_len)
|
||||||
|
|
||||||
|
@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)
|
||||||
|
minimum_indent = self._minimum_indent_for_selection()
|
||||||
|
for lineno in range(s_y, e_y):
|
||||||
|
if commented:
|
||||||
|
self._comment_remove(lineno, prefix)
|
||||||
|
else:
|
||||||
|
self._comment_add(lineno, prefix, minimum_indent)
|
||||||
|
|
||||||
DISPATCH = {
|
DISPATCH = {
|
||||||
# movement
|
# movement
|
||||||
b'KEY_UP': up,
|
b'KEY_UP': up,
|
||||||
|
|||||||
@@ -688,7 +688,7 @@ class Grammars:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
grammar_path = self._scope_to_files.pop(scope)
|
grammar_path = self._scope_to_files.pop(scope)
|
||||||
with open(grammar_path) as f:
|
with open(grammar_path, encoding='UTF-8') as f:
|
||||||
ret = self._raw[scope] = json.load(f)
|
ret = self._raw[scope] = json.load(f)
|
||||||
|
|
||||||
file_types = frozenset(ret.get('fileTypes', ()))
|
file_types = frozenset(ret.get('fileTypes', ()))
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ class History:
|
|||||||
history_dir = xdg_data('history')
|
history_dir = xdg_data('history')
|
||||||
os.makedirs(history_dir, exist_ok=True)
|
os.makedirs(history_dir, exist_ok=True)
|
||||||
for filename in os.listdir(history_dir):
|
for filename in os.listdir(history_dir):
|
||||||
with open(os.path.join(history_dir, filename)) as f:
|
history_filename = os.path.join(history_dir, filename)
|
||||||
|
with open(history_filename, encoding='UTF-8') as f:
|
||||||
self.data[filename] = f.read().splitlines()
|
self.data[filename] = f.read().splitlines()
|
||||||
self._orig_len[filename] = len(self.data[filename])
|
self._orig_len[filename] = len(self.data[filename])
|
||||||
try:
|
try:
|
||||||
@@ -28,5 +29,6 @@ class History:
|
|||||||
for k, v in self.data.items():
|
for k, v in self.data.items():
|
||||||
new_history = v[self._orig_len[k]:]
|
new_history = v[self._orig_len[k]:]
|
||||||
if new_history:
|
if new_history:
|
||||||
with open(os.path.join(history_dir, k), 'a+') as f:
|
history_filename = os.path.join(history_dir, k)
|
||||||
|
with open(history_filename, 'a+', encoding='UTF-8') as f:
|
||||||
f.write('\n'.join(new_history) + '\n')
|
f.write('\n'.join(new_history) + '\n')
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
|
|
||||||
if '-' in args.filenames:
|
if '-' in args.filenames:
|
||||||
print('reading stdin...', file=sys.stderr)
|
print('reading stdin...', file=sys.stderr)
|
||||||
stdin = sys.stdin.read()
|
stdin = sys.stdin.buffer.read().decode()
|
||||||
tty = os.open(CONSOLE, os.O_RDONLY)
|
tty = os.open(CONSOLE, os.O_RDONLY)
|
||||||
os.dup2(tty, sys.stdin.fileno())
|
os.dup2(tty, sys.stdin.fileno())
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class Perf:
|
|||||||
def save_profiles(self, filename: str) -> None:
|
def save_profiles(self, filename: str) -> None:
|
||||||
assert self._prof is not None
|
assert self._prof is not None
|
||||||
self._prof.dump_stats(f'{filename}.pstats')
|
self._prof.dump_stats(f'{filename}.pstats')
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w', encoding='UTF-8') as f:
|
||||||
f.write('μs\tevent\n')
|
f.write('μs\tevent\n')
|
||||||
for name, duration in self._records:
|
for name, duration in self._records:
|
||||||
f.write(f'{int(duration * 1000 * 1000)}\t{name}\n')
|
f.write(f'{int(duration * 1000 * 1000)}\t{name}\n')
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ EditResult = enum.Enum('EditResult', 'EXIT NEXT PREV OPEN')
|
|||||||
SEQUENCE_KEYNAME = {
|
SEQUENCE_KEYNAME = {
|
||||||
'\x1bOH': b'KEY_HOME',
|
'\x1bOH': b'KEY_HOME',
|
||||||
'\x1bOF': b'KEY_END',
|
'\x1bOF': b'KEY_END',
|
||||||
|
'\x1b[1~': b'KEY_HOME',
|
||||||
|
'\x1b[4~': b'KEY_END',
|
||||||
'\x1b[1;2A': b'KEY_SR',
|
'\x1b[1;2A': b'KEY_SR',
|
||||||
'\x1b[1;2B': b'KEY_SF',
|
'\x1b[1;2B': b'KEY_SF',
|
||||||
'\x1b[1;2C': b'KEY_SRIGHT',
|
'\x1b[1;2C': b'KEY_SRIGHT',
|
||||||
@@ -60,6 +62,7 @@ SEQUENCE_KEYNAME = {
|
|||||||
'\x1b[1;6D': b'kLFT6', # Shift + ^Left
|
'\x1b[1;6D': b'kLFT6', # Shift + ^Left
|
||||||
'\x1b[1;6H': b'kHOM6', # Shift + ^Home
|
'\x1b[1;6H': b'kHOM6', # Shift + ^Home
|
||||||
'\x1b[1;6F': b'kEND6', # Shift + ^End
|
'\x1b[1;6F': b'kEND6', # Shift + ^End
|
||||||
|
'\x1b[~': b'KEY_BTAB', # Shift + Tab
|
||||||
}
|
}
|
||||||
KEYNAME_REWRITE = {
|
KEYNAME_REWRITE = {
|
||||||
# windows-curses: numeric pad arrow keys
|
# windows-curses: numeric pad arrow keys
|
||||||
@@ -457,6 +460,21 @@ 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.startswith(':expandtabs'):
|
||||||
|
for file in self.files:
|
||||||
|
file.buf.expandtabs = True
|
||||||
|
self.status.update('updated!')
|
||||||
|
elif response.startswith(':noexpandtabs'):
|
||||||
|
for file in self.files:
|
||||||
|
file.buf.expandtabs = False
|
||||||
|
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
|
||||||
@@ -476,7 +494,7 @@ class Screen:
|
|||||||
self.file.filename = filename
|
self.file.filename = filename
|
||||||
|
|
||||||
if os.path.isfile(self.file.filename):
|
if os.path.isfile(self.file.filename):
|
||||||
with open(self.file.filename, newline='') as f:
|
with open(self.file.filename, encoding='UTF-8', newline='') as f:
|
||||||
*_, sha256 = get_lines(f)
|
*_, sha256 = get_lines(f)
|
||||||
else:
|
else:
|
||||||
sha256 = hashlib.sha256(b'').hexdigest()
|
sha256 = hashlib.sha256(b'').hexdigest()
|
||||||
@@ -489,7 +507,7 @@ class Screen:
|
|||||||
self.status.update('(file changed on disk, not implemented)')
|
self.status.update('(file changed on disk, not implemented)')
|
||||||
return PromptResult.CANCELLED
|
return PromptResult.CANCELLED
|
||||||
|
|
||||||
with open(self.file.filename, 'w', newline='') as f:
|
with open(self.file.filename, 'w', encoding='UTF-8', newline='') as f:
|
||||||
f.write(contents)
|
f.write(contents)
|
||||||
|
|
||||||
self.file.modified = False
|
self.file.modified = False
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ def _highlight_output(theme: Theme, compiler: Compiler, filename: str) -> int:
|
|||||||
|
|
||||||
if theme.default.bg is not None:
|
if theme.default.bg is not None:
|
||||||
print('\x1b[48;2;{r};{g};{b}m'.format(**theme.default.bg._asdict()))
|
print('\x1b[48;2;{r};{g};{b}m'.format(**theme.default.bg._asdict()))
|
||||||
with open(filename) as f:
|
with open(filename, encoding='UTF-8') as f:
|
||||||
for line_idx, line in enumerate(f):
|
for line_idx, line in enumerate(f):
|
||||||
first_line = line_idx == 0
|
first_line = line_idx == 0
|
||||||
state, regions = highlight_line(compiler, state, line, first_line)
|
state, regions = highlight_line(compiler, state, line, first_line)
|
||||||
@@ -54,7 +54,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
parser.add_argument('filename')
|
parser.add_argument('filename')
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
with open(args.filename) as f:
|
with open(args.filename, encoding='UTF-8') as f:
|
||||||
first_line = next(f, '')
|
first_line = next(f, '')
|
||||||
|
|
||||||
theme = Theme.from_filename(args.theme)
|
theme = Theme.from_filename(args.theme)
|
||||||
|
|||||||
@@ -147,5 +147,5 @@ class Theme(NamedTuple):
|
|||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return cls.blank()
|
return cls.blank()
|
||||||
else:
|
else:
|
||||||
with open(filename) as f:
|
with open(filename, encoding='UTF-8') as f:
|
||||||
return cls.from_dct(json.load(f))
|
return cls.from_dct(json.load(f))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = babi
|
name = babi
|
||||||
version = 0.0.13
|
version = 0.0.18
|
||||||
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
|
||||||
|
|||||||
205
tests/features/comment_test.py
Normal file
205
tests/features/comment_test.py
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from testing.runner import and_exit
|
||||||
|
from testing.runner import trigger_command_mode
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def three_lines_with_indentation(tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('line_0\n line_1\n line_2')
|
||||||
|
return f
|
||||||
|
|
||||||
|
|
||||||
|
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_empty_line_trailing_whitespace(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('1\n\n2\n')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.press('S-Down')
|
||||||
|
h.press('S-Down')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('# 1\n#\n# 2')
|
||||||
|
|
||||||
|
|
||||||
|
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_some_code_with_indentation(run, three_lines_with_indentation):
|
||||||
|
with run(str(three_lines_with_indentation)) 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\n line_2\n')
|
||||||
|
|
||||||
|
h.press('S-Up')
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\n line_1\n line_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_some_code_on_indent_part(run, three_lines_with_indentation):
|
||||||
|
with run(str(three_lines_with_indentation)) as h, and_exit(h):
|
||||||
|
h.press('Down')
|
||||||
|
h.press('S-Down')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\n # line_1\n # line_2\n')
|
||||||
|
|
||||||
|
h.press('S-Up')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\n line_1\n line_2\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_comment_some_code_on_tabs_part(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('line_0\n\tline_1\n\t\tline_2')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.await_text('line_0\n line_1\n line_2')
|
||||||
|
h.press('Down')
|
||||||
|
h.press('S-Down')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\n # line_1\n # line_2')
|
||||||
|
|
||||||
|
h.press('S-Up')
|
||||||
|
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('line_0\n line_1\n line_2')
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('comment', ('# ', '#'))
|
||||||
|
def test_remove_comment_with_comment_elsewhere_in_line(run, tmpdir, comment):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write(f'{comment}print("not a # comment here!")\n')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':comment')
|
||||||
|
|
||||||
|
h.await_text('\nprint("not a # comment here!")\n')
|
||||||
@@ -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),
|
||||||
|
|||||||
45
tests/features/expandtabs_test.py
Normal file
45
tests/features/expandtabs_test.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
from testing.runner import and_exit
|
||||||
|
from testing.runner import trigger_command_mode
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_expandtabs(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('a')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.press('Left')
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':expandtabs')
|
||||||
|
h.await_text('updated!')
|
||||||
|
h.press('Tab')
|
||||||
|
h.press('^S')
|
||||||
|
assert f.read() == ' a\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_noexpandtabs(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('a')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.press('Left')
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':noexpandtabs')
|
||||||
|
h.await_text('updated!')
|
||||||
|
h.press('Tab')
|
||||||
|
h.press('^S')
|
||||||
|
assert f.read() == '\ta\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_indent_with_expandtabs(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('a\nb\nc')
|
||||||
|
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':noexpandtabs')
|
||||||
|
h.await_text('updated!')
|
||||||
|
for _ in range(3):
|
||||||
|
h.press('S-Down')
|
||||||
|
h.press('Tab')
|
||||||
|
h.press('^S')
|
||||||
|
assert f.read() == '\ta\n\tb\n\tc\n'
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
from testing.runner import trigger_command_mode
|
||||||
|
|
||||||
|
|
||||||
def test_indent_at_beginning_of_line(run):
|
def test_indent_at_beginning_of_line(run):
|
||||||
@@ -12,11 +13,12 @@ def test_indent_at_beginning_of_line(run):
|
|||||||
|
|
||||||
def test_indent_not_full_tab(run):
|
def test_indent_not_full_tab(run):
|
||||||
with run() as h, and_exit(h):
|
with run() as h, and_exit(h):
|
||||||
h.press('h')
|
h.press('hello')
|
||||||
|
h.press('Home')
|
||||||
|
h.press('Right')
|
||||||
h.press('Tab')
|
h.press('Tab')
|
||||||
h.press('ello')
|
|
||||||
h.await_text('h ello')
|
h.await_text('h ello')
|
||||||
h.await_cursor_position(x=8, y=1)
|
h.await_cursor_position(x=4, y=1)
|
||||||
|
|
||||||
|
|
||||||
def test_indent_fixes_eof(run):
|
def test_indent_fixes_eof(run):
|
||||||
@@ -86,6 +88,20 @@ def test_dedent_selection(run, tmpdir):
|
|||||||
h.await_text('\n1\n2\n 3\n')
|
h.await_text('\n1\n2\n 3\n')
|
||||||
|
|
||||||
|
|
||||||
|
def test_dedent_selection_with_noexpandtabs(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('1\n\t2\n\t\t3\n')
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
trigger_command_mode(h)
|
||||||
|
h.press_and_enter(':noexpandtabs')
|
||||||
|
h.await_text('updated!')
|
||||||
|
for _ in range(3):
|
||||||
|
h.press('S-Down')
|
||||||
|
h.press('BTab')
|
||||||
|
h.press('^S')
|
||||||
|
assert f.read() == '1\n2\n\t3\n'
|
||||||
|
|
||||||
|
|
||||||
def test_dedent_beginning_of_line(run, tmpdir):
|
def test_dedent_beginning_of_line(run, tmpdir):
|
||||||
f = tmpdir.join('f')
|
f = tmpdir.join('f')
|
||||||
f.write(' hi\n')
|
f.write(' hi\n')
|
||||||
|
|||||||
@@ -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