Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4881953763 | ||
|
|
8f91c12a45 | ||
|
|
5df223f81e | ||
|
|
57bae10448 | ||
|
|
a2afbfa07b | ||
|
|
229ec77f4f | ||
|
|
5a25901cdb | ||
|
|
9c5f28d475 | ||
|
|
a87497cbe2 | ||
|
|
d7622f38c6 | ||
|
|
e474396790 | ||
|
|
e6a0353650 | ||
|
|
e0a59e3f9c | ||
|
|
787dc0d18f | ||
|
|
fd9393c8b1 | ||
|
|
eb26d93e03 | ||
|
|
055d738142 | ||
|
|
29062628f9 | ||
|
|
1fab2a4b71 | ||
|
|
9f5e8c02cb | ||
|
|
31624856d2 | ||
|
|
97b3b4deef | ||
|
|
41880d5f8c | ||
|
|
effe988f60 | ||
|
|
84b20a4016 | ||
|
|
5d2c9532a3 | ||
|
|
33ff8d9726 | ||
|
|
f0b2af9a9f | ||
|
|
fc21a144aa | ||
|
|
973b4c3cf8 | ||
|
|
bd60977438 | ||
|
|
144bbb9daf | ||
|
|
7c16cd966e | ||
|
|
dd19b26fa2 | ||
|
|
dca410dd44 | ||
|
|
ed51b6e6dc | ||
|
|
18b5e258f6 | ||
|
|
e7108f843b | ||
|
|
ff8d3f10fb | ||
|
|
8f603b8e14 | ||
|
|
c184468843 | ||
|
|
c5653976c7 | ||
|
|
d81bb12ff7 | ||
|
|
afe461372e | ||
|
|
b486047e90 |
@@ -1,6 +1,8 @@
|
||||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
||||
[](https://dev.azure.com/asottile/asottile/_build/latest?definitionId=29&branchName=master)
|
||||
|
||||

|
||||
|
||||
babi
|
||||
====
|
||||
|
||||
@@ -12,7 +14,7 @@ a text editor, eventually...
|
||||
|
||||
### why is it called babi?
|
||||
|
||||
I usually 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`.
|
||||
|
||||
### quitting babi
|
||||
@@ -87,7 +89,7 @@ this opens the file, displays it, and can be edited and can save! unknown keys
|
||||
are displayed as errors in the status bar. babi will scroll if the cursor
|
||||
goes off screen either from resize events or from movement. babi can edit
|
||||
multiple files. babi has a command mode (so you can quit it like vim
|
||||
<kbd>:q</kbd>!). babi also support syntax highlighting
|
||||
<kbd>:q</kbd>!). babi also supports syntax highlighting
|
||||
|
||||

|
||||
|
||||
|
||||
@@ -10,11 +10,11 @@ resources:
|
||||
type: github
|
||||
endpoint: github
|
||||
name: asottile/azure-pipeline-templates
|
||||
ref: refs/tags/v1.0.0
|
||||
ref: refs/tags/v2.0.0
|
||||
|
||||
jobs:
|
||||
- template: job--pre-commit.yml@asottile
|
||||
- template: job--python-tox.yml@asottile
|
||||
parameters:
|
||||
toxenvs: [py36, py37, py38]
|
||||
toxenvs: [pypy3, py36, py37, py38, py39]
|
||||
os: linux
|
||||
|
||||
25
babi/buf.py
25
babi/buf.py
@@ -19,11 +19,11 @@ DelCallback = Callable[['Buf', int, str], None]
|
||||
InsCallback = Callable[['Buf', int], None]
|
||||
|
||||
|
||||
def _offsets(s: str) -> Tuple[int, ...]:
|
||||
def _offsets(s: str, tab_size: int) -> Tuple[int, ...]:
|
||||
ret = [0]
|
||||
for c in s:
|
||||
if c == '\t':
|
||||
ret.append(ret[-1] + (4 - ret[-1] % 4))
|
||||
ret.append(ret[-1] + (tab_size - ret[-1] % tab_size))
|
||||
else:
|
||||
ret.append(ret[-1] + wcwidth(c))
|
||||
return tuple(ret)
|
||||
@@ -57,8 +57,10 @@ class DelModification(NamedTuple):
|
||||
|
||||
|
||||
class Buf:
|
||||
def __init__(self, lines: List[str]) -> None:
|
||||
def __init__(self, lines: List[str], tab_size: int = 4) -> None:
|
||||
self._lines = lines
|
||||
self.expandtabs = True
|
||||
self.tab_size = tab_size
|
||||
self.file_y = self.y = self._x = self._x_hint = 0
|
||||
|
||||
self._set_callbacks: List[SetCallback] = [self._set_cb]
|
||||
@@ -136,6 +138,10 @@ class Buf:
|
||||
if self[-1] != '':
|
||||
self.append('')
|
||||
|
||||
def set_tab_size(self, tab_size: int) -> None:
|
||||
self.tab_size = tab_size
|
||||
self._positions = [None]
|
||||
|
||||
# event handling
|
||||
|
||||
def add_set_callback(self, cb: SetCallback) -> None:
|
||||
@@ -219,7 +225,8 @@ class Buf:
|
||||
self._extend_positions(idx)
|
||||
value = self._positions[idx]
|
||||
if value is None:
|
||||
value = self._positions[idx] = _offsets(self._lines[idx])
|
||||
value = _offsets(self._lines[idx], self.tab_size)
|
||||
self._positions[idx] = value
|
||||
return value
|
||||
|
||||
def line_x(self, margin: Margin) -> int:
|
||||
@@ -236,9 +243,17 @@ class Buf:
|
||||
|
||||
# rendered lines
|
||||
|
||||
@property
|
||||
def tab_string(self) -> str:
|
||||
if self.expandtabs:
|
||||
return ' ' * self.tab_size
|
||||
else:
|
||||
return '\t'
|
||||
|
||||
def rendered_line(self, idx: int, margin: Margin) -> str:
|
||||
x = self._cursor_x if idx == self.y else 0
|
||||
return scrolled_line(self._lines[idx].expandtabs(4), x, margin.cols)
|
||||
expanded = self._lines[idx].expandtabs(self.tab_size)
|
||||
return scrolled_line(expanded, x, margin.cols)
|
||||
|
||||
# movement
|
||||
|
||||
|
||||
@@ -33,14 +33,17 @@ class ColorManager(NamedTuple):
|
||||
return self.raw_color_pair(fg_i, bg_i)
|
||||
|
||||
def raw_color_pair(self, fg: int, bg: int) -> int:
|
||||
try:
|
||||
return self.raw_pairs[(fg, bg)]
|
||||
except KeyError:
|
||||
pass
|
||||
if curses.COLORS > 0:
|
||||
try:
|
||||
return self.raw_pairs[(fg, bg)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
|
||||
curses.init_pair(n, fg, bg)
|
||||
return n
|
||||
n = self.raw_pairs[(fg, bg)] = len(self.raw_pairs) + 1
|
||||
curses.init_pair(n, fg, bg)
|
||||
return n
|
||||
else:
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def make(cls) -> 'ColorManager':
|
||||
|
||||
110
babi/file.py
110
babi/file.py
@@ -6,6 +6,7 @@ import hashlib
|
||||
import io
|
||||
import itertools
|
||||
import os.path
|
||||
import re
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import cast
|
||||
@@ -38,6 +39,8 @@ if TYPE_CHECKING:
|
||||
|
||||
TCallable = TypeVar('TCallable', bound=Callable[..., Any])
|
||||
|
||||
WS_RE = re.compile(r'^\s*')
|
||||
|
||||
|
||||
def get_lines(sio: IO[str]) -> Tuple[List[str], str, bool, str]:
|
||||
sha256 = hashlib.sha256()
|
||||
@@ -233,7 +236,7 @@ class File:
|
||||
sio = io.StringIO(stdin)
|
||||
lines, self.nl, mixed, self.sha256 = get_lines(sio)
|
||||
elif self.filename is not None and os.path.isfile(self.filename):
|
||||
with open(self.filename, newline='') as f:
|
||||
with open(self.filename, encoding='UTF-8', newline='') as f:
|
||||
lines, self.nl, mixed, self.sha256 = get_lines(f)
|
||||
else:
|
||||
if self.filename is not None:
|
||||
@@ -244,7 +247,7 @@ class File:
|
||||
status.update('(new file)')
|
||||
lines, self.nl, mixed, self.sha256 = get_lines(io.StringIO(''))
|
||||
|
||||
self.buf = Buf(lines)
|
||||
self.buf = Buf(lines, self.buf.tab_size)
|
||||
|
||||
if mixed:
|
||||
status.update(f'mixed newlines will be converted to {self.nl!r}')
|
||||
@@ -521,20 +524,29 @@ class File:
|
||||
assert self.selection.start is not None
|
||||
sel_y, sel_x = self.selection.start
|
||||
(s_y, _), (e_y, _) = self.selection.get()
|
||||
tab_string = self.buf.tab_string
|
||||
tab_size = len(tab_string)
|
||||
for l_y in range(s_y, e_y + 1):
|
||||
if self.buf[l_y]:
|
||||
self.buf[l_y] = ' ' * 4 + self.buf[l_y]
|
||||
self.buf[l_y] = tab_string + self.buf[l_y]
|
||||
if l_y == self.buf.y:
|
||||
self.buf.x += 4
|
||||
self.buf.x += tab_size
|
||||
if l_y == sel_y and sel_x != 0:
|
||||
sel_x += 4
|
||||
sel_x += tab_size
|
||||
self.selection.set(sel_y, sel_x, self.buf.y, self.buf.x)
|
||||
|
||||
@edit_action('insert tab', final=False)
|
||||
def _tab(self, margin: Margin) -> None:
|
||||
n = 4 - self.buf.x % 4
|
||||
tab_string = self.buf.tab_string
|
||||
if tab_string == '\t':
|
||||
n = 1
|
||||
else:
|
||||
n = self.buf.tab_size - self.buf.x % self.buf.tab_size
|
||||
tab_string = tab_string[:n]
|
||||
line = self.buf[self.buf.y]
|
||||
self.buf[self.buf.y] = line[:self.buf.x] + n * ' ' + line[self.buf.x:]
|
||||
self.buf[self.buf.y] = (
|
||||
line[:self.buf.x] + tab_string + line[self.buf.x:]
|
||||
)
|
||||
self.buf.x += n
|
||||
self.buf.restore_eof_invariant()
|
||||
|
||||
@@ -544,11 +556,10 @@ class File:
|
||||
else:
|
||||
self._tab(margin)
|
||||
|
||||
@staticmethod
|
||||
def _dedent_line(s: str) -> int:
|
||||
bound = min(len(s), 4)
|
||||
def _dedent_line(self, s: str) -> int:
|
||||
bound = min(len(s), len(self.buf.tab_string))
|
||||
i = 0
|
||||
while i < bound and s[i] == ' ':
|
||||
while i < bound and s[i] in (' ', '\t'):
|
||||
i += 1
|
||||
return i
|
||||
|
||||
@@ -641,9 +652,9 @@ class File:
|
||||
self.buf[self.buf.y] += self.buf.pop(self.buf.y + 1)
|
||||
self.buf.restore_eof_invariant()
|
||||
|
||||
def _sort(self, margin: Margin, s_y: int, e_y: int) -> None:
|
||||
def _sort(self, margin: Margin, s_y: int, e_y: int, reverse: bool) -> None:
|
||||
# self.buf intentionally does not support slicing so we use islice
|
||||
lines = sorted(itertools.islice(self.buf, s_y, e_y))
|
||||
lines = sorted(itertools.islice(self.buf, s_y, e_y), reverse=reverse)
|
||||
for i, line in zip(range(s_y, e_y), lines):
|
||||
self.buf[i] = line
|
||||
|
||||
@@ -651,18 +662,75 @@ class File:
|
||||
self.buf.x = 0
|
||||
self.buf.scroll_screen_if_needed(margin)
|
||||
|
||||
@edit_action('sort', final=True)
|
||||
def sort(self, margin: Margin) -> None:
|
||||
self._sort(margin, 0, len(self.buf) - 1)
|
||||
|
||||
@edit_action('sort selection', final=True)
|
||||
@clear_selection
|
||||
def sort_selection(self, margin: Margin) -> None:
|
||||
def _selection_lines(self) -> Tuple[int, int]:
|
||||
(s_y, _), (e_y, _) = self.selection.get()
|
||||
e_y = min(e_y + 1, len(self.buf) - 1)
|
||||
if self.buf[e_y - 1] == '':
|
||||
e_y -= 1
|
||||
self._sort(margin, s_y, e_y)
|
||||
return s_y, e_y
|
||||
|
||||
@edit_action('sort', final=True)
|
||||
def sort(self, margin: Margin, reverse: bool = False) -> None:
|
||||
self._sort(margin, 0, len(self.buf) - 1, reverse=reverse)
|
||||
|
||||
@edit_action('sort selection', final=True)
|
||||
@clear_selection
|
||||
def sort_selection(self, margin: Margin, reverse: bool = False) -> None:
|
||||
s_y, e_y = self._selection_lines()
|
||||
self._sort(margin, s_y, e_y, reverse=reverse)
|
||||
|
||||
def _is_commented(self, lineno: int, prefix: str) -> bool:
|
||||
return self.buf[lineno].lstrip().startswith(prefix)
|
||||
|
||||
def _indent(self, lineno: int) -> str:
|
||||
ws_match = WS_RE.match(self.buf[lineno])
|
||||
assert ws_match is not None
|
||||
return ws_match[0]
|
||||
|
||||
def _minimum_indent_for_selection(self) -> int:
|
||||
s_y, e_y = self._selection_lines()
|
||||
return min(len(self._indent(lineno)) for lineno in range(s_y, e_y))
|
||||
|
||||
def _comment_remove(self, lineno: int, prefix: str) -> None:
|
||||
line = self.buf[lineno]
|
||||
indent = self._indent(lineno)
|
||||
ws_len = len(indent)
|
||||
|
||||
if line.startswith(f'{prefix} ', ws_len):
|
||||
self.buf[lineno] = f'{indent}{line[ws_len + len(prefix) + 1:]}'
|
||||
elif line.startswith(prefix, ws_len):
|
||||
self.buf[lineno] = f'{indent}{line[ws_len + len(prefix):]}'
|
||||
|
||||
if self.buf.y == lineno and self.buf.x > ws_len:
|
||||
self.buf.x -= len(line) - len(self.buf[lineno])
|
||||
|
||||
def _comment_add(self, lineno: int, prefix: str, s_offset: int) -> None:
|
||||
line = self.buf[lineno]
|
||||
|
||||
self.buf[lineno] = f'{line[:s_offset]}{prefix} {line[s_offset:]}'
|
||||
|
||||
if lineno == self.buf.y and self.buf.x > s_offset:
|
||||
self.buf.x += len(self.buf[lineno]) - len(line)
|
||||
|
||||
@edit_action('comment', final=True)
|
||||
def toggle_comment(self, prefix: str) -> None:
|
||||
if self._is_commented(self.buf.y, prefix):
|
||||
self._comment_remove(self.buf.y, prefix)
|
||||
else:
|
||||
ws_len = len(self._indent(self.buf.y))
|
||||
self._comment_add(self.buf.y, prefix, ws_len)
|
||||
|
||||
@edit_action('comment selection', final=True)
|
||||
@clear_selection
|
||||
def toggle_comment_selection(self, prefix: str) -> None:
|
||||
s_y, e_y = self._selection_lines()
|
||||
commented = self._is_commented(s_y, prefix)
|
||||
minimum_indent = self._minimum_indent_for_selection()
|
||||
for lineno in range(s_y, e_y):
|
||||
if commented:
|
||||
self._comment_remove(lineno, prefix)
|
||||
else:
|
||||
self._comment_add(lineno, prefix, minimum_indent)
|
||||
|
||||
DISPATCH = {
|
||||
# movement
|
||||
|
||||
@@ -273,6 +273,7 @@ class CompiledRegsetRule(CompiledRule, Protocol):
|
||||
class Entry(NamedTuple):
|
||||
scope: Tuple[str, ...]
|
||||
rule: CompiledRule
|
||||
start: Tuple[str, int]
|
||||
reg: _Reg = ERR_REG
|
||||
boundary: bool = False
|
||||
|
||||
@@ -284,7 +285,7 @@ def _inner_capture_parse(
|
||||
scope: Scope,
|
||||
rule: CompiledRule,
|
||||
) -> Regions:
|
||||
state = State.root(Entry(scope + rule.name, rule))
|
||||
state = State.root(Entry(scope + rule.name, rule, (s, 0)))
|
||||
_, regions = highlight_line(compiler, state, s, first_line=False)
|
||||
return tuple(
|
||||
r._replace(start=r.start + start, end=r.end + start) for r in regions
|
||||
@@ -440,7 +441,8 @@ class EndRule(NamedTuple):
|
||||
|
||||
boundary = match.end() == len(match.string)
|
||||
reg = make_reg(expand_escaped(match, self.end))
|
||||
state = state.push(Entry(next_scope, self, reg, boundary))
|
||||
start = (match.string, match.start())
|
||||
state = state.push(Entry(next_scope, self, start, reg, boundary))
|
||||
regions = _captures(compiler, scope, match, self.begin_captures)
|
||||
return state, True, regions
|
||||
|
||||
@@ -455,7 +457,16 @@ class EndRule(NamedTuple):
|
||||
if m.start() > pos:
|
||||
ret.append(Region(pos, m.start(), state.cur.scope))
|
||||
ret.extend(_captures(compiler, state.cur.scope, m, self.end_captures))
|
||||
return state.pop(), m.end(), False, tuple(ret)
|
||||
# this is probably a bug in the grammar, but it pushed and popped at
|
||||
# the same position.
|
||||
# we'll advance the highlighter by one position to get past the loop
|
||||
# this appears to be what vs code does as well
|
||||
if state.entries[-1].start == (m.string, m.end()):
|
||||
ret.append(Region(m.end(), m.end() + 1, state.cur.scope))
|
||||
end = m.end() + 1
|
||||
else:
|
||||
end = m.end()
|
||||
return state.pop(), end, False, tuple(ret)
|
||||
|
||||
def search(
|
||||
self,
|
||||
@@ -501,7 +512,9 @@ class WhileRule(NamedTuple):
|
||||
|
||||
boundary = match.end() == len(match.string)
|
||||
reg = make_reg(expand_escaped(match, self.while_))
|
||||
state = state.push_while(self, Entry(next_scope, self, reg, boundary))
|
||||
start = (match.string, match.start())
|
||||
entry = Entry(next_scope, self, start, reg, boundary)
|
||||
state = state.push_while(self, entry)
|
||||
regions = _captures(compiler, scope, match, self.begin_captures)
|
||||
return state, True, regions
|
||||
|
||||
@@ -541,7 +554,7 @@ class Compiler:
|
||||
self._rule_to_grammar: Dict[_Rule, Grammar] = {}
|
||||
self._c_rules: Dict[_Rule, CompiledRule] = {}
|
||||
root = self._compile_root(grammar)
|
||||
self.root_state = State.root(Entry(root.name, root))
|
||||
self.root_state = State.root(Entry(root.name, root, ('', 0)))
|
||||
|
||||
def _visit_rule(self, grammar: Grammar, rule: _Rule) -> _Rule:
|
||||
self._rule_to_grammar[rule] = grammar
|
||||
@@ -675,7 +688,7 @@ class Grammars:
|
||||
pass
|
||||
|
||||
grammar_path = self._scope_to_files.pop(scope)
|
||||
with open(grammar_path) as f:
|
||||
with open(grammar_path, encoding='UTF-8') as f:
|
||||
ret = self._raw[scope] = json.load(f)
|
||||
|
||||
file_types = frozenset(ret.get('fileTypes', ()))
|
||||
|
||||
@@ -19,7 +19,8 @@ class History:
|
||||
history_dir = xdg_data('history')
|
||||
os.makedirs(history_dir, exist_ok=True)
|
||||
for filename in os.listdir(history_dir):
|
||||
with open(os.path.join(history_dir, filename)) as f:
|
||||
history_filename = os.path.join(history_dir, filename)
|
||||
with open(history_filename, encoding='UTF-8') as f:
|
||||
self.data[filename] = f.read().splitlines()
|
||||
self._orig_len[filename] = len(self.data[filename])
|
||||
try:
|
||||
@@ -28,5 +29,6 @@ class History:
|
||||
for k, v in self.data.items():
|
||||
new_history = v[self._orig_len[k]:]
|
||||
if new_history:
|
||||
with open(os.path.join(history_dir, k), 'a+') as f:
|
||||
history_filename = os.path.join(history_dir, k)
|
||||
with open(history_filename, 'a+', encoding='UTF-8') as f:
|
||||
f.write('\n'.join(new_history) + '\n')
|
||||
|
||||
@@ -2,6 +2,7 @@ import argparse
|
||||
import curses
|
||||
import os
|
||||
import re
|
||||
import signal
|
||||
import sys
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
@@ -132,12 +133,17 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
|
||||
if '-' in args.filenames:
|
||||
print('reading stdin...', file=sys.stderr)
|
||||
stdin = sys.stdin.read()
|
||||
stdin = sys.stdin.buffer.read().decode()
|
||||
tty = os.open(CONSOLE, os.O_RDONLY)
|
||||
os.dup2(tty, sys.stdin.fileno())
|
||||
else:
|
||||
stdin = ''
|
||||
|
||||
# ignore backgrounding signals, we'll handle those in curses
|
||||
# fixes a problem with ^Z on termination which would break the terminal
|
||||
if sys.platform != 'win32': # pragma: win32 no cover # pragma: no branch
|
||||
signal.signal(signal.SIGTSTP, signal.SIG_IGN)
|
||||
|
||||
with perf_log(args.perf_log) as perf, make_stdscr() as stdscr:
|
||||
if args.key_debug:
|
||||
return _key_debug(stdscr, perf)
|
||||
|
||||
@@ -36,7 +36,7 @@ class Perf:
|
||||
def save_profiles(self, filename: str) -> None:
|
||||
assert self._prof is not None
|
||||
self._prof.dump_stats(f'{filename}.pstats')
|
||||
with open(filename, 'w') as f:
|
||||
with open(filename, 'w', encoding='UTF-8') as f:
|
||||
f.write('μs\tevent\n')
|
||||
for name, duration in self._records:
|
||||
f.write(f'{int(duration * 1000 * 1000)}\t{name}\n')
|
||||
|
||||
117
babi/reg.py
117
babi/reg.py
@@ -6,80 +6,36 @@ from typing import Tuple
|
||||
|
||||
import onigurumacffi
|
||||
|
||||
from babi.cached_property import cached_property
|
||||
|
||||
_BACKREF_RE = re.compile(r'((?<!\\)(?:\\\\)*)\\([0-9]+)')
|
||||
|
||||
|
||||
def _replace_esc(s: str, chars: str) -> str:
|
||||
"""replace the given escape sequences of `chars` with \\uffff"""
|
||||
for c in chars:
|
||||
if f'\\{c}' in s:
|
||||
break
|
||||
else:
|
||||
return s
|
||||
|
||||
b = []
|
||||
i = 0
|
||||
length = len(s)
|
||||
while i < length:
|
||||
try:
|
||||
sbi = s.index('\\', i)
|
||||
except ValueError:
|
||||
b.append(s[i:])
|
||||
break
|
||||
if sbi > i:
|
||||
b.append(s[i:sbi])
|
||||
b.append('\\')
|
||||
i = sbi + 1
|
||||
if i < length:
|
||||
if s[i] in chars:
|
||||
b.append('\uffff')
|
||||
else:
|
||||
b.append(s[i])
|
||||
i += 1
|
||||
return ''.join(b)
|
||||
_FLAGS = {
|
||||
# (first_line, boundary)
|
||||
(False, False): (
|
||||
onigurumacffi.OnigSearchOption.NOT_END_STRING |
|
||||
onigurumacffi.OnigSearchOption.NOT_BEGIN_STRING |
|
||||
onigurumacffi.OnigSearchOption.NOT_BEGIN_POSITION
|
||||
),
|
||||
(False, True): (
|
||||
onigurumacffi.OnigSearchOption.NOT_END_STRING |
|
||||
onigurumacffi.OnigSearchOption.NOT_BEGIN_STRING
|
||||
),
|
||||
(True, False): (
|
||||
onigurumacffi.OnigSearchOption.NOT_END_STRING |
|
||||
onigurumacffi.OnigSearchOption.NOT_BEGIN_POSITION
|
||||
),
|
||||
(True, True): onigurumacffi.OnigSearchOption.NOT_END_STRING,
|
||||
}
|
||||
|
||||
|
||||
class _Reg:
|
||||
def __init__(self, s: str) -> None:
|
||||
self._pattern = s
|
||||
self._reg = onigurumacffi.compile(self._pattern)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f'{type(self).__name__}({self._pattern!r})'
|
||||
|
||||
@cached_property
|
||||
def _reg(self) -> onigurumacffi._Pattern:
|
||||
return onigurumacffi.compile(_replace_esc(self._pattern, 'z'))
|
||||
|
||||
@cached_property
|
||||
def _reg_no_A(self) -> onigurumacffi._Pattern:
|
||||
return onigurumacffi.compile(_replace_esc(self._pattern, 'Az'))
|
||||
|
||||
@cached_property
|
||||
def _reg_no_G(self) -> onigurumacffi._Pattern:
|
||||
return onigurumacffi.compile(_replace_esc(self._pattern, 'Gz'))
|
||||
|
||||
@cached_property
|
||||
def _reg_no_A_no_G(self) -> onigurumacffi._Pattern:
|
||||
return onigurumacffi.compile(_replace_esc(self._pattern, 'AGz'))
|
||||
|
||||
def _get_reg(
|
||||
self,
|
||||
first_line: bool,
|
||||
boundary: bool,
|
||||
) -> onigurumacffi._Pattern:
|
||||
if boundary:
|
||||
if first_line:
|
||||
return self._reg
|
||||
else:
|
||||
return self._reg_no_A
|
||||
else:
|
||||
if first_line:
|
||||
return self._reg_no_G
|
||||
else:
|
||||
return self._reg_no_A_no_G
|
||||
|
||||
def search(
|
||||
self,
|
||||
line: str,
|
||||
@@ -87,7 +43,7 @@ class _Reg:
|
||||
first_line: bool,
|
||||
boundary: bool,
|
||||
) -> Optional[Match[str]]:
|
||||
return self._get_reg(first_line, boundary).search(line, pos)
|
||||
return self._reg.search(line, pos, flags=_FLAGS[first_line, boundary])
|
||||
|
||||
def match(
|
||||
self,
|
||||
@@ -96,36 +52,18 @@ class _Reg:
|
||||
first_line: bool,
|
||||
boundary: bool,
|
||||
) -> Optional[Match[str]]:
|
||||
return self._get_reg(first_line, boundary).match(line, pos)
|
||||
return self._reg.match(line, pos, flags=_FLAGS[first_line, boundary])
|
||||
|
||||
|
||||
class _RegSet:
|
||||
def __init__(self, *s: str) -> None:
|
||||
self._patterns = s
|
||||
self._set = onigurumacffi.compile_regset(*self._patterns)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
args = ', '.join(repr(s) for s in self._patterns)
|
||||
return f'{type(self).__name__}({args})'
|
||||
|
||||
@cached_property
|
||||
def _set(self) -> onigurumacffi._RegSet:
|
||||
return onigurumacffi.compile_regset(*self._patterns)
|
||||
|
||||
@cached_property
|
||||
def _set_no_A(self) -> onigurumacffi._RegSet:
|
||||
patterns = (_replace_esc(p, 'A') for p in self._patterns)
|
||||
return onigurumacffi.compile_regset(*patterns)
|
||||
|
||||
@cached_property
|
||||
def _set_no_G(self) -> onigurumacffi._RegSet:
|
||||
patterns = (_replace_esc(p, 'G') for p in self._patterns)
|
||||
return onigurumacffi.compile_regset(*patterns)
|
||||
|
||||
@cached_property
|
||||
def _set_no_A_no_G(self) -> onigurumacffi._RegSet:
|
||||
patterns = (_replace_esc(p, 'AG') for p in self._patterns)
|
||||
return onigurumacffi.compile_regset(*patterns)
|
||||
|
||||
def search(
|
||||
self,
|
||||
line: str,
|
||||
@@ -133,16 +71,7 @@ class _RegSet:
|
||||
first_line: bool,
|
||||
boundary: bool,
|
||||
) -> Tuple[int, Optional[Match[str]]]:
|
||||
if boundary:
|
||||
if first_line:
|
||||
return self._set.search(line, pos)
|
||||
else:
|
||||
return self._set_no_A.search(line, pos)
|
||||
else:
|
||||
if first_line:
|
||||
return self._set_no_G.search(line, pos)
|
||||
else:
|
||||
return self._set_no_A_no_G.search(line, pos)
|
||||
return self._set.search(line, pos, flags=_FLAGS[first_line, boundary])
|
||||
|
||||
|
||||
def expand_escaped(match: Match[str], s: str) -> str:
|
||||
@@ -151,4 +80,4 @@ def expand_escaped(match: Match[str], s: str) -> str:
|
||||
|
||||
make_reg = functools.lru_cache(maxsize=None)(_Reg)
|
||||
make_regset = functools.lru_cache(maxsize=None)(_RegSet)
|
||||
ERR_REG = make_reg(')this pattern always triggers an error when used(')
|
||||
ERR_REG = make_reg('$ ^')
|
||||
|
||||
@@ -226,7 +226,10 @@ class Screen:
|
||||
if self._buffered_input is not None:
|
||||
wch, self._buffered_input = self._buffered_input, None
|
||||
else:
|
||||
wch = self.stdscr.get_wch()
|
||||
try:
|
||||
wch = self.stdscr.get_wch()
|
||||
except curses.error: # pragma: no cover (macos bug?)
|
||||
wch = self.stdscr.get_wch()
|
||||
if isinstance(wch, str) and wch == '\x1b':
|
||||
wch = self._get_sequence(wch)
|
||||
if len(wch) == 2:
|
||||
@@ -418,7 +421,9 @@ class Screen:
|
||||
|
||||
def command(self) -> Optional[EditResult]:
|
||||
response = self.prompt('', history='command')
|
||||
if response == ':q':
|
||||
if response is PromptResult.CANCELLED:
|
||||
pass
|
||||
elif response == ':q':
|
||||
return self.quit_save_modified()
|
||||
elif response == ':q!':
|
||||
return EditResult.EXIT
|
||||
@@ -433,7 +438,41 @@ class Screen:
|
||||
else:
|
||||
self.file.sort(self.margin)
|
||||
self.status.update('sorted!')
|
||||
elif response is not PromptResult.CANCELLED:
|
||||
elif response == ':sort!':
|
||||
if self.file.selection.start:
|
||||
self.file.sort_selection(self.margin, reverse=True)
|
||||
else:
|
||||
self.file.sort(self.margin, reverse=True)
|
||||
self.status.update('sorted!')
|
||||
elif response.startswith((':tabstop ', ':tabsize ')):
|
||||
_, _, tab_size = response.partition(' ')
|
||||
try:
|
||||
parsed_tab_size = int(tab_size)
|
||||
except ValueError:
|
||||
self.status.update(f'invalid size: {tab_size}')
|
||||
else:
|
||||
if parsed_tab_size <= 0:
|
||||
self.status.update(f'invalid size: {parsed_tab_size}')
|
||||
else:
|
||||
for file in self.files:
|
||||
file.buf.set_tab_size(parsed_tab_size)
|
||||
self.status.update('updated!')
|
||||
elif response.startswith(':expandtabs'):
|
||||
for file in self.files:
|
||||
file.buf.expandtabs = True
|
||||
self.status.update('updated!')
|
||||
elif response.startswith(':noexpandtabs'):
|
||||
for file in self.files:
|
||||
file.buf.expandtabs = False
|
||||
self.status.update('updated!')
|
||||
elif response == ':comment' or response.startswith(':comment '):
|
||||
_, _, comment = response.partition(' ')
|
||||
comment = (comment or '#').strip()
|
||||
if self.file.selection.start:
|
||||
self.file.toggle_comment_selection(comment)
|
||||
else:
|
||||
self.file.toggle_comment(comment)
|
||||
else:
|
||||
self.status.update(f'invalid command: {response}')
|
||||
return None
|
||||
|
||||
@@ -452,7 +491,7 @@ class Screen:
|
||||
self.file.filename = filename
|
||||
|
||||
if os.path.isfile(self.file.filename):
|
||||
with open(self.file.filename, newline='') as f:
|
||||
with open(self.file.filename, encoding='UTF-8', newline='') as f:
|
||||
*_, sha256 = get_lines(f)
|
||||
else:
|
||||
sha256 = hashlib.sha256(b'').hexdigest()
|
||||
@@ -465,7 +504,7 @@ class Screen:
|
||||
self.status.update('(file changed on disk, not implemented)')
|
||||
return PromptResult.CANCELLED
|
||||
|
||||
with open(self.file.filename, 'w', newline='') as f:
|
||||
with open(self.file.filename, 'w', encoding='UTF-8', newline='') as f:
|
||||
f.write(contents)
|
||||
|
||||
self.file.modified = False
|
||||
|
||||
@@ -37,7 +37,7 @@ def _highlight_output(theme: Theme, compiler: Compiler, filename: str) -> int:
|
||||
|
||||
if theme.default.bg is not None:
|
||||
print('\x1b[48;2;{r};{g};{b}m'.format(**theme.default.bg._asdict()))
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding='UTF-8') as f:
|
||||
for line_idx, line in enumerate(f):
|
||||
first_line = line_idx == 0
|
||||
state, regions = highlight_line(compiler, state, line, first_line)
|
||||
@@ -54,7 +54,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
|
||||
parser.add_argument('filename')
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
with open(args.filename) as f:
|
||||
with open(args.filename, encoding='UTF-8') as f:
|
||||
first_line = next(f, '')
|
||||
|
||||
theme = Theme.from_filename(args.theme)
|
||||
|
||||
@@ -147,5 +147,5 @@ class Theme(NamedTuple):
|
||||
if not os.path.exists(filename):
|
||||
return cls.blank()
|
||||
else:
|
||||
with open(filename) as f:
|
||||
with open(filename, encoding='UTF-8') as f:
|
||||
return cls.from_dct(json.load(f))
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[metadata]
|
||||
name = babi
|
||||
version = 0.0.9
|
||||
version = 0.0.17
|
||||
description = a text editor
|
||||
long_description = file: README.md
|
||||
long_description_content_type = text/markdown
|
||||
@@ -24,7 +24,7 @@ packages = find:
|
||||
install_requires =
|
||||
babi-grammars
|
||||
identify
|
||||
onigurumacffi>=0.0.10
|
||||
onigurumacffi>=0.0.18
|
||||
importlib_metadata>=1;python_version<"3.8"
|
||||
windows-curses;sys_platform=="win32"
|
||||
python_requires = >=3.6.1
|
||||
|
||||
191
tests/features/comment_test.py
Normal file
191
tests/features/comment_test.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import pytest
|
||||
|
||||
from testing.runner import and_exit
|
||||
from testing.runner import trigger_command_mode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def three_lines_with_indentation(tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('line_0\n line_1\n line_2')
|
||||
return f
|
||||
|
||||
|
||||
def test_comment_some_code(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('S-Down')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('# line_0\n# line_1\nline_2\n')
|
||||
|
||||
|
||||
def test_comment_some_code_with_alternate_comment_character(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('S-Down')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment //')
|
||||
|
||||
h.await_text('// line_0\n// line_1\nline_2\n')
|
||||
|
||||
|
||||
def test_comment_partially_commented(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('#')
|
||||
h.press('S-Down')
|
||||
h.await_text('#line_0\nline_1\nline_2')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\nline_1\nline_2\n')
|
||||
|
||||
|
||||
def test_comment_partially_uncommented(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('Down')
|
||||
h.press('#')
|
||||
h.press('Up')
|
||||
h.press('S-Down')
|
||||
h.await_text('line_0\n#line_1\nline_2')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('# line_0\n# #line_1\nline_2\n')
|
||||
|
||||
|
||||
def test_comment_single_line(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('# line_0\nline_1\n')
|
||||
|
||||
|
||||
def test_uncomment_single_line(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('#')
|
||||
h.await_text('#line_0\nline_1\n')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\nline_1\n')
|
||||
|
||||
|
||||
def test_comment_with_trailing_whitespace(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment // ')
|
||||
|
||||
h.await_text('// line_0\nline_1\n')
|
||||
|
||||
|
||||
def test_comment_some_code_with_indentation(run, three_lines_with_indentation):
|
||||
with run(str(three_lines_with_indentation)) as h, and_exit(h):
|
||||
h.press('S-Down')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('# line_0\n# line_1\n line_2\n')
|
||||
|
||||
h.press('S-Up')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\n line_1\n line_2\n')
|
||||
|
||||
|
||||
def test_comment_some_code_on_indent_part(run, three_lines_with_indentation):
|
||||
with run(str(three_lines_with_indentation)) as h, and_exit(h):
|
||||
h.press('Down')
|
||||
h.press('S-Down')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\n # line_1\n # line_2\n')
|
||||
|
||||
h.press('S-Up')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\n line_1\n line_2\n')
|
||||
|
||||
|
||||
def test_comment_some_code_on_tabs_part(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('line_0\n\tline_1\n\t\tline_2')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
h.await_text('line_0\n line_1\n line_2')
|
||||
h.press('Down')
|
||||
h.press('S-Down')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\n # line_1\n # line_2')
|
||||
|
||||
h.press('S-Up')
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('line_0\n line_1\n line_2')
|
||||
|
||||
|
||||
def test_comment_cursor_at_end_of_line(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('# ')
|
||||
h.press('End')
|
||||
h.await_cursor_position(x=8, y=1)
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_cursor_position(x=6, y=1)
|
||||
|
||||
|
||||
def test_add_comment_moves_cursor(run, ten_lines):
|
||||
with run(str(ten_lines)) as h, and_exit(h):
|
||||
h.press('End')
|
||||
|
||||
h.await_cursor_position(x=6, y=1)
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_cursor_position(x=8, y=1)
|
||||
|
||||
|
||||
def test_do_not_move_if_cursor_before_comment(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('\t\tfoo')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
h.press('Right')
|
||||
|
||||
h.await_cursor_position(x=4, y=1)
|
||||
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_cursor_position(x=4, y=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('comment', ('# ', '#'))
|
||||
def test_remove_comment_with_comment_elsewhere_in_line(run, tmpdir, comment):
|
||||
f = tmpdir.join('f')
|
||||
f.write(f'{comment}print("not a # comment here!")\n')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':comment')
|
||||
|
||||
h.await_text('\nprint("not a # comment here!")\n')
|
||||
@@ -303,6 +303,7 @@ class DeferredRunner:
|
||||
self.color_pairs = {0: (7, 0)}
|
||||
self.screen = Screen(width, height)
|
||||
self._n_colors, self._can_change_color = {
|
||||
'xterm-mono': (0, False),
|
||||
'screen': (8, False),
|
||||
'screen-256color': (256, False),
|
||||
'xterm-256color': (256, True),
|
||||
@@ -391,6 +392,7 @@ class DeferredRunner:
|
||||
|
||||
_curses_cbreak = _curses_endwin = _curses_noecho = _curses__noop
|
||||
_curses_nonl = _curses_raw = _curses_use_default_colors = _curses__noop
|
||||
_curses_set_escdelay = _curses__noop
|
||||
|
||||
_curses_error = curses.error # so we don't mock the exception
|
||||
|
||||
|
||||
45
tests/features/expandtabs_test.py
Normal file
45
tests/features/expandtabs_test.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from testing.runner import and_exit
|
||||
from testing.runner import trigger_command_mode
|
||||
|
||||
|
||||
def test_set_expandtabs(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('a')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
h.press('Left')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':expandtabs')
|
||||
h.await_text('updated!')
|
||||
h.press('Tab')
|
||||
h.press('^S')
|
||||
assert f.read() == ' a\n'
|
||||
|
||||
|
||||
def test_set_noexpandtabs(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('a')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
h.press('Left')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':noexpandtabs')
|
||||
h.await_text('updated!')
|
||||
h.press('Tab')
|
||||
h.press('^S')
|
||||
assert f.read() == '\ta\n'
|
||||
|
||||
|
||||
def test_indent_with_expandtabs(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('a\nb\nc')
|
||||
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':noexpandtabs')
|
||||
h.await_text('updated!')
|
||||
for _ in range(3):
|
||||
h.press('S-Down')
|
||||
h.press('Tab')
|
||||
h.press('^S')
|
||||
assert f.read() == '\ta\n\tb\n\tc\n'
|
||||
@@ -1,4 +1,5 @@
|
||||
from testing.runner import and_exit
|
||||
from testing.runner import trigger_command_mode
|
||||
|
||||
|
||||
def test_indent_at_beginning_of_line(run):
|
||||
@@ -12,11 +13,12 @@ def test_indent_at_beginning_of_line(run):
|
||||
|
||||
def test_indent_not_full_tab(run):
|
||||
with run() as h, and_exit(h):
|
||||
h.press('h')
|
||||
h.press('hello')
|
||||
h.press('Home')
|
||||
h.press('Right')
|
||||
h.press('Tab')
|
||||
h.press('ello')
|
||||
h.await_text('h ello')
|
||||
h.await_cursor_position(x=8, y=1)
|
||||
h.await_cursor_position(x=4, y=1)
|
||||
|
||||
|
||||
def test_indent_fixes_eof(run):
|
||||
@@ -86,6 +88,20 @@ def test_dedent_selection(run, tmpdir):
|
||||
h.await_text('\n1\n2\n 3\n')
|
||||
|
||||
|
||||
def test_dedent_selection_with_noexpandtabs(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write('1\n\t2\n\t\t3\n')
|
||||
with run(str(f)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':noexpandtabs')
|
||||
h.await_text('updated!')
|
||||
for _ in range(3):
|
||||
h.press('S-Down')
|
||||
h.press('BTab')
|
||||
h.press('^S')
|
||||
assert f.read() == '1\n2\n\t3\n'
|
||||
|
||||
|
||||
def test_dedent_beginning_of_line(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write(' hi\n')
|
||||
|
||||
@@ -21,6 +21,16 @@ def test_sort_entire_file(run, unsorted):
|
||||
assert unsorted.read() == 'a\nb\nc\nd\n'
|
||||
|
||||
|
||||
def test_reverse_sort_entire_file(run, unsorted):
|
||||
with run(str(unsorted)) as h, and_exit(h):
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':sort!')
|
||||
h.await_text('sorted!')
|
||||
h.await_cursor_position(x=0, y=1)
|
||||
h.press('^S')
|
||||
assert unsorted.read() == 'd\nc\nb\na\n'
|
||||
|
||||
|
||||
def test_sort_selection(run, unsorted):
|
||||
with run(str(unsorted)) as h, and_exit(h):
|
||||
h.press('S-Down')
|
||||
@@ -32,6 +42,18 @@ def test_sort_selection(run, unsorted):
|
||||
assert unsorted.read() == 'b\nd\nc\na\n'
|
||||
|
||||
|
||||
def test_reverse_sort_selection(run, unsorted):
|
||||
with run(str(unsorted)) as h, and_exit(h):
|
||||
h.press('Down')
|
||||
h.press('S-Down')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(':sort!')
|
||||
h.await_text('sorted!')
|
||||
h.await_cursor_position(x=0, y=2)
|
||||
h.press('^S')
|
||||
assert unsorted.read() == 'd\nc\nb\na\n'
|
||||
|
||||
|
||||
def test_sort_selection_does_not_include_eof(run, unsorted):
|
||||
with run(str(unsorted)) as h, and_exit(h):
|
||||
for _ in range(5):
|
||||
|
||||
@@ -153,3 +153,8 @@ def test_syntax_highlighting_tabs_after_line_creation(run, tmpdir):
|
||||
h.press('Enter')
|
||||
|
||||
h.await_text('foo\n x\nx\ny\n')
|
||||
|
||||
|
||||
def test_does_not_crash_with_no_color_support(run):
|
||||
with run(term='xterm-mono') as h, and_exit(h):
|
||||
pass
|
||||
|
||||
30
tests/features/tabsize_test.py
Normal file
30
tests/features/tabsize_test.py
Normal file
@@ -0,0 +1,30 @@
|
||||
import pytest
|
||||
|
||||
from testing.runner import and_exit
|
||||
from testing.runner import trigger_command_mode
|
||||
|
||||
|
||||
@pytest.mark.parametrize('setting', ('tabsize', 'tabstop'))
|
||||
def test_set_tabstop(run, setting):
|
||||
with run() as h, and_exit(h):
|
||||
h.press('a')
|
||||
h.press('Left')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(f':{setting} 2')
|
||||
h.await_text('updated!')
|
||||
h.press('Tab')
|
||||
h.await_text('\n a')
|
||||
h.await_cursor_position(x=2, y=1)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('tabsize', ('-1', '0', 'wat'))
|
||||
def test_set_invalid_tabstop(run, tabsize):
|
||||
with run() as h, and_exit(h):
|
||||
h.press('a')
|
||||
h.press('Left')
|
||||
trigger_command_mode(h)
|
||||
h.press_and_enter(f':tabstop {tabsize}')
|
||||
h.await_text(f'invalid size: {tabsize}')
|
||||
h.press('Tab')
|
||||
h.await_text(' a')
|
||||
h.await_cursor_position(x=4, y=1)
|
||||
@@ -637,3 +637,25 @@ def test_backslash_z(compiler_state):
|
||||
assert regions2 == (
|
||||
Region(0, 6, ('test', 'comment')),
|
||||
)
|
||||
|
||||
|
||||
def test_buggy_begin_end_grammar(compiler_state):
|
||||
# before this would result in an infinite loop of start / end
|
||||
compiler, state = compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '(?=</style)',
|
||||
'end': '(?=</style)',
|
||||
'name': 'css',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, 'test </style', True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 5, ('test',)),
|
||||
Region(5, 6, ('test', 'css')),
|
||||
Region(6, 12, ('test',)),
|
||||
)
|
||||
|
||||
@@ -35,9 +35,8 @@ def test_reg_other_escapes_left_untouched():
|
||||
def test_reg_not_out_of_bounds_at_end():
|
||||
# the only way this is triggerable is with an illegal regex, we'd rather
|
||||
# produce an error about the regex being wrong than an IndexError
|
||||
reg = _Reg('\\A\\')
|
||||
with pytest.raises(onigurumacffi.OnigError) as excinfo:
|
||||
reg.search('\\', 0, first_line=False, boundary=False)
|
||||
_Reg('\\A\\')
|
||||
msg, = excinfo.value.args
|
||||
assert msg == 'end pattern at escape'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user