LCOV - code coverage report
Current view: top level - src - syntax.rs (source / functions) Coverage Total Hit
Test: Hunky Coverage Lines: 98.2 % 55 54
Test Date: 2026-10-10 16:52:03 Functions: 50.0 % 18 9

            Line data    Source code
       1              : use ratatui::style::Color;
       2              : use std::path::Path;
       3              : use syntect::easy::HighlightLines;
       4              : use syntect::highlighting::ThemeSet;
       5              : use syntect::parsing::SyntaxSet;
       6              : 
       7              : pub struct SyntaxHighlighter {
       8              :     syntax_set: SyntaxSet,
       9              :     theme_set: ThemeSet,
      10              : }
      11              : 
      12              : impl SyntaxHighlighter {
      13           35 :     pub fn new() -> Self {
      14           35 :         Self {
      15           35 :             syntax_set: SyntaxSet::load_defaults_newlines(),
      16           35 :             theme_set: ThemeSet::load_defaults(),
      17           35 :         }
      18           35 :     }
      19              : 
      20              :     /// Get a highlighter for a specific file that can be used to highlight multiple lines sequentially
      21           11 :     pub fn create_highlighter(&self, file_path: &Path) -> FileHighlighter<'_> {
      22              :         #[cfg(not(feature = "browser"))]
      23           11 :         let syntax = self
      24           11 :             .syntax_set
      25           11 :             .find_syntax_for_file(file_path)
      26           11 :             .ok()
      27           11 :             .flatten()
      28           11 :             .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
      29              :         #[cfg(feature = "browser")]
      30              :         let syntax = file_path
      31              :             .extension()
      32              :             .and_then(|extension| extension.to_str())
      33              :             .and_then(|extension| self.syntax_set.find_syntax_by_extension(extension))
      34              :             .unwrap_or_else(|| self.syntax_set.find_syntax_plain_text());
      35              : 
      36           11 :         let theme = &self.theme_set.themes["base16-ocean.dark"];
      37              : 
      38           11 :         FileHighlighter {
      39           11 :             highlighter: HighlightLines::new(syntax, theme),
      40           11 :             syntax_set: &self.syntax_set,
      41           11 :         }
      42           11 :     }
      43              : 
      44              :     #[allow(dead_code)]
      45            1 :     pub fn detect_language(&self, file_path: &Path) -> Option<String> {
      46              :         #[cfg(not(feature = "browser"))]
      47            1 :         return self
      48            1 :             .syntax_set
      49            1 :             .find_syntax_for_file(file_path)
      50            1 :             .ok()
      51            1 :             .flatten()
      52            1 :             .map(|s| s.name.clone());
      53              :         #[cfg(feature = "browser")]
      54              :         file_path
      55              :             .extension()
      56              :             .and_then(|extension| extension.to_str())
      57              :             .and_then(|extension| self.syntax_set.find_syntax_by_extension(extension))
      58              :             .map(|syntax| syntax.name.clone())
      59            1 :     }
      60              : }
      61              : 
      62              : pub struct FileHighlighter<'a> {
      63              :     highlighter: HighlightLines<'a>,
      64              :     syntax_set: &'a SyntaxSet,
      65              : }
      66              : 
      67              : impl<'a> FileHighlighter<'a> {
      68              :     /// Highlight a single line (must be called sequentially for proper context)
      69           30 :     pub fn highlight_line(&mut self, line: &str) -> Vec<(Color, String)> {
      70           30 :         let mut result = Vec::new();
      71              : 
      72           30 :         if let Ok(ranges) = self.highlighter.highlight_line(line, self.syntax_set) {
      73          134 :             for (style, text) in ranges {
      74          134 :                 let color = syntect_color_to_ratatui(style.foreground);
      75          134 :                 result.push((color, text.to_string()));
      76          134 :             }
      77            0 :         }
      78              : 
      79           30 :         result
      80           30 :     }
      81              : }
      82              : 
      83              : /// Convert syntect color to ratatui color
      84              : /// Uses 256-color palette for better terminal compatibility (macOS Terminal.app)
      85          134 : fn syntect_color_to_ratatui(color: syntect::highlighting::Color) -> Color {
      86          134 :     rgb_to_ansi256(color.r, color.g, color.b)
      87          134 : }
      88              : 
      89              : /// Convert RGB color to nearest ANSI 256-color palette index
      90              : /// This provides better compatibility with terminals that don't support 24-bit true color
      91          136 : fn rgb_to_ansi256(r: u8, g: u8, b: u8) -> Color {
      92              :     // Check if it's a grayscale color
      93          136 :     let max_diff = r.abs_diff(g).max(g.abs_diff(b)).max(r.abs_diff(b));
      94              : 
      95          136 :     if max_diff < 10 {
      96              :         // It's grayscale - use the 24 grayscale colors (232-255)
      97              :         // Map 0-255 to 0-23
      98            1 :         let gray_index = ((r as u16 * 23) / 255) as u8;
      99            1 :         return Color::Indexed(232 + gray_index);
     100          135 :     }
     101              : 
     102              :     // Map to 6x6x6 color cube (16-231)
     103              :     // Each component is mapped to 0-5
     104          135 :     let r_index = (r as u16 * 5 / 255) as u8;
     105          135 :     let g_index = (g as u16 * 5 / 255) as u8;
     106          135 :     let b_index = (b as u16 * 5 / 255) as u8;
     107              : 
     108          135 :     let index = 16 + 36 * r_index + 6 * g_index + b_index;
     109          135 :     Color::Indexed(index)
     110          136 : }
     111              : 
     112              : impl Default for SyntaxHighlighter {
     113            1 :     fn default() -> Self {
     114            1 :         Self::new()
     115            1 :     }
     116              : }
     117              : 
     118              : #[cfg(test)]
     119              : #[path = "../tests/syntax.rs"]
     120              : mod tests;
        

Generated by: LCOV version 2.0-1