50 Commits

Author SHA1 Message Date
Anthony Sottile
c7904fdf9b v0.0.21 2021-01-29 20:31:27 -08:00
Anthony Sottile
f6db46736c Merge pull request #124 from theendlessriver13/io_error_save
fix crashing on permission denied error
2021-01-29 20:30:34 -08:00
theendlessriver13
e059dc294b fix crashing on permission denied 2021-01-30 04:09:22 +01:00
Anthony Sottile
4dc42aa628 Merge pull request #123 from asottile/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-01-25 12:10:22 -08:00
pre-commit-ci[bot]
25659ca5c7 [pre-commit.ci] pre-commit autoupdate 2021-01-25 16:48:35 +00:00
Anthony Sottile
bd8dc3beb0 v0.0.20 2021-01-18 15:08:22 -08:00
Anthony Sottile
324513c36a Merge pull request #122 from asottile/backspace_blank_line_at_eof
fix backspacing an extra blank line at end of file
2021-01-18 15:07:48 -08:00
Anthony Sottile
09643b0f80 fix backspacing an extra blank line at end of file 2021-01-18 14:57:34 -08:00
Anthony Sottile
8245ee56a5 Merge pull request #119 from asottile/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2021-01-04 09:18:29 -08:00
pre-commit-ci[bot]
8aaee402e7 [pre-commit.ci] pre-commit autoupdate 2021-01-04 16:53:18 +00:00
Anthony Sottile
641eed65d5 Merge pull request #118 from asottile/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2020-12-21 09:32:09 -08:00
pre-commit-ci[bot]
694853e341 [pre-commit.ci] pre-commit autoupdate 2020-12-21 16:51:45 +00:00
Anthony Sottile
1a4d15206c Merge pull request #113 from asottile/case_insensitive_short_answers
ignore case for quick prompt answers
2020-11-20 12:34:40 -08:00
Anthony Sottile
4ca3b0d1e5 ignore case for quick prompt answers 2020-11-20 12:22:55 -08:00
Anthony Sottile
47868e77a2 Merge pull request #112 from asottile/all-repos_autofix_gh-sponsors
Add link to GitHub Sponsors
2020-11-19 17:12:44 -08:00
Anthony Sottile
9906e223bd Add link to GitHub Sponsors
at the time of writing I am currently unemployed.  I'd love to make open
source a full time career.  if you or your company is deriving value from
this free software, please consider [sponsoring].

[sponsoring]: https://github.com/sponsors/asottile

