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    pub fn new(diff_data: &DiffData) -> Self {
17        let ss = SyntaxSet::load_defaults_newlines();
18        let ts = ThemeSet::load_defaults();
19        let theme = &ts.themes["base16-ocean.dark"];
20
21        let mut cache = HashMap::new();
22
23        for (fi, file) in diff_data.files.iter().enumerate() {
24            // Detect syntax from filename extension
25            let filename = file.target_file.trim_start_matches("b/");
26            let syntax = ss
27                .find_syntax_for_file(filename)
28                .ok()
29                .flatten()
30                .unwrap_or_else(|| ss.find_syntax_plain_text());
31
32            let mut highlighter = HighlightLines::new(syntax, theme);
33
34            for (hi, hunk) in file.hunks.iter().enumerate() {
35                for (li, line) in hunk.lines.iter().enumerate() {
36                    let spans = match highlighter.highlight_line(&line.content, &ss) {
37                        Ok(regions) => regions
38                            .into_iter()
39                            .map(|(style, text)| {
40                                (syntect_to_ratatui_style(style), text.to_string())
41                            })
42                            .collect(),
43                        Err(_) => {
44                            // Fallback: raw text with default style
45                            vec![(Style::default(), line.content.clone())]
46                        }
47                    };
48                    cache.insert((fi, hi, li), spans);
49                }
50            }
51        }
52
53        Self { cache }
54    }
55
56    /// Look up cached highlighted spans for a specific line.
57    pub fn get(&self, file_idx: usize, hunk_idx: usize, line_idx: usize) -> Option<&Vec<(Style, String)>> {
58        self.cache.get(&(file_idx, hunk_idx, line_idx))
59    }
60}
61
62/// Convert a syntect Style to a ratatui Style (foreground color only).
63fn syntect_to_ratatui_style(syntect_style: SyntectStyle) -> Style {
64    let fg = syntect_style.foreground;
65    Style::default().fg(Color::Rgb(fg.r, fg.g, fg.b))
66}