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