use iced_core as core; use crate::core::text::highlighter::{self, Format}; use crate::core::{Color, Font}; use std::ops::Range; use syntect::highlighting; use syntect::parsing; pub struct Highlighter { syntaxes: parsing::SyntaxSet, syntax: parsing::SyntaxReference, theme: highlighting::Theme, caches: Vec<(parsing::ParseState, parsing::ScopeStack)>, current_line: usize, } const LINES_PER_SNAPSHOT: usize = 50; impl highlighter::Highlighter for Highlighter { type Settings = Settings; type Highlight = Highlight; type Iterator<'a> = Box, Self::Highlight)> + 'a>; fn new(settings: &Self::Settings) -> Self { let syntaxes = parsing::SyntaxSet::load_defaults_nonewlines(); let syntax = syntaxes .find_syntax_by_token(&settings.extension) .unwrap_or_else(|| syntaxes.find_syntax_plain_text()); let theme = highlighting::ThemeSet::load_defaults() .themes .remove(settings.theme.key()) .unwrap(); let parser = parsing::ParseState::new(syntax); let stack = parsing::ScopeStack::new(); Highlighter { syntax: syntax.clone(), syntaxes, theme, caches: vec![(parser, stack)], current_line: 0, } } fn update(&mut self, new_settings: &Self::Settings) { self.syntax = self .syntaxes .find_syntax_by_token(&new_settings.extension) .unwrap_or_else(|| self.syntaxes.find_syntax_plain_text()) .clone(); self.theme = highlighting::ThemeSet::load_defaults() .themes .remove(new_settings.theme.key()) .unwrap(); // Restart the highlighter self.change_line(0); } fn change_line(&mut self, line: usize) { let snapshot = line / LINES_PER_SNAPSHOT; if snapshot <= self.caches.len() { self.caches.truncate(snapshot); self.current_line = snapshot * LINES_PER_SNAPSHOT; } else { self.caches.truncate(1); self.current_line = 0; } let (parser, stack) = self.caches.last().cloned().unwrap_or_else(|| { ( parsing::ParseState::new(&self.syntax), parsing::ScopeStack::new(), ) }); self.caches.push((parser, stack)); } fn highlight_line(&mut self, line: &str) -> Self::Iterator<'_> { if self.current_line / LINES_PER_SNAPSHOT >= self.caches.len() { let (parser, stack) = self.caches.last().expect("Caches must not be empty"); self.caches.push((parser.clone(), stack.clone())); } self.current_line += 1; let (parser, stack) = self.caches.last_mut().expect("Caches must not be empty"); let ops = parser.parse_line(line, &self.syntaxes).unwrap_or_default(); let highlighter = highlighting::Highlighter::new(&self.theme); Box::new( ScopeRangeIterator { ops, line_length: line.len(), index: 0, last_str_index: 0, } .filter_map(move |(range, scope)| { let _ = stack.apply(&scope); if range.is_empty() { None } else { Some(( range, Highlight( highlighter.style_mod_for_stack(&stack.scopes), ), )) } }), ) } fn current_line(&self) -> usize { self.current_line } } #[derive(Debug, Clone, PartialEq)] pub struct Settings { pub theme: Theme, pub extension: String, } pub struct Highlight(highlighting::StyleModifier); impl Highlight { pub fn color(&self) -> Option { self.0.foreground.map(|color| { Color::from_rgba8(color.r, color.g, color.b, color.a as f32 / 255.0) }) } pub fn font(&self) -> Option { None } pub fn to_format(&self) -> Format { Format { color: self.color(), font: self.font(), } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Theme { SolarizedDark, InspiredGitHub, Base16Mocha, } impl Theme { pub const ALL: &[Self] = &[Self::SolarizedDark, Self::InspiredGitHub, Self::Base16Mocha]; fn key(&self) -> &'static str { match self { Theme::InspiredGitHub => "InspiredGitHub", Theme::Base16Mocha => "base16-mocha.dark", Theme::SolarizedDark => "Solarized (dark)", } } } impl std::fmt::Display for Theme { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Theme::InspiredGitHub => write!(f, "Inspired GitHub"), Theme::Base16Mocha => write!(f, "Mocha"), Theme::SolarizedDark => write!(f, "Solarized Dark"), } } } pub struct ScopeRangeIterator { ops: Vec<(usize, parsing::ScopeStackOp)>, line_length: usize, index: usize, last_str_index: usize, } impl Iterator for ScopeRangeIterator { type Item = (std::ops::Range, parsing::ScopeStackOp); fn next(&mut self) -> Option { if self.index > self.ops.len() { return None; } let next_str_i = if self.index == self.ops.len() { self.line_length } else { self.ops[self.index].0 }; let range = self.last_str_index..next_str_i; self.last_str_index = next_str_i; let op = if self.index == 0 { parsing::ScopeStackOp::Noop } else { self.ops[self.index - 1].1.clone() }; self.index += 1; Some((range, op)) } }