1use std::borrow::Cow;
2use std::fmt::{self, Debug, Formatter};
3use std::io::{self, BufRead, BufReader};
4use std::ops::Range;
5use std::sync::Arc;
6
7use ratatui::style::{Color, Style, Stylize};
8use ratatui::text::{Line, Span, Text};
9pub use syntect;
10use syntect::easy::HighlightLines;
11use syntect::highlighting::Theme;
12use syntect::parsing::{SyntaxReference, SyntaxSet};
13#[cfg(feature = "termprofile")]
14use termprofile::TermProfile;
15
16use crate::Converter;
17
18type GutterFn = dyn Fn(usize, Style) -> Vec<Span<'static>> + Send + Sync;
19
20#[derive(Clone)]
21struct GutterTemplate(Arc<GutterFn>);
22
23impl Debug for GutterTemplate {
24 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
25 f.write_str("GutterTemplate(<fn>)")
26 }
27}
28
29#[derive(Clone, Debug)]
32pub struct Highlighter {
33 theme: Theme,
34 override_background: Option<Color>,
35 line_number_style: Option<Style>,
36 line_number_separator_style: Option<Style>,
37 gutter_template: Option<GutterTemplate>,
38 line_numbers: bool,
39 line_number_padding: usize,
40 line_number_separator: String,
41 #[cfg(feature = "termprofile")]
42 profile: TermProfile,
43 highlight_ranges: Vec<Range<usize>>,
44 highlight_style: Style,
45 converter: Converter,
46}
47
48impl Highlighter {
49 pub fn new(theme: Theme) -> Self {
51 Self {
52 theme,
53 override_background: None,
54 line_number_style: None,
55 line_number_separator_style: None,
56 gutter_template: None,
57 line_numbers: true,
58 line_number_padding: 4,
59 line_number_separator: "│".to_string(),
60 #[cfg(feature = "termprofile")]
61 profile: TermProfile::TrueColor,
62 highlight_ranges: Vec::new(),
63 highlight_style: Style::new().bg(Color::Yellow),
64 converter: Converter::new(),
65 }
66 }
67
68 #[cfg(feature = "termprofile")]
72 pub fn with_profile(theme: Theme, profile: TermProfile) -> Self {
73 let mut this = Self::new(theme);
74 this.profile = profile;
75 this.converter = Converter::with_profile(profile);
76 this
77 }
78
79 pub fn override_background<C>(mut self, background: C) -> Self
82 where
83 C: Into<Color>,
84 {
85 let background = background.into();
86 self.override_background = Some(self.adapt_color(background).unwrap_or(Color::Reset));
87 self
88 }
89
90 pub fn line_numbers(mut self, line_numbers: bool) -> Self {
92 self.line_numbers = line_numbers;
93 self
94 }
95
96 pub fn line_number_padding(mut self, padding: usize) -> Self {
98 self.line_number_padding = padding;
99 self
100 }
101
102 pub fn line_number_style<S>(mut self, style: S) -> Self
104 where
105 S: Into<Style>,
106 {
107 self.line_number_style = Some(self.adapt_style(style.into()));
108 self
109 }
110
111 pub fn line_number_separator_style<S>(mut self, style: S) -> Self
113 where
114 S: Into<Style>,
115 {
116 self.line_number_separator_style = Some(self.adapt_style(style.into()));
117 self
118 }
119
120 pub fn line_number_separator<T>(mut self, separator: T) -> Self
122 where
123 T: Into<String>,
124 {
125 self.line_number_separator = separator.into();
126 self
127 }
128
129 pub fn highlight_range(mut self, range: Range<usize>) -> Self {
131 self.highlight_ranges.push(range);
132 self
133 }
134
135 pub fn highlight_style(mut self, style: Style) -> Self {
139 self.highlight_style = self.adapt_style(style);
140 self
141 }
142
143 pub fn gutter_template<F>(mut self, template: F) -> Self
151 where
152 F: Fn(usize, Style) -> Vec<Span<'static>> + Send + Sync + 'static,
153 {
154 self.gutter_template = Some(GutterTemplate(Arc::new(template)));
155 self
156 }
157
158 pub fn get_background_color(&self) -> Option<Color> {
162 if let Some(bg) = self.override_background {
163 Some(bg)
164 } else {
165 self.theme
166 .settings
167 .background
168 .and_then(|bg| self.converter.syntect_color_to_tui(bg))
169 }
170 }
171
172 pub fn get_line_number_style(&self) -> Style {
174 if let Some(style) = self.line_number_style {
175 return style;
176 }
177 let mut style = Style::new();
178 if let Some(fg) = self
179 .theme
180 .settings
181 .gutter_foreground
182 .and_then(|fg| self.converter.syntect_color_to_tui(fg))
183 {
184 style = style.fg(fg);
185 } else {
186 style = style.dark_gray();
187 }
188 if let Some(bg) = self.get_background_color() {
189 style = style.bg(bg);
190 }
191 self.adapt_style(style)
192 }
193
194 pub fn highlight_reader<R>(
196 &self,
197 reader: R,
198 syntax: &SyntaxReference,
199 syntaxes: &SyntaxSet,
200 ) -> Result<Text<'static>, crate::Error>
201 where
202 R: io::Read,
203 {
204 let mut reader = BufReader::new(reader);
205 let mut highlighter = HighlightLines::new(syntax, &self.theme);
206 let line_number_style = self.get_line_number_style();
207 let mut line = String::new();
208 let mut formatted = Vec::new();
209 let mut i = 0;
210 while reader.read_line(&mut line).map_err(crate::Error::Read)? > 0 {
211 let highlighted =
212 self.highlight_line(&line, &mut highlighter, i, line_number_style, syntaxes)?;
213 formatted.push(highlighted);
214 line.clear();
215 i += 1;
216 }
217 Ok(Text::from_iter(formatted))
218 }
219
220 pub fn highlight_lines<'a, T>(
222 &self,
223 source: T,
224 syntax: &SyntaxReference,
225 syntaxes: &SyntaxSet,
226 ) -> Result<Text<'static>, crate::Error>
227 where
228 T: IntoIterator<Item = &'a str>,
229 {
230 let mut highlighter = HighlightLines::new(syntax, &self.theme);
231 let line_number_style = self.get_line_number_style();
232 let formatted: Result<Vec<_>, crate::Error> = source
233 .into_iter()
234 .enumerate()
235 .map(|(i, line)| {
236 self.highlight_line(line, &mut highlighter, i, line_number_style, syntaxes)
237 })
238 .collect();
239 let formatted = formatted?;
240 Ok(Text::from_iter(formatted))
241 }
242
243 pub fn highlight_line(
245 &self,
246 line: &str,
247 highlighter: &mut HighlightLines,
248 line_number: usize,
249 line_number_style: Style,
250 syntaxes: &SyntaxSet,
251 ) -> Result<Line<'static>, crate::Error> {
252 let line: Cow<_> = if line.ends_with("\n") {
253 line.into()
254 } else {
255 (line.to_string() + "\n").into()
256 };
257 let regions = highlighter
258 .highlight_line(&line, syntaxes)
259 .map_err(crate::Error::Highlight)?;
260 Ok(self.to_line(®ions, line_number, line_number_style))
261 }
262
263 fn get_initial_spans(
264 &self,
265 line_number: usize,
266 line_number_style: Style,
267 ) -> Vec<Span<'static>> {
268 let line_number = line_number + 1;
270 if let Some(template) = &self.gutter_template {
271 return template.0(line_number, line_number_style);
272 }
273
274 if self.line_numbers {
275 let line_number = line_number.to_string();
276 let spaces = self
277 .line_number_padding
278 .saturating_sub(line_number.len())
279 .saturating_sub(2);
281 vec![
282 Span::styled(" ".repeat(spaces), line_number_style),
283 Span::styled(line_number, line_number_style),
284 Span::styled(" ", line_number_style),
285 Span::styled(
286 self.line_number_separator.clone(),
287 self.line_number_separator_style
288 .unwrap_or(line_number_style),
289 ),
290 Span::styled(" ", line_number_style),
291 ]
292 } else {
293 vec![]
294 }
295 }
296
297 fn to_line(
298 &self,
299 v: &[(syntect::highlighting::Style, &str)],
300 line_number: usize,
301 line_number_style: Style,
302 ) -> Line<'static> {
303 let mut spans = self.get_initial_spans(line_number, line_number_style);
304 let highlight_row = self
305 .highlight_ranges
306 .iter()
307 .any(|r| r.contains(&line_number));
308
309 for &(ref style, mut text) in v {
310 let ends_with_newline = text.ends_with('\n');
311 if ends_with_newline {
312 text = &text[..text.len() - 1];
313 }
314
315 let mut tui_style = self.syntect_style_to_tui(*style);
316 if highlight_row {
317 tui_style = tui_style.patch(self.highlight_style);
318 }
319
320 spans.push(Span::styled(text.to_string(), tui_style));
321 }
322
323 let mut line = Line::from_iter(spans);
324 if highlight_row {
325 line = line.patch_style(self.highlight_style);
326 }
327 self.apply_background(line)
328 }
329
330 fn adapt_style(&self, style: Style) -> Style {
331 #[cfg(feature = "termprofile")]
332 return self.profile.adapt_style(style);
333 #[cfg(not(feature = "termprofile"))]
334 return style;
335 }
336
337 fn adapt_color(&self, color: Color) -> Option<Color> {
338 #[cfg(feature = "termprofile")]
339 return self.profile.adapt_color(color);
340
341 #[cfg(not(feature = "termprofile"))]
342 return Some(color);
343 }
344
345 fn apply_background<'a, S>(&self, item: S) -> S
346 where
347 S: Stylize<'a, S>,
348 {
349 if let Some(bg) = self.override_background {
350 return item.bg(bg);
351 };
352 if let Some(bg) = self
353 .theme
354 .settings
355 .background
356 .and_then(|bg| self.converter.syntect_color_to_tui(bg))
357 {
358 return item.bg(bg);
359 }
360 item
361 }
362
363 fn syntect_style_to_tui(&self, style: syntect::highlighting::Style) -> ratatui::style::Style {
364 let mut tui_style = self.converter.syntect_style_to_tui(style);
365
366 if let Some(bg) = self.override_background {
367 tui_style = tui_style.bg(bg);
368 }
369 tui_style
370 }
371}