27 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
Anthony Sottile
d7622f38c6 v0.0.16 2020-08-29 13:07:01 -07:00
Anthony Sottile
e474396790 Merge pull request #92 from asottile/xterm_mono
do not crash if the terminal does not have color support
2020-08-29 13:06:34 -07:00
Anthony Sottile
e6a0353650 do not crash if the terminal does not have color support 2020-08-29 12:40:12 -07:00
Anthony Sottile
e0a59e3f9c v0.0.15 2020-08-28 21:17:51 -07:00
Anthony Sottile
787dc0d18f Merge pull request #91 from asottile/fix_cursor_position_comment
fix position changes when commenting and cursor is before comment
2020-08-28 21:17:27 -07:00
Anthony Sottile
fd9393c8b1 fix position changes when commenting and cursor is before comment 2020-08-28 21:09:16 -07:00
Anthony Sottile
eb26d93e03 Merge pull request #90 from asottile/fix_out_of_bounds_on_uncomment
Fix out of bounds on uncomment
2020-08-28 21:00:52 -07:00
Anthony Sottile
055d738142 Fix out of bounds on uncomment 2020-08-28 20:52:01 -07:00
Anthony Sottile
29062628f9 v0.0.14 2020-08-24 14:03:36 -07:00
Anthony Sottile
1fab2a4b71 Merge pull request #85 from asottile/comment
add :comment command for toggling comments
2020-08-24 14:02:23 -07:00
Anthony Sottile
9f5e8c02cb add :comment command for toggling comments 2020-08-24 13:52:35 -07:00
Anthony Sottile
31624856d2 Merge pull request #84 from asottile/asottile-patch-1
add logo to README
2020-08-11 23:27:25 -07:00
Anthony Sottile
97b3b4deef add logo to README 2020-08-11 23:20:48 -07:00
17 changed files with 410 additions and 33 deletions

View File

@@ -1,6 +1,8 @@
[![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.babi?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master) [![Build Status](https://dev.azure.com/asottile/asottile/_apis/build/status/asottile.babi?branchName=master)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
[![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/29/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master) [![Azure DevOps coverage](https://img.shields.io/azure-devops/coverage/asottile/asottile/29/master.svg)](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
![babi logo](https://user-images.githubusercontent.com/1810591/89981369-9ed84e80-dc28-11ea-9708-5f4c49c09632.png)
babi babi
==== ====

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

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

View File

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

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,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

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.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

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

View File

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

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

View File

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