Skip to main content

semantic_diff/
highlight.rs

1use crate::diff::DiffData;
2use ratatui::style::{Color, Style};
3use std::collections::HashMap;
4use syntect::easy::HighlightLines;
5use syntect::highlighting::{Style as SyntectStyle, ThemeSet};
6use syntect::parsing::SyntaxSet;
7
8/// Pre-computed syntax highlighting cache.
9/// Keyed by (file_idx, hunk_idx, line_idx) -> Vec<(ratatui Style, text)>.
10pub struct HighlightCache {
11    cache: HashMap<(usize, usize, usize), Vec<(Style, String)>>,
12}
13
14impl HighlightCache {
15    /// Pre-compute syntax highlighting for all diff lines.
16    /// `syntect_theme_name` should be a key present in syntect's default ThemeSet
17    /// (e.g. "base16-ocean.dark" or "base16-ocean.light").
18    pub fn new(diff_data: &DiffData, syntect_theme_name: &str) -> Self {
19        let ss = SyntaxSet::load_defaults_newlines();
20        let ts = ThemeSet::load_defaults();
21        // Fall back to "base16-ocean.dark" if the requested theme is not found.
22        let theme = ts
23            .themes
24            .get(syntect_theme_name)
25            .unwrap_or_else(|| &ts.themes["base16-ocean.dark"]);
26
27        let mut cache = HashMap::new();
28
29        for (fi, file) in diff_data.files.iter().enumerate() {
30            // Detect syntax from filename extension
31            let filename = file.target_file.trim_start_matches("b/");
32            let syntax = ss
33                .find_syntax_for_file(filename)
34                .ok()
35                .flatten()
36                .unwrap_or_else(|| ss.find_syntax_plain_text());
37
38            let mut highlighter = HighlightLines::new(syntax, theme);
39
40            for (hi, hunk) in file.hunks.iter().enumerate() {
41                for (li, line) in hunk.lines.iter().enumerate() {
42                    let spans = match highlighter.highlight_line(&line.content, &ss) {
43                        Ok(regions) => regions
44                            .into_iter()
45                            .map(|(style, text)| {
46                                (syntect_to_ratatui_style(style), text.to_string())
47                            })
48                            .collect(),
49                        Err(_) => {
50                            // Fallback: raw text with default style
51                            vec![(Style::default(), line.content.clone())]
52                        }
53                    };
54                    cache.insert((fi, hi, li), spans);
55                }
56            }
57        }
58
59        Self { cache }
60    }
61
62    /// Look up cached highlighted spans for a specific line.
63    pub fn get(&self, file_idx: usize, hunk_idx: usize, line_idx: usize) -> Option<&Vec<(Style, String)>> {
64        self.cache.get(&(file_idx, hunk_idx, line_idx))
65    }
66}
67
68/// Convert a syntect Style to a ratatui Style (foreground color only).
69fn syntect_to_ratatui_style(syntect_style: SyntectStyle) -> Style {
70    let fg = syntect_style.foreground;
71    Style::default().fg(Color::Rgb(fg.r, fg.g, fg.b))
72}