quinjet 0.0.71

A fast, live, keyboard-first Git source-control interface for the terminal
#[cfg_attr(not(test), expect(clippy::wildcard_imports, reason = "shared"))]
use super::*;

#[expect(
    clippy::too_many_arguments,
    reason = "diff rendering returns file hits while registering review hits"
)]
pub(super) fn draw_unified_diff(
    frame: &mut Frame<'_>,
    area: Rect,
    app: &App,
    rows: &[usize],
    diff_scroll: usize,
    review_hits: &mut Vec<ContentReviewHit>,
    theme: &Theme,
) -> Vec<ContentFileHit> {
    let first_index = rows.get(diff_scroll).copied().unwrap_or_default();
    let emphasis = visible_intraline_emphasis(
        &app.document.lines,
        rows.iter()
            .copied()
            .skip(diff_scroll)
            .take(area.height as usize),
    );
    let sticky = app
        .document
        .lines
        .get(first_index)
        .filter(|line| line.kind != DiffLineKind::FileHeader)
        .and_then(|_| app.preview_header_at(first_index));
    let content_y = area.y + u16::from(sticky.is_some());
    let content_height = area.height.saturating_sub(u16::from(sticky.is_some()));
    let mut hits = Vec::new();
    let mut image_state = ImageDrawState::new(rows.len() <= usize::from(content_height));
    if let Some(header) = sticky {
        let sticky_area = Rect::new(area.x, area.y, area.width, 1);
        draw_file_header(frame, sticky_area, header, app, theme);
        if let Some(path) = file_header_path(header) {
            hits.push(ContentFileHit {
                area: sticky_area,
                path: path.into(),
            });
        }
    }
    for (offset, line_index) in rows
        .iter()
        .copied()
        .skip(diff_scroll)
        .take(content_height as usize)
        .enumerate()
    {
        let Some(line) = app.document.lines.get(line_index) else {
            continue;
        };
        let row_area = Rect::new(area.x, content_y + cells(offset), area.width, 1);
        if let Some(thread_id) = app.pull_request_review_line_threads.get(&line_index) {
            review_hits.push(ContentReviewHit {
                area: row_area,
                thread_id: thread_id.clone(),
            });
        }
        match line.kind {
            DiffLineKind::FileHeader => {
                draw_file_header(frame, row_area, line, app, theme);
                if let Some(path) = file_header_path(line) {
                    hits.push(ContentFileHit {
                        area: row_area,
                        path: path.into(),
                    });
                }
            }
            DiffLineKind::FileFooter => {
                draw_file_footer(frame, row_area, theme);
            }
            DiffLineKind::Image => draw_image_line(
                frame,
                row_area,
                line,
                content_height.saturating_sub(cells(offset)),
                theme,
                selected_image_protocol(),
                &mut image_state,
            ),
            _ => draw_unified_line(
                frame,
                row_area,
                line,
                app.horizontal_scroll,
                emphasis.get(&line_index),
                review_line_selected(app, line, None),
                theme,
            ),
        }
    }
    hits
}

#[expect(
    clippy::too_many_arguments,
    reason = "the renderer needs the whole row context in one call"
)]
pub(super) fn draw_unified_line(
    frame: &mut Frame<'_>,
    area: Rect,
    line: &DiffLine,
    horizontal_scroll: usize,
    emphasis: Option<&Range<usize>>,
    selected: bool,
    theme: &Theme,
) {
    let content_area = area;
    let old = line
        .old_line
        .map_or(String::new(), |number| number.to_string());
    let new = line
        .new_line
        .map_or(String::new(), |number| number.to_string());
    let (marker, marker_style) = marker_for(line.kind, theme);
    let mut spans = vec![
        Span::styled(format!("{old:>4} "), Style::default().fg(theme.muted)),
        Span::styled(format!("{new:>4} "), Style::default().fg(theme.muted)),
        Span::styled(marker, marker_style),
    ];
    spans.extend(highlight_spans(
        &line.spans,
        horizontal_scroll,
        content_area.width.saturating_sub(12) as usize,
        line.kind,
        emphasis,
        theme,
    ));
    frame.render_widget(
        Paragraph::new(Line::from(spans)).style(line_background(line.kind, selected, theme)),
        content_area,
    );
}

pub(super) fn file_header_path(line: &DiffLine) -> Option<&str> {
    line.spans
        .first()
        .map(|span| span.text.split("  · ").next().unwrap_or(span.text.as_str()))
}

pub(super) fn draw_file_header(
    frame: &mut Frame<'_>,
    area: Rect,
    line: &DiffLine,
    app: &App,
    theme: &Theme,
) {
    if area.width == 0 {
        return;
    }
    let label = line
        .spans
        .first()
        .map(|span| span.text.as_str())
        .unwrap_or_default();
    let disclosure = if app.preview_files_collapsible() {
        disclosure_glyph(
            file_header_path(line).is_none_or(|path| !app.preview_file_collapsed(path)),
        )
    } else {
        " "
    };
    let additions = line.spans.get(1).map_or("+0", |span| span.text.as_str());
    let deletions = line.spans.get(2).map_or("-0", |span| span.text.as_str());
    let path = file_header_path(line).unwrap_or(label);
    let icon = file_icon_span(Path::new(path), theme);
    let reserved = 10_usize + additions.width() + deletions.width();
    let label_width = (area.width as usize).saturating_sub(reserved);
    let label = if label.width() <= label_width {
        label.to_owned()
    } else {
        truncate_path(path, label_width)
    };
    let fill = (area.width as usize)
        .saturating_sub(reserved)
        .saturating_sub(label.width());
    let selected = file_header_path(line).is_some_and(|path| app.preview_file_selected(path));
    let background = if selected {
        theme.selected
    } else {
        theme.panel_alt
    };
    frame.render_widget(
        Paragraph::new(Line::from(vec![
            Span::styled("─", Style::default().fg(theme.border)),
            Span::styled(format!(" {disclosure} "), Style::default().fg(theme.muted)),
            icon,
            Span::raw(" "),
            Span::styled(
                label,
                Style::default().fg(theme.text).add_modifier(Modifier::BOLD),
            ),
            Span::styled("─".repeat(fill), Style::default().fg(theme.border)),
            Span::styled(" ", Style::default()),
            Span::styled(
                additions.to_owned(),
                Style::default()
                    .fg(theme.added)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(" ", Style::default()),
            Span::styled(
                deletions.to_owned(),
                Style::default()
                    .fg(theme.removed)
                    .add_modifier(Modifier::BOLD),
            ),
            Span::styled(" ─", Style::default().fg(theme.border)),
        ]))
        .style(Style::default().bg(background)),
        area,
    );
}

pub(super) fn draw_file_footer(frame: &mut Frame<'_>, area: Rect, theme: &Theme) {
    if area.width == 0 {
        return;
    }
    frame.render_widget(
        Paragraph::new("─".repeat(area.width as usize))
            .style(Style::default().fg(theme.border).bg(theme.panel)),
        area,
    );
}