use ratatui::style::Color;
use syntect::highlighting::{HighlightIterator, HighlightState, Highlighter, Theme, ThemeSet};
use syntect::parsing::{ParseState, ScopeStack, SyntaxDefinition, SyntaxReference, SyntaxSet};
pub type HighlightPair = (Vec<Vec<Color>>, Vec<Vec<Color>>);
pub struct SyntaxHighlighter {
syntax_set: SyntaxSet,
theme: Theme,
}
pub struct HighlightCache {
pub file_path: String,
pub left_colors: Vec<Vec<Color>>,
pub right_colors: Vec<Vec<Color>>,
processed_up_to: usize,
incremental: Option<IncrementalState>,
}
struct IncrementalState {
left_parse_state: ParseState,
left_highlight_state: HighlightState,
right_parse_state: ParseState,
right_highlight_state: HighlightState,
left_lines: Vec<String>,
right_lines: Vec<String>,
hunk_starts: Vec<usize>,
}
impl HighlightCache {
pub fn from_precomputed(
file_path: String,
left_colors: Vec<Vec<Color>>,
right_colors: Vec<Vec<Color>>,
) -> Self {
let processed = left_colors.len();
Self {
file_path,
left_colors,
right_colors,
processed_up_to: processed,
incremental: None,
}
}
}
const TOML_SYNTAX: &str = r#"%YAML 1.2
---
name: TOML
file_extensions: [toml]
scope: source.toml
contexts:
main:
- match: '#.*$'
scope: comment.line.number-sign.toml
- match: '\[{1,2}'
scope: punctuation.definition.table.begin.toml
push: table_name
- match: '([A-Za-z0-9_.-]+)\s*(=)'
captures:
1: entity.name.tag.toml
2: punctuation.separator.key-value.toml
- match: '"""'
scope: punctuation.definition.string.begin.toml
push: triple_double_string
- match: "'''"
scope: punctuation.definition.string.begin.toml
push: triple_single_string
- match: '"'
scope: punctuation.definition.string.begin.toml
push: double_string
- match: "'"
scope: punctuation.definition.string.begin.toml
push: single_string
- match: '\b(true|false)\b'
scope: constant.language.boolean.toml
- match: '\b\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?\b'
scope: constant.other.datetime.toml
- match: '[+-]?\b\d[\d_]*(\.[\d_]+)?([eE][+-]?\d+)?\b'
scope: constant.numeric.toml
table_name:
- match: '\]{1,2}'
scope: punctuation.definition.table.end.toml
pop: true
- match: '[^\]]+'
scope: entity.name.section.toml
double_string:
- meta_scope: string.quoted.double.toml
- match: '\\.'
scope: constant.character.escape.toml
- match: '"'
scope: punctuation.definition.string.end.toml
pop: true
single_string:
- meta_scope: string.quoted.single.toml
- match: "'"
scope: punctuation.definition.string.end.toml
pop: true
triple_double_string:
- meta_scope: string.quoted.triple.double.toml
- match: '\\.'
scope: constant.character.escape.toml
- match: '"""'
scope: punctuation.definition.string.end.toml
pop: true
triple_single_string:
- meta_scope: string.quoted.triple.single.toml
- match: "'''"
scope: punctuation.definition.string.end.toml
pop: true
"#;
pub const DEFAULT_THEME: &str = "base16-eighties.dark";
pub fn theme_names() -> Vec<String> {
let mut names: Vec<String> = ThemeSet::load_defaults().themes.into_keys().collect();
names.sort();
names
}
impl Default for SyntaxHighlighter {
fn default() -> Self {
Self::new()
}
}
impl SyntaxHighlighter {
pub fn new() -> Self {
Self::with_theme(DEFAULT_THEME).expect("default theme is bundled with syntect")
}
pub fn with_theme(theme_name: &str) -> Option<Self> {
let theme = ThemeSet::load_defaults().themes.remove(theme_name)?;
let mut builder = SyntaxSet::load_defaults_newlines().into_builder();
if let Ok(toml_def) = SyntaxDefinition::load_from_str(TOML_SYNTAX, true, None) {
builder.add(toml_def);
}
let syntax_set = builder.build();
Some(Self { syntax_set, theme })
}
fn find_syntax(&self, path: &str, first_line: Option<&str>) -> Option<&SyntaxReference> {
if let Some(ext) = path.rsplit('.').next() {
if let Some(syn) = self.syntax_set.find_syntax_by_extension(ext) {
return Some(syn);
}
}
if let Some(line) = first_line {
let syn = self.syntax_set.find_syntax_by_first_line(line)?;
if syn.name != "Plain Text" {
return Some(syn);
}
}
None
}
pub fn create_cache(
&self,
file_path: &str,
left_lines: Vec<String>,
right_lines: Vec<String>,
hunk_starts: Vec<usize>,
) -> Option<HighlightCache> {
let first_content = left_lines
.iter()
.enumerate()
.find(|(i, s)| !hunk_starts.contains(i) && !s.is_empty())
.map(|(_, s)| s.as_str());
let syntax = self.find_syntax(file_path, first_content)?;
let highlighter = Highlighter::new(&self.theme);
Some(HighlightCache {
file_path: file_path.to_string(),
left_colors: Vec::with_capacity(left_lines.len()),
right_colors: Vec::with_capacity(right_lines.len()),
processed_up_to: 0,
incremental: Some(IncrementalState {
left_parse_state: ParseState::new(syntax),
left_highlight_state: HighlightState::new(&highlighter, ScopeStack::new()),
right_parse_state: ParseState::new(syntax),
right_highlight_state: HighlightState::new(&highlighter, ScopeStack::new()),
left_lines,
right_lines,
hunk_starts,
}),
})
}
pub fn extend_cache(&self, cache: &mut HighlightCache, up_to: usize) {
let inc = match &mut cache.incremental {
Some(inc) => inc,
None => return, };
let target = up_to.min(inc.left_lines.len());
if cache.processed_up_to >= target {
return;
}
let highlighter = Highlighter::new(&self.theme);
for i in cache.processed_up_to..target {
if inc.hunk_starts.contains(&i) {
if let Some(syntax) = self.find_syntax(&cache.file_path, None) {
inc.left_parse_state = ParseState::new(syntax);
inc.left_highlight_state = HighlightState::new(&highlighter, ScopeStack::new());
inc.right_parse_state = ParseState::new(syntax);
inc.right_highlight_state =
HighlightState::new(&highlighter, ScopeStack::new());
}
cache.left_colors.push(Vec::new());
cache.right_colors.push(Vec::new());
continue;
}
let left = highlight_line_colors(
&inc.left_lines[i],
&mut inc.left_parse_state,
&mut inc.left_highlight_state,
&self.syntax_set,
&highlighter,
);
cache.left_colors.push(left);
let right = highlight_line_colors(
&inc.right_lines[i],
&mut inc.right_parse_state,
&mut inc.right_highlight_state,
&self.syntax_set,
&highlighter,
);
cache.right_colors.push(right);
}
cache.processed_up_to = target;
}
pub fn highlight_all_lines(
&self,
file_path: &str,
left_lines: &[String],
right_lines: &[String],
hunk_starts: &[usize],
) -> Option<HighlightPair> {
let first_content = left_lines
.iter()
.enumerate()
.find(|(i, s)| !hunk_starts.contains(i) && !s.is_empty())
.map(|(_, s)| s.as_str());
let syntax = self.find_syntax(file_path, first_content)?;
let highlighter = Highlighter::new(&self.theme);
let mut left_parse = ParseState::new(syntax);
let mut left_hl = HighlightState::new(&highlighter, ScopeStack::new());
let mut right_parse = ParseState::new(syntax);
let mut right_hl = HighlightState::new(&highlighter, ScopeStack::new());
let mut left_colors = Vec::with_capacity(left_lines.len());
let mut right_colors = Vec::with_capacity(right_lines.len());
for (i, (l, r)) in left_lines.iter().zip(right_lines.iter()).enumerate() {
if hunk_starts.contains(&i) {
left_parse = ParseState::new(syntax);
left_hl = HighlightState::new(&highlighter, ScopeStack::new());
right_parse = ParseState::new(syntax);
right_hl = HighlightState::new(&highlighter, ScopeStack::new());
left_colors.push(Vec::new());
right_colors.push(Vec::new());
continue;
}
left_colors.push(highlight_line_colors(
l,
&mut left_parse,
&mut left_hl,
&self.syntax_set,
&highlighter,
));
right_colors.push(highlight_line_colors(
r,
&mut right_parse,
&mut right_hl,
&self.syntax_set,
&highlighter,
));
}
Some((left_colors, right_colors))
}
}
fn highlight_line_colors(
line: &str,
parse_state: &mut ParseState,
highlight_state: &mut HighlightState,
syntax_set: &SyntaxSet,
highlighter: &Highlighter,
) -> Vec<Color> {
let line_with_nl = format!("{}\n", line);
let ops = match parse_state.parse_line(&line_with_nl, syntax_set) {
Ok(ops) => ops,
Err(_) => return Vec::new(),
};
let mut colors = Vec::new();
for (style, text) in HighlightIterator::new(highlight_state, &ops, &line_with_nl, highlighter) {
let color = syntect_to_ratatui_color(style.foreground);
for _ in text.chars() {
colors.push(color);
}
}
colors.pop();
colors
}
fn syntect_to_ratatui_color(c: syntect::highlighting::Color) -> Color {
Color::Rgb(c.r, c.g, c.b)
}