use crate::console::RenderContext;
use crate::panel::{BorderStyle, Panel};
use crate::renderable::{Renderable, Segment};
use crate::style::{Color, Style};
use crate::text::{Span, Text};
use std::sync::OnceLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
use syntect::parsing::SyntaxSet;
use syntect::util::LinesWithEndings;
static SYNTAX_SET: OnceLock<SyntaxSet> = OnceLock::new();
static THEME_SET: OnceLock<ThemeSet> = OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Theme {
#[default]
Monokai,
InspiredGitHub,
SolarizedDark,
SolarizedLight,
Base16OceanDark,
}
impl Theme {
fn name(&self) -> &'static str {
match self {
Theme::Monokai => "base16-monokai.dark",
Theme::InspiredGitHub => "InspiredGitHub",
Theme::SolarizedDark => "Solarized (dark)",
Theme::SolarizedLight => "Solarized (light)",
Theme::Base16OceanDark => "base16-ocean.dark",
}
}
}
#[derive(Debug, Clone)]
pub struct SyntaxConfig {
pub line_numbers: bool,
pub panel: bool,
pub border_style: BorderStyle,
pub theme: Theme,
pub start_line: usize,
pub highlight_lines: Vec<usize>,
}
impl Default for SyntaxConfig {
fn default() -> Self {
SyntaxConfig {
line_numbers: true,
panel: true,
border_style: BorderStyle::Rounded,
theme: Theme::Monokai,
start_line: 1,
highlight_lines: Vec::new(),
}
}
}
#[derive(Debug)]
pub struct Syntax {
code: String,
language: String,
config: SyntaxConfig,
}
impl Syntax {
pub fn new(code: &str, language: &str) -> Self {
Syntax {
code: code.to_string(),
language: language.to_string(),
config: SyntaxConfig::default(),
}
}
pub fn config(mut self, config: SyntaxConfig) -> Self {
self.config = config;
self
}
pub fn line_numbers(mut self, show: bool) -> Self {
self.config.line_numbers = show;
self
}
pub fn panel(mut self, panel: bool) -> Self {
self.config.panel = panel;
self
}
pub fn theme(mut self, theme: Theme) -> Self {
self.config.theme = theme;
self
}
pub fn highlight_lines(mut self, lines: Vec<usize>) -> Self {
self.config.highlight_lines = lines;
self
}
fn convert_style(syntect_style: SyntectStyle) -> Style {
let mut style = Style::new().foreground(Color::rgb(
syntect_style.foreground.r,
syntect_style.foreground.g,
syntect_style.foreground.b,
));
if syntect_style.background.a > 0 {
style = style.background(Color::rgb(
syntect_style.background.r,
syntect_style.background.g,
syntect_style.background.b,
));
}
if syntect_style
.font_style
.contains(syntect::highlighting::FontStyle::BOLD)
{
style = style.bold();
}
if syntect_style
.font_style
.contains(syntect::highlighting::FontStyle::ITALIC)
{
style = style.italic();
}
if syntect_style
.font_style
.contains(syntect::highlighting::FontStyle::UNDERLINE)
{
style = style.underline();
}
style
}
fn highlight(&self) -> (Vec<Vec<Span>>, Option<Color>) {
let syntax_set = SYNTAX_SET.get_or_init(SyntaxSet::load_defaults_newlines);
let theme_set = THEME_SET.get_or_init(ThemeSet::load_defaults);
let syntax = syntax_set
.find_syntax_by_extension(&self.language)
.or_else(|| syntax_set.find_syntax_by_name(&self.language))
.unwrap_or_else(|| syntax_set.find_syntax_plain_text());
let theme = theme_set
.themes
.get(self.config.theme.name())
.unwrap_or_else(|| theme_set.themes.values().next().unwrap());
let theme_bg = theme.settings.background.map(|c| Color::rgb(c.r, c.g, c.b));
let mut highlighter = HighlightLines::new(syntax, theme);
let mut lines = Vec::new();
let line_count = self.code.lines().count();
let line_number_width = (line_count + self.config.start_line).to_string().len();
for (i, line) in LinesWithEndings::from(&self.code).enumerate() {
let line = line.replace('\t', " ");
let line_num = i + self.config.start_line;
let mut spans = Vec::new();
if self.config.line_numbers {
let is_highlighted = self.config.highlight_lines.contains(&line_num);
let line_color = if is_highlighted {
Color::Yellow
} else {
Color::BrightBlack
};
let line_style = Style::new().foreground(line_color);
let marker = if is_highlighted { "→ " } else { " " };
spans.push(Span::styled(marker.to_string(), line_style));
spans.push(Span::styled(
format!("{:>width$} │ ", line_num, width = line_number_width),
line_style,
));
}
let highlighted = highlighter
.highlight_line(&line, syntax_set)
.unwrap_or_default();
for (style, text) in highlighted {
let text = text.trim_end_matches('\n').to_string();
if !text.is_empty() {
spans.push(Span::styled(text, Self::convert_style(style)));
}
}
lines.push(spans);
}
(lines, theme_bg)
}
}
impl Renderable for Syntax {
fn render(&self, context: &RenderContext) -> Vec<Segment> {
let (mut highlighted_lines, theme_bg) = self.highlight();
if let Some(bg) = theme_bg {
for line in &mut highlighted_lines {
for span in line {
if span.style.background.is_none() {
span.style = span.style.background(bg);
}
}
}
}
if self.config.panel {
let mut text = Text::new();
for (i, spans) in highlighted_lines.iter().enumerate() {
for span in spans {
text.push_span(span.clone());
}
if i < highlighted_lines.len() - 1 {
text.push("\n");
}
}
let mut panel = Panel::new(text)
.title(&self.language)
.border_style(self.config.border_style)
.style(Style::new().foreground(Color::BrightBlack));
if let Some(bg) = theme_bg {
let current_style = Style::new().foreground(Color::BrightBlack);
panel = panel.style(current_style.background(bg));
}
panel.render(context)
} else {
highlighted_lines.into_iter().map(Segment::line).collect()
}
}
}
pub fn highlight(code: &str, language: &str) -> Syntax {
Syntax::new(code, language)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_syntax_basic() {
let syntax = Syntax::new("let x = 42;", "rust");
let context = RenderContext {
width: 60,
height: None,
};
let segments = syntax.render(&context);
assert!(!segments.is_empty());
}
#[test]
fn test_syntax_without_panel() {
let syntax = Syntax::new("print('hello')", "python").panel(false);
let context = RenderContext {
width: 60,
height: None,
};
let segments = syntax.render(&context);
assert!(!segments.is_empty());
}
#[test]
fn test_syntax_without_line_numbers() {
let syntax = Syntax::new("x = 1", "python").line_numbers(false);
let context = RenderContext {
width: 60,
height: None,
};
let segments = syntax.render(&context);
assert!(!segments.is_empty());
}
#[test]
fn test_syntax_themes() {
let syntax = Syntax::new("let x = 42;", "rust").theme(Theme::SolarizedDark);
let context = RenderContext {
width: 60,
height: None,
};
let segments = syntax.render(&context);
assert!(!segments.is_empty());
}
}