1use std::path::Path;
15use std::sync::LazyLock;
16
17use ratatui::style::{Color, Style};
18use ratatui::text::{Line, Span};
19use syntect::easy::HighlightLines;
20use syntect::highlighting::FontStyle;
21use syntect::parsing::{SyntaxReference, SyntaxSet};
22use syntect::util::LinesWithEndings;
23
24use crate::{OUTPUT_INDENT, text_rows};
25
26static SYNTAXES: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
27
28static THEME: LazyLock<syntect::highlighting::Theme> = LazyLock::new(|| {
29 let themes = two_face::theme::extra();
30 themes.get(two_face::theme::EmbeddedThemeName::Ansi).clone()
31});
32
33#[derive(Clone, Copy, Debug)]
35pub enum Language<'a> {
36 Named(&'a str),
39 File(&'a str),
41}
42
43pub(crate) fn warm_up() {
46 std::thread::spawn(|| {
47 LazyLock::force(&SYNTAXES);
48 LazyLock::force(&THEME);
49 });
50}
51
52pub fn highlight(code: &str, language: Language<'_>) -> Option<Vec<Vec<Span<'static>>>> {
55 let syntax = syntax(language)?;
56 let mut lines = HighlightLines::new(syntax, &THEME);
57 LinesWithEndings::from(code)
58 .map(|line| {
59 let pieces = lines.highlight_line(line, &SYNTAXES).ok()?;
60 let spans = pieces.into_iter().filter_map(|(style, text)| {
61 let text = text.trim_end_matches(['\n', '\r']).replace('\t', " ");
62 (!text.is_empty()).then(|| Span::styled(text, style_of(style)))
63 });
64 Some(spans.collect())
65 })
66 .collect()
67}
68
69pub fn code_rows(code: &str, language: Language<'_>) -> Vec<Line<'static>> {
73 let Some(lines) = highlight(code, language) else {
74 return text_rows(code, Style::new().dim());
75 };
76 let rows = lines
77 .into_iter()
78 .map(|spans| Line::from([vec![Span::from(OUTPUT_INDENT)], spans].concat()));
79 rows.collect()
80}
81
82fn syntax(language: Language<'_>) -> Option<&'static SyntaxReference> {
84 let syntaxes: &'static SyntaxSet = &SYNTAXES;
85 let found = match language {
86 Language::Named(name) => syntaxes.find_syntax_by_token(name.trim()),
87 Language::File(path) => {
88 let path = Path::new(path);
89 let by =
90 |name: Option<&std::ffi::OsStr>| syntaxes.find_syntax_by_extension(name?.to_str()?);
91 by(path.extension()).or_else(|| by(path.file_name()))
92 }
93 };
94 found.filter(|syntax| syntax.name != "Plain Text")
95}
96
97fn style_of(style: syntect::highlighting::Style) -> Style {
99 let mut drawn = Style::new();
100 if let Some(color) = color(style.foreground) {
101 drawn = drawn.fg(color);
102 }
103 let font = style.font_style;
104 if font.contains(FontStyle::BOLD) {
105 drawn = drawn.bold();
106 }
107 if font.contains(FontStyle::ITALIC) {
108 drawn = drawn.italic();
109 }
110 if font.contains(FontStyle::UNDERLINE) {
111 drawn = drawn.underlined();
112 }
113 drawn
114}
115
116fn color(color: syntect::highlighting::Color) -> Option<Color> {
120 match color.a {
121 0 => Some(Color::Indexed(color.r)),
122 1 => None,
123 _ => Some(Color::Rgb(color.r, color.g, color.b)),
124 }
125}
126
127#[cfg(test)]
128mod tests;