Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4cba4a884 | ||
|
|
38faec4519 | ||
|
|
b10afd5bd8 | ||
|
|
b1080319ae | ||
|
|
89dee66711 | ||
|
|
3fb588bba9 | ||
|
|
99be8b59c9 | ||
|
|
d4da5eb800 | ||
|
|
0c3abfc727 | ||
|
|
c3a34c6262 | ||
|
|
ae30b5763c | ||
|
|
1cad38d632 | ||
|
|
d2a7014925 | ||
|
|
9eae3da253 | ||
|
|
075338563e | ||
|
|
a7e83ef089 | ||
|
|
03656c04bb | ||
|
|
4be3cbff35 | ||
|
|
99b3371739 | ||
|
|
3c9c6173c3 | ||
|
|
84015d3ac4 | ||
|
|
d7ffdd1db8 | ||
|
|
4e0c02eaa2 | ||
|
|
79919ece2a | ||
|
|
f0ad0e4977 | ||
|
|
f18796b78d | ||
|
|
1698533787 | ||
|
|
10fca36ea3 | ||
|
|
e6aab391f6 | ||
|
|
ebee8fe6ff | ||
|
|
04fc97a8f9 | ||
|
|
c49e722498 | ||
|
|
396d0e3a93 | ||
|
|
660bf9bac0 | ||
|
|
0b3918f26f | ||
|
|
9dffc276dc | ||
|
|
ea6dbb69a6 | ||
|
|
eeeba9e11d | ||
|
|
b5538b3818 | ||
|
|
16c60d68ad | ||
|
|
27aa865989 | ||
|
|
4b13488e8f | ||
|
|
b4593d281a | ||
|
|
3ddf1c72f8 | ||
|
|
8f91b8c9ff | ||
|
|
c48d3ed741 | ||
|
|
43a650925b | ||
|
|
3116828e44 | ||
|
|
9bc43d58e4 | ||
|
|
bbc9647eb3 | ||
|
|
e36ed09f1f | ||
|
|
fb52b5f71c | ||
|
|
1916b49a06 | ||
|
|
66c178b0b7 | ||
|
|
ea4058549f | ||
|
|
cb6b431308 | ||
|
|
a8283adb54 | ||
|
|
40bf9969fb | ||
|
|
a04b8fdca6 | ||
|
|
76eef9adb6 | ||
|
|
fa962d6cb9 | ||
|
|
f40f93b983 | ||
|
|
af01959a48 | ||
|
|
16f4ec3681 | ||
|
|
291d34028a | ||
|
|
2c200b97ed | ||
|
|
fc185b0eef | ||
|
|
21357ed235 | ||
|
|
1a023d3830 | ||
|
|
61063b306c | ||
|
|
628d3ced55 | ||
|
|
b6dc975143 | ||
|
|
4537df6aa1 | ||
|
|
1d1307aa1c | ||
|
|
194d1c5b9b | ||
|
|
e59c860097 | ||
|
|
0a39d73959 | ||
|
|
f8bf24482e | ||
|
|
50079514fd | ||
|
|
96402e30cf | ||
|
|
048ed590ff | ||
|
|
ab6cfcc8c9 | ||
|
|
6348313071 | ||
|
|
c59e00975b | ||
|
|
e3ba08a331 | ||
|
|
6bdf0ff2ea | ||
|
|
bc699e60e1 | ||
|
|
151a56c7f8 | ||
|
|
d958934fdd | ||
|
|
9a8bda1f15 | ||
|
|
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,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: v4.0.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
@@ -10,35 +10,35 @@ repos:
|
|||||||
- id: double-quote-string-fixer
|
- id: double-quote-string-fixer
|
||||||
- id: name-tests-test
|
- id: name-tests-test
|
||||||
- id: requirements-txt-fixer
|
- id: requirements-txt-fixer
|
||||||
- repo: https://gitlab.com/pycqa/flake8
|
- repo: https://github.com/PyCQA/flake8
|
||||||
rev: 3.8.0
|
rev: 4.0.1
|
||||||
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.7
|
||||||
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.6.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: reorder-python-imports
|
- id: reorder-python-imports
|
||||||
args: [--py3-plus]
|
args: [--py3-plus, --add-import, 'from __future__ import annotations']
|
||||||
- repo: https://github.com/asottile/add-trailing-comma
|
- repo: https://github.com/asottile/add-trailing-comma
|
||||||
rev: v2.0.1
|
rev: v2.2.1
|
||||||
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.29.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: pyupgrade
|
- id: pyupgrade
|
||||||
args: [--py36-plus]
|
args: [--py37-plus]
|
||||||
- repo: https://github.com/asottile/setup-cfg-fmt
|
- repo: https://github.com/asottile/setup-cfg-fmt
|
||||||
rev: v1.9.0
|
rev: v1.20.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.910-1
|
||||||
hooks:
|
hooks:
|
||||||
- id: mypy
|
- id: mypy
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -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> +
|
||||||
@@ -79,6 +87,25 @@ here's a modified vs dark plus theme that works:
|
|||||||
./bin/download-theme vs-dark-asottile https://gist.github.com/asottile/b465856c82b1aaa4ba8c7c6314a72e13/raw/22d602fb355fb12b04f176a733941ba5713bc36c/vs_dark_asottile.json
|
./bin/download-theme vs-dark-asottile https://gist.github.com/asottile/b465856c82b1aaa4ba8c7c6314a72e13/raw/22d602fb355fb12b04f176a733941ba5713bc36c/vs_dark_asottile.json
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### keyboard shortcuts on macos
|
||||||
|
|
||||||
|
to get the most out of babi's built in keyboard shortcuts, a few settings must
|
||||||
|
be changed on macos with Terminal.app:
|
||||||
|
|
||||||
|
- in **System Preferences**: **Keyboard** > **Shortcuts** >
|
||||||
|
**Mission Control**: disable or rebind "Move left a space" and
|
||||||
|
"Move right a space" (the defaults `⌃ →` and `⌃ ←` conflict)
|
||||||
|
- in **Terminal.app**: **Terminal** > **Preferences** > **Profiles** >
|
||||||
|
**Keyboard**:
|
||||||
|
- check **Use Option as Meta key**
|
||||||
|
- ensure the following keys are enabled:
|
||||||
|
- `⌃ →`: `\033[1;5C`
|
||||||
|
- `⌃ ←`: `\033[1;5D`
|
||||||
|
- `⇧ ↑`: `\033[1;2A`
|
||||||
|
- `⇧ ↓`: `\033[1;2B`
|
||||||
|
- `⇧ →`: `\033[1;2C`
|
||||||
|
- `⇧ ←`: `\033[1;2D`
|
||||||
|
|
||||||
## demos
|
## demos
|
||||||
|
|
||||||
most things work! here's a few screenshots
|
most things work! here's a few screenshots
|
||||||
|
|||||||
@@ -10,11 +10,10 @@ resources:
|
|||||||
type: github
|
type: github
|
||||||
endpoint: github
|
endpoint: github
|
||||||
name: asottile/azure-pipeline-templates
|
name: asottile/azure-pipeline-templates
|
||||||
ref: refs/tags/v2.0.0
|
ref: refs/tags/v2.1.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: [py37, py38, py39]
|
||||||
os: linux
|
os: linux
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from babi.main import main
|
from babi.main import main
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|||||||
47
babi/buf.py
47
babi/buf.py
@@ -1,12 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import bisect
|
import bisect
|
||||||
import contextlib
|
import contextlib
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi._types import Protocol
|
from babi._types import Protocol
|
||||||
from babi.horizontal_scrolling import line_x
|
from babi.horizontal_scrolling import line_x
|
||||||
@@ -19,7 +18,7 @@ DelCallback = Callable[['Buf', int, str], None]
|
|||||||
InsCallback = Callable[['Buf', int], None]
|
InsCallback = Callable[['Buf', int], None]
|
||||||
|
|
||||||
|
|
||||||
def _offsets(s: str, tab_size: int) -> Tuple[int, ...]:
|
def _offsets(s: str, tab_size: int) -> tuple[int, ...]:
|
||||||
ret = [0]
|
ret = [0]
|
||||||
for c in s:
|
for c in s:
|
||||||
if c == '\t':
|
if c == '\t':
|
||||||
@@ -30,14 +29,14 @@ def _offsets(s: str, tab_size: int) -> Tuple[int, ...]:
|
|||||||
|
|
||||||
|
|
||||||
class Modification(Protocol):
|
class Modification(Protocol):
|
||||||
def __call__(self, buf: 'Buf') -> None: ...
|
def __call__(self, buf: Buf) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
class SetModification(NamedTuple):
|
class SetModification(NamedTuple):
|
||||||
idx: int
|
idx: int
|
||||||
s: str
|
s: str
|
||||||
|
|
||||||
def __call__(self, buf: 'Buf') -> None:
|
def __call__(self, buf: Buf) -> None:
|
||||||
buf[self.idx] = self.s
|
buf[self.idx] = self.s
|
||||||
|
|
||||||
|
|
||||||
@@ -45,29 +44,29 @@ class InsModification(NamedTuple):
|
|||||||
idx: int
|
idx: int
|
||||||
s: str
|
s: str
|
||||||
|
|
||||||
def __call__(self, buf: 'Buf') -> None:
|
def __call__(self, buf: Buf) -> None:
|
||||||
buf.insert(self.idx, self.s)
|
buf.insert(self.idx, self.s)
|
||||||
|
|
||||||
|
|
||||||
class DelModification(NamedTuple):
|
class DelModification(NamedTuple):
|
||||||
idx: int
|
idx: int
|
||||||
|
|
||||||
def __call__(self, buf: 'Buf') -> None:
|
def __call__(self, buf: Buf) -> None:
|
||||||
del buf[self.idx]
|
del buf[self.idx]
|
||||||
|
|
||||||
|
|
||||||
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.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
|
||||||
|
|
||||||
self._set_callbacks: List[SetCallback] = [self._set_cb]
|
self._set_callbacks: list[SetCallback] = [self._set_cb]
|
||||||
self._del_callbacks: List[DelCallback] = [self._del_cb]
|
self._del_callbacks: list[DelCallback] = [self._del_cb]
|
||||||
self._ins_callbacks: List[InsCallback] = [self._ins_cb]
|
self._ins_callbacks: list[InsCallback] = [self._ins_cb]
|
||||||
|
|
||||||
self._positions: List[Optional[Tuple[int, ...]]] = []
|
self._positions: list[tuple[int, ...] | None] = []
|
||||||
|
|
||||||
# read only interface
|
# read only interface
|
||||||
|
|
||||||
@@ -163,16 +162,16 @@ class Buf:
|
|||||||
self._ins_callbacks.remove(cb)
|
self._ins_callbacks.remove(cb)
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def record(self) -> Generator[List[Modification], None, None]:
|
def record(self) -> Generator[list[Modification], None, None]:
|
||||||
modifications: List[Modification] = []
|
modifications: list[Modification] = []
|
||||||
|
|
||||||
def set_cb(buf: 'Buf', idx: int, victim: str) -> None:
|
def set_cb(buf: Buf, idx: int, victim: str) -> None:
|
||||||
modifications.append(SetModification(idx, victim))
|
modifications.append(SetModification(idx, victim))
|
||||||
|
|
||||||
def del_cb(buf: 'Buf', idx: int, victim: str) -> None:
|
def del_cb(buf: Buf, idx: int, victim: str) -> None:
|
||||||
modifications.append(InsModification(idx, victim))
|
modifications.append(InsModification(idx, victim))
|
||||||
|
|
||||||
def ins_cb(buf: 'Buf', idx: int) -> None:
|
def ins_cb(buf: Buf, idx: int) -> None:
|
||||||
modifications.append(DelModification(idx))
|
modifications.append(DelModification(idx))
|
||||||
|
|
||||||
self.add_set_callback(set_cb)
|
self.add_set_callback(set_cb)
|
||||||
@@ -185,7 +184,7 @@ class Buf:
|
|||||||
self.remove_del_callback(del_cb)
|
self.remove_del_callback(del_cb)
|
||||||
self.remove_set_callback(set_cb)
|
self.remove_set_callback(set_cb)
|
||||||
|
|
||||||
def apply(self, modifications: List[Modification]) -> List[Modification]:
|
def apply(self, modifications: list[Modification]) -> list[Modification]:
|
||||||
with self.record() as ret_modifications:
|
with self.record() as ret_modifications:
|
||||||
for modification in reversed(modifications):
|
for modification in reversed(modifications):
|
||||||
modification(self)
|
modification(self)
|
||||||
@@ -209,19 +208,19 @@ class Buf:
|
|||||||
def _extend_positions(self, idx: int) -> None:
|
def _extend_positions(self, idx: int) -> None:
|
||||||
self._positions.extend([None] * (1 + idx - len(self._positions)))
|
self._positions.extend([None] * (1 + idx - len(self._positions)))
|
||||||
|
|
||||||
def _set_cb(self, buf: 'Buf', idx: int, victim: str) -> None:
|
def _set_cb(self, buf: Buf, idx: int, victim: str) -> None:
|
||||||
self._extend_positions(idx)
|
self._extend_positions(idx)
|
||||||
self._positions[idx] = None
|
self._positions[idx] = None
|
||||||
|
|
||||||
def _del_cb(self, buf: 'Buf', idx: int, victim: str) -> None:
|
def _del_cb(self, buf: Buf, idx: int, victim: str) -> None:
|
||||||
self._extend_positions(idx)
|
self._extend_positions(idx)
|
||||||
del self._positions[idx]
|
del self._positions[idx]
|
||||||
|
|
||||||
def _ins_cb(self, buf: 'Buf', idx: int) -> None:
|
def _ins_cb(self, buf: Buf, idx: int) -> None:
|
||||||
self._extend_positions(idx)
|
self._extend_positions(idx)
|
||||||
self._positions.insert(idx, None)
|
self._positions.insert(idx, None)
|
||||||
|
|
||||||
def line_positions(self, idx: int) -> Tuple[int, ...]:
|
def line_positions(self, idx: int) -> tuple[int, ...]:
|
||||||
self._extend_positions(idx)
|
self._extend_positions(idx)
|
||||||
value = self._positions[idx]
|
value = self._positions[idx]
|
||||||
if value is None:
|
if value is None:
|
||||||
@@ -236,7 +235,7 @@ class Buf:
|
|||||||
def _cursor_x(self) -> int:
|
def _cursor_x(self) -> int:
|
||||||
return self.line_positions(self.y)[self.x]
|
return self.line_positions(self.y)[self.x]
|
||||||
|
|
||||||
def cursor_position(self, margin: Margin) -> Tuple[int, int]:
|
def cursor_position(self, margin: Margin) -> tuple[int, int]:
|
||||||
y = self.y - self.file_y + margin.header
|
y = self.y - self.file_y + margin.header
|
||||||
x = self._cursor_x - self.line_x(margin)
|
x = self._cursor_x - self.line_x(margin)
|
||||||
return y, x
|
return y, x
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
if sys.version_info >= (3, 8): # pragma: no cover (>=py38)
|
if sys.version_info >= (3, 8): # pragma: no cover (>=py38)
|
||||||
@@ -5,8 +7,6 @@ if sys.version_info >= (3, 8): # pragma: no cover (>=py38)
|
|||||||
else: # pragma: no cover (<py38)
|
else: # pragma: no cover (<py38)
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Optional
|
|
||||||
from typing import Type
|
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
TSelf = TypeVar('TSelf')
|
TSelf = TypeVar('TSelf')
|
||||||
@@ -18,8 +18,8 @@ else: # pragma: no cover (<py38)
|
|||||||
|
|
||||||
def __get__(
|
def __get__(
|
||||||
self,
|
self,
|
||||||
instance: Optional[TSelf],
|
instance: TSelf | None,
|
||||||
owner: Optional[Type[TSelf]] = None,
|
owner: type[TSelf] | None = None,
|
||||||
) -> TRet:
|
) -> TRet:
|
||||||
assert instance is not None
|
assert instance is not None
|
||||||
ret = instance.__dict__[self._func.__name__] = self._func(instance)
|
ret = instance.__dict__[self._func.__name__] = self._func(instance)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
# TODO: find a standard which defines these
|
# TODO: find a standard which defines these
|
||||||
@@ -11,7 +13,7 @@ class Color(NamedTuple):
|
|||||||
b: int
|
b: int
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def parse(cls, s: str) -> 'Color':
|
def parse(cls, s: str) -> Color:
|
||||||
if s.startswith('#') and len(s) >= 7:
|
if s.startswith('#') and len(s) >= 7:
|
||||||
return cls(r=int(s[1:3], 16), g=int(s[3:5], 16), b=int(s[5:7], 16))
|
return cls(r=int(s[1:3], 16), g=int(s[3:5], 16), b=int(s[5:7], 16))
|
||||||
elif s.startswith('#'):
|
elif s.startswith('#'):
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import itertools
|
import itertools
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi._types import Protocol
|
from babi._types import Protocol
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
@@ -19,19 +18,19 @@ class KD(Protocol):
|
|||||||
@property
|
@property
|
||||||
def n(self) -> int: ...
|
def n(self) -> int: ...
|
||||||
@property
|
@property
|
||||||
def left(self) -> Optional['KD']: ...
|
def left(self) -> KD | None: ...
|
||||||
@property
|
@property
|
||||||
def right(self) -> Optional['KD']: ...
|
def right(self) -> KD | None: ...
|
||||||
|
|
||||||
|
|
||||||
class _KD(NamedTuple):
|
class _KD(NamedTuple):
|
||||||
color: Color
|
color: Color
|
||||||
n: int
|
n: int
|
||||||
left: Optional[KD]
|
left: KD | None
|
||||||
right: Optional[KD]
|
right: KD | None
|
||||||
|
|
||||||
|
|
||||||
def _build(colors: List[Tuple[Color, int]], depth: int = 0) -> Optional[KD]:
|
def _build(colors: list[tuple[Color, int]], depth: int = 0) -> KD | None:
|
||||||
if not colors:
|
if not colors:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -46,11 +45,11 @@ def _build(colors: List[Tuple[Color, int]], depth: int = 0) -> Optional[KD]:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def nearest(color: Color, colors: Optional[KD]) -> int:
|
def nearest(color: Color, colors: KD | None) -> int:
|
||||||
best = 0
|
best = 0
|
||||||
dist = 2 ** 32
|
dist = 2 ** 32
|
||||||
|
|
||||||
def _search(kd: Optional[KD], *, depth: int) -> None:
|
def _search(kd: KD | None, *, depth: int) -> None:
|
||||||
nonlocal best
|
nonlocal best
|
||||||
nonlocal dist
|
nonlocal dist
|
||||||
|
|
||||||
@@ -77,7 +76,7 @@ def nearest(color: Color, colors: Optional[KD]) -> int:
|
|||||||
|
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=1)
|
@functools.lru_cache(maxsize=1)
|
||||||
def make_256() -> Optional[KD]:
|
def make_256() -> KD | None:
|
||||||
vals = (0, 95, 135, 175, 215, 255)
|
vals = (0, 95, 135, 175, 215, 255)
|
||||||
colors = [
|
colors = [
|
||||||
(Color(r, g, b), i)
|
(Color(r, g, b), i)
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
from typing import Dict
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi import color_kd
|
from babi import color_kd
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
|
|
||||||
|
|
||||||
def _color_to_curses(color: Color) -> Tuple[int, int, int]:
|
def _color_to_curses(color: Color) -> tuple[int, int, int]:
|
||||||
factor = 1000 / 255
|
factor = 1000 / 255
|
||||||
return int(color.r * factor), int(color.g * factor), int(color.b * factor)
|
return int(color.r * factor), int(color.g * factor), int(color.b * factor)
|
||||||
|
|
||||||
|
|
||||||
class ColorManager(NamedTuple):
|
class ColorManager(NamedTuple):
|
||||||
colors: Dict[Color, int]
|
colors: dict[Color, int]
|
||||||
raw_pairs: Dict[Tuple[int, int], int]
|
raw_pairs: dict[tuple[int, int], int]
|
||||||
|
|
||||||
def init_color(self, color: Color) -> None:
|
def init_color(self, color: Color) -> None:
|
||||||
if curses.can_change_color():
|
if curses.can_change_color():
|
||||||
@@ -27,7 +26,7 @@ class ColorManager(NamedTuple):
|
|||||||
else:
|
else:
|
||||||
self.colors[color] = -1
|
self.colors[color] = -1
|
||||||
|
|
||||||
def color_pair(self, fg: Optional[Color], bg: Optional[Color]) -> int:
|
def color_pair(self, fg: Color | None, bg: Color | None) -> int:
|
||||||
fg_i = self.colors[fg] if fg is not None else -1
|
fg_i = self.colors[fg] if fg is not None else -1
|
||||||
bg_i = self.colors[bg] if bg is not None else -1
|
bg_i = self.colors[bg] if bg is not None else -1
|
||||||
return self.raw_color_pair(fg_i, bg_i)
|
return self.raw_color_pair(fg_i, bg_i)
|
||||||
@@ -46,5 +45,5 @@ class ColorManager(NamedTuple):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls) -> 'ColorManager':
|
def make(cls) -> ColorManager:
|
||||||
return cls({}, {})
|
return cls({}, {})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Generic
|
from typing import Generic
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
from typing import Mapping
|
from typing import Mapping
|
||||||
|
|||||||
69
babi/file.py
69
babi/file.py
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
@@ -12,15 +14,11 @@ from typing import Callable
|
|||||||
from typing import cast
|
from typing import cast
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import IO
|
from typing import IO
|
||||||
from typing import List
|
|
||||||
from typing import Match
|
from typing import Match
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Tuple
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.buf import Modification
|
from babi.buf import Modification
|
||||||
@@ -42,7 +40,7 @@ TCallable = TypeVar('TCallable', bound=Callable[..., Any])
|
|||||||
WS_RE = re.compile(r'^\s*')
|
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()
|
||||||
lines = []
|
lines = []
|
||||||
newlines = collections.Counter({'\n': 0}) # default to `\n`
|
newlines = collections.Counter({'\n': 0}) # default to `\n`
|
||||||
@@ -64,7 +62,7 @@ def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]:
|
|||||||
|
|
||||||
class Action:
|
class Action:
|
||||||
def __init__(
|
def __init__(
|
||||||
self, *, name: str, modifications: List[Modification],
|
self, *, name: str, modifications: list[Modification],
|
||||||
start_x: int, start_y: int, start_modified: bool,
|
start_x: int, start_y: int, start_modified: bool,
|
||||||
end_x: int, end_y: int, end_modified: bool,
|
end_x: int, end_y: int, end_modified: bool,
|
||||||
final: bool,
|
final: bool,
|
||||||
@@ -79,7 +77,7 @@ class Action:
|
|||||||
self.end_modified = end_modified
|
self.end_modified = end_modified
|
||||||
self.final = final
|
self.final = final
|
||||||
|
|
||||||
def apply(self, file: 'File') -> 'Action':
|
def apply(self, file: File) -> Action:
|
||||||
action = Action(
|
action = Action(
|
||||||
name=self.name, modifications=file.buf.apply(self.modifications),
|
name=self.name, modifications=file.buf.apply(self.modifications),
|
||||||
start_x=self.end_x, start_y=self.end_y,
|
start_x=self.end_x, start_y=self.end_y,
|
||||||
@@ -98,7 +96,7 @@ class Action:
|
|||||||
|
|
||||||
def action(func: TCallable) -> TCallable:
|
def action(func: TCallable) -> TCallable:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def action_inner(self: 'File', *args: Any, **kwargs: Any) -> Any:
|
def action_inner(self: File, *args: Any, **kwargs: Any) -> Any:
|
||||||
self.finalize_previous_action()
|
self.finalize_previous_action()
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
return cast(TCallable, action_inner)
|
return cast(TCallable, action_inner)
|
||||||
@@ -111,7 +109,7 @@ def edit_action(
|
|||||||
) -> Callable[[TCallable], TCallable]:
|
) -> Callable[[TCallable], TCallable]:
|
||||||
def edit_action_decorator(func: TCallable) -> TCallable:
|
def edit_action_decorator(func: TCallable) -> TCallable:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def edit_action_inner(self: 'File', *args: Any, **kwargs: Any) -> Any:
|
def edit_action_inner(self: File, *args: Any, **kwargs: Any) -> Any:
|
||||||
with self.edit_action_context(name, final=final):
|
with self.edit_action_context(name, final=final):
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
return cast(TCallable, edit_action_inner)
|
return cast(TCallable, edit_action_inner)
|
||||||
@@ -120,7 +118,7 @@ def edit_action(
|
|||||||
|
|
||||||
def keep_selection(func: TCallable) -> TCallable:
|
def keep_selection(func: TCallable) -> TCallable:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def keep_selection_inner(self: 'File', *args: Any, **kwargs: Any) -> Any:
|
def keep_selection_inner(self: File, *args: Any, **kwargs: Any) -> Any:
|
||||||
with self.select():
|
with self.select():
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
return cast(TCallable, keep_selection_inner)
|
return cast(TCallable, keep_selection_inner)
|
||||||
@@ -128,7 +126,7 @@ def keep_selection(func: TCallable) -> TCallable:
|
|||||||
|
|
||||||
def clear_selection(func: TCallable) -> TCallable:
|
def clear_selection(func: TCallable) -> TCallable:
|
||||||
@functools.wraps(func)
|
@functools.wraps(func)
|
||||||
def clear_selection_inner(self: 'File', *args: Any, **kwargs: Any) -> Any:
|
def clear_selection_inner(self: File, *args: Any, **kwargs: Any) -> Any:
|
||||||
ret = func(self, *args, **kwargs)
|
ret = func(self, *args, **kwargs)
|
||||||
self.selection.clear()
|
self.selection.clear()
|
||||||
return ret
|
return ret
|
||||||
@@ -143,7 +141,7 @@ class Found(NamedTuple):
|
|||||||
class _SearchIter:
|
class _SearchIter:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
file: 'File',
|
file: File,
|
||||||
reg: Pattern[str],
|
reg: Pattern[str],
|
||||||
*,
|
*,
|
||||||
offset: int,
|
offset: int,
|
||||||
@@ -155,7 +153,7 @@ class _SearchIter:
|
|||||||
self._start_x = file.buf.x + offset
|
self._start_x = file.buf.x + offset
|
||||||
self._start_y = file.buf.y
|
self._start_y = file.buf.y
|
||||||
|
|
||||||
def __iter__(self) -> '_SearchIter':
|
def __iter__(self) -> _SearchIter:
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def _stop_if_past_original(self, y: int, match: Match[str]) -> Found:
|
def _stop_if_past_original(self, y: int, match: Match[str]) -> Found:
|
||||||
@@ -168,7 +166,7 @@ class _SearchIter:
|
|||||||
raise StopIteration()
|
raise StopIteration()
|
||||||
return Found(y, match)
|
return Found(y, match)
|
||||||
|
|
||||||
def __next__(self) -> Tuple[int, Match[str]]:
|
def __next__(self) -> tuple[int, Match[str]]:
|
||||||
x = self.file.buf.x + self.offset
|
x = self.file.buf.x + self.offset
|
||||||
y = self.file.buf.y
|
y = self.file.buf.y
|
||||||
|
|
||||||
@@ -200,25 +198,25 @@ class _SearchIter:
|
|||||||
class File:
|
class File:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
filename: Optional[str],
|
filename: str | None,
|
||||||
initial_line: int,
|
initial_line: int,
|
||||||
color_manager: ColorManager,
|
color_manager: ColorManager,
|
||||||
hl_factories: Tuple[HLFactory, ...],
|
hl_factories: tuple[HLFactory, ...],
|
||||||
) -> None:
|
) -> None:
|
||||||
self.filename = filename
|
self.filename = filename
|
||||||
self.initial_line = initial_line
|
self.initial_line = initial_line
|
||||||
self.modified = False
|
self.modified = False
|
||||||
self.buf = Buf([])
|
self.buf = Buf([])
|
||||||
self.nl = '\n'
|
self.nl = '\n'
|
||||||
self.sha256: Optional[str] = None
|
self.sha256: str | None = None
|
||||||
self._in_edit_action = False
|
self._in_edit_action = False
|
||||||
self.undo_stack: List[Action] = []
|
self.undo_stack: list[Action] = []
|
||||||
self.redo_stack: List[Action] = []
|
self.redo_stack: list[Action] = []
|
||||||
self._hl_factories = hl_factories
|
self._hl_factories = hl_factories
|
||||||
self._trailing_whitespace = TrailingWhitespace(color_manager)
|
self._trailing_whitespace = TrailingWhitespace(color_manager)
|
||||||
self._replace_hl = Replace()
|
self._replace_hl = Replace()
|
||||||
self.selection = Selection()
|
self.selection = Selection()
|
||||||
self._file_hls: Tuple[FileHL, ...] = ()
|
self._file_hls: tuple[FileHL, ...] = ()
|
||||||
|
|
||||||
def ensure_loaded(
|
def ensure_loaded(
|
||||||
self,
|
self,
|
||||||
@@ -395,14 +393,14 @@ class File:
|
|||||||
@clear_selection
|
@clear_selection
|
||||||
def replace(
|
def replace(
|
||||||
self,
|
self,
|
||||||
screen: 'Screen',
|
screen: Screen,
|
||||||
reg: Pattern[str],
|
reg: Pattern[str],
|
||||||
replace: str,
|
replace: str,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.finalize_previous_action()
|
self.finalize_previous_action()
|
||||||
|
|
||||||
count = 0
|
count = 0
|
||||||
res: Union[str, PromptResult] = ''
|
res: str | PromptResult = ''
|
||||||
search = _SearchIter(self, reg, offset=0)
|
search = _SearchIter(self, reg, offset=0)
|
||||||
for line_y, match in search:
|
for line_y, match in search:
|
||||||
end = match.end()
|
end = match.end()
|
||||||
@@ -476,7 +474,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
|
||||||
@@ -593,7 +595,7 @@ class File:
|
|||||||
|
|
||||||
@edit_action('cut selection', final=True)
|
@edit_action('cut selection', final=True)
|
||||||
@clear_selection
|
@clear_selection
|
||||||
def cut_selection(self, margin: Margin) -> Tuple[str, ...]:
|
def cut_selection(self, margin: Margin) -> tuple[str, ...]:
|
||||||
ret = []
|
ret = []
|
||||||
(s_y, s_x), (e_y, e_x) = self.selection.get()
|
(s_y, s_x), (e_y, e_x) = self.selection.get()
|
||||||
if s_y == e_y:
|
if s_y == e_y:
|
||||||
@@ -613,7 +615,7 @@ class File:
|
|||||||
self.buf.scroll_screen_if_needed(margin)
|
self.buf.scroll_screen_if_needed(margin)
|
||||||
return tuple(ret)
|
return tuple(ret)
|
||||||
|
|
||||||
def cut(self, cut_buffer: Tuple[str, ...]) -> Tuple[str, ...]:
|
def cut(self, cut_buffer: tuple[str, ...]) -> tuple[str, ...]:
|
||||||
# only continue a cut if the last action is a non-final cut
|
# only continue a cut if the last action is a non-final cut
|
||||||
if not self._continue_last_action('cut'):
|
if not self._continue_last_action('cut'):
|
||||||
cut_buffer = ()
|
cut_buffer = ()
|
||||||
@@ -626,7 +628,7 @@ class File:
|
|||||||
self.buf.x = 0
|
self.buf.x = 0
|
||||||
return cut_buffer + (victim,)
|
return cut_buffer + (victim,)
|
||||||
|
|
||||||
def _uncut(self, cut_buffer: Tuple[str, ...], margin: Margin) -> None:
|
def _uncut(self, cut_buffer: tuple[str, ...], margin: Margin) -> None:
|
||||||
for cut_line in cut_buffer:
|
for cut_line in cut_buffer:
|
||||||
line = self.buf[self.buf.y]
|
line = self.buf[self.buf.y]
|
||||||
before, after = line[:self.buf.x], line[self.buf.x:]
|
before, after = line[:self.buf.x], line[self.buf.x:]
|
||||||
@@ -637,14 +639,14 @@ class File:
|
|||||||
|
|
||||||
@edit_action('uncut', final=True)
|
@edit_action('uncut', final=True)
|
||||||
@clear_selection
|
@clear_selection
|
||||||
def uncut(self, cut_buffer: Tuple[str, ...], margin: Margin) -> None:
|
def uncut(self, cut_buffer: tuple[str, ...], margin: Margin) -> None:
|
||||||
self._uncut(cut_buffer, margin)
|
self._uncut(cut_buffer, margin)
|
||||||
|
|
||||||
@edit_action('uncut selection', final=True)
|
@edit_action('uncut selection', final=True)
|
||||||
@clear_selection
|
@clear_selection
|
||||||
def uncut_selection(
|
def uncut_selection(
|
||||||
self,
|
self,
|
||||||
cut_buffer: Tuple[str, ...], margin: Margin,
|
cut_buffer: tuple[str, ...], margin: Margin,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._uncut(cut_buffer, margin)
|
self._uncut(cut_buffer, margin)
|
||||||
self.buf.up(margin)
|
self.buf.up(margin)
|
||||||
@@ -662,7 +664,7 @@ 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]:
|
def _selection_lines(self) -> tuple[int, int]:
|
||||||
(s_y, _), (e_y, _) = self.selection.get()
|
(s_y, _), (e_y, _) = self.selection.get()
|
||||||
e_y = min(e_y + 1, len(self.buf) - 1)
|
e_y = min(e_y + 1, len(self.buf) - 1)
|
||||||
if self.buf[e_y - 1] == '':
|
if self.buf[e_y - 1] == '':
|
||||||
@@ -707,7 +709,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)
|
||||||
@@ -845,12 +850,12 @@ class File:
|
|||||||
|
|
||||||
def move_cursor(
|
def move_cursor(
|
||||||
self,
|
self,
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: curses._CursesWindow,
|
||||||
margin: Margin,
|
margin: Margin,
|
||||||
) -> None:
|
) -> None:
|
||||||
stdscr.move(*self.buf.cursor_position(margin))
|
stdscr.move(*self.buf.cursor_position(margin))
|
||||||
|
|
||||||
def draw(self, stdscr: 'curses._CursesWindow', margin: Margin) -> None:
|
def draw(self, stdscr: curses._CursesWindow, margin: Margin) -> None:
|
||||||
to_display = min(self.buf.displayable_count, margin.body_lines)
|
to_display = min(self.buf.displayable_count, margin.body_lines)
|
||||||
|
|
||||||
for file_hl in self._file_hls:
|
for file_hl in self._file_hls:
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
|
||||||
from typing import FrozenSet
|
|
||||||
from typing import List
|
|
||||||
from typing import Match
|
from typing import Match
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
from typing import TypeVar
|
from typing import TypeVar
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ def uniquely_constructed(t: T) -> T:
|
|||||||
return t
|
return t
|
||||||
|
|
||||||
|
|
||||||
def _split_name(s: Optional[str]) -> Tuple[str, ...]:
|
def _split_name(s: str | None) -> tuple[str, ...]:
|
||||||
if s is None:
|
if s is None:
|
||||||
return ()
|
return ()
|
||||||
else:
|
else:
|
||||||
@@ -44,17 +42,17 @@ def _split_name(s: Optional[str]) -> Tuple[str, ...]:
|
|||||||
class _Rule(Protocol):
|
class _Rule(Protocol):
|
||||||
"""hax for recursive types python/mypy#731"""
|
"""hax for recursive types python/mypy#731"""
|
||||||
@property
|
@property
|
||||||
def name(self) -> Tuple[str, ...]: ...
|
def name(self) -> tuple[str, ...]: ...
|
||||||
@property
|
@property
|
||||||
def match(self) -> Optional[str]: ...
|
def match(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def begin(self) -> Optional[str]: ...
|
def begin(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def end(self) -> Optional[str]: ...
|
def end(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def while_(self) -> Optional[str]: ...
|
def while_(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def content_name(self) -> Tuple[str, ...]: ...
|
def content_name(self) -> tuple[str, ...]: ...
|
||||||
@property
|
@property
|
||||||
def captures(self) -> Captures: ...
|
def captures(self) -> Captures: ...
|
||||||
@property
|
@property
|
||||||
@@ -64,39 +62,39 @@ class _Rule(Protocol):
|
|||||||
@property
|
@property
|
||||||
def while_captures(self) -> Captures: ...
|
def while_captures(self) -> Captures: ...
|
||||||
@property
|
@property
|
||||||
def include(self) -> Optional[str]: ...
|
def include(self) -> str | None: ...
|
||||||
@property
|
@property
|
||||||
def patterns(self) -> 'Tuple[_Rule, ...]': ...
|
def patterns(self) -> tuple[_Rule, ...]: ...
|
||||||
@property
|
@property
|
||||||
def repository(self) -> 'FChainMap[str, _Rule]': ...
|
def repository(self) -> FChainMap[str, _Rule]: ...
|
||||||
|
|
||||||
|
|
||||||
@uniquely_constructed
|
@uniquely_constructed
|
||||||
class Rule(NamedTuple):
|
class Rule(NamedTuple):
|
||||||
name: Tuple[str, ...]
|
name: tuple[str, ...]
|
||||||
match: Optional[str]
|
match: str | None
|
||||||
begin: Optional[str]
|
begin: str | None
|
||||||
end: Optional[str]
|
end: str | None
|
||||||
while_: Optional[str]
|
while_: str | None
|
||||||
content_name: Tuple[str, ...]
|
content_name: tuple[str, ...]
|
||||||
captures: Captures
|
captures: Captures
|
||||||
begin_captures: Captures
|
begin_captures: Captures
|
||||||
end_captures: Captures
|
end_captures: Captures
|
||||||
while_captures: Captures
|
while_captures: Captures
|
||||||
include: Optional[str]
|
include: str | None
|
||||||
patterns: Tuple[_Rule, ...]
|
patterns: tuple[_Rule, ...]
|
||||||
repository: FChainMap[str, _Rule]
|
repository: FChainMap[str, _Rule]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(
|
def make(
|
||||||
cls,
|
cls,
|
||||||
dct: Dict[str, Any],
|
dct: dict[str, Any],
|
||||||
parent_repository: FChainMap[str, _Rule],
|
parent_repository: FChainMap[str, _Rule],
|
||||||
) -> _Rule:
|
) -> _Rule:
|
||||||
if 'repository' in dct:
|
if 'repository' in dct:
|
||||||
# this looks odd, but it's so we can have a self-referential
|
# this looks odd, but it's so we can have a self-referential
|
||||||
# immutable-after-construction chain map
|
# immutable-after-construction chain map
|
||||||
repository_dct: Dict[str, _Rule] = {}
|
repository_dct: dict[str, _Rule] = {}
|
||||||
repository = FChainMap(parent_repository, repository_dct)
|
repository = FChainMap(parent_repository, repository_dct)
|
||||||
for k, sub_dct in dct['repository'].items():
|
for k, sub_dct in dct['repository'].items():
|
||||||
repository_dct[k] = Rule.make(sub_dct, repository)
|
repository_dct[k] = Rule.make(sub_dct, repository)
|
||||||
@@ -183,15 +181,15 @@ class Rule(NamedTuple):
|
|||||||
class Grammar(NamedTuple):
|
class Grammar(NamedTuple):
|
||||||
scope_name: str
|
scope_name: str
|
||||||
repository: FChainMap[str, _Rule]
|
repository: FChainMap[str, _Rule]
|
||||||
patterns: Tuple[_Rule, ...]
|
patterns: tuple[_Rule, ...]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, data: Dict[str, Any]) -> 'Grammar':
|
def make(cls, data: dict[str, Any]) -> Grammar:
|
||||||
scope_name = data['scopeName']
|
scope_name = data['scopeName']
|
||||||
if 'repository' in data:
|
if 'repository' in data:
|
||||||
# this looks odd, but it's so we can have a self-referential
|
# this looks odd, but it's so we can have a self-referential
|
||||||
# immutable-after-construction chain map
|
# immutable-after-construction chain map
|
||||||
repository_dct: Dict[str, _Rule] = {}
|
repository_dct: dict[str, _Rule] = {}
|
||||||
repository = FChainMap(repository_dct)
|
repository = FChainMap(repository_dct)
|
||||||
for k, dct in data['repository'].items():
|
for k, dct in data['repository'].items():
|
||||||
repository_dct[k] = Rule.make(dct, repository)
|
repository_dct[k] = Rule.make(dct, repository)
|
||||||
@@ -212,54 +210,54 @@ class Region(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class State(NamedTuple):
|
class State(NamedTuple):
|
||||||
entries: Tuple['Entry', ...]
|
entries: tuple[Entry, ...]
|
||||||
while_stack: Tuple[Tuple['WhileRule', int], ...]
|
while_stack: tuple[tuple[WhileRule, int], ...]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def root(cls, entry: 'Entry') -> 'State':
|
def root(cls, entry: Entry) -> State:
|
||||||
return cls((entry,), ())
|
return cls((entry,), ())
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def cur(self) -> 'Entry':
|
def cur(self) -> Entry:
|
||||||
return self.entries[-1]
|
return self.entries[-1]
|
||||||
|
|
||||||
def push(self, entry: 'Entry') -> 'State':
|
def push(self, entry: Entry) -> State:
|
||||||
return self._replace(entries=(*self.entries, entry))
|
return self._replace(entries=(*self.entries, entry))
|
||||||
|
|
||||||
def pop(self) -> 'State':
|
def pop(self) -> State:
|
||||||
return self._replace(entries=self.entries[:-1])
|
return self._replace(entries=self.entries[:-1])
|
||||||
|
|
||||||
def push_while(self, rule: 'WhileRule', entry: 'Entry') -> 'State':
|
def push_while(self, rule: WhileRule, entry: Entry) -> State:
|
||||||
entries = (*self.entries, entry)
|
entries = (*self.entries, entry)
|
||||||
while_stack = (*self.while_stack, (rule, len(entries)))
|
while_stack = (*self.while_stack, (rule, len(entries)))
|
||||||
return self._replace(entries=entries, while_stack=while_stack)
|
return self._replace(entries=entries, while_stack=while_stack)
|
||||||
|
|
||||||
def pop_while(self) -> 'State':
|
def pop_while(self) -> State:
|
||||||
entries, while_stack = self.entries[:-1], self.while_stack[:-1]
|
entries, while_stack = self.entries[:-1], self.while_stack[:-1]
|
||||||
return self._replace(entries=entries, while_stack=while_stack)
|
return self._replace(entries=entries, while_stack=while_stack)
|
||||||
|
|
||||||
|
|
||||||
class CompiledRule(Protocol):
|
class CompiledRule(Protocol):
|
||||||
@property
|
@property
|
||||||
def name(self) -> Tuple[str, ...]: ...
|
def name(self) -> tuple[str, ...]: ...
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
state: State,
|
state: State,
|
||||||
) -> Tuple[State, bool, Regions]:
|
) -> tuple[State, bool, Regions]:
|
||||||
...
|
...
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
@@ -267,19 +265,19 @@ class CompiledRegsetRule(CompiledRule, Protocol):
|
|||||||
@property
|
@property
|
||||||
def regset(self) -> _RegSet: ...
|
def regset(self) -> _RegSet: ...
|
||||||
@property
|
@property
|
||||||
def u_rules(self) -> Tuple[_Rule, ...]: ...
|
def u_rules(self) -> tuple[_Rule, ...]: ...
|
||||||
|
|
||||||
|
|
||||||
class Entry(NamedTuple):
|
class Entry(NamedTuple):
|
||||||
scope: Tuple[str, ...]
|
scope: tuple[str, ...]
|
||||||
rule: CompiledRule
|
rule: CompiledRule
|
||||||
start: Tuple[str, int]
|
start: tuple[str, int]
|
||||||
reg: _Reg = ERR_REG
|
reg: _Reg = ERR_REG
|
||||||
boundary: bool = False
|
boundary: bool = False
|
||||||
|
|
||||||
|
|
||||||
def _inner_capture_parse(
|
def _inner_capture_parse(
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
start: int,
|
start: int,
|
||||||
s: str,
|
s: str,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
@@ -293,12 +291,12 @@ def _inner_capture_parse(
|
|||||||
|
|
||||||
|
|
||||||
def _captures(
|
def _captures(
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
scope: Scope,
|
scope: Scope,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
captures: Captures,
|
captures: Captures,
|
||||||
) -> Regions:
|
) -> Regions:
|
||||||
ret: List[Region] = []
|
ret: list[Region] = []
|
||||||
pos, pos_end = match.span()
|
pos, pos_end = match.span()
|
||||||
for i, u_rule in captures:
|
for i, u_rule in captures:
|
||||||
try:
|
try:
|
||||||
@@ -347,12 +345,12 @@ def _captures(
|
|||||||
|
|
||||||
def _do_regset(
|
def _do_regset(
|
||||||
idx: int,
|
idx: int,
|
||||||
match: Optional[Match[str]],
|
match: Match[str] | None,
|
||||||
rule: CompiledRegsetRule,
|
rule: CompiledRegsetRule,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
pos: int,
|
pos: int,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -369,73 +367,73 @@ def _do_regset(
|
|||||||
|
|
||||||
@uniquely_constructed
|
@uniquely_constructed
|
||||||
class PatternRule(NamedTuple):
|
class PatternRule(NamedTuple):
|
||||||
name: Tuple[str, ...]
|
name: tuple[str, ...]
|
||||||
regset: _RegSet
|
regset: _RegSet
|
||||||
u_rules: Tuple[_Rule, ...]
|
u_rules: tuple[_Rule, ...]
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
state: State,
|
state: State,
|
||||||
) -> Tuple[State, bool, Regions]:
|
) -> tuple[State, bool, Regions]:
|
||||||
raise AssertionError(f'unreachable {self}')
|
raise AssertionError(f'unreachable {self}')
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
idx, match = self.regset.search(line, pos, first_line, boundary)
|
idx, match = self.regset.search(line, pos, first_line, boundary)
|
||||||
return _do_regset(idx, match, self, compiler, state, pos)
|
return _do_regset(idx, match, self, compiler, state, pos)
|
||||||
|
|
||||||
|
|
||||||
@uniquely_constructed
|
@uniquely_constructed
|
||||||
class MatchRule(NamedTuple):
|
class MatchRule(NamedTuple):
|
||||||
name: Tuple[str, ...]
|
name: tuple[str, ...]
|
||||||
captures: Captures
|
captures: Captures
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
state: State,
|
state: State,
|
||||||
) -> Tuple[State, bool, Regions]:
|
) -> tuple[State, bool, Regions]:
|
||||||
scope = state.cur.scope + self.name
|
scope = state.cur.scope + self.name
|
||||||
return state, False, _captures(compiler, scope, match, self.captures)
|
return state, False, _captures(compiler, scope, match, self.captures)
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
raise AssertionError(f'unreachable {self}')
|
raise AssertionError(f'unreachable {self}')
|
||||||
|
|
||||||
|
|
||||||
@uniquely_constructed
|
@uniquely_constructed
|
||||||
class EndRule(NamedTuple):
|
class EndRule(NamedTuple):
|
||||||
name: Tuple[str, ...]
|
name: tuple[str, ...]
|
||||||
content_name: Tuple[str, ...]
|
content_name: tuple[str, ...]
|
||||||
begin_captures: Captures
|
begin_captures: Captures
|
||||||
end_captures: Captures
|
end_captures: Captures
|
||||||
end: str
|
end: str
|
||||||
regset: _RegSet
|
regset: _RegSet
|
||||||
u_rules: Tuple[_Rule, ...]
|
u_rules: tuple[_Rule, ...]
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
state: State,
|
state: State,
|
||||||
) -> Tuple[State, bool, Regions]:
|
) -> tuple[State, bool, Regions]:
|
||||||
scope = state.cur.scope + self.name
|
scope = state.cur.scope + self.name
|
||||||
next_scope = scope + self.content_name
|
next_scope = scope + self.content_name
|
||||||
|
|
||||||
@@ -448,11 +446,11 @@ class EndRule(NamedTuple):
|
|||||||
|
|
||||||
def _end_ret(
|
def _end_ret(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
pos: int,
|
pos: int,
|
||||||
m: Match[str],
|
m: Match[str],
|
||||||
) -> Tuple[State, int, bool, Regions]:
|
) -> tuple[State, int, bool, Regions]:
|
||||||
ret = []
|
ret = []
|
||||||
if m.start() > pos:
|
if m.start() > pos:
|
||||||
ret.append(Region(pos, m.start(), state.cur.scope))
|
ret.append(Region(pos, m.start(), state.cur.scope))
|
||||||
@@ -470,13 +468,13 @@ class EndRule(NamedTuple):
|
|||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
end_match = state.cur.reg.search(line, pos, first_line, boundary)
|
end_match = state.cur.reg.search(line, pos, first_line, boundary)
|
||||||
if end_match is not None and end_match.start() == pos:
|
if end_match is not None and end_match.start() == pos:
|
||||||
return self._end_ret(compiler, state, pos, end_match)
|
return self._end_ret(compiler, state, pos, end_match)
|
||||||
@@ -493,20 +491,20 @@ class EndRule(NamedTuple):
|
|||||||
|
|
||||||
@uniquely_constructed
|
@uniquely_constructed
|
||||||
class WhileRule(NamedTuple):
|
class WhileRule(NamedTuple):
|
||||||
name: Tuple[str, ...]
|
name: tuple[str, ...]
|
||||||
content_name: Tuple[str, ...]
|
content_name: tuple[str, ...]
|
||||||
begin_captures: Captures
|
begin_captures: Captures
|
||||||
while_captures: Captures
|
while_captures: Captures
|
||||||
while_: str
|
while_: str
|
||||||
regset: _RegSet
|
regset: _RegSet
|
||||||
u_rules: Tuple[_Rule, ...]
|
u_rules: tuple[_Rule, ...]
|
||||||
|
|
||||||
def start(
|
def start(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
match: Match[str],
|
match: Match[str],
|
||||||
state: State,
|
state: State,
|
||||||
) -> Tuple[State, bool, Regions]:
|
) -> tuple[State, bool, Regions]:
|
||||||
scope = state.cur.scope + self.name
|
scope = state.cur.scope + self.name
|
||||||
next_scope = scope + self.content_name
|
next_scope = scope + self.content_name
|
||||||
|
|
||||||
@@ -520,13 +518,13 @@ class WhileRule(NamedTuple):
|
|||||||
|
|
||||||
def continues(
|
def continues(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[int, bool, Regions]]:
|
) -> tuple[int, bool, Regions] | None:
|
||||||
match = state.cur.reg.match(line, pos, first_line, boundary)
|
match = state.cur.reg.match(line, pos, first_line, boundary)
|
||||||
if match is None:
|
if match is None:
|
||||||
return None
|
return None
|
||||||
@@ -536,23 +534,23 @@ class WhileRule(NamedTuple):
|
|||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Tuple[State, int, bool, Regions]]:
|
) -> tuple[State, int, bool, Regions] | None:
|
||||||
idx, match = self.regset.search(line, pos, first_line, boundary)
|
idx, match = self.regset.search(line, pos, first_line, boundary)
|
||||||
return _do_regset(idx, match, self, compiler, state, pos)
|
return _do_regset(idx, match, self, compiler, state, pos)
|
||||||
|
|
||||||
|
|
||||||
class Compiler:
|
class Compiler:
|
||||||
def __init__(self, grammar: Grammar, grammars: 'Grammars') -> None:
|
def __init__(self, grammar: Grammar, grammars: Grammars) -> None:
|
||||||
self._root_scope = grammar.scope_name
|
self._root_scope = grammar.scope_name
|
||||||
self._grammars = grammars
|
self._grammars = grammars
|
||||||
self._rule_to_grammar: Dict[_Rule, Grammar] = {}
|
self._rule_to_grammar: dict[_Rule, Grammar] = {}
|
||||||
self._c_rules: Dict[_Rule, CompiledRule] = {}
|
self._c_rules: dict[_Rule, CompiledRule] = {}
|
||||||
root = self._compile_root(grammar)
|
root = self._compile_root(grammar)
|
||||||
self.root_state = State.root(Entry(root.name, root, ('', 0)))
|
self.root_state = State.root(Entry(root.name, root, ('', 0)))
|
||||||
|
|
||||||
@@ -566,7 +564,7 @@ class Compiler:
|
|||||||
grammar: Grammar,
|
grammar: Grammar,
|
||||||
repository: FChainMap[str, _Rule],
|
repository: FChainMap[str, _Rule],
|
||||||
s: str,
|
s: str,
|
||||||
) -> Tuple[List[str], Tuple[_Rule, ...]]:
|
) -> tuple[list[str], tuple[_Rule, ...]]:
|
||||||
if s == '$self':
|
if s == '$self':
|
||||||
return self._patterns(grammar, grammar.patterns)
|
return self._patterns(grammar, grammar.patterns)
|
||||||
elif s == '$base':
|
elif s == '$base':
|
||||||
@@ -586,10 +584,10 @@ class Compiler:
|
|||||||
def _patterns(
|
def _patterns(
|
||||||
self,
|
self,
|
||||||
grammar: Grammar,
|
grammar: Grammar,
|
||||||
rules: Tuple[_Rule, ...],
|
rules: tuple[_Rule, ...],
|
||||||
) -> Tuple[List[str], Tuple[_Rule, ...]]:
|
) -> tuple[list[str], tuple[_Rule, ...]]:
|
||||||
ret_regs = []
|
ret_regs = []
|
||||||
ret_rules: List[_Rule] = []
|
ret_rules: list[_Rule] = []
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
if rule.include is not None:
|
if rule.include is not None:
|
||||||
tmp_regs, tmp_rules = self._include(
|
tmp_regs, tmp_rules = self._include(
|
||||||
@@ -676,12 +674,12 @@ class Grammars:
|
|||||||
|
|
||||||
unknown_grammar = {'scopeName': 'source.unknown', 'patterns': []}
|
unknown_grammar = {'scopeName': 'source.unknown', 'patterns': []}
|
||||||
self._raw = {'source.unknown': unknown_grammar}
|
self._raw = {'source.unknown': unknown_grammar}
|
||||||
self._file_types: List[Tuple[FrozenSet[str], str]] = []
|
self._file_types: list[tuple[frozenset[str], str]] = []
|
||||||
self._first_line: List[Tuple[_Reg, str]] = []
|
self._first_line: list[tuple[_Reg, str]] = []
|
||||||
self._parsed: Dict[str, Grammar] = {}
|
self._parsed: dict[str, Grammar] = {}
|
||||||
self._compiled: Dict[str, Compiler] = {}
|
self._compiled: dict[str, Compiler] = {}
|
||||||
|
|
||||||
def _raw_for_scope(self, scope: str) -> Dict[str, Any]:
|
def _raw_for_scope(self, scope: str) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
return self._raw[scope]
|
return self._raw[scope]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -747,12 +745,12 @@ class Grammars:
|
|||||||
|
|
||||||
|
|
||||||
def highlight_line(
|
def highlight_line(
|
||||||
compiler: 'Compiler',
|
compiler: Compiler,
|
||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
) -> Tuple[State, Regions]:
|
) -> tuple[State, Regions]:
|
||||||
ret: List[Region] = []
|
ret: list[Region] = []
|
||||||
pos = 0
|
pos = 0
|
||||||
boundary = state.cur.boundary
|
boundary = state.cur.boundary
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import os.path
|
import os.path
|
||||||
from typing import Dict
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from babi.user_data import xdg_data
|
from babi.user_data import xdg_data
|
||||||
|
|
||||||
|
|
||||||
class History:
|
class History:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._orig_len: Dict[str, int] = collections.defaultdict(int)
|
self._orig_len: dict[str, int] = collections.defaultdict(int)
|
||||||
self.data: Dict[str, List[str]] = collections.defaultdict(list)
|
self.data: dict[str, list[str]] = collections.defaultdict(list)
|
||||||
self.prev: Dict[str, str] = {}
|
self.prev: dict[str, str] = {}
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def save(self) -> Generator[None, None, None]:
|
def save(self) -> Generator[None, None, None]:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
from typing import Dict
|
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
@@ -13,7 +14,7 @@ class Replace:
|
|||||||
include_edge = True
|
include_edge = True
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.regions: Dict[int, HLs] = collections.defaultdict(tuple)
|
self.regions: dict[int, HLs] = collections.defaultdict(tuple)
|
||||||
|
|
||||||
def highlight_until(self, lines: Buf, idx: int) -> None:
|
def highlight_until(self, lines: Buf, idx: int) -> None:
|
||||||
"""our highlight regions are populated in other ways"""
|
"""our highlight regions are populated in other ways"""
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import curses
|
import curses
|
||||||
from typing import Dict
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.hl.interface import HL
|
from babi.hl.interface import HL
|
||||||
@@ -13,9 +12,9 @@ class Selection:
|
|||||||
include_edge = True
|
include_edge = True
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.regions: Dict[int, HLs] = collections.defaultdict(tuple)
|
self.regions: dict[int, HLs] = collections.defaultdict(tuple)
|
||||||
self.start: Optional[Tuple[int, int]] = None
|
self.start: tuple[int, int] | None = None
|
||||||
self.end: Optional[Tuple[int, int]] = None
|
self.end: tuple[int, int] | None = None
|
||||||
|
|
||||||
def register_callbacks(self, buf: Buf) -> None:
|
def register_callbacks(self, buf: Buf) -> None:
|
||||||
"""our highlight regions are populated in other ways"""
|
"""our highlight regions are populated in other ways"""
|
||||||
@@ -39,7 +38,7 @@ class Selection:
|
|||||||
)
|
)
|
||||||
self.regions[e_y] = (HL(x=0, end=e_x, attr=attr),)
|
self.regions[e_y] = (HL(x=0, end=e_x, attr=attr),)
|
||||||
|
|
||||||
def get(self) -> Tuple[Tuple[int, int], Tuple[int, int]]:
|
def get(self) -> tuple[tuple[int, int], tuple[int, int]]:
|
||||||
assert self.start is not None and self.end is not None
|
assert self.start is not None and self.end is not None
|
||||||
if self.start < self.end:
|
if self.start < self.end:
|
||||||
return self.start, self.end
|
return self.start, self.end
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import functools
|
import functools
|
||||||
import math
|
import math
|
||||||
from typing import Callable
|
from typing import Callable
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.color_manager import ColorManager
|
from babi.color_manager import ColorManager
|
||||||
@@ -21,7 +20,7 @@ from babi.user_data import prefix_data
|
|||||||
from babi.user_data import xdg_config
|
from babi.user_data import xdg_config
|
||||||
from babi.user_data import xdg_data
|
from babi.user_data import xdg_data
|
||||||
|
|
||||||
A_ITALIC = getattr(curses, 'A_ITALIC', 0x80000000) # new in py37
|
A_ITALIC = getattr(curses, 'A_ITALIC', 0x80000000) # not always present
|
||||||
|
|
||||||
|
|
||||||
class FileSyntax:
|
class FileSyntax:
|
||||||
@@ -37,12 +36,12 @@ class FileSyntax:
|
|||||||
self._theme = theme
|
self._theme = theme
|
||||||
self._color_manager = color_manager
|
self._color_manager = color_manager
|
||||||
|
|
||||||
self.regions: List[HLs] = []
|
self.regions: list[HLs] = []
|
||||||
self._states: List[State] = []
|
self._states: list[State] = []
|
||||||
|
|
||||||
# this will be assigned a functools.lru_cache per instance for
|
# this will be assigned a functools.lru_cache per instance for
|
||||||
# better hit rate and memory usage
|
# better hit rate and memory usage
|
||||||
self._hl: Optional[Callable[[State, str, bool], Tuple[State, HLs]]]
|
self._hl: Callable[[State, str, bool], tuple[State, HLs]] | None
|
||||||
self._hl = None
|
self._hl = None
|
||||||
|
|
||||||
def attr(self, style: Style) -> int:
|
def attr(self, style: Style) -> int:
|
||||||
@@ -59,7 +58,7 @@ class FileSyntax:
|
|||||||
state: State,
|
state: State,
|
||||||
line: str,
|
line: str,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
) -> Tuple[State, HLs]:
|
) -> tuple[State, HLs]:
|
||||||
new_state, regions = highlight_line(
|
new_state, regions = highlight_line(
|
||||||
self._compiler, state, f'{line}\n', first_line=first_line,
|
self._compiler, state, f'{line}\n', first_line=first_line,
|
||||||
)
|
)
|
||||||
@@ -68,7 +67,7 @@ class FileSyntax:
|
|||||||
new_end = regions[-1]._replace(end=regions[-1].end - 1)
|
new_end = regions[-1]._replace(end=regions[-1].end - 1)
|
||||||
regions = regions[:-1] + (new_end,)
|
regions = regions[:-1] + (new_end,)
|
||||||
|
|
||||||
regs: List[HL] = []
|
regs: list[HL] = []
|
||||||
for r in regions:
|
for r in regions:
|
||||||
style = self._theme.select(r.scope)
|
style = self._theme.select(r.scope)
|
||||||
if style == self._theme.default:
|
if style == self._theme.default:
|
||||||
@@ -115,8 +114,7 @@ class FileSyntax:
|
|||||||
state = self._states[-1]
|
state = self._states[-1]
|
||||||
|
|
||||||
for i in range(len(self._states), idx):
|
for i in range(len(self._states), idx):
|
||||||
# https://github.com/python/mypy/issues/8579
|
state, regions = self._hl(state, lines[i], i == 0)
|
||||||
state, regions = self._hl(state, lines[i], i == 0) # type: ignore
|
|
||||||
self._states.append(state)
|
self._states.append(state)
|
||||||
self.regions.append(regions)
|
self.regions.append(regions)
|
||||||
|
|
||||||
@@ -134,7 +132,7 @@ class Syntax(NamedTuple):
|
|||||||
compiler = self.grammars.blank_compiler()
|
compiler = self.grammars.blank_compiler()
|
||||||
return FileSyntax(compiler, self.theme, self.color_manager)
|
return FileSyntax(compiler, self.theme, self.color_manager)
|
||||||
|
|
||||||
def _init_screen(self, stdscr: 'curses._CursesWindow') -> None:
|
def _init_screen(self, stdscr: curses._CursesWindow) -> None:
|
||||||
default_fg, default_bg = self.theme.default.fg, self.theme.default.bg
|
default_fg, default_bg = self.theme.default.fg, self.theme.default.bg
|
||||||
all_colors = {c for c in (default_fg, default_bg) if c is not None}
|
all_colors = {c for c in (default_fg, default_bg) if c is not None}
|
||||||
todo = list(self.theme.rules.children.values())
|
todo = list(self.theme.rules.children.values())
|
||||||
@@ -155,9 +153,9 @@ class Syntax(NamedTuple):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def from_screen(
|
def from_screen(
|
||||||
cls,
|
cls,
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: curses._CursesWindow,
|
||||||
color_manager: ColorManager,
|
color_manager: ColorManager,
|
||||||
) -> 'Syntax':
|
) -> Syntax:
|
||||||
grammars = Grammars(prefix_data('grammar_v1'), xdg_data('grammar_v1'))
|
grammars = Grammars(prefix_data('grammar_v1'), xdg_data('grammar_v1'))
|
||||||
theme = Theme.from_filename(xdg_config('theme.json'))
|
theme = Theme.from_filename(xdg_config('theme.json'))
|
||||||
ret = cls(grammars, theme, color_manager)
|
ret = cls(grammars, theme, color_manager)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.color_manager import ColorManager
|
from babi.color_manager import ColorManager
|
||||||
@@ -13,7 +14,7 @@ class TrailingWhitespace:
|
|||||||
def __init__(self, color_manager: ColorManager) -> None:
|
def __init__(self, color_manager: ColorManager) -> None:
|
||||||
self._color_manager = color_manager
|
self._color_manager = color_manager
|
||||||
|
|
||||||
self.regions: List[HLs] = []
|
self.regions: list[HLs] = []
|
||||||
|
|
||||||
def _trailing_ws(self, line: str) -> HLs:
|
def _trailing_ws(self, line: str) -> HLs:
|
||||||
if not line:
|
if not line:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from babi.cached_property import cached_property
|
from babi.cached_property import cached_property
|
||||||
@@ -34,7 +36,7 @@ def scrolled_line(s: str, x: int, width: int) -> str:
|
|||||||
|
|
||||||
class _CalcWidth:
|
class _CalcWidth:
|
||||||
@cached_property
|
@cached_property
|
||||||
def _window(self) -> 'curses._CursesWindow':
|
def _window(self) -> curses._CursesWindow:
|
||||||
return curses.newwin(1, 10)
|
return curses.newwin(1, 10)
|
||||||
|
|
||||||
def wcwidth(self, c: str) -> int:
|
def wcwidth(self, c: str) -> int:
|
||||||
|
|||||||
21
babi/main.py
21
babi/main.py
@@ -1,13 +1,12 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import curses
|
import curses
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
import sys
|
import sys
|
||||||
from typing import List
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
from babi.file import File
|
from babi.file import File
|
||||||
@@ -44,9 +43,9 @@ def _edit(screen: Screen, stdin: str) -> EditResult:
|
|||||||
|
|
||||||
|
|
||||||
def c_main(
|
def c_main(
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: curses._CursesWindow,
|
||||||
filenames: List[Optional[str]],
|
filenames: list[str | None],
|
||||||
positions: List[int],
|
positions: list[int],
|
||||||
stdin: str,
|
stdin: str,
|
||||||
perf: Perf,
|
perf: Perf,
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -73,7 +72,7 @@ def c_main(
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int:
|
def _key_debug(stdscr: curses._CursesWindow, perf: Perf) -> int:
|
||||||
screen = Screen(stdscr, ['<<key debug>>'], [0], perf)
|
screen = Screen(stdscr, ['<<key debug>>'], [0], perf)
|
||||||
screen.file.buf = Buf([''])
|
screen.file.buf = Buf([''])
|
||||||
|
|
||||||
@@ -91,11 +90,11 @@ def _key_debug(stdscr: 'curses._CursesWindow', perf: Perf) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def _filenames(filenames: List[str]) -> Tuple[List[Optional[str]], List[int]]:
|
def _filenames(filenames: list[str]) -> tuple[list[str | None], list[int]]:
|
||||||
if not filenames:
|
if not filenames:
|
||||||
return [None], [0]
|
return [None], [0]
|
||||||
|
|
||||||
ret_filenames: List[Optional[str]] = []
|
ret_filenames: list[str | None] = []
|
||||||
ret_positions = []
|
ret_positions = []
|
||||||
|
|
||||||
filenames_iter = iter(filenames)
|
filenames_iter = iter(filenames)
|
||||||
@@ -122,7 +121,7 @@ def _filenames(filenames: List[str]) -> Tuple[List[Optional[str]], List[int]]:
|
|||||||
return ret_filenames, ret_positions
|
return ret_filenames, ret_positions
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('filenames', metavar='filename', nargs='*')
|
parser.add_argument('filenames', metavar='filename', nargs='*')
|
||||||
parser.add_argument('--perf-log')
|
parser.add_argument('--perf-log')
|
||||||
@@ -153,4 +152,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
|
|
||||||
@@ -31,5 +33,5 @@ class Margin(NamedTuple):
|
|||||||
return int(self.lines / 2 + .5)
|
return int(self.lines / 2 + .5)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_current_screen(cls) -> 'Margin':
|
def from_current_screen(cls) -> Margin:
|
||||||
return cls(curses.LINES, curses.COLS)
|
return cls(curses.LINES, curses.COLS)
|
||||||
|
|||||||
15
babi/perf.py
15
babi/perf.py
@@ -1,18 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import cProfile
|
import cProfile
|
||||||
import time
|
import time
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
|
|
||||||
class Perf:
|
class Perf:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._prof: Optional[cProfile.Profile] = None
|
self._prof: cProfile.Profile | None = None
|
||||||
self._records: List[Tuple[str, float]] = []
|
self._records: list[tuple[str, float]] = []
|
||||||
self._name: Optional[str] = None
|
self._name: str | None = None
|
||||||
self._time: Optional[float] = None
|
self._time: float | None = None
|
||||||
|
|
||||||
def start(self, name: str) -> None:
|
def start(self, name: str) -> None:
|
||||||
if self._prof:
|
if self._prof:
|
||||||
@@ -43,7 +42,7 @@ class Perf:
|
|||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def perf_log(filename: Optional[str]) -> Generator[Perf, None, None]:
|
def perf_log(filename: str | None) -> Generator[Perf, None, None]:
|
||||||
perf = Perf()
|
perf = Perf()
|
||||||
if filename is None:
|
if filename is None:
|
||||||
yield perf
|
yield perf
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import enum
|
import enum
|
||||||
from typing import List
|
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from babi.horizontal_scrolling import line_x
|
from babi.horizontal_scrolling import line_x
|
||||||
from babi.horizontal_scrolling import scrolled_line
|
from babi.horizontal_scrolling import scrolled_line
|
||||||
@@ -16,7 +14,7 @@ PromptResult = enum.Enum('PromptResult', 'CANCELLED')
|
|||||||
|
|
||||||
|
|
||||||
class Prompt:
|
class Prompt:
|
||||||
def __init__(self, screen: 'Screen', prompt: str, lst: List[str]) -> None:
|
def __init__(self, screen: Screen, prompt: str, lst: list[str]) -> None:
|
||||||
self._screen = screen
|
self._screen = screen
|
||||||
self._prompt = prompt
|
self._prompt = prompt
|
||||||
self._lst = lst
|
self._lst = lst
|
||||||
@@ -31,7 +29,7 @@ class Prompt:
|
|||||||
def _s(self, s: str) -> None:
|
def _s(self, s: str) -> None:
|
||||||
self._lst[self._y] = s
|
self._lst[self._y] = s
|
||||||
|
|
||||||
def _render_prompt(self, *, base: Optional[str] = None) -> None:
|
def _render_prompt(self, *, base: str | None = None) -> None:
|
||||||
base = base or self._prompt
|
base = base or self._prompt
|
||||||
if not base or self._screen.margin.cols < 7:
|
if not base or self._screen.margin.cols < 7:
|
||||||
prompt_s = ''
|
prompt_s = ''
|
||||||
@@ -100,7 +98,7 @@ class Prompt:
|
|||||||
def _resize(self) -> None:
|
def _resize(self) -> None:
|
||||||
self._screen.resize()
|
self._screen.resize()
|
||||||
|
|
||||||
def _check_failed(self, idx: int, s: str) -> Tuple[bool, int]:
|
def _check_failed(self, idx: int, s: str) -> tuple[bool, int]:
|
||||||
failed = False
|
failed = False
|
||||||
for search_idx in range(idx, -1, -1):
|
for search_idx in range(idx, -1, -1):
|
||||||
if s in self._lst[search_idx]:
|
if s in self._lst[search_idx]:
|
||||||
@@ -111,7 +109,7 @@ class Prompt:
|
|||||||
failed = True
|
failed = True
|
||||||
return failed, idx
|
return failed, idx
|
||||||
|
|
||||||
def _reverse_search(self) -> Union[None, str, PromptResult]:
|
def _reverse_search(self) -> None | str | PromptResult:
|
||||||
reverse_s = ''
|
reverse_s = ''
|
||||||
idx = self._y
|
idx = self._y
|
||||||
while True:
|
while True:
|
||||||
@@ -177,7 +175,7 @@ class Prompt:
|
|||||||
self._s = self._s[:self._x] + c + self._s[self._x:]
|
self._s = self._s[:self._x] + c + self._s[self._x:]
|
||||||
self._x += len(c)
|
self._x += len(c)
|
||||||
|
|
||||||
def run(self) -> Union[PromptResult, str]:
|
def run(self) -> PromptResult | str:
|
||||||
while True:
|
while True:
|
||||||
self._render_prompt()
|
self._render_prompt()
|
||||||
|
|
||||||
|
|||||||
10
babi/reg.py
10
babi/reg.py
@@ -1,8 +1,8 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from typing import Match
|
from typing import Match
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
import onigurumacffi
|
import onigurumacffi
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ class _Reg:
|
|||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Match[str]]:
|
) -> Match[str] | None:
|
||||||
return self._reg.search(line, pos, flags=_FLAGS[first_line, boundary])
|
return self._reg.search(line, pos, flags=_FLAGS[first_line, boundary])
|
||||||
|
|
||||||
def match(
|
def match(
|
||||||
@@ -51,7 +51,7 @@ class _Reg:
|
|||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Optional[Match[str]]:
|
) -> Match[str] | None:
|
||||||
return self._reg.match(line, pos, flags=_FLAGS[first_line, boundary])
|
return self._reg.match(line, pos, flags=_FLAGS[first_line, boundary])
|
||||||
|
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class _RegSet:
|
|||||||
pos: int,
|
pos: int,
|
||||||
first_line: bool,
|
first_line: bool,
|
||||||
boundary: bool,
|
boundary: bool,
|
||||||
) -> Tuple[int, Optional[Match[str]]]:
|
) -> tuple[int, Match[str] | None]:
|
||||||
return self._set.search(line, pos, flags=_FLAGS[first_line, boundary])
|
return self._set.search(line, pos, flags=_FLAGS[first_line, boundary])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
import enum
|
import enum
|
||||||
@@ -5,14 +7,11 @@ import hashlib
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import signal
|
import signal
|
||||||
|
import sre_parse
|
||||||
import sys
|
import sys
|
||||||
from typing import Generator
|
from typing import Generator
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Pattern
|
from typing import Pattern
|
||||||
from typing import Tuple
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
from babi.color_manager import ColorManager
|
from babi.color_manager import ColorManager
|
||||||
from babi.file import Action
|
from babi.file import Action
|
||||||
@@ -38,6 +37,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 +61,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,21 +95,22 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Key(NamedTuple):
|
class Key(NamedTuple):
|
||||||
wch: Union[int, str]
|
wch: int | str
|
||||||
keyname: bytes
|
keyname: bytes
|
||||||
|
|
||||||
|
|
||||||
class Screen:
|
class Screen:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
stdscr: 'curses._CursesWindow',
|
stdscr: curses._CursesWindow,
|
||||||
filenames: List[Optional[str]],
|
filenames: list[str | None],
|
||||||
initial_lines: List[int],
|
initial_lines: list[int],
|
||||||
perf: Perf,
|
perf: Perf,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.stdscr = stdscr
|
self.stdscr = stdscr
|
||||||
@@ -122,9 +125,9 @@ class Screen:
|
|||||||
self.perf = perf
|
self.perf = perf
|
||||||
self.status = Status()
|
self.status = Status()
|
||||||
self.margin = Margin.from_current_screen()
|
self.margin = Margin.from_current_screen()
|
||||||
self.cut_buffer: Tuple[str, ...] = ()
|
self.cut_buffer: tuple[str, ...] = ()
|
||||||
self.cut_selection = False
|
self.cut_selection = False
|
||||||
self._buffered_input: Union[int, str, None] = None
|
self._buffered_input: int | str | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def file(self) -> File:
|
def file(self) -> File:
|
||||||
@@ -267,9 +270,9 @@ class Screen:
|
|||||||
def quick_prompt(
|
def quick_prompt(
|
||||||
self,
|
self,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
opt_strs: Tuple[str, ...],
|
opt_strs: tuple[str, ...],
|
||||||
) -> Union[str, PromptResult]:
|
) -> 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,18 +309,18 @@ 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,
|
||||||
prompt: str,
|
prompt: str,
|
||||||
*,
|
*,
|
||||||
allow_empty: bool = False,
|
allow_empty: bool = False,
|
||||||
history: Optional[str] = None,
|
history: str | None = None,
|
||||||
default_prev: bool = False,
|
default_prev: bool = False,
|
||||||
default: Optional[str] = None,
|
default: str | None = None,
|
||||||
) -> Union[str, PromptResult]:
|
) -> str | PromptResult:
|
||||||
default = default or ''
|
default = default or ''
|
||||||
self.status.clear()
|
self.status.clear()
|
||||||
if history is not None:
|
if history is not None:
|
||||||
@@ -374,7 +377,7 @@ class Screen:
|
|||||||
else:
|
else:
|
||||||
self.file.uncut(self.cut_buffer, self.margin)
|
self.file.uncut(self.cut_buffer, self.margin)
|
||||||
|
|
||||||
def _get_search_re(self, prompt: str) -> Union[Pattern[str], PromptResult]:
|
def _get_search_re(self, prompt: str) -> Pattern[str] | PromptResult:
|
||||||
response = self.prompt(prompt, history='search', default_prev=True)
|
response = self.prompt(prompt, history='search', default_prev=True)
|
||||||
if response is PromptResult.CANCELLED:
|
if response is PromptResult.CANCELLED:
|
||||||
return response
|
return response
|
||||||
@@ -387,8 +390,8 @@ class Screen:
|
|||||||
def _undo_redo(
|
def _undo_redo(
|
||||||
self,
|
self,
|
||||||
op: str,
|
op: str,
|
||||||
from_stack: List[Action],
|
from_stack: list[Action],
|
||||||
to_stack: List[Action],
|
to_stack: list[Action],
|
||||||
) -> None:
|
) -> None:
|
||||||
if not from_stack:
|
if not from_stack:
|
||||||
self.status.update(f'nothing to {op}!')
|
self.status.update(f'nothing to {op}!')
|
||||||
@@ -417,9 +420,14 @@ class Screen:
|
|||||||
'replace with', history='replace', allow_empty=True,
|
'replace with', history='replace', allow_empty=True,
|
||||||
)
|
)
|
||||||
if response is not PromptResult.CANCELLED:
|
if response is not PromptResult.CANCELLED:
|
||||||
self.file.replace(self, search_response, response)
|
try:
|
||||||
|
sre_parse.parse_template(response, search_response)
|
||||||
|
except re.error:
|
||||||
|
self.status.update('invalid replacement string')
|
||||||
|
else:
|
||||||
|
self.file.replace(self, search_response, response)
|
||||||
|
|
||||||
def command(self) -> Optional[EditResult]:
|
def command(self) -> EditResult | None:
|
||||||
response = self.prompt('', history='command')
|
response = self.prompt('', history='command')
|
||||||
if response is PromptResult.CANCELLED:
|
if response is PromptResult.CANCELLED:
|
||||||
pass
|
pass
|
||||||
@@ -476,7 +484,7 @@ class Screen:
|
|||||||
self.status.update(f'invalid command: {response}')
|
self.status.update(f'invalid command: {response}')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save(self) -> Optional[PromptResult]:
|
def save(self) -> PromptResult | None:
|
||||||
self.file.finalize_previous_action()
|
self.file.finalize_previous_action()
|
||||||
|
|
||||||
# TODO: make directories if they don't exist
|
# TODO: make directories if they don't exist
|
||||||
@@ -490,22 +498,28 @@ class Screen:
|
|||||||
else:
|
else:
|
||||||
self.file.filename = filename
|
self.file.filename = filename
|
||||||
|
|
||||||
if os.path.isfile(self.file.filename):
|
if not os.path.isfile(self.file.filename):
|
||||||
|
sha256: str | None = None
|
||||||
|
else:
|
||||||
with open(self.file.filename, encoding='UTF-8', newline='') as f:
|
with open(self.file.filename, encoding='UTF-8', newline='') as f:
|
||||||
*_, sha256 = get_lines(f)
|
*_, sha256 = get_lines(f)
|
||||||
else:
|
|
||||||
sha256 = hashlib.sha256(b'').hexdigest()
|
|
||||||
|
|
||||||
contents = self.file.nl.join(self.file.buf)
|
contents = self.file.nl.join(self.file.buf)
|
||||||
sha256_to_save = hashlib.sha256(contents.encode()).hexdigest()
|
sha256_to_save = hashlib.sha256(contents.encode()).hexdigest()
|
||||||
|
|
||||||
# the file on disk is the same as when we opened it
|
# the file on disk is the same as when we opened it
|
||||||
if sha256 not in (self.file.sha256, sha256_to_save):
|
if sha256 not in (None, self.file.sha256, sha256_to_save):
|
||||||
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
|
||||||
@@ -522,7 +536,7 @@ class Screen:
|
|||||||
first = False
|
first = False
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def save_filename(self) -> Optional[PromptResult]:
|
def save_filename(self) -> PromptResult | None:
|
||||||
response = self.prompt('enter filename', default=self.file.filename)
|
response = self.prompt('enter filename', default=self.file.filename)
|
||||||
if response is PromptResult.CANCELLED:
|
if response is PromptResult.CANCELLED:
|
||||||
return PromptResult.CANCELLED
|
return PromptResult.CANCELLED
|
||||||
@@ -530,7 +544,7 @@ class Screen:
|
|||||||
self.file.filename = response
|
self.file.filename = response
|
||||||
return self.save()
|
return self.save()
|
||||||
|
|
||||||
def open_file(self) -> Optional[EditResult]:
|
def open_file(self) -> EditResult | None:
|
||||||
response = self.prompt('enter filename', history='open')
|
response = self.prompt('enter filename', history='open')
|
||||||
if response is not PromptResult.CANCELLED:
|
if response is not PromptResult.CANCELLED:
|
||||||
opened = File(response, 0, self.color_manager, self.hl_factories)
|
opened = File(response, 0, self.color_manager, self.hl_factories)
|
||||||
@@ -539,7 +553,7 @@ class Screen:
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def quit_save_modified(self) -> Optional[EditResult]:
|
def quit_save_modified(self) -> EditResult | None:
|
||||||
if self.file.modified:
|
if self.file.modified:
|
||||||
response = self.quick_prompt(
|
response = self.quick_prompt(
|
||||||
'file is modified - save', ('yes', 'no'),
|
'file is modified - save', ('yes', 'no'),
|
||||||
@@ -587,9 +601,12 @@ 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')
|
||||||
@@ -610,7 +627,7 @@ def _init_screen() -> 'curses._CursesWindow':
|
|||||||
|
|
||||||
|
|
||||||
@contextlib.contextmanager
|
@contextlib.contextmanager
|
||||||
def make_stdscr() -> Generator['curses._CursesWindow', None, None]:
|
def make_stdscr() -> Generator[curses._CursesWindow, None, None]:
|
||||||
"""essentially `curses.wrapper` but split out to implement ^Z"""
|
"""essentially `curses.wrapper` but split out to implement ^Z"""
|
||||||
try:
|
try:
|
||||||
yield _init_screen()
|
yield _init_screen()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from babi.margin import Margin
|
from babi.margin import Margin
|
||||||
@@ -16,7 +18,7 @@ class Status:
|
|||||||
def clear(self) -> None:
|
def clear(self) -> None:
|
||||||
self._status = ''
|
self._status = ''
|
||||||
|
|
||||||
def draw(self, stdscr: 'curses._CursesWindow', margin: Margin) -> None:
|
def draw(self, stdscr: curses._CursesWindow, margin: Margin) -> None:
|
||||||
if margin.footer or self._status:
|
if margin.footer or self._status:
|
||||||
stdscr.insstr(margin.lines - 1, 0, ' ' * margin.cols)
|
stdscr.insstr(margin.lines - 1, 0, ' ' * margin.cols)
|
||||||
if self._status:
|
if self._status:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
from typing import Optional
|
|
||||||
from typing import Sequence
|
from typing import Sequence
|
||||||
|
|
||||||
from babi.highlight import Compiler
|
from babi.highlight import Compiler
|
||||||
@@ -47,7 +48,7 @@ def _highlight_output(theme: Theme, compiler: Compiler, filename: str) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def main(argv: Optional[Sequence[str]] = None) -> int:
|
def main(argv: Sequence[str] | None = None) -> int:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('--theme', default=xdg_config('theme.json'))
|
parser.add_argument('--theme', default=xdg_config('theme.json'))
|
||||||
parser.add_argument('--grammar-dir', default=prefix_data('grammar_v1'))
|
parser.add_argument('--grammar-dir', default=prefix_data('grammar_v1'))
|
||||||
@@ -66,4 +67,4 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import os.path
|
import os.path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from typing import Dict
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Optional
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from babi._types import Protocol
|
from babi._types import Protocol
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
@@ -13,32 +12,32 @@ from babi.fdict import FDict
|
|||||||
|
|
||||||
|
|
||||||
class Style(NamedTuple):
|
class Style(NamedTuple):
|
||||||
fg: Optional[Color]
|
fg: Color | None
|
||||||
bg: Optional[Color]
|
bg: Color | None
|
||||||
b: bool
|
b: bool
|
||||||
i: bool
|
i: bool
|
||||||
u: bool
|
u: bool
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def blank(cls) -> 'Style':
|
def blank(cls) -> Style:
|
||||||
return cls(fg=None, bg=None, b=False, i=False, u=False)
|
return cls(fg=None, bg=None, b=False, i=False, u=False)
|
||||||
|
|
||||||
|
|
||||||
class PartialStyle(NamedTuple):
|
class PartialStyle(NamedTuple):
|
||||||
fg: Optional[Color] = None
|
fg: Color | None = None
|
||||||
bg: Optional[Color] = None
|
bg: Color | None = None
|
||||||
b: Optional[bool] = None
|
b: bool | None = None
|
||||||
i: Optional[bool] = None
|
i: bool | None = None
|
||||||
u: Optional[bool] = None
|
u: bool | None = None
|
||||||
|
|
||||||
def overlay_on(self, dct: Dict[str, Any]) -> None:
|
def overlay_on(self, dct: dict[str, Any]) -> None:
|
||||||
for attr in self._fields:
|
for attr in self._fields:
|
||||||
value = getattr(self, attr)
|
value = getattr(self, attr)
|
||||||
if value is not None:
|
if value is not None:
|
||||||
dct[attr] = value
|
dct[attr] = value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dct(cls, dct: Dict[str, Any]) -> 'PartialStyle':
|
def from_dct(cls, dct: dict[str, Any]) -> PartialStyle:
|
||||||
kv = cls()._asdict()
|
kv = cls()._asdict()
|
||||||
if 'foreground' in dct:
|
if 'foreground' in dct:
|
||||||
kv['fg'] = Color.parse(dct['foreground'])
|
kv['fg'] = Color.parse(dct['foreground'])
|
||||||
@@ -57,7 +56,7 @@ class _TrieNode(Protocol):
|
|||||||
@property
|
@property
|
||||||
def style(self) -> PartialStyle: ...
|
def style(self) -> PartialStyle: ...
|
||||||
@property
|
@property
|
||||||
def children(self) -> FDict[str, '_TrieNode']: ...
|
def children(self) -> FDict[str, _TrieNode]: ...
|
||||||
|
|
||||||
|
|
||||||
class TrieNode(NamedTuple):
|
class TrieNode(NamedTuple):
|
||||||
@@ -65,7 +64,7 @@ class TrieNode(NamedTuple):
|
|||||||
children: FDict[str, _TrieNode]
|
children: FDict[str, _TrieNode]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dct(cls, dct: Dict[str, Any]) -> _TrieNode:
|
def from_dct(cls, dct: dict[str, Any]) -> _TrieNode:
|
||||||
children = FDict({
|
children = FDict({
|
||||||
k: TrieNode.from_dct(v) for k, v in dct['children'].items()
|
k: TrieNode.from_dct(v) for k, v in dct['children'].items()
|
||||||
})
|
})
|
||||||
@@ -77,7 +76,7 @@ class Theme(NamedTuple):
|
|||||||
rules: _TrieNode
|
rules: _TrieNode
|
||||||
|
|
||||||
@functools.lru_cache(maxsize=None)
|
@functools.lru_cache(maxsize=None)
|
||||||
def select(self, scope: Tuple[str, ...]) -> Style:
|
def select(self, scope: tuple[str, ...]) -> Style:
|
||||||
if not scope:
|
if not scope:
|
||||||
return self.default
|
return self.default
|
||||||
else:
|
else:
|
||||||
@@ -92,7 +91,7 @@ class Theme(NamedTuple):
|
|||||||
return Style(**style)
|
return Style(**style)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_dct(cls, data: Dict[str, Any]) -> 'Theme':
|
def from_dct(cls, data: dict[str, Any]) -> Theme:
|
||||||
default = Style.blank()._asdict()
|
default = Style.blank()._asdict()
|
||||||
|
|
||||||
for k in ('foreground', 'editor.foreground'):
|
for k in ('foreground', 'editor.foreground'):
|
||||||
@@ -105,7 +104,7 @@ class Theme(NamedTuple):
|
|||||||
default['bg'] = Color.parse(data['colors'][k])
|
default['bg'] = Color.parse(data['colors'][k])
|
||||||
break
|
break
|
||||||
|
|
||||||
root: Dict[str, Any] = {'children': {}}
|
root: dict[str, Any] = {'children': {}}
|
||||||
rules = data.get('tokenColors', []) + data.get('settings', [])
|
rules = data.get('tokenColors', []) + data.get('settings', [])
|
||||||
for rule in rules:
|
for rule in rules:
|
||||||
if 'scope' not in rule:
|
if 'scope' not in rule:
|
||||||
@@ -139,11 +138,11 @@ class Theme(NamedTuple):
|
|||||||
return cls(Style(**default), TrieNode.from_dct(root))
|
return cls(Style(**default), TrieNode.from_dct(root))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def blank(cls) -> 'Theme':
|
def blank(cls) -> Theme:
|
||||||
return cls(Style.blank(), TrieNode.from_dct({'children': {}}))
|
return cls(Style.blank(), TrieNode.from_dct({'children': {}}))
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_filename(cls, filename: str) -> 'Theme':
|
def from_filename(cls, filename: str) -> Theme:
|
||||||
if not os.path.exists(filename):
|
if not os.path.exists(filename):
|
||||||
return cls.blank()
|
return cls.blank()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os.path
|
import os.path
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import io
|
import io
|
||||||
import json
|
import json
|
||||||
@@ -85,4 +87,4 @@ def main() -> int:
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
exit(main())
|
raise SystemExit(main())
|
||||||
|
|||||||
21
setup.cfg
21
setup.cfg
@@ -1,6 +1,6 @@
|
|||||||
[metadata]
|
[metadata]
|
||||||
name = babi
|
name = babi
|
||||||
version = 0.0.17
|
version = 0.0.24
|
||||||
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
|
||||||
@@ -13,9 +13,10 @@ classifiers =
|
|||||||
License :: OSI Approved :: MIT License
|
License :: OSI Approved :: MIT License
|
||||||
Programming Language :: Python :: 3
|
Programming Language :: Python :: 3
|
||||||
Programming Language :: Python :: 3 :: Only
|
Programming Language :: Python :: 3 :: Only
|
||||||
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 :: 3.10
|
||||||
Programming Language :: Python :: Implementation :: CPython
|
Programming Language :: Python :: Implementation :: CPython
|
||||||
Programming Language :: Python :: Implementation :: PyPy
|
Programming Language :: Python :: Implementation :: PyPy
|
||||||
|
|
||||||
@@ -25,20 +26,20 @@ install_requires =
|
|||||||
babi-grammars
|
babi-grammars
|
||||||
identify
|
identify
|
||||||
onigurumacffi>=0.0.18
|
onigurumacffi>=0.0.18
|
||||||
importlib_metadata>=1;python_version<"3.8"
|
importlib-metadata>=1;python_version<"3.8"
|
||||||
windows-curses;sys_platform=="win32"
|
windows-curses;sys_platform=="win32"
|
||||||
python_requires = >=3.6.1
|
python_requires = >=3.7
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
babi = babi.main:main
|
|
||||||
babi-textmate-demo = babi.textmate_demo:main
|
|
||||||
|
|
||||||
[options.packages.find]
|
[options.packages.find]
|
||||||
exclude =
|
exclude =
|
||||||
tests*
|
tests*
|
||||||
testing*
|
testing*
|
||||||
|
|
||||||
|
[options.entry_points]
|
||||||
|
console_scripts =
|
||||||
|
babi = babi.main:main
|
||||||
|
babi-textmate-demo = babi.textmate_demo:main
|
||||||
|
|
||||||
[bdist_wheel]
|
[bdist_wheel]
|
||||||
universal = True
|
universal = True
|
||||||
|
|
||||||
@@ -52,6 +53,8 @@ disallow_any_generics = true
|
|||||||
disallow_incomplete_defs = true
|
disallow_incomplete_defs = true
|
||||||
disallow_untyped_defs = true
|
disallow_untyped_defs = true
|
||||||
no_implicit_optional = true
|
no_implicit_optional = true
|
||||||
|
warn_redundant_casts = true
|
||||||
|
warn_unused_ignores = true
|
||||||
|
|
||||||
[mypy-testing.*]
|
[mypy-testing.*]
|
||||||
disallow_untyped_defs = false
|
disallow_untyped_defs = false
|
||||||
|
|||||||
2
setup.py
2
setup.py
@@ -1,2 +1,4 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
setup()
|
setup()
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
import enum
|
import enum
|
||||||
import re
|
import re
|
||||||
from typing import List
|
|
||||||
from typing import Tuple
|
|
||||||
|
|
||||||
from hecate import Runner
|
from hecate import Runner
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ def to_attrs(screen, width):
|
|||||||
fg = bg = -1
|
fg = bg = -1
|
||||||
attr = 0
|
attr = 0
|
||||||
idx = 0
|
idx = 0
|
||||||
ret: List[List[Tuple[int, int, int]]]
|
ret: list[list[tuple[int, int, int]]]
|
||||||
ret = [[] for _ in range(len(screen.splitlines()))]
|
ret = [[] for _ in range(len(screen.splitlines()))]
|
||||||
|
|
||||||
for tp, match in tokenize_colors(screen):
|
for tp, match in tokenize_colors(screen):
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.buf import Buf
|
from babi.buf import Buf
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from babi import color_kd
|
from babi import color_kd
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.fdict import FChainMap
|
from babi.fdict import FChainMap
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
@@ -21,6 +23,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')
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from typing import List
|
|
||||||
from typing import NamedTuple
|
from typing import NamedTuple
|
||||||
from typing import Tuple
|
|
||||||
from typing import Union
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -148,7 +147,7 @@ class AssertScreenLineEquals(NamedTuple):
|
|||||||
|
|
||||||
class AssertScreenAttrEquals(NamedTuple):
|
class AssertScreenAttrEquals(NamedTuple):
|
||||||
n: int
|
n: int
|
||||||
attr: List[Tuple[int, int, int]]
|
attr: list[tuple[int, int, int]]
|
||||||
|
|
||||||
def __call__(self, screen: Screen) -> None:
|
def __call__(self, screen: Screen) -> None:
|
||||||
assert screen.attrs[self.n] == self.attr
|
assert screen.attrs[self.n] == self.attr
|
||||||
@@ -170,7 +169,7 @@ class Resize(NamedTuple):
|
|||||||
|
|
||||||
|
|
||||||
class KeyPress(NamedTuple):
|
class KeyPress(NamedTuple):
|
||||||
wch: Union[int, str]
|
wch: int | str
|
||||||
|
|
||||||
def __call__(self, screen: Screen) -> None:
|
def __call__(self, screen: Screen) -> None:
|
||||||
raise AssertionError('unreachable')
|
raise AssertionError('unreachable')
|
||||||
@@ -236,7 +235,7 @@ class CursesScreen:
|
|||||||
class Key(NamedTuple):
|
class Key(NamedTuple):
|
||||||
tmux: str
|
tmux: str
|
||||||
curses: bytes
|
curses: bytes
|
||||||
wch: Union[int, str]
|
wch: int | str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def value(self) -> int:
|
def value(self) -> int:
|
||||||
@@ -290,6 +289,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}
|
||||||
@@ -299,7 +299,7 @@ class DeferredRunner:
|
|||||||
def __init__(self, command, width=80, height=24, term='screen'):
|
def __init__(self, command, width=80, height=24, term='screen'):
|
||||||
self.command = command
|
self.command = command
|
||||||
self._i = 0
|
self._i = 0
|
||||||
self._ops: List[Op] = []
|
self._ops: list[Op] = []
|
||||||
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 = {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from babi.screen import VERSION_STR
|
from babi.screen import VERSION_STR
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
@@ -20,6 +22,16 @@ def test_replace_invalid_regex(run):
|
|||||||
h.await_text("invalid regex: '('")
|
h.await_text("invalid regex: '('")
|
||||||
|
|
||||||
|
|
||||||
|
def test_replace_invalid_replacement(run, ten_lines):
|
||||||
|
with run(str(ten_lines)) as h, and_exit(h):
|
||||||
|
h.press('^\\')
|
||||||
|
h.await_text('search (to replace):')
|
||||||
|
h.press_and_enter('line_0')
|
||||||
|
h.await_text('replace with:')
|
||||||
|
h.press_and_enter('\\')
|
||||||
|
h.await_text('invalid replacement string')
|
||||||
|
|
||||||
|
|
||||||
def test_replace_cancel_at_replace_string(run):
|
def test_replace_cancel_at_replace_string(run):
|
||||||
with run() as h, and_exit(h):
|
with run() as h, and_exit(h):
|
||||||
h.press('^\\')
|
h.press('^\\')
|
||||||
@@ -30,7 +42,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 +51,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(' *')
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from babi.screen import VERSION_STR
|
from babi.screen import VERSION_STR
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
@@ -128,6 +130,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):
|
||||||
@@ -150,6 +164,18 @@ def test_save_via_ctrl_o_set_filename(run, tmpdir):
|
|||||||
assert f.read() == 'hello world\n'
|
assert f.read() == 'hello world\n'
|
||||||
|
|
||||||
|
|
||||||
|
def test_save_via_ctrl_o_new_filename(run, tmpdir):
|
||||||
|
f = tmpdir.join('f')
|
||||||
|
f.write('wat\n')
|
||||||
|
with run(str(f)) as h, and_exit(h):
|
||||||
|
h.press('^O')
|
||||||
|
h.await_text('enter filename: ')
|
||||||
|
h.press_and_enter('new')
|
||||||
|
h.await_text('saved! (1 line written)')
|
||||||
|
assert f.read() == 'wat\n'
|
||||||
|
assert tmpdir.join('fnew').read() == 'wat\n'
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize('key', ('^C', 'Enter'))
|
@pytest.mark.parametrize('key', ('^C', 'Enter'))
|
||||||
def test_save_via_ctrl_o_cancelled(run, key):
|
def test_save_via_ctrl_o_cancelled(run, key):
|
||||||
with run() as h, and_exit(h):
|
with run() as h, and_exit(h):
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import shlex
|
import shlex
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
@@ -50,6 +52,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 +86,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 *')
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import curses
|
import curses
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from testing.runner import and_exit
|
from testing.runner import and_exit
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import io
|
import io
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.highlight import highlight_line
|
from babi.highlight import highlight_line
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import contextlib
|
import contextlib
|
||||||
import curses
|
import curses
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi import main
|
from babi import main
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import onigurumacffi
|
import onigurumacffi
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from babi.color import Color
|
from babi.color import Color
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user