Committed via https://github.com/asottile/all-repos
2020-11-19 16:58:49 -08:00
Anthony Sottile
a5101007cd Merge pull request #111 from rmorshea/patch-1
Add video explaining babi vs nano
2020-11-18 12:08:01 -08:00
Ryan Morshead
572151197d Add video explaining babi vs nano
closes: #110
2020-11-18 11:52:50 -08:00
Anthony Sottile
a8a5afc6ed Merge pull request #108 from asottile/pre-commit-ci-update-config
[pre-commit.ci] pre-commit autoupdate
2020-11-16 09:46:42 -08:00
pre-commit-ci[bot]
e523a694b6 [pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
2020-11-16 17:12:39 +00:00
pre-commit-ci[bot]
8161c9e34f [pre-commit.ci] pre-commit autoupdate 2020-11-16 17:11:07 +00:00
Anthony Sottile
0b5e187be5 v0.0.19 2020-11-07 12:04:31 -08:00
Anthony Sottile
ee13b7bb78 Merge pull request #106 from asottile/missing_escdelay_some_curses
fix set_escdelay when the curses library does not support it
2020-11-07 12:03:36 -08:00
Anthony Sottile
cad35f7b4d Merge pull request #107 from asottile/pci
use pre-commit.ci
2020-11-07 12:03:13 -08:00
Anthony Sottile
c1a9823894 use pre-commit.ci 2020-11-07 11:51:07 -08:00
Anthony Sottile
899eb6f879 fix set_escdelay when the curses library does not support it 2020-11-07 11:25:04 -08:00
Anthony Sottile
945e0b1620 Merge pull request #104 from villelaitila/feature/101-ctrl-d-to-forward-delete
Familiar forward deletion keyboard shortcut Ctrl-D (#101)
2020-11-03 10:43:38 -08:00
Ville Laitila
1f348882b8 Familiar forward deletion keyboard shortcut Ctrl-D
With help of the mapping rules, this is easily implemented and all
the unit tests will also test usage of Ctrl-D already as Del char
behaves similarly.
2020-11-03 10:32:17 -08:00
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
23 changed files with 360 additions and 61 deletions

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
github: asottile

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v2.5.0
rev: v3.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
@@ -11,34 +11,34 @@ repos:
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.0
rev: 3.8.4
hooks:
- id: flake8
additional_dependencies: [flake8-typing-imports==1.7.0]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v1.5.2
rev: v1.5.4
hooks:
- id: autopep8
- repo: https://github.com/asottile/reorder_python_imports
rev: v2.3.0
rev: v2.3.6
hooks:
- id: reorder-python-imports
args: [--py3-plus]
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.0.1
rev: v2.1.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v2.4.1
rev: v2.7.4
hooks:
- id: pyupgrade
args: [--py36-plus]
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v1.9.0
rev: v1.16.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.770
rev: v0.800
hooks:
- id: mypy

View File

@@ -1,5 +1,6 @@
[![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)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/asottile/babi/master.svg)](https://results.pre-commit.ci/latest/github/asottile/babi/master)
![babi logo](https://user-images.githubusercontent.com/1810591/89981369-9ed84e80-dc28-11ea-9708-5f4c49c09632.png)
@@ -17,6 +18,13 @@ a text editor, eventually...
I used to use the text editor `nano`, frequently I typo this. on a qwerty
keyboard, when the right hand is shifted left by one, `nano` becomes `babi`.
### babi vs. nano
here is a youtube video where I discuss the motivation for creating and using
`babi` instead of `nano`:
[![youtube video about babi](https://img.youtube.com/vi/WyR1hAGmR3g/mqdefault.jpg)](https://youtu.be/WyR1hAGmR3g)
### quitting babi
currently you can quit `babi` by using <kbd>^X</kbd> (or via <kbd>esc</kbd> +

View File

@@ -13,7 +13,6 @@ resources:
ref: refs/tags/v2.0.0
jobs:
- template: job--pre-commit.yml@asottile
- template: job--python-tox.yml@asottile
parameters:
toxenvs: [pypy3, py36, py37, py38, py39]

View File

@@ -59,6 +59,7 @@ class DelModification(NamedTuple):
class Buf:
def __init__(self, lines: List[str], tab_size: int = 4) -> None:
self._lines = lines
self.expandtabs = True
self.tab_size = tab_size
self.file_y = self.y = self._x = self._x_hint = 0
@@ -242,6 +243,13 @@ class Buf:
# 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:
x = self._cursor_x if idx == self.y else 0
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)
def raw_color_pair(self, fg: int, bg: int) -> int:
try:
return self.raw_pairs[(fg, bg)]
except KeyError:
pass
if curses.COLORS > 0:
try:
return self.raw_pairs[(fg, bg)]
except KeyError:
pass
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
curses.init_pair(n, fg, bg)
return n
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
curses.init_pair(n, fg, bg)
return n
else:
return 0
@classmethod
def make(cls) -> 'ColorManager':

View File

@@ -236,7 +236,7 @@ class File:
sio = io.StringIO(stdin)
lines, self.nl, mixed, self.sha256 = get_lines(sio)
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)
else:
if self.filename is not None:
@@ -476,7 +476,11 @@ class File:
if self.buf.y == 0 and self.buf.x == 0:
pass
# backspace at the end of the file does not change the contents
elif self.buf.y == len(self.buf) - 1:
elif (
self.buf.y == len(self.buf) - 1 and
# still allow backspace if there are 2+ blank lines
self.buf[self.buf.y - 1] != ''
):
self.buf.left(margin)
# at the beginning of the line, we join the current line and
# the previous line
@@ -524,20 +528,29 @@ class File:
assert self.selection.start is not None
sel_y, sel_x = self.selection.start
(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):
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:
self.buf.x += self.buf.tab_size
self.buf.x += tab_size
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)
@edit_action('insert tab', final=False)
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]
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.restore_eof_invariant()
@@ -548,9 +561,9 @@ class File:
self._tab(margin)
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
while i < bound and s[i] == ' ':
while i < bound and s[i] in (' ', '\t'):
i += 1
return i
@@ -673,39 +686,58 @@ class File:
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]
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()}'
indent = self._indent(lineno)
ws_len = len(indent)
def _comment_add(self, lineno: int, prefix: str) -> None:
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]
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 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:
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)
@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)
self._comment_add(lineno, prefix, minimum_indent)
DISPATCH = {
# movement

View File

@@ -688,7 +688,7 @@ class Grammars:
pass
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)
file_types = frozenset(ret.get('fileTypes', ()))

