Files
babi/babi/hl/syntax.py
2020-03-28 16:28:26 -07:00

166 lines
5.3 KiB
Python

import curses
import functools
import math
from typing import Callable
from typing import List
from typing import NamedTuple
from typing import Optional
from typing import Tuple
from babi.buf import Buf
from babi.color_manager import ColorManager
from babi.highlight import Compiler
from babi.highlight import Grammars
from babi.highlight import highlight_line
from babi.highlight import State
from babi.hl.interface import HL
from babi.hl.interface import HLs
from babi.theme import Style
from babi.theme import Theme
from babi.user_data import prefix_data
from babi.user_data import xdg_config
from babi.user_data import xdg_data
A_ITALIC = getattr(curses, 'A_ITALIC', 0x80000000) # new in py37
class FileSyntax:
include_edge = False
def __init__(
self,
compiler: Compiler,
theme: Theme,
color_manager: ColorManager,
) -> None:
self._compiler = compiler
self._theme = theme
self._color_manager = color_manager
self.regions: List[HLs] = []
self._states: List[State] = []
# this will be assigned a functools.lru_cache per instance for
# better hit rate and memory usage
self._hl: Optional[Callable[[State, str, bool], Tuple[State, HLs]]]
self._hl = None
def attr(self, style: Style) -> int:
pair = self._color_manager.color_pair(style.fg, style.bg)
return (
curses.color_pair(pair) |
curses.A_BOLD * style.b |
A_ITALIC * style.i |
curses.A_UNDERLINE * style.u
)
def _hl_uncached(
self,
state: State,
line: str,
first_line: bool,
) -> Tuple[State, HLs]:
new_state, regions = highlight_line(
self._compiler, state, f'{line}\n', first_line=first_line,
)
# remove the trailing newline
new_end = regions[-1]._replace(end=regions[-1].end - 1)
regions = regions[:-1] + (new_end,)
regs: List[HL] = []
for r in regions:
style = self._theme.select(r.scope)
if style == self._theme.default:
continue
attr = self.attr(style)
if (
regs and
regs[-1].attr == attr and
regs[-1].end == r.start
):
regs[-1] = regs[-1]._replace(end=r.end)
else:
regs.append(HL(x=r.start, end=r.end, attr=attr))
return new_state, tuple(regs)
def _set_cb(self, lines: Buf, idx: int, victim: str) -> None:
del self.regions[idx:]
del self._states[idx:]
def _del_cb(self, lines: Buf, idx: int, victim: str) -> None:
del self.regions[idx:]
del self._states[idx:]
def _ins_cb(self, lines: Buf, idx: int) -> None:
del self.regions[idx:]
del self._states[idx:]
def register_callbacks(self, buf: Buf) -> None:
buf.add_set_callback(self._set_cb)
buf.add_del_callback(self._del_cb)
buf.add_ins_callback(self._ins_cb)
def highlight_until(self, lines: Buf, idx: int) -> None:
if self._hl is None:
# the docs claim better performance with power of two sizing
size = max(4096, 2 ** (int(math.log(len(lines), 2)) + 2))
self._hl = functools.lru_cache(maxsize=size)(self._hl_uncached)
if not self._states:
state = self._compiler.root_state
else:
state = self._states[-1]
for i in range(len(self._states), idx):
# https://github.com/python/mypy/issues/8579
state, regions = self._hl(state, lines[i], i == 0) # type: ignore
self._states.append(state)
self.regions.append(regions)
class Syntax(NamedTuple):
grammars: Grammars
theme: Theme
color_manager: ColorManager
def file_highlighter(self, filename: str, first_line: str) -> FileSyntax:
compiler = self.grammars.compiler_for_file(filename, first_line)
return FileSyntax(compiler, self.theme, self.color_manager)
def blank_file_highlighter(self) -> FileSyntax:
compiler = self.grammars.blank_compiler()
return FileSyntax(compiler, self.theme, self.color_manager)
def _init_screen(self, stdscr: 'curses._CursesWindow') -> None:
default_fg, default_bg = self.theme.default.fg, self.theme.default.bg
all_colors = {c for c in (default_fg, default_bg) if c is not None}
todo = list(self.theme.rules.children.values())
while todo:
rule = todo.pop()
if rule.style.fg is not None:
all_colors.add(rule.style.fg)
if rule.style.bg is not None:
all_colors.add(rule.style.bg)
todo.extend(rule.children.values())
for color in sorted(all_colors):
self.color_manager.init_color(color)
pair = self.color_manager.color_pair(default_fg, default_bg)
stdscr.bkgd(' ', curses.color_pair(pair))
@classmethod
def from_screen(
cls,
stdscr: 'curses._CursesWindow',
color_manager: ColorManager,
) -> 'Syntax':
grammars = Grammars(prefix_data('grammar_v1'), xdg_data('grammar_v1'))
theme = Theme.from_filename(xdg_config('theme.json'))
ret = cls(grammars, theme, color_manager)
ret._init_screen(stdscr)
return ret