Syntax highlighting

This commit is contained in:
Anthony Sottile
2020-02-22 16:34:47 -08:00
parent 1d06a77d44
commit 697b012027
29 changed files with 2515 additions and 18 deletions

63
tests/color_kd_test.py Normal file
View 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

View 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
View 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})'

View File

@@ -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)

View 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
View 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
View File

147
tests/hl/syntax_test.py Normal file
View 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
View 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
View 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
View 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'