View File

@@ -19,7 +19,8 @@ class History:
history_dir = xdg_data('history')
os.makedirs(history_dir, exist_ok=True)
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._orig_len[filename] = len(self.data[filename])
try:
@@ -28,5 +29,6 @@ class History:
for k, v in self.data.items():
new_history = v[self._orig_len[k]:]
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')

View File

@@ -133,7 +133,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
if '-' in args.filenames:
print('reading stdin...', file=sys.stderr)
stdin = sys.stdin.read()
stdin = sys.stdin.buffer.read().decode()
tty = os.open(CONSOLE, os.O_RDONLY)
os.dup2(tty, sys.stdin.fileno())
else:

View File

@@ -36,7 +36,7 @@ class Perf:
def save_profiles(self, filename: str) -> None:
assert self._prof is not None
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')
for name, duration in self._records:
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 = {
'\x1bOH': b'KEY_HOME',
'\x1bOF': b'KEY_END',
'\x1b[1~': b'KEY_HOME',
'\x1b[4~': b'KEY_END',
'\x1b[1;2A': b'KEY_SR',
'\x1b[1;2B': b'KEY_SF',
'\x1b[1;2C': b'KEY_SRIGHT',
@@ -60,6 +62,7 @@ SEQUENCE_KEYNAME = {
'\x1b[1;6D': b'kLFT6', # Shift + ^Left
'\x1b[1;6H': b'kHOM6', # Shift + ^Home
'\x1b[1;6F': b'kEND6', # Shift + ^End
'\x1b[~': b'KEY_BTAB', # Shift + Tab
}
KEYNAME_REWRITE = {
# windows-curses: numeric pad arrow keys
@@ -93,6 +96,7 @@ KEYNAME_REWRITE = {
b'^?': b'KEY_BACKSPACE',
# linux, perhaps others
b'^H': b'KEY_BACKSPACE', # ^Backspace on my keyboard
b'^D': b'KEY_DC',
b'PADENTER': b'^M', # Enter on numpad
}
@@ -269,7 +273,7 @@ class Screen:
prompt: str,
opt_strs: Tuple[str, ...],
) -> Union[str, PromptResult]:
opts = [opt[0] for opt in opt_strs]
opts = {opt[0] for opt in opt_strs}
while True:
x = 0
prompt_line = self.margin.lines - 1
@@ -306,8 +310,8 @@ class Screen:
self.resize()
elif key.keyname == b'^C':
return self.status.cancelled()
elif isinstance(key.wch, str) and key.wch in opts:
return key.wch
elif isinstance(key.wch, str) and key.wch.lower() in opts:
return key.wch.lower()
def prompt(
self,
@@ -457,6 +461,14 @@ class Screen:
for file in self.files:
file.buf.set_tab_size(parsed_tab_size)
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()
@@ -483,7 +495,7 @@ class Screen:
self.file.filename = 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)
else:
sha256 = hashlib.sha256(b'').hexdigest()
@@ -496,8 +508,14 @@ class Screen:
self.status.update('(file changed on disk, not implemented)')
return PromptResult.CANCELLED
with open(self.file.filename, 'w', newline='') as f:
f.write(contents)
try:
with open(
self.file.filename, 'w', encoding='UTF-8', newline='',
) as f:
f.write(contents)
except OSError as e:
self.status.update(f'cannot save file: {e}')
return PromptResult.CANCELLED
self.file.modified = False
self.file.sha256 = sha256_to_save
@@ -581,7 +599,10 @@ class Screen:
def _init_screen() -> 'curses._CursesWindow':
# set the escape delay so curses does not pause waiting for sequences
if sys.version_info >= (3, 9): # pragma: no cover
if (
sys.version_info >= (3, 9) and
hasattr(curses, 'set_escdelay')
): # pragma: no cover
curses.set_escdelay(25)
else: # pragma: no cover
os.environ.setdefault('ESCDELAY', '25')

View File

@@ -37,7 +37,7 @@ def _highlight_output(theme: Theme, compiler: Compiler, filename: str) -> int:
if theme.default.bg is not None:
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):
first_line = line_idx == 0
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')
args = parser.parse_args(argv)
with open(args.filename) as f:
with open(args.filename, encoding='UTF-8') as f:
first_line = next(f, '')
theme = Theme.from_filename(args.theme)

View File

@@ -147,5 +147,5 @@ class Theme(NamedTuple):
if not os.path.exists(filename):
return cls.blank()
else:
with open(filename) as f:
with open(filename, encoding='UTF-8') as f:
return cls.from_dct(json.load(f))

