use ratatui::text::{Line, Span};
use rho_tools::tool_card::{DiffRow, DiffRowKind};
use super::{
render::{truncate_keep_end, wrap_line_hard},
theme::Theme,
tool_diff::{gutter_width, push_diff_content_row, DiffRowFrame, DiffSyntax},
};
pub(super) fn patch_rows(patch: &str) -> Vec<DiffRow> {
let mut rows = Vec::new();
let mut old_line = 0u32;
let mut new_line = 0u32;
let mut old_left = 0u32;
let mut new_left = 0u32;
for line in patch.lines() {
if old_left > 0 || new_left > 0 {
let (kind, number) = match line.as_bytes().first() {
Some(b'+') => {
new_left = new_left.saturating_sub(1);
new_line += 1;
(DiffRowKind::Added, new_line - 1)
}
Some(b'-') => {
old_left = old_left.saturating_sub(1);
old_line += 1;
(DiffRowKind::Removed, old_line - 1)
}
Some(b'\\') => {
rows.push(DiffRow::new(DiffRowKind::Meta, None, line));
continue;
}
Some(_) | None => {
old_left = old_left.saturating_sub(1);
new_left = new_left.saturating_sub(1);
old_line += 1;
new_line += 1;
(DiffRowKind::Context, new_line - 1)
}
};
rows.push(DiffRow::new(
kind,
Some(number),
line.get(1..).unwrap_or_default(),
));
continue;
}
if let Some(hunk) = HunkHeader::parse(line) {
(old_line, old_left, new_line, new_left) =
(hunk.old_start, hunk.old_len, hunk.new_start, hunk.new_len);
rows.push(DiffRow::new(DiffRowKind::Skip, None, line));
continue;
}
if is_hidden_header(line) {
continue;
}
rows.push(DiffRow::new(DiffRowKind::Meta, None, line));
}
rows
}
pub(super) fn diff_pane_lines(
heading: &str,
path: &str,
rows: &[DiffRow],
width: usize,
) -> Vec<Line<'static>> {
let mut lines = vec![
Line::from(Span::styled(
truncate_keep_end(heading, width),
Theme::tool_path(),
)),
Line::raw(""),
];
if rows.is_empty() {
lines.push(Line::from(Span::styled("no textual changes", Theme::dim())));
return lines;
}
let frame = DiffRowFrame {
indent: "",
gutter: gutter_width(rows),
width,
};
let mut syntax = DiffSyntax::for_file(path);
for row in rows {
let style = match row.kind {
DiffRowKind::Added | DiffRowKind::Removed | DiffRowKind::Context => {
push_diff_content_row(&mut lines, row, frame, &mut syntax);
continue;
}
DiffRowKind::Skip => {
let _ = syntax.paint_row(row);
Theme::accent()
}
DiffRowKind::Meta | DiffRowKind::File => Theme::dim(),
};
lines.extend(
wrap_line_hard(&row.text, width)
.into_iter()
.map(|chunk| Line::from(Span::styled(chunk.to_string(), style))),
);
}
lines
}
pub(super) fn row_stats(rows: &[DiffRow]) -> (u64, u64) {
rows.iter()
.fold((0, 0), |(added, removed), row| match row.kind {
DiffRowKind::Added => (added + 1, removed),
DiffRowKind::Removed => (added, removed + 1),
DiffRowKind::Context | DiffRowKind::File | DiffRowKind::Skip | DiffRowKind::Meta => {
(added, removed)
}
})
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct HunkHeader {
old_start: u32,
old_len: u32,
new_start: u32,
new_len: u32,
}
impl HunkHeader {
fn parse(line: &str) -> Option<Self> {
let (ranges, _) = line.strip_prefix("@@ ")?.split_once(" @@")?;
let (old, new) = ranges.split_once(' ')?;
let range = |range: &str, sign: char| -> Option<(u32, u32)> {
let range = range.strip_prefix(sign)?;
match range.split_once(',') {
Some((start, len)) => Some((start.parse().ok()?, len.parse().ok()?)),
None => Some((range.parse().ok()?, 1)),
}
};
let (old_start, old_len) = range(old, '-')?;
let (new_start, new_len) = range(new, '+')?;
Some(Self {
old_start,
old_len,
new_start,
new_len,
})
}
}
fn is_hidden_header(line: &str) -> bool {
line.starts_with("diff --git ")
|| line.starts_with("index ")
|| line.starts_with("--- ")
|| line.starts_with("+++ ")
}
#[cfg(test)]
#[path = "diff_pane_tests.rs"]
mod tests;