166 lines
5.3 KiB
Python
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
|