Syntax highlighting
This commit is contained in:
63
tests/color_kd_test.py
Normal file
63
tests/color_kd_test.py
Normal file
@@ -0,0 +1,63 @@
|
||||
from babi import color_kd
|
||||
from babi.color import Color
|
||||
|
||||
|
||||
def test_build_trivial():
|
||||
assert color_kd._build([]) is None
|
||||
|
||||
|
||||
def test_build_single_node():
|
||||
kd = color_kd._build([(Color(0, 0, 0), 255)])
|
||||
assert kd == color_kd._KD(Color(0, 0, 0), 255, left=None, right=None)
|
||||
|
||||
|
||||
def test_build_many_colors():
|
||||
kd = color_kd._build([
|
||||
(Color(0, 106, 200), 255),
|
||||
(Color(1, 105, 201), 254),
|
||||
(Color(2, 104, 202), 253),
|
||||
(Color(3, 103, 203), 252),
|
||||
(Color(4, 102, 204), 251),
|
||||
(Color(5, 101, 205), 250),
|
||||
(Color(6, 100, 206), 249),
|
||||
])
|
||||
|
||||
# each level is sorted by the next dimension
|
||||
assert kd == color_kd._KD(
|
||||
Color(3, 103, 203),
|
||||
252,
|
||||
left=color_kd._KD(
|
||||
Color(1, 105, 201), 254,
|
||||
left=color_kd._KD(Color(2, 104, 202), 253, None, None),
|
||||
right=color_kd._KD(Color(0, 106, 200), 255, None, None),
|
||||
),
|
||||
right=color_kd._KD(
|
||||
Color(5, 101, 205), 250,
|
||||
left=color_kd._KD(Color(6, 100, 206), 249, None, None),
|
||||
right=color_kd._KD(Color(4, 102, 204), 251, None, None),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def test_nearest_trivial():
|
||||
assert color_kd.nearest(Color(0, 0, 0), None) == 0
|
||||
|
||||
|
||||
def test_nearest_one_node():
|
||||
kd = color_kd._build([(Color(100, 100, 100), 99)])
|
||||
assert color_kd.nearest(Color(0, 0, 0), kd) == 99
|
||||
|
||||
|
||||
def test_nearest_on_square_distance():
|
||||
kd = color_kd._build([
|
||||
(Color(50, 50, 50), 255),
|
||||
(Color(50, 51, 50), 254),
|
||||
])
|
||||
assert color_kd.nearest(Color(0, 0, 0), kd) == 255
|
||||
assert color_kd.nearest(Color(52, 52, 52), kd) == 254
|
||||
|
||||
|
||||
def test_smoke_kd_256():
|
||||
kd_256 = color_kd.make_256()
|
||||
assert color_kd.nearest(Color(0, 0, 0), kd_256) == 16
|
||||
assert color_kd.nearest(Color(0x1e, 0x77, 0xd3), kd_256) == 32
|
||||
16
tests/color_manager_test.py
Normal file
16
tests/color_manager_test.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import pytest
|
||||
|
||||
from babi.color import Color
|
||||
from babi.color_manager import _color_to_curses
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('color', 'expected'),
|
||||
(
|
||||
(Color(0x00, 0x00, 0x00), (0, 0, 0)),
|
||||
(Color(0xff, 0xff, 0xff), (1000, 1000, 1000)),
|
||||
(Color(0x1e, 0x77, 0xd3), (117, 466, 827)),
|
||||
),
|
||||
)
|
||||
def test_color_to_curses(color, expected):
|
||||
assert _color_to_curses(color) == expected
|
||||
7
tests/fdict_test.py
Normal file
7
tests/fdict_test.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from babi.fdict import FDict
|
||||
|
||||
|
||||
def test_fdict_repr():
|
||||
# mostly because this shouldn't get hit elsewhere but is uesful for
|
||||
# debugging purposes
|
||||
assert repr(FDict({1: 2, 3: 4})) == 'FDict({1: 2, 3: 4})'
|
||||
@@ -23,6 +23,13 @@ def xdg_data_home(tmpdir):
|
||||
yield data_home
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def xdg_config_home(tmpdir):
|
||||
config_home = tmpdir.join('config_home')
|
||||
with mock.patch.dict(os.environ, {'XDG_CONFIG_HOME': str(config_home)}):
|
||||
yield config_home
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ten_lines(tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
@@ -175,6 +182,10 @@ class CursesScreen:
|
||||
attr = attr & ~(0xff << 8)
|
||||
return (fg, bg, attr)
|
||||
|
||||
def bkgd(self, c, attr):
|
||||
assert c == ' '
|
||||
self._bkgd_attr = self._to_attr(attr)
|
||||
|
||||
def keypad(self, val):
|
||||
pass
|
||||
|
||||
@@ -368,6 +379,9 @@ class DeferredRunner:
|
||||
def _curses_start_color(self):
|
||||
curses.COLORS = self._n_colors
|
||||
|
||||
def _curses_can_change_color(self):
|
||||
return self._can_change_color
|
||||
|
||||
def _curses_init_pair(self, pair, fg, bg):
|
||||
self.color_pairs[pair] = (fg, bg)
|
||||
|
||||
|
||||
80
tests/features/syntax_highlight_test.py
Normal file
80
tests/features/syntax_highlight_test.py
Normal file
@@ -0,0 +1,80 @@
|
||||
import curses
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from testing.runner import and_exit
|
||||
|
||||
|
||||
THEME = json.dumps({
|
||||
'colors': {'background': '#00d700', 'foreground': '#303030'},
|
||||
'tokenColors': [
|
||||
{'scope': 'comment', 'settings': {'foreground': '#767676'}},
|
||||
{
|
||||
'scope': 'diffremove',
|
||||
'settings': {'foreground': '#5f0000', 'background': '#ff5f5f'},
|
||||
},
|
||||
{'scope': 'tqs', 'settings': {'foreground': '#00005f'}},
|
||||
{'scope': 'b', 'settings': {'fontStyle': 'bold'}},
|
||||
{'scope': 'i', 'settings': {'fontStyle': 'italic'}},
|
||||
{'scope': 'u', 'settings': {'fontStyle': 'underline'}},
|
||||
],
|
||||
})
|
||||
SYNTAX = json.dumps({
|
||||
'scopeName': 'source.demo',
|
||||
'fileTypes': ['demo'],
|
||||
'firstLineMatch': '^#!/usr/bin/(env demo|demo)$',
|
||||
'patterns': [
|
||||
{'match': r'#.*$\n?', 'name': 'comment'},
|
||||
{'match': r'^-.*$\n?', 'name': 'diffremove'},
|
||||
{'begin': '"""', 'end': '"""', 'name': 'tqs'},
|
||||
],
|
||||
})
|
||||
DEMO_S = '''\
|
||||
- foo
|
||||
# comment here
|
||||
uncolored
|
||||
"""tqs!
|
||||
still more
|
||||
"""
|
||||
'''
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def theme_and_grammar(xdg_data_home, xdg_config_home):
|
||||
xdg_config_home.join('babi/theme.json').ensure().write(THEME)
|
||||
xdg_data_home.join('babi/textmate_syntax/demo.json').ensure().write(SYNTAX)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def demo(tmpdir):
|
||||
f = tmpdir.join('f.demo')
|
||||
f.write(DEMO_S)
|
||||
yield f
|
||||
|
||||
|
||||
def test_syntax_highlighting(run, demo):
|
||||
with run(str(demo), term='screen-256color', width=20) as h, and_exit(h):
|
||||
h.await_text('still more')
|
||||
for i, attr in enumerate([
|
||||
[(236, 40, curses.A_REVERSE)] * 20, # header
|
||||
[(52, 203, 0)] * 5 + [(236, 40, 0)] * 15, # - foo
|
||||
[(243, 40, 0)] * 14 + [(236, 40, 0)] * 6, # # comment here
|
||||
[(236, 40, 0)] * 20, # uncolored
|
||||
[(17, 40, 0)] * 7 + [(236, 40, 0)] * 13, # """tqs!
|
||||
[(17, 40, 0)] * 10 + [(236, 40, 0)] * 10, # still more
|
||||
[(17, 40, 0)] * 3 + [(236, 40, 0)] * 17, # """
|
||||
]):
|
||||
h.assert_screen_attr_equals(i, attr)
|
||||
|
||||
|
||||
def test_syntax_highlighting_does_not_highlight_arrows(run, tmpdir):
|
||||
f = tmpdir.join('f')
|
||||
f.write(
|
||||
f'#!/usr/bin/env demo\n'
|
||||
f'# l{"o" * 15}ng comment\n',
|
||||
)
|
||||
|
||||
with run(str(f), term='screen-256color', width=20) as h, and_exit(h):
|
||||
h.await_text('loooo')
|
||||
h.assert_screen_attr_equals(2, [(243, 40, 0)] * 19 + [(236, 40, 0)])
|
||||
530
tests/highlight_test.py
Normal file
530
tests/highlight_test.py
Normal file
@@ -0,0 +1,530 @@
|
||||
from babi.highlight import Grammar
|
||||
from babi.highlight import Grammars
|
||||
from babi.highlight import highlight_line
|
||||
from babi.highlight import Region
|
||||
|
||||
|
||||
def _compiler_state(grammar_dct, *others):
|
||||
grammar = Grammar.from_data(grammar_dct)
|
||||
grammars = [grammar, *(Grammar.from_data(dct) for dct in others)]
|
||||
compiler = Grammars(grammars).compiler_for_scope(grammar.scope_name)
|
||||
return compiler, compiler.root_state
|
||||
|
||||
|
||||
def test_backslash_a():
|
||||
grammar = {
|
||||
'scopeName': 'test',
|
||||
'patterns': [{'name': 'aaa', 'match': r'\Aa+'}],
|
||||
}
|
||||
compiler, state = _compiler_state(grammar)
|
||||
|
||||
state, (region_0,) = highlight_line(compiler, state, 'aaa', True)
|
||||
state, (region_1,) = highlight_line(compiler, state, 'aaa', False)
|
||||
|
||||
# \A should only match at the beginning of the file
|
||||
assert region_0 == Region(0, 3, ('test', 'aaa'))
|
||||
assert region_1 == Region(0, 3, ('test',))
|
||||
|
||||
|
||||
BEGIN_END_NO_NL = {
|
||||
'scopeName': 'test',
|
||||
'patterns': [{
|
||||
'begin': 'x',
|
||||
'end': 'x',
|
||||
'patterns': [
|
||||
{'match': r'\Ga', 'name': 'ga'},
|
||||
{'match': 'a', 'name': 'noga'},
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
def test_backslash_g_inline():
|
||||
compiler, state = _compiler_state(BEGIN_END_NO_NL)
|
||||
|
||||
_, regions = highlight_line(compiler, state, 'xaax', True)
|
||||
assert regions == (
|
||||
Region(0, 1, ('test',)),
|
||||
Region(1, 2, ('test', 'ga')),
|
||||
Region(2, 3, ('test', 'noga')),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_backslash_g_next_line():
|
||||
compiler, state = _compiler_state(BEGIN_END_NO_NL)
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, 'x\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, 'aax\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 1, ('test',)),
|
||||
Region(1, 2, ('test',)),
|
||||
)
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'noga')),
|
||||
Region(1, 2, ('test', 'noga')),
|
||||
Region(2, 3, ('test',)),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_end_before_other_match():
|
||||
compiler, state = _compiler_state(BEGIN_END_NO_NL)
|
||||
|
||||
state, regions = highlight_line(compiler, state, 'xazzx', True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 1, ('test',)),
|
||||
Region(1, 2, ('test', 'ga')),
|
||||
Region(2, 4, ('test',)),
|
||||
Region(4, 5, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
BEGIN_END_NL = {
|
||||
'scopeName': 'test',
|
||||
'patterns': [{
|
||||
'begin': r'x$\n?',
|
||||
'end': 'x',
|
||||
'patterns': [
|
||||
{'match': r'\Ga', 'name': 'ga'},
|
||||
{'match': 'a', 'name': 'noga'},
|
||||
],
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
def test_backslash_g_captures_nl():
|
||||
compiler, state = _compiler_state(BEGIN_END_NL)
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, 'x\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, 'aax\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 2, ('test',)),
|
||||
)
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'ga')),
|
||||
Region(1, 2, ('test', 'noga')),
|
||||
Region(2, 3, ('test',)),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_backslash_g_captures_nl_next_line():
|
||||
compiler, state = _compiler_state(BEGIN_END_NL)
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, 'x\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, 'aa\n', False)
|
||||
state, regions3 = highlight_line(compiler, state, 'aax\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 2, ('test',)),
|
||||
)
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'ga')),
|
||||
Region(1, 2, ('test', 'noga')),
|
||||
Region(2, 3, ('test',)),
|
||||
)
|
||||
assert regions3 == (
|
||||
Region(0, 1, ('test', 'ga')),
|
||||
Region(1, 2, ('test', 'noga')),
|
||||
Region(2, 3, ('test',)),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_while_no_nl():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [{
|
||||
'begin': '> ',
|
||||
'while': '> ',
|
||||
'contentName': 'while',
|
||||
'patterns': [
|
||||
{'match': r'\Ga', 'name': 'ga'},
|
||||
{'match': 'a', 'name': 'noga'},
|
||||
],
|
||||
}],
|
||||
})
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, '> aa\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, '> aa\n', False)
|
||||
state, regions3 = highlight_line(compiler, state, 'after\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 2, ('test',)),
|
||||
Region(2, 3, ('test', 'while', 'ga')),
|
||||
Region(3, 4, ('test', 'while', 'noga')),
|
||||
Region(4, 5, ('test', 'while')),
|
||||
)
|
||||
assert regions2 == (
|
||||
Region(0, 2, ('test', 'while')),
|
||||
Region(2, 3, ('test', 'while', 'ga')),
|
||||
Region(3, 4, ('test', 'while', 'noga')),
|
||||
Region(4, 5, ('test', 'while')),
|
||||
)
|
||||
assert regions3 == (
|
||||
Region(0, 6, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_complex_captures():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'match': '(<).([^>]+)(>)',
|
||||
'captures': {
|
||||
'1': {'name': 'lbracket'},
|
||||
'2': {
|
||||
'patterns': [
|
||||
{'match': 'a', 'name': 'a'},
|
||||
{'match': 'z', 'name': 'z'},
|
||||
],
|
||||
},
|
||||
'3': {'name': 'rbracket'},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, '<qabz>', first_line=True)
|
||||
assert regions == (
|
||||
Region(0, 1, ('test', 'lbracket')),
|
||||
Region(1, 2, ('test',)),
|
||||
Region(2, 3, ('test', 'a')),
|
||||
Region(3, 4, ('test',)),
|
||||
Region(4, 5, ('test', 'z')),
|
||||
Region(5, 6, ('test', 'rbracket')),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_multiple_applied_to_same_capture():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'match': '((a)) ((b) c) (d (e)) ((f) )',
|
||||
'name': 'matched',
|
||||
'captures': {
|
||||
'1': {'name': 'g1'},
|
||||
'2': {'name': 'g2'},
|
||||
'3': {'name': 'g3'},
|
||||
'4': {'name': 'g4'},
|
||||
'5': {'name': 'g5'},
|
||||
'6': {'name': 'g6'},
|
||||
'7': {
|
||||
'patterns': [
|
||||
{'match': 'f', 'name': 'g7f'},
|
||||
{'match': ' ', 'name': 'g7space'},
|
||||
],
|
||||
},
|
||||
# this one has to backtrack some
|
||||
'8': {'name': 'g8'},
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, 'a b c d e f ', True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 1, ('test', 'matched', 'g1', 'g2')),
|
||||
Region(1, 2, ('test', 'matched')),
|
||||
Region(2, 3, ('test', 'matched', 'g3', 'g4')),
|
||||
Region(3, 5, ('test', 'matched', 'g3')),
|
||||
Region(5, 6, ('test', 'matched')),
|
||||
Region(6, 8, ('test', 'matched', 'g5')),
|
||||
Region(8, 9, ('test', 'matched', 'g5', 'g6')),
|
||||
Region(9, 10, ('test', 'matched')),
|
||||
Region(10, 11, ('test', 'matched', 'g7f', 'g8')),
|
||||
Region(11, 12, ('test', 'matched', 'g7space')),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_ignores_empty():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [{
|
||||
'match': '(.*) hi',
|
||||
'captures': {'1': {'name': 'before'}},
|
||||
}],
|
||||
})
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, ' hi\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, 'o hi\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 3, ('test',)),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'before')),
|
||||
Region(1, 4, ('test',)),
|
||||
Region(4, 5, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_ignores_invalid_out_of_bounds():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [{'match': '.', 'captures': {'1': {'name': 'oob'}}}],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, 'x', first_line=True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 1, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_begin_end():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '(""")',
|
||||
'end': '(""")',
|
||||
'beginCaptures': {'1': {'name': 'startquote'}},
|
||||
'endCaptures': {'1': {'name': 'endquote'}},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, '"""x"""', True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 3, ('test', 'startquote')),
|
||||
Region(3, 4, ('test',)),
|
||||
Region(4, 7, ('test', 'endquote')),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_while_captures():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '(>) ',
|
||||
'while': '(>) ',
|
||||
'beginCaptures': {'1': {'name': 'bblock'}},
|
||||
'whileCaptures': {'1': {'name': 'wblock'}},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, '> x\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, '> x\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 1, ('test', 'bblock')),
|
||||
Region(1, 2, ('test',)),
|
||||
Region(2, 4, ('test',)),
|
||||
)
|
||||
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'wblock')),
|
||||
Region(1, 2, ('test',)),
|
||||
Region(2, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_implies_begin_end_captures():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '(""")',
|
||||
'end': '(""")',
|
||||
'captures': {'1': {'name': 'quote'}},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, '"""x"""', True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 3, ('test', 'quote')),
|
||||
Region(3, 4, ('test',)),
|
||||
Region(4, 7, ('test', 'quote')),
|
||||
)
|
||||
|
||||
|
||||
def test_captures_implies_begin_while_captures():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '(>) ',
|
||||
'while': '(>) ',
|
||||
'captures': {'1': {'name': 'block'}},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, '> x\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, '> x\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 1, ('test', 'block')),
|
||||
Region(1, 2, ('test',)),
|
||||
Region(2, 4, ('test',)),
|
||||
)
|
||||
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'block')),
|
||||
Region(1, 2, ('test',)),
|
||||
Region(2, 4, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_include_self():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '<',
|
||||
'end': '>',
|
||||
'contentName': 'bracketed',
|
||||
'patterns': [{'include': '$self'}],
|
||||
},
|
||||
{'match': '.', 'name': 'content'},
|
||||
],
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, '<<_>>', first_line=True)
|
||||
assert regions == (
|
||||
Region(0, 1, ('test',)),
|
||||
Region(1, 2, ('test', 'bracketed')),
|
||||
Region(2, 3, ('test', 'bracketed', 'bracketed', 'content')),
|
||||
Region(3, 4, ('test', 'bracketed', 'bracketed')),
|
||||
Region(4, 5, ('test', 'bracketed')),
|
||||
)
|
||||
|
||||
|
||||
def test_include_repository_rule():
|
||||
compiler, state = _compiler_state({
|
||||
'scopeName': 'test',
|
||||
'patterns': [{'include': '#impl'}],
|
||||
'repository': {
|
||||
'impl': {
|
||||
'patterns': [
|
||||
{'match': 'a', 'name': 'a'},
|
||||
{'match': '.', 'name': 'other'},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
state, regions = highlight_line(compiler, state, 'az', first_line=True)
|
||||
|
||||
assert regions == (
|
||||
Region(0, 1, ('test', 'a')),
|
||||
Region(1, 2, ('test', 'other')),
|
||||
)
|
||||
|
||||
|
||||
def test_include_other_grammar():
|
||||
compiler, state = _compiler_state(
|
||||
{
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '<',
|
||||
'end': '>',
|
||||
'name': 'angle',
|
||||
'patterns': [{'include': 'other.grammar'}],
|
||||
},
|
||||
{
|
||||
'begin': '`',
|
||||
'end': '`',
|
||||
'name': 'tick',
|
||||
'patterns': [{'include': 'other.grammar#backtick'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'scopeName': 'other.grammar',
|
||||
'patterns': [
|
||||
{'match': 'a', 'name': 'roota'},
|
||||
{'match': '.', 'name': 'rootother'},
|
||||
],
|
||||
'repository': {
|
||||
'backtick': {
|
||||
'patterns': [
|
||||
{'match': 'a', 'name': 'ticka'},
|
||||
{'match': '.', 'name': 'tickother'},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, '<az>\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, '`az`\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 1, ('test', 'angle')),
|
||||
Region(1, 2, ('test', 'angle', 'roota')),
|
||||
Region(2, 3, ('test', 'angle', 'rootother')),
|
||||
Region(3, 4, ('test', 'angle')),
|
||||
Region(4, 5, ('test',)),
|
||||
)
|
||||
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'tick')),
|
||||
Region(1, 2, ('test', 'tick', 'ticka')),
|
||||
Region(2, 3, ('test', 'tick', 'tickother')),
|
||||
Region(3, 4, ('test', 'tick')),
|
||||
Region(4, 5, ('test',)),
|
||||
)
|
||||
|
||||
|
||||
def test_include_base():
|
||||
compiler, state = _compiler_state(
|
||||
{
|
||||
'scopeName': 'test',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '<',
|
||||
'end': '>',
|
||||
'name': 'bracket',
|
||||
# $base from root grammar includes itself
|
||||
'patterns': [{'include': '$base'}],
|
||||
},
|
||||
{'include': 'other.grammar'},
|
||||
{'match': 'z', 'name': 'testz'},
|
||||
],
|
||||
},
|
||||
{
|
||||
'scopeName': 'other.grammar',
|
||||
'patterns': [
|
||||
{
|
||||
'begin': '`',
|
||||
'end': '`',
|
||||
'name': 'tick',
|
||||
# $base from included grammar includes the root
|
||||
'patterns': [{'include': '$base'}],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
state, regions1 = highlight_line(compiler, state, '<z>\n', True)
|
||||
state, regions2 = highlight_line(compiler, state, '`z`\n', False)
|
||||
|
||||
assert regions1 == (
|
||||
Region(0, 1, ('test', 'bracket')),
|
||||
Region(1, 2, ('test', 'bracket', 'testz')),
|
||||
Region(2, 3, ('test', 'bracket')),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
|
||||
assert regions2 == (
|
||||
Region(0, 1, ('test', 'tick')),
|
||||
Region(1, 2, ('test', 'tick', 'testz')),
|
||||
Region(2, 3, ('test', 'tick')),
|
||||
Region(3, 4, ('test',)),
|
||||
)
|
||||
0
tests/hl/__init__.py
Normal file
0
tests/hl/__init__.py
Normal file
147
tests/hl/syntax_test.py
Normal file
147
tests/hl/syntax_test.py
Normal file
@@ -0,0 +1,147 @@
|
||||
import contextlib
|
||||
import curses
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
|
||||
from babi.color_manager import ColorManager
|
||||
from babi.highlight import Grammar
|
||||
from babi.highlight import Grammars
|
||||
from babi.hl.syntax import Syntax
|
||||
from babi.theme import Color
|
||||
from babi.theme import Theme
|
||||
|
||||
|
||||
class FakeCurses:
|
||||
def __init__(self, *, n_colors, can_change_color):
|
||||
self._n_colors = n_colors
|
||||
self._can_change_color = can_change_color
|
||||
self.colors = {}
|
||||
self.pairs = {}
|
||||
|
||||
def _curses__can_change_color(self):
|
||||
return self._can_change_color
|
||||
|
||||
def _curses__init_color(self, n, r, g, b):
|
||||
self.colors[n] = (r, g, b)
|
||||
|
||||
def _curses__init_pair(self, n, fg, bg):
|
||||
self.pairs[n] = (fg, bg)
|
||||
|
||||
def _curses__color_pair(self, n):
|
||||
assert n == 0 or n in self.pairs
|
||||
return n << 8
|
||||
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def patch(cls, **kwargs):
|
||||
fake = cls(**kwargs)
|
||||
with mock.patch.object(curses, 'COLORS', fake._n_colors, create=True):
|
||||
with mock.patch.multiple(
|
||||
curses,
|
||||
can_change_color=fake._curses__can_change_color,
|
||||
color_pair=fake._curses__color_pair,
|
||||
init_color=fake._curses__init_color,
|
||||
init_pair=fake._curses__init_pair,
|
||||
):
|
||||
yield fake
|
||||
|
||||
|
||||
class FakeScreen:
|
||||
def __init__(self):
|
||||
self.attr = 0
|
||||
|
||||
def bkgd(self, c, attr):
|
||||
assert c == ' '
|
||||
self.attr = attr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def stdscr():
|
||||
return FakeScreen()
|
||||
|
||||
|
||||
THEME = Theme.from_dct({
|
||||
'colors': {'foreground': '#cccccc', 'background': '#333333'},
|
||||
'tokenColors': [
|
||||
{'scope': 'string', 'settings': {'foreground': '#009900'}},
|
||||
{'scope': 'keyword', 'settings': {'background': '#000000'}},
|
||||
{'scope': 'keyword', 'settings': {'fontStyle': 'bold'}},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def syntax():
|
||||
return Syntax(Grammars([Grammar.blank()]), THEME, ColorManager.make())
|
||||
|
||||
|
||||
def test_init_screen_low_color(stdscr, syntax):
|
||||
with FakeCurses.patch(n_colors=16, can_change_color=False) as fake_curses:
|
||||
syntax._init_screen(stdscr)
|
||||
assert syntax.color_manager.colors == {}
|
||||
assert syntax.color_manager.raw_pairs == {}
|
||||
assert fake_curses.colors == {}
|
||||
assert fake_curses.pairs == {}
|
||||
assert stdscr.attr == 0
|
||||
|
||||
|
||||
def test_init_screen_256_color(stdscr, syntax):
|
||||
with FakeCurses.patch(n_colors=256, can_change_color=False) as fake_curses:
|
||||
syntax._init_screen(stdscr)
|
||||
assert syntax.color_manager.colors == {
|
||||
Color.parse('#cccccc'): 252,
|
||||
Color.parse('#333333'): 236,
|
||||
Color.parse('#000000'): 16,
|
||||
Color.parse('#009900'): 28,
|
||||
}
|
||||
assert syntax.color_manager.raw_pairs == {(252, 236): 1}
|
||||
assert fake_curses.colors == {}
|
||||
assert fake_curses.pairs == {1: (252, 236)}
|
||||
assert stdscr.attr == 1 << 8
|
||||
|
||||
|
||||
def test_init_screen_true_color(stdscr, syntax):
|
||||
with FakeCurses.patch(n_colors=256, can_change_color=True) as fake_curses:
|
||||
syntax._init_screen(stdscr)
|
||||
# weird colors happened with low color numbers so it counts down from max
|
||||
assert syntax.color_manager.colors == {
|
||||
Color.parse('#000000'): 255,
|
||||
Color.parse('#009900'): 254,
|
||||
Color.parse('#333333'): 253,
|
||||
Color.parse('#cccccc'): 252,
|
||||
}
|
||||
assert syntax.color_manager.raw_pairs == {(252, 253): 1}
|
||||
assert fake_curses.colors == {
|
||||
255: (0, 0, 0),
|
||||
254: (0, 600, 0),
|
||||
253: (200, 200, 200),
|
||||
252: (800, 800, 800),
|
||||
}
|
||||
assert fake_curses.pairs == {1: (252, 253)}
|
||||
assert stdscr.attr == 1 << 8
|
||||
|
||||
|
||||
def test_lazily_instantiated_pairs(stdscr, syntax):
|
||||
# pairs are assigned lazily to avoid hard upper limit (256) on pairs
|
||||
with FakeCurses.patch(n_colors=256, can_change_color=False) as fake_curses:
|
||||
syntax._init_screen(stdscr)
|
||||
|
||||
assert len(syntax.color_manager.raw_pairs) == 1
|
||||
assert len(fake_curses.pairs) == 1
|
||||
|
||||
style = THEME.select(('string.python',))
|
||||
attr = syntax.get_blank_file_highlighter().attr(style)
|
||||
assert attr == 2 << 8
|
||||
|
||||
assert len(syntax.color_manager.raw_pairs) == 2
|
||||
assert len(fake_curses.pairs) == 2
|
||||
|
||||
|
||||
def test_style_attributes_applied(stdscr, syntax):
|
||||
with FakeCurses.patch(n_colors=256, can_change_color=False):
|
||||
syntax._init_screen(stdscr)
|
||||
|
||||
style = THEME.select(('keyword.python',))
|
||||
attr = syntax.get_blank_file_highlighter().attr(style)
|
||||
assert attr == 2 << 8 | curses.A_BOLD
|
||||
74
tests/reg_test.py
Normal file
74
tests/reg_test.py
Normal file
@@ -0,0 +1,74 @@
|
||||
import onigurumacffi
|
||||
import pytest
|
||||
|
||||
from babi.reg import _Reg
|
||||
from babi.reg import _RegSet
|
||||
|
||||
|
||||
def test_reg_first_line():
|
||||
reg = _Reg(r'\Ahello')
|
||||
assert reg.match('hello', 0, first_line=True, boundary=True)
|
||||
assert reg.search('hello', 0, first_line=True, boundary=True)
|
||||
assert not reg.match('hello', 0, first_line=False, boundary=True)
|
||||
assert not reg.search('hello', 0, first_line=False, boundary=True)
|
||||
|
||||
|
||||
def test_reg_boundary():
|
||||
reg = _Reg(r'\Ghello')
|
||||
assert reg.search('ohello', 1, first_line=True, boundary=True)
|
||||
assert reg.match('ohello', 1, first_line=True, boundary=True)
|
||||
assert not reg.search('ohello', 1, first_line=True, boundary=False)
|
||||
assert not reg.match('ohello', 1, first_line=True, boundary=False)
|
||||
|
||||
|
||||
def test_reg_neither():
|
||||
reg = _Reg(r'(\A|\G)hello')
|
||||
assert not reg.search('hello', 0, first_line=False, boundary=False)
|
||||
assert not reg.search('ohello', 1, first_line=False, boundary=False)
|
||||
|
||||
|
||||
def test_reg_other_escapes_left_untouched():
|
||||
reg = _Reg(r'(^|\A|\G)\w\s\w')
|
||||
assert reg.match('a b', 0, first_line=False, boundary=False)
|
||||
|
||||
|
||||
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)
|
||||
msg, = excinfo.value.args
|
||||
assert msg == 'end pattern at escape'
|
||||
|
||||
|
||||
def test_reg_repr():
|
||||
assert repr(_Reg(r'\A123')) == r"_Reg('\\A123')"
|
||||
|
||||
|
||||
def test_regset_first_line():
|
||||
regset = _RegSet(r'\Ahello', 'hello')
|
||||
idx, _ = regset.search('hello', 0, first_line=True, boundary=True)
|
||||
assert idx == 0
|
||||
idx, _ = regset.search('hello', 0, first_line=False, boundary=True)
|
||||
assert idx == 1
|
||||
|
||||
|
||||
def test_regset_boundary():
|
||||
regset = _RegSet(r'\Ghello', 'hello')
|
||||
idx, _ = regset.search('ohello', 1, first_line=True, boundary=True)
|
||||
assert idx == 0
|
||||
idx, _ = regset.search('ohello', 1, first_line=True, boundary=False)
|
||||
assert idx == 1
|
||||
|
||||
|
||||
def test_regset_neither():
|
||||
regset = _RegSet(r'\Ahello', r'\Ghello', 'hello')
|
||||
idx, _ = regset.search('hello', 0, first_line=False, boundary=False)
|
||||
assert idx == 2
|
||||
idx, _ = regset.search('ohello', 1, first_line=False, boundary=False)
|
||||
assert idx == 2
|
||||
|
||||
|
||||
def test_regset_repr():
|
||||
assert repr(_RegSet('ohai', r'\Aworld')) == r"_RegSet('ohai', '\\Aworld')"
|
||||
85
tests/theme_test.py
Normal file
85
tests/theme_test.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import pytest
|
||||
|
||||
from babi.color import Color
|
||||
from babi.theme import Theme
|
||||
|
||||
THEME = Theme.from_dct({
|
||||
'colors': {'foreground': '#100000', 'background': '#aaaaaa'},
|
||||
'tokenColors': [
|
||||
{'scope': 'foo.bar', 'settings': {'foreground': '#200000'}},
|
||||
{'scope': 'foo', 'settings': {'foreground': '#300000'}},
|
||||
{'scope': 'parent foo.bar', 'settings': {'foreground': '#400000'}},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
def unhex(color):
|
||||
return f'#{hex(color.r << 16 | color.g << 8 | color.b)[2:]}'
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
('scope', 'expected'),
|
||||
(
|
||||
pytest.param(('',), '#100000', id='trivial'),
|
||||
pytest.param(('unknown',), '#100000', id='unknown'),
|
||||
pytest.param(('foo.bar',), '#200000', id='exact match'),
|
||||
pytest.param(('foo.baz',), '#300000', id='prefix match'),
|
||||
pytest.param(('src.diff', 'foo.bar'), '#200000', id='nested scope'),
|
||||
pytest.param(
|
||||
('foo.bar', 'unrelated'), '#200000',
|
||||
id='nested scope not last one',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_select(scope, expected):
|
||||
ret = THEME.select(scope)
|
||||
assert unhex(ret.fg) == expected
|
||||
|
||||
|
||||
def test_theme_default_settings_from_no_scope():
|
||||
theme = Theme.from_dct({
|
||||
'tokenColors': [
|
||||
{'settings': {'foreground': '#cccccc', 'background': '#333333'}},
|
||||
],
|
||||
})
|
||||
assert theme.default.fg == Color.parse('#cccccc')
|
||||
assert theme.default.bg == Color.parse('#333333')
|
||||
|
||||
|
||||
def test_theme_default_settings_from_empty_string_scope():
|
||||
theme = Theme.from_dct({
|
||||
'tokenColors': [
|
||||
{
|
||||
'scope': '',
|
||||
'settings': {'foreground': '#cccccc', 'background': '#333333'},
|
||||
},
|
||||
],
|
||||
})
|
||||
assert theme.default.fg == Color.parse('#cccccc')
|
||||
assert theme.default.bg == Color.parse('#333333')
|
||||
|
||||
|
||||
def test_theme_scope_split_by_commas():
|
||||
theme = Theme.from_dct({
|
||||
'colors': {'foreground': '#cccccc', 'background': '#333333'},
|
||||
'tokenColors': [
|
||||
{'scope': 'a, b, c', 'settings': {'fontStyle': 'italic'}},
|
||||
],
|
||||
})
|
||||
assert theme.select(('d',)).i is False
|
||||
assert theme.select(('a',)).i is True
|
||||
assert theme.select(('b',)).i is True
|
||||
assert theme.select(('c',)).i is True
|
||||
|
||||
|
||||
def test_theme_scope_as_A_list():
|
||||
theme = Theme.from_dct({
|
||||
'colors': {'foreground': '#cccccc', 'background': '#333333'},
|
||||
'tokenColors': [
|
||||
{'scope': ['a', 'b', 'c'], 'settings': {'fontStyle': 'underline'}},
|
||||
],
|
||||
})
|
||||
assert theme.select(('d',)).u is False
|
||||
assert theme.select(('a',)).u is True
|
||||
assert theme.select(('b',)).u is True
|
||||
assert theme.select(('c',)).u is True
|
||||
20
tests/user_data_test.py
Normal file
20
tests/user_data_test.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
from unittest import mock
|
||||
|
||||
from babi.user_data import xdg_data
|
||||
|
||||
|
||||
def test_when_xdg_data_home_is_set():
|
||||
with mock.patch.dict(os.environ, {'XDG_DATA_HOME': '/foo'}):
|
||||
ret = xdg_data('history', 'command')
|
||||
assert ret == '/foo/babi/history/command'
|
||||
|
||||
|
||||
def test_when_xdg_data_home_is_not_set():
|
||||
def fake_expanduser(s):
|
||||
return s.replace('~', '/home/username')
|
||||
|
||||
with mock.patch.object(os.path, 'expanduser', fake_expanduser):
|
||||
with mock.patch.dict(os.environ, clear=True):
|
||||
ret = xdg_data('history')
|
||||
assert ret == '/home/username/.local/share/babi/history'
|
||||
Reference in New Issue
Block a user