View File

@@ -1,6 +1,6 @@
[metadata]
name = babi
version = 0.0.14
version = 0.0.21
description = a text editor
long_description = file: README.md
long_description_content_type = text/markdown
@@ -16,6 +16,7 @@ classifiers =
Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: Implementation :: CPython
Programming Language :: Python :: Implementation :: PyPy

View File

@@ -1,7 +1,16 @@
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')
@@ -12,6 +21,20 @@ def test_comment_some_code(run, ten_lines):
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')
@@ -73,3 +96,110 @@ def test_comment_with_trailing_whitespace(run, ten_lines):
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

@@ -290,6 +290,7 @@ KEYS = [
Key('^_', b'^_', '\x1f'),
Key('^\\', b'^\\', '\x1c'),
Key('!resize', b'KEY_RESIZE', curses.KEY_RESIZE),
Key('^D', b'^D', '\x04'),
]
KEYS_TMUX = {k.tmux: k.wch for k in KEYS}
KEYS_CURSES = {k.value: k.curses for k in KEYS}
@@ -303,6 +304,7 @@ class DeferredRunner:
self.color_pairs = {0: (7, 0)}
self.screen = Screen(width, height)
self._n_colors, self._can_change_color = {
'xterm-mono': (0, False),
'screen': (8, False),
'screen-256color': (256, False),
'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 trigger_command_mode
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):
with run() as h, and_exit(h):
h.press('h')
h.press('hello')
h.press('Home')
h.press('Right')
h.press('Tab')
h.press('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):
@@ -86,6 +88,20 @@ def test_dedent_selection(run, tmpdir):
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):
f = tmpdir.join('f')
f.write(' hi\n')

View File

@@ -30,7 +30,8 @@ def test_replace_cancel_at_replace_string(run):
h.await_text('cancelled')
def test_replace_actual_contents(run, ten_lines):
@pytest.mark.parametrize('key', ('y', 'Y'))
def test_replace_actual_contents(run, ten_lines, key):
with run(str(ten_lines)) as h, and_exit(h):
h.press('^\\')
h.await_text('search (to replace):')
@@ -38,7 +39,7 @@ def test_replace_actual_contents(run, ten_lines):
h.await_text('replace with:')
h.press_and_enter('ohai')
h.await_text('replace [yes, no, all]?')
h.press('y')
h.press(key)
h.await_text_missing('line_0')
h.await_text('ohai')
h.await_text(' *')

View File

@@ -128,6 +128,18 @@ def test_save_file_when_it_did_not_exist(run, tmpdir):
assert f.read() == 'hello world\n'
def test_saving_file_permission_denied(run, tmpdir):
f = tmpdir.join('f').ensure()
f.chmod(0o400)
with run(str(f)) as h, and_exit(h):
h.press('hello world')
h.press('^S')
# the filename message is missing as it is too long to be captured
h.await_text('cannot save file: [Errno 13] Permission denied:')
h.await_text(' *')
def test_save_via_ctrl_o(run, tmpdir):
f = tmpdir.join('f')
with run(str(f)) as h, and_exit(h):

View File

@@ -153,3 +153,8 @@ def test_syntax_highlighting_tabs_after_line_creation(run, tmpdir):
h.press('Enter')
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

View File

@@ -50,6 +50,18 @@ def test_backspace_at_end_of_file_still_allows_scrolling_down(run, tmpdir):
h.await_text_missing('*')
def test_backspace_deletes_newline_at_end_of_file(run, tmpdir):
f = tmpdir.join('f')
f.write('foo\n\n')
with run(str(f)) as h, and_exit(h):
h.press('^End')
h.press('BSpace')
h.press('^S')
assert f.read() == 'foo\n'
@pytest.mark.parametrize('key', ('BSpace', '^H'))
def test_backspace_deletes_text(run, tmpdir, key):
f = tmpdir.join('f')
@@ -72,14 +84,15 @@ def test_delete_at_end_of_file(run, tmpdir):
h.await_text_missing('*')
def test_delete_removes_character_afterwards(run, tmpdir):
@pytest.mark.parametrize('key', ('DC', '^D'))
def test_delete_removes_character_afterwards(run, tmpdir, key):
f = tmpdir.join('f')
f.write('hello world')
with run(str(f)) as h, and_exit(h):
h.await_text('hello world')
h.press('Right')
h.press('DC')
h.press(key)
h.await_text('hllo world')
h.await_text('f *')