giff 1.2.0

Visualizes the differences between the current HEAD and a specified branch in a git repository using a formatted table output in your terminal. The differences are displayed with color-coded additions and deletions for better readability.
use ratatui::{
    style::{Color, Modifier, Style},
    text::{Line, Span},
};
use std::sync::LazyLock;
use syntect::easy::HighlightLines;
use syntect::highlighting::{self, ThemeSet};
use syntect::parsing::SyntaxSet;

use super::theme::Theme;

pub static SYNTAX_SET: LazyLock<SyntaxSet> = LazyLock::new(two_face::syntax::extra_newlines);
pub static THEME_SET: LazyLock<ThemeSet> = LazyLock::new(ThemeSet::load_defaults);

fn to_ratatui_color(c: highlighting::Color) -> Color {
    Color::Rgb(c.r, c.g, c.b)
}

pub fn highlight_code(code: &str, highlighter: &mut HighlightLines) -> Vec<Span<'static>> {
    match highlighter.highlight_line(code, &SYNTAX_SET) {
        Ok(ranges) => ranges
            .into_iter()
            .map(|(style, text)| {
                Span::styled(
                    text.to_owned(),
                    Style::default().fg(to_ratatui_color(style.foreground)),
                )
            })
            .collect(),
        Err(_) => vec![Span::raw(code.to_owned())],
    }
}

fn highlight_code_with_bg(
    code: &str,
    highlighter: &mut HighlightLines,
    bg: Color,
) -> Vec<Span<'static>> {
    match highlighter.highlight_line(code, &SYNTAX_SET) {
        Ok(ranges) => ranges
            .into_iter()
            .map(|(style, text)| {
                Span::styled(
                    text.to_owned(),
                    Style::default()
                        .fg(to_ratatui_color(style.foreground))
                        .bg(bg),
                )
            })
            .collect(),
        Err(_) => vec![Span::styled(code.to_owned(), Style::default().bg(bg))],
    }
}

pub fn highlight_line_changes(
    lines: &[(usize, String)],
    filename: &str,
    theme: &Theme,
) -> Vec<Line<'static>> {
    let syntax = SYNTAX_SET
        .find_syntax_for_file(filename)
        .ok()
        .flatten()
        .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text());
    let fallback_name = if theme.is_dark {
        "base16-ocean.dark"
    } else {
        "base16-ocean.light"
    };
    let syn_theme = match THEME_SET.themes.get(&theme.syntax_theme) {
        Some(t) => t,
        None => match THEME_SET
            .themes
            .get(fallback_name)
            .or_else(|| THEME_SET.themes.values().next())
        {
            Some(t) => t,
            None => {
                return lines
                    .iter()
                    .map(|(num, line)| {
                        if *num == 0 {
                            return Line::from(Span::raw(""));
                        }
                        Line::from(Span::raw(format!("{:4}   {}", num, line)))
                    })
                    .collect()
            }
        },
    };
    let mut highlighter = HighlightLines::new(syntax, syn_theme);

    let fg_line_num = theme.fg_line_num;
    let bg_removed = theme.bg_removed;
    let bg_added = theme.bg_added;
    let fg_removed_marker = theme.fg_removed_marker;
    let fg_added_marker = theme.fg_added_marker;

    lines
        .iter()
        .map(|(line_num, line)| {
            // Gap/placeholder line for side-by-side alignment
            if *line_num == 0 {
                return Line::from(Span::raw(""));
            }
            if let Some(rest) = line.strip_prefix('-') {
                let mut spans = vec![
                    Span::styled(
                        format!("{:4} ", line_num),
                        Style::default().fg(fg_line_num).bg(bg_removed),
                    ),
                    Span::styled(
                        "- ",
                        Style::default()
                            .fg(fg_removed_marker)
                            .bg(bg_removed)
                            .add_modifier(Modifier::BOLD),
                    ),
                ];
                spans.extend(highlight_code_with_bg(rest, &mut highlighter, bg_removed));
                Line::from(spans)
            } else if let Some(rest) = line.strip_prefix('+') {
                let mut spans = vec![
                    Span::styled(
                        format!("{:4} ", line_num),
                        Style::default().fg(fg_line_num).bg(bg_added),
                    ),
                    Span::styled(
                        "+ ",
                        Style::default()
                            .fg(fg_added_marker)
                            .bg(bg_added)
                            .add_modifier(Modifier::BOLD),
                    ),
                ];
                spans.extend(highlight_code_with_bg(rest, &mut highlighter, bg_added));
                Line::from(spans)
            } else {
                // Context lines in git diff format have a leading space;
                // strip it so code aligns with changed lines (whose +/-
                // prefix is also stripped) and so indentation-sensitive
                // languages (Python, YAML) highlight correctly.
                let content = line.strip_prefix(' ').unwrap_or(line);
                let mut spans = vec![
                    Span::styled(format!("{:4} ", line_num), Style::default().fg(fg_line_num)),
                    Span::styled("  ", Style::default()),
                ];
                spans.extend(highlight_code(content, &mut highlighter));
                Line::from(spans)
            }
        })
        .collect()
}