Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7904fdf9b | ||
|
|
f6db46736c | ||
|
|
e059dc294b | ||
|
|
4dc42aa628 | ||
|
|
25659ca5c7 | ||
|
|
bd8dc3beb0 | ||
|
|
324513c36a | ||
|
|
09643b0f80 | ||
|
|
8245ee56a5 | ||
|
|
8aaee402e7 | ||
|
|
641eed65d5 | ||
|
|
694853e341 | ||
|
|
1a4d15206c | ||
|
|
4ca3b0d1e5 | ||
|
|
47868e77a2 | ||
|
|
9906e223bd | ||
|
|
a5101007cd | ||
|
|
572151197d | ||
|
|
a8a5afc6ed | ||
|
|
e523a694b6 | ||
|
|
8161c9e34f | ||
|
|
0b5e187be5 | ||
|
|
ee13b7bb78 | ||
|
|
cad35f7b4d | ||
|
|
c1a9823894 | ||
|
|
899eb6f879 | ||
|
|
945e0b1620 | ||
|
|
1f348882b8 | ||
|
|
604942306f | ||
|
|
00570f8eda | ||
|
|
51a7b10192 | ||
|
|
4d1101daf9 | ||
|
|
08ec1874d1 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: asottile
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v2.5.0
|
rev: v3.4.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -11,34 +11,34 @@ repos:
|
|||||||
- id: name-tests-test
|
- id: name-tests-test
|
||||||
- id: requirements-txt-fixer
|
- id: requirements-txt-fixer
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://gitlab.com/pycqa/flake8
|
||||||
rev: 3.8.0
|
rev: 3.8.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: flake8
|
- id: flake8
|
||||||
additional_dependencies: [flake8-typing-imports==1.7.0]
|
additional_dependencies: [flake8-typing-imports==1.7.0]
|
||||||
- repo: https://github.com/pre-commit/mirrors-autopep8
|
- repo: https://github.com/pre-commit/mirrors-autopep8
|
||||||
rev: v1.5.2
|
rev: v1.5.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: autopep8
|
- id: autopep8
|
||||||
- repo: https://github.com/asottile/reorder_python_imports
|
- repo: https://github.com/asottile/reorder_python_imports
|
||||||
rev: v2.3.0
|
rev: v2.3.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: [--py3-plus]
|
args: [--py3-plus]
|
||||||
- repo: https://github.com/asottile/add-trailing-comma
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
rev: v2.0.1
|
rev: v2.1.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: add-trailing-comma
|
- id: add-trailing-comma
|
||||||
args: [--py36-plus]
|
args: [--py36-plus]
|
||||||
- repo: https://github.com/asottile/pyupgrade
|
- repo: https://github.com/asottile/pyupgrade
|
||||||
rev: v2.4.1
|
rev: v2.7.4
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py36-plus]
|
args: [--py36-plus]
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v1.9.0
|
rev: v1.16.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: setup-cfg-fmt
|
- id: setup-cfg-fmt
|
||||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||||
rev: v0.770
|
rev: v0.800
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
[](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)
|
||||||
|
[](https://results.pre-commit.ci/latest/github/asottile/babi/master)
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -17,6 +18,13 @@ a text editor, eventually...
|
|||||||
I used to use the text editor `nano`, frequently I typo this. on a qwerty
|
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`.
|
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`:
|
||||||
|
|
||||||
|
[](https://youtu.be/WyR1hAGmR3g)
|
||||||
|
|
||||||
### quitting babi
|
### quitting babi
|
||||||
|
|
||||||
currently you can quit `babi` by using <kbd>^X</kbd> (or via <kbd>esc</kbd> +
|
currently you can quit `babi` by using <kbd>^X</kbd> (or via <kbd>esc</kbd> +
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ resources:
|
|||||||
ref: refs/tags/v2.0.0
|
ref: refs/tags/v2.0.0
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
- template: job--pre-commit.yml@asottile
|
|
||||||
- template: job--python-tox.yml@asottile
|
- template: job--python-tox.yml@asottile
|
||||||
parameters:
|
parameters:
|
||||||
toxenvs: [pypy3, py36, py37, py38, py39]
|
toxenvs: [pypy3, py36, py37, py38, py39]
|
||||||
|
|||||||
11
babi/file.py
11
babi/file.py
@@ -476,7 +476,11 @@ class File:
|
|||||||
if self.buf.y == 0 and self.buf.x == 0:
|
if self.buf.y == 0 and self.buf.x == 0:
|
||||||
pass
|
pass
|
||||||
# backspace at the end of the file does not change the contents
|
# 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)
|
self.buf.left(margin)
|
||||||
# at the beginning of the line, we join the current line and
|
# at the beginning of the line, we join the current line and
|
||||||
# the previous line
|
# the previous line
|
||||||
@@ -707,7 +711,10 @@ class File:
|
|||||||
def _comment_add(self, lineno: int, prefix: str, s_offset: int) -> None:
|
def _comment_add(self, lineno: int, prefix: str, s_offset: int) -> None:
|
||||||
line = self.buf[lineno]
|
line = self.buf[lineno]
|
||||||
|
|
||||||
self.buf[lineno] = f'{line[:s_offset]}{prefix} {line[s_offset:]}'
|
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:
|
if lineno == self.buf.y and self.buf.x > s_offset:
|
||||||
self.buf.x += len(self.buf[lineno]) - len(line)
|
self.buf.x += len(self.buf[lineno]) - len(line)
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -93,6 +96,7 @@ KEYNAME_REWRITE = {
|
|||||||
b'^?': b'KEY_BACKSPACE',
|
b'^?': b'KEY_BACKSPACE',
|
||||||
# linux, perhaps others
|
# linux, perhaps others
|
||||||
b'^H': b'KEY_BACKSPACE', # ^Backspace on my keyboard
|
b'^H': b'KEY_BACKSPACE', # ^Backspace on my keyboard
|
||||||
|
b'^D': b'KEY_DC',
|
||||||
b'PADENTER': b'^M', # Enter on numpad
|
b'PADENTER': b'^M', # Enter on numpad
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,7 +273,7 @@ class Screen:
|
|||||||
prompt: str,
|
prompt: str,
|
||||||
opt_strs: Tuple[str, ...],
|
opt_strs: Tuple[str, ...],
|
||||||
) -> Union[str, PromptResult]:
|
) -> Union[str, PromptResult]:
|
||||||
opts = [opt[0] for opt in opt_strs]
|
opts = {opt[0] for opt in opt_strs}
|
||||||
while True:
|
while True:
|
||||||
x = 0
|
x = 0
|
||||||
prompt_line = self.margin.lines - 1
|
prompt_line = self.margin.lines - 1
|
||||||
@@ -306,8 +310,8 @@ class Screen:
|
|||||||
self.resize()
|
self.resize()
|
||||||
elif key.keyname == b'^C':
|
elif key.keyname == b'^C':
|
||||||
return self.status.cancelled()
|
return self.status.cancelled()
|
||||||
elif isinstance(key.wch, str) and key.wch in opts:
|
elif isinstance(key.wch, str) and key.wch.lower() in opts:
|
||||||
return key.wch
|
return key.wch.lower()
|
||||||
|
|
||||||
def prompt(
|
def prompt(
|
||||||
self,
|
self,
|
||||||
@@ -504,8 +508,14 @@ 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', encoding='UTF-8', newline='') as f:
|
try:
|
||||||
f.write(contents)
|
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.modified = False
|
||||||
self.file.sha256 = sha256_to_save
|
self.file.sha256 = sha256_to_save
|
||||||
@@ -589,7 +599,10 @@ class Screen:
|
|||||||
|
|
||||||
def _init_screen() -> 'curses._CursesWindow':
|
def _init_screen() -> 'curses._CursesWindow':
|
||||||
# set the escape delay so curses does not pause waiting for sequences
|
# 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)
|
curses.set_escdelay(25)
|
||||||
else: # pragma: no cover
|
else: # pragma: no cover
|
||||||
os.environ.setdefault('ESCDELAY', '25')
|
os.environ.setdefault('ESCDELAY', '25')
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = babi
|
name = babi
|
||||||
version = 0.0.17
|
version = 0.0.21
|
||||||
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
|
||||||
@@ -16,6 +16,7 @@ classifiers =
|
|||||||
Programming Language :: Python :: 3.6
|
Programming Language :: Python :: 3.6
|
||||||
Programming Language :: Python :: 3.7
|
Programming Language :: Python :: 3.7
|
||||||
Programming Language :: Python :: 3.8
|
Programming Language :: Python :: 3.8
|
||||||
|
Programming Language :: Python :: 3.9
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
|
||||||
|
|||||||
@@ -21,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')
|
||||||
|
|||||||
@@ -290,6 +290,7 @@ KEYS = [
|
|||||||
Key('^_', b'^_', '\x1f'),
|
Key('^_', b'^_', '\x1f'),
|
||||||
Key('^\\', b'^\\', '\x1c'),
|
Key('^\\', b'^\\', '\x1c'),
|
||||||
Key('!resize', b'KEY_RESIZE', curses.KEY_RESIZE),
|
Key('!resize', b'KEY_RESIZE', curses.KEY_RESIZE),
|
||||||
|
Key('^D', b'^D', '\x04'),
|
||||||
]
|
]
|
||||||
KEYS_TMUX = {k.tmux: k.wch for k in KEYS}
|
KEYS_TMUX = {k.tmux: k.wch for k in KEYS}
|
||||||
KEYS_CURSES = {k.value: k.curses for k in KEYS}
|
KEYS_CURSES = {k.value: k.curses for k in KEYS}
|
||||||
|
|||||||
@@ -30,7 +30,8 @@ def test_replace_cancel_at_replace_string(run):
|
|||||||
h.await_text('cancelled')
|
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):
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
h.press('^\\')
|
h.press('^\\')
|
||||||
h.await_text('search (to replace):')
|
h.await_text('search (to replace):')
|
||||||
@@ -38,7 +39,7 @@ def test_replace_actual_contents(run, ten_lines):
|
|||||||
h.await_text('replace with:')
|
h.await_text('replace with:')
|
||||||
h.press_and_enter('ohai')
|
h.press_and_enter('ohai')
|
||||||
h.await_text('replace [yes, no, all]?')
|
h.await_text('replace [yes, no, all]?')
|
||||||
h.press('y')
|
h.press(key)
|
||||||
h.await_text_missing('line_0')
|
h.await_text_missing('line_0')
|
||||||
h.await_text('ohai')
|
h.await_text('ohai')
|
||||||
h.await_text(' *')
|
h.await_text(' *')
|
||||||
|
|||||||
@@ -128,6 +128,18 @@ def test_save_file_when_it_did_not_exist(run, tmpdir):
|
|||||||
assert f.read() == 'hello world\n'
|
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):
|
def test_save_via_ctrl_o(run, tmpdir):
|
||||||
f = tmpdir.join('f')
|
f = tmpdir.join('f')
|
||||||
with run(str(f)) as h, and_exit(h):
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ def test_backspace_at_end_of_file_still_allows_scrolling_down(run, tmpdir):
|
|||||||
h.await_text_missing('*')
|
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'))
|
@pytest.mark.parametrize('key', ('BSpace', '^H'))
|
||||||
def test_backspace_deletes_text(run, tmpdir, key):
|
def test_backspace_deletes_text(run, tmpdir, key):
|
||||||
f = tmpdir.join('f')
|
f = tmpdir.join('f')
|
||||||
@@ -72,14 +84,15 @@ def test_delete_at_end_of_file(run, tmpdir):
|
|||||||
h.await_text_missing('*')
|
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 = tmpdir.join('f')
|
||||||
f.write('hello world')
|
f.write('hello world')
|
||||||
|
|
||||||
with run(str(f)) as h, and_exit(h):
|
with run(str(f)) as h, and_exit(h):
|
||||||
h.await_text('hello world')
|
h.await_text('hello world')
|
||||||
h.press('Right')
|
h.press('Right')
|
||||||
h.press('DC')
|
h.press(key)
|
||||||
h.await_text('hllo world')
|
h.await_text('hllo world')
|
||||||
h.await_text('f *')
|
h.await_text('f *')
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user