1use syntect::easy::HighlightLines;
6use syntect::highlighting::{ThemeSet, Style as SyntectStyle};
7use syntect::parsing::SyntaxSet;
8use syntect::util::LinesWithEndings;
9
10use crate::console::{ConsoleOptions, RenderResult, Renderable};
11use crate::segment::Segment;
12use crate::style::Style;
13
14#[derive(Debug, Clone)]
16pub struct Syntax {
17 pub code: String,
19 pub language: String,
21 pub theme: String,
23 pub start_line: usize,
25 pub line_numbers: bool,
27 pub highlight: bool,
29 pub background_color: Option<crate::color::Color>,
31 pub tab_size: usize,
33}
34
35impl Syntax {
36 pub fn new(code: impl Into<String>, language: impl Into<String>) -> Self {
38 Self {
39 code: code.into(),
40 language: language.into(),
41 theme: "base16-ocean.dark".to_string(),
42 start_line: 1,
43 line_numbers: false,
44 highlight: true,
45 background_color: None,
46 tab_size: 4,
47 }
48 }
49
50 pub fn theme(mut self, theme: impl Into<String>) -> Self { self.theme = theme.into(); self }
52
53 pub fn line_numbers(mut self) -> Self { self.line_numbers = true; self }
55
56 pub fn start_line(mut self, n: usize) -> Self { self.start_line = n; self }
58
59 pub fn background(mut self, color: crate::color::Color) -> Self { self.background_color = Some(color); self }
61}
62
63impl Renderable for Syntax {
64 fn render(&self, _options: &ConsoleOptions) -> RenderResult {
65 if !self.highlight || self.language.is_empty() {
66 let lines: Vec<Vec<Segment>> = self
68 .code
69 .lines()
70 .map(|line| vec![Segment::new(line), Segment::line()])
71 .collect();
72 return RenderResult { lines, items: Vec::new() };
73 }
74
75 let ss = SyntaxSet::load_defaults_newlines();
76 let ts = ThemeSet::load_defaults();
77
78 let syntax = ss
79 .find_syntax_by_name(&self.language)
80 .or_else(|| ss.find_syntax_by_extension(&self.language))
81 .unwrap_or_else(|| ss.find_syntax_plain_text());
82
83 let theme = &ts.themes[&self.theme];
84
85 let mut highlighter = HighlightLines::new(syntax, theme);
86
87 let mut lines: Vec<Vec<Segment>> = Vec::new();
88 let line_num_width = if self.line_numbers {
89 (self.code.lines().count().saturating_add(self.start_line))
90 .to_string()
91 .len()
92 } else {
93 0
94 };
95
96 for (i, line) in LinesWithEndings::from(&self.code).enumerate() {
97 let mut line_segments: Vec<Segment> = Vec::new();
98
99 if self.line_numbers {
101 let num = i + self.start_line;
102 let num_str = format!("{:>width$} │ ", num, width = line_num_width);
103 line_segments.push(Segment::new(num_str));
104 }
105
106 match highlighter.highlight_line(line, &ss) {
108 Ok(highlighted) => {
109 for (syntect_style, text) in &highlighted {
110 let style = syntect_to_rich_style(syntect_style);
111 line_segments.push(Segment::styled(
112 text.to_string(),
113 style,
114 ));
115 }
116 }
117 Err(_) => {
118 line_segments.push(Segment::new(line));
119 }
120 }
121
122 lines.push(line_segments);
123 }
124
125 RenderResult { lines, items: Vec::new() }
126 }
127}
128
129fn syntect_to_rich_style(ss: &SyntectStyle) -> Style {
131 let mut style = Style::new();
132 let fg = ss.foreground;
133 style = style.color(crate::color::Color::from_rgb(fg.r, fg.g, fg.b));
134
135 if ss.font_style.contains(syntect::highlighting::FontStyle::BOLD) {
136 style = style.bold(true);
137 }
138 if ss.font_style.contains(syntect::highlighting::FontStyle::ITALIC) {
139 style = style.italic(true);
140 }
141 if ss.font_style.contains(syntect::highlighting::FontStyle::UNDERLINE) {
142 style = style.underline(true);
143 }
144 style
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_syntax_no_highlight() {
153 let s = Syntax::new("fn main() {}", "rust");
154 let opts = ConsoleOptions::default();
155 let result = s.render(&opts);
156 let ansi = result.to_ansi();
157 assert!(ansi.contains("fn main"));
158 }
159
160 #[test]
161 fn test_syntax_line_numbers() {
162 let s = Syntax::new("line1\nline2\nline3", "").line_numbers();
163 let opts = ConsoleOptions::default();
164 let result = s.render(&opts);
165 let ansi = result.to_ansi();
166 assert!(ansi.contains("1"));
167 }
168}