use std::borrow::Cow;
use std::fmt::{self, Debug, Formatter};
use std::io::{self, BufRead, BufReader};
use std::ops::Range;
use std::sync::Arc;
use ratatui_core::style::{Color, Style, Stylize};
use ratatui_core::text::{Line, Span, Text};
pub use syntect;
use syntect::easy::HighlightLines;
use syntect::highlighting::Theme;
use syntect::parsing::{SyntaxReference, SyntaxSet};
#[cfg(feature = "termprofile")]
use termprofile::TermProfile;
use crate::Converter;
type GutterFn = dyn Fn(usize, Style) -> Vec<Span<'static>> + Send + Sync;
#[derive(Clone)]
struct GutterTemplate(Arc<GutterFn>);
impl Debug for GutterTemplate {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("GutterTemplate(<fn>)")
}
}
#[derive(Clone, Debug)]
pub struct Highlighter {
theme: Theme,
override_background: Option<Color>,
line_number_style: Option<Style>,
line_number_separator_style: Option<Style>,
gutter_template: Option<GutterTemplate>,
line_numbers: bool,
line_number_padding: usize,
line_number_separator: String,
#[cfg(feature = "termprofile")]
profile: TermProfile,
highlight_ranges: Vec<Range<usize>>,
highlight_style: Style,
converter: Converter,
}
impl Highlighter {
pub fn new(theme: Theme) -> Self {
Self {
theme,
override_background: None,
line_number_style: None,
line_number_separator_style: None,
gutter_template: None,
line_numbers: true,
line_number_padding: 4,
line_number_separator: "│".to_string(),
#[cfg(feature = "termprofile")]
profile: TermProfile::TrueColor,
highlight_ranges: Vec::new(),
highlight_style: Style::new().bg(Color::Yellow),
converter: Converter::new(),
}
}
#[cfg(feature = "termprofile")]
pub fn with_profile(theme: Theme, profile: TermProfile) -> Self {
let mut this = Self::new(theme);
this.profile = profile;
this.converter = Converter::with_profile(profile);
this
}
pub fn override_background<C>(mut self, background: C) -> Self
where
C: Into<Color>,
{
let background = background.into();
self.override_background = Some(self.adapt_color(background).unwrap_or(Color::Reset));
self
}
pub fn line_numbers(mut self, line_numbers: bool) -> Self {
self.line_numbers = line_numbers;
self
}
pub fn line_number_padding(mut self, padding: usize) -> Self {
self.line_number_padding = padding;
self
}
pub fn line_number_style<S>(mut self, style: S) -> Self
where
S: Into<Style>,
{
self.line_number_style = Some(self.adapt_style(style.into()));
self
}
pub fn line_number_separator_style<S>(mut self, style: S) -> Self
where
S: Into<Style>,
{
self.line_number_separator_style = Some(self.adapt_style(style.into()));
self
}
pub fn line_number_separator<T>(mut self, separator: T) -> Self
where
T: Into<String>,
{
self.line_number_separator = separator.into();
self
}
pub fn highlight_range(mut self, range: Range<usize>) -> Self {
self.highlight_ranges.push(range);
self
}
pub fn highlight_style(mut self, style: Style) -> Self {
self.highlight_style = self.adapt_style(style);
self
}
pub fn gutter_template<F>(mut self, template: F) -> Self
where
F: Fn(usize, Style) -> Vec<Span<'static>> + Send + Sync + 'static,
{
self.gutter_template = Some(GutterTemplate(Arc::new(template)));
self
}
pub fn get_background_color(&self) -> Option<Color> {
if let Some(bg) = self.override_background {
Some(bg)
} else {
self.theme
.settings
.background
.and_then(|bg| self.converter.syntect_color_to_tui(bg))
}
}
pub fn get_line_number_style(&self) -> Style {
if let Some(style) = self.line_number_style {
return style;
}
let mut style = Style::new();
if let Some(fg) = self
.theme
.settings
.gutter_foreground
.and_then(|fg| self.converter.syntect_color_to_tui(fg))
{
style = style.fg(fg);
} else {
style = style.dark_gray();
}
if let Some(bg) = self.get_background_color() {
style = style.bg(bg);
}
self.adapt_style(style)
}
pub fn highlight_reader<R>(
&self,
reader: R,
syntax: &SyntaxReference,
syntaxes: &SyntaxSet,
) -> Result<Text<'static>, crate::Error>
where
R: io::Read,
{
let mut reader = BufReader::new(reader);
let mut highlighter = HighlightLines::new(syntax, &self.theme);
let line_number_style = self.get_line_number_style();
let mut line = String::new();
let mut formatted = Vec::new();
let mut i = 0;
while reader.read_line(&mut line).map_err(crate::Error::Read)? > 0 {
let highlighted =
self.highlight_line(&line, &mut highlighter, i, line_number_style, syntaxes)?;
formatted.push(highlighted);
line.clear();
i += 1;
}
Ok(Text::from_iter(formatted))
}
pub fn highlight_lines<'a, T>(
&self,
source: T,
syntax: &SyntaxReference,
syntaxes: &SyntaxSet,
) -> Result<Text<'static>, crate::Error>
where
T: IntoIterator<Item = &'a str>,
{
let mut highlighter = HighlightLines::new(syntax, &self.theme);
let line_number_style = self.get_line_number_style();
let formatted: Result<Vec<_>, crate::Error> = source
.into_iter()
.enumerate()
.map(|(i, line)| {
self.highlight_line(line, &mut highlighter, i, line_number_style, syntaxes)
})
.collect();
let formatted = formatted?;
Ok(Text::from_iter(formatted))
}
pub fn highlight_line(
&self,
line: &str,
highlighter: &mut HighlightLines,
line_number: usize,
line_number_style: Style,
syntaxes: &SyntaxSet,
) -> Result<Line<'static>, crate::Error> {
let line: Cow<_> = if line.ends_with("\n") {
line.into()
} else {
(line.to_string() + "\n").into()
};
let regions = highlighter
.highlight_line(&line, syntaxes)
.map_err(crate::Error::Highlight)?;
Ok(self.to_line(®ions, line_number, line_number_style))
}
fn get_initial_spans(
&self,
line_number: usize,
line_number_style: Style,
) -> Vec<Span<'static>> {
let line_number = line_number + 1;
if let Some(template) = &self.gutter_template {
return template.0(line_number, line_number_style);
}
if self.line_numbers {
let line_number = line_number.to_string();
let spaces = self
.line_number_padding
.saturating_sub(line_number.len())
.saturating_sub(2);
vec![
Span::styled(" ".repeat(spaces), line_number_style),
Span::styled(line_number, line_number_style),
Span::styled(" ", line_number_style),
Span::styled(
self.line_number_separator.clone(),
self.line_number_separator_style
.unwrap_or(line_number_style),
),
Span::styled(" ", line_number_style),
]
} else {
vec![]
}
}
fn to_line(
&self,
v: &[(syntect::highlighting::Style, &str)],
line_number: usize,
line_number_style: Style,
) -> Line<'static> {
let mut spans = self.get_initial_spans(line_number, line_number_style);
let highlight_row = self
.highlight_ranges
.iter()
.any(|r| r.contains(&line_number));
for &(ref style, mut text) in v {
let ends_with_newline = text.ends_with('\n');
if ends_with_newline {
text = &text[..text.len() - 1];
}
let mut tui_style = self.syntect_style_to_tui(*style);
if highlight_row {
tui_style = tui_style.patch(self.highlight_style);
}
spans.push(Span::styled(text.to_string(), tui_style));
}
let mut line = Line::from_iter(spans);
if highlight_row {
line = line.patch_style(self.highlight_style);
}
self.apply_background(line)
}
fn adapt_style(&self, style: Style) -> Style {
#[cfg(feature = "termprofile")]
return self.profile.adapt_style(style);
#[cfg(not(feature = "termprofile"))]
return style;
}
fn adapt_color(&self, color: Color) -> Option<Color> {
#[cfg(feature = "termprofile")]
return self.profile.adapt_color(color);
#[cfg(not(feature = "termprofile"))]
return Some(color);
}
fn apply_background<'a, S>(&self, item: S) -> S
where
S: Stylize<'a, S>,
{
if let Some(bg) = self.override_background {
return item.bg(bg);
};
if let Some(bg) = self
.theme
.settings
.background
.and_then(|bg| self.converter.syntect_color_to_tui(bg))
{
return item.bg(bg);
}
item
}
fn syntect_style_to_tui(
&self,
style: syntect::highlighting::Style,
) -> ratatui_core::style::Style {
let mut tui_style = self.converter.syntect_style_to_tui(style);
if let Some(bg) = self.override_background {
tui_style = tui_style.bg(bg);
}
tui_style
}
}