14 Commits

Author SHA1 Message Date
Anthony Sottile
604942306f v0.0.18 2020-10-24 12:56:04 -07:00
Anthony Sottile
00570f8eda Merge pull request #100 from theendlessriver13/fix_keys_for_win_terminal
fix keys for new windows terminal
2020-10-24 12:55:42 -07:00
Jonas Kittner
51a7b10192 fix keys for windows terminal when using xterm 2020-10-24 21:46:03 +02:00
Anthony Sottile
4d1101daf9 Merge pull request #96 from brynphillips/fix_comment_whitespace
fix whitespace on added comment
2020-09-04 22:19:59 -07:00
shazbot
08ec1874d1 fix whitespace on blank line with added comment 2020-09-04 22:11:04 -07:00
Anthony Sottile
4881953763 v0.0.17 2020-09-04 14:20:32 -07:00
Anthony Sottile
8f91c12a45 Merge pull request #89 from AndrewLaneX/expandtabs
Add :expandtabs and :noexpandtabs
2020-09-04 14:19:45 -07:00
Andrew Lane
5df223f81e Add :expandtabs and :noexpandtabs 2020-09-04 17:07:12 -04:00
Anthony Sottile
57bae10448 Merge pull request #87 from KeisukeFD/fix_comment
fix comments behavior on multiple lines with indentation #86
2020-09-04 13:52:29 -07:00
Valentin Malissen
a2afbfa07b fix comments behavior on multiple lines with indentation 2020-09-04 13:43:22 -07:00
Anthony Sottile
229ec77f4f Merge pull request #94 from 7brokenmirrors/utf8-default-encoding
Add checks for UTF-8
2020-09-01 10:04:29 -07:00
7brokenmirrors
5a25901cdb Add "encoding='UTF-8'" to open() calls 2020-09-01 09:54:56 -07:00
Anthony Sottile
9c5f28d475 Merge pull request #93 from asottile/harden_indent_test
improve indent-mod test
2020-08-29 19:29:33 -07:00
Anthony Sottile
a87497cbe2 improve indent-mod test 2020-08-29 19:20:00 -07:00
13 changed files with 236 additions and 40 deletions

View File

@@ -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)

View File

@@ -236,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:
@@ -524,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()
@@ -548,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
@@ -673,44 +682,58 @@ class File:
def _is_commented(self, lineno: int, prefix: str) -> bool: def _is_commented(self, lineno: int, prefix: str) -> bool:
return self.buf[lineno].lstrip().startswith(prefix) 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: def _comment_remove(self, lineno: int, prefix: str) -> None:
line = self.buf[lineno] line = self.buf[lineno]
ws_match = WS_RE.match(line) indent = self._indent(lineno)
assert ws_match is not None ws_len = len(indent)
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: if line.startswith(f'{prefix} ', ws_len):
prefix = f'{prefix} ' 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] line = self.buf[lineno]
ws_match = WS_RE.match(line)
assert ws_match is not None if not line:
ws_len = len(ws_match[0]) self.buf[lineno] = f'{prefix}'
self.buf[lineno] = f'{ws_match[0]}{prefix}{line[ws_len:]}' else:
if lineno == self.buf.y and self.buf.x > ws_len: self.buf[lineno] = f'{line[:s_offset]}{prefix} {line[s_offset:]}'
self.buf.x += len(prefix)
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) @edit_action('comment', final=True)
def toggle_comment(self, prefix: str) -> None: def toggle_comment(self, prefix: str) -> None:
if self._is_commented(self.buf.y, prefix): if self._is_commented(self.buf.y, prefix):
self._comment_remove(self.buf.y, prefix) self._comment_remove(self.buf.y, prefix)
else: else:
self._comment_add(self.buf.y, prefix) ws_len = len(self._indent(self.buf.y))
self._comment_add(self.buf.y, prefix, ws_len)
@edit_action('comment selection', final=True) @edit_action('comment selection', final=True)
@clear_selection @clear_selection
def toggle_comment_selection(self, prefix: str) -> None: def toggle_comment_selection(self, prefix: str) -> None:
s_y, e_y = self._selection_lines() s_y, e_y = self._selection_lines()
commented = self._is_commented(s_y, prefix) commented = self._is_commented(s_y, prefix)
minimum_indent = self._minimum_indent_for_selection()
for lineno in range(s_y, e_y): for lineno in range(s_y, e_y):
if commented: if commented:
self._comment_remove(lineno, prefix) self._comment_remove(lineno, prefix)
else: else:
self._comment_add(lineno, prefix) self._comment_add(lineno, prefix, minimum_indent)
DISPATCH = { DISPATCH = {
# movement # movement

View File

@@ -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', ()))

View File

@@ -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')

View File

@@ -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:

View File

@@ -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')

View File

@@ -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,14 @@ 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 '): elif response == ':comment' or response.startswith(':comment '):
_, _, comment = response.partition(' ') _, _, comment = response.partition(' ')
comment = (comment or '#').strip() comment = (comment or '#').strip()
@@ -483,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()
@@ -496,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

View File

@@ -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)

View File

@@ -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))

View File

@@ -1,6 +1,6 @@
[metadata] [metadata]
name = babi name = babi
version = 0.0.16 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

View File

@@ -1,7 +1,16 @@
import pytest
from testing.runner import and_exit from testing.runner import and_exit
from testing.runner import trigger_command_mode 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): def test_comment_some_code(run, ten_lines):
with run(str(ten_lines)) as h, and_exit(h): with run(str(ten_lines)) as h, and_exit(h):
h.press('S-Down') h.press('S-Down')
@@ -12,6 +21,20 @@ def test_comment_some_code(run, ten_lines):
h.await_text('# line_0\n# line_1\nline_2\n') 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): def test_comment_some_code_with_alternate_comment_character(run, ten_lines):
with run(str(ten_lines)) as h, and_exit(h): with run(str(ten_lines)) as h, and_exit(h):
h.press('S-Down') h.press('S-Down')
@@ -75,6 +98,62 @@ def test_comment_with_trailing_whitespace(run, ten_lines):
h.await_text('// line_0\nline_1\n') 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): def test_comment_cursor_at_end_of_line(run, ten_lines):
with run(str(ten_lines)) as h, and_exit(h): with run(str(ten_lines)) as h, and_exit(h):
h.press('# ') h.press('# ')
@@ -112,3 +191,15 @@ def test_do_not_move_if_cursor_before_comment(run, tmpdir):
h.press_and_enter(':comment') h.press_and_enter(':comment')
h.await_cursor_position(x=4, y=1) 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')

View 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'

View File

@@ -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')