use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Pos, Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::{Color, Style};
pub struct DiffView {
lines: Vec<DiffLine>,
scroll: u16,
rect: Rect,
}
#[derive(Debug, Clone)]
enum DiffLine {
Header(String),
Hunk(String),
Add(u64, String), Del(u64, String), Ctx(u64, String), }
impl DiffView {
pub fn new(diff_text: &str) -> Self {
let mut lines = Vec::new();
let mut old_num: u64 = 0;
let mut new_num: u64 = 0;
for line_str in diff_text.lines() {
if line_str.starts_with("@@") {
if let Some((o, n)) = parse_hunk(line_str) {
old_num = o;
new_num = n;
}
lines.push(DiffLine::Hunk(line_str.to_string()));
} else if line_str.starts_with("---") || line_str.starts_with("+++") {
lines.push(DiffLine::Header(line_str.to_string()));
} else if line_str.starts_with('+') {
let text = &line_str[1..];
lines.push(DiffLine::Add(new_num, text.to_string()));
new_num += 1;
} else if line_str.starts_with('-') {
let text = &line_str[1..];
lines.push(DiffLine::Del(old_num, text.to_string()));
old_num += 1;
} else {
let text = if line_str.starts_with(' ') { &line_str[1..] } else { line_str };
lines.push(DiffLine::Ctx(new_num, text.to_string()));
old_num += 1;
new_num += 1;
}
}
Self { lines, scroll: 0, rect: Rect::default() }
}
pub fn line_count(&self) -> usize { self.lines.len() }
}
fn parse_hunk(header: &str) -> Option<(u64, u64)> {
let parts: Vec<&str> = header.split_whitespace().collect();
if parts.len() >= 3 {
let old = parts[1].trim_start_matches('-').split(',').next()?.parse().ok()?;
let new = parts[2].trim_start_matches('+').split(',').next()?.parse().ok()?;
Some((old, new))
} else {
None
}
}
const NUM_W: u16 = 4;
const GUTTER_W: u16 = NUM_W + 1;
impl Component for DiffView {
fn render(&self, cx: &mut RenderCx) {
let vp = self.rect;
let visible = vp.height.max(1) as usize;
let start = (self.scroll as usize).min(self.lines.len().saturating_sub(visible));
let end = (start + visible).min(self.lines.len());
let gray = Style::default().fg(Color::Gray);
let add_style = Style::default().fg(Color::Green);
let del_style = Style::default().fg(Color::Red);
let ctx_style = Style::default();
for i in start..end {
let line = &self.lines[i];
let y = vp.y.saturating_add((i - start) as u16);
match line {
DiffLine::Header(s) | DiffLine::Hunk(s) => {
cx.buffer.write_text(Pos { x: vp.x, y }, vp, s, &ctx_style);
}
DiffLine::Add(n, text) => {
let num = format!("{:>4}", n);
cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &add_style);
cx.buffer.write_text(Pos { x: vp.x + 4, y }, vp, " +", &add_style);
let code_x = vp.x + GUTTER_W + 2;
cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &add_style);
}
DiffLine::Del(n, text) => {
let num = format!("{:>4}", n);
cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &del_style);
cx.buffer.write_text(Pos { x: vp.x + 4, y }, vp, " -", &del_style);
let code_x = vp.x + GUTTER_W + 2;
cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &del_style);
}
DiffLine::Ctx(n, text) => {
let num = format!("{:>4}", n);
cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &gray);
let code_x = vp.x + GUTTER_W + 2;
cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &ctx_style);
}
}
}
}
fn measure(&self, _c: Constraint, _cx: &mut MeasureCx) -> Size {
Size { width: 80, height: self.lines.len().max(1) as u16 }
}
fn event(&mut self, event: &Event, cx: &mut EventCx) {
if let Event::Key(key) = event {
let visible = self.rect.height.max(1) as u16;
let total = self.lines.len().max(1) as u16;
let max_scroll = total.saturating_sub(visible);
match &key.key {
crate::event::Key::Up => { self.scroll = self.scroll.saturating_sub(1); cx.invalidate_paint(); }
crate::event::Key::Down => { self.scroll = (self.scroll + 1).min(max_scroll); cx.invalidate_paint(); }
crate::event::Key::PageUp => { self.scroll = self.scroll.saturating_sub(visible); cx.invalidate_paint(); }
crate::event::Key::PageDown => { self.scroll = (self.scroll + visible).min(max_scroll); cx.invalidate_paint(); }
_ => {}
}
}
}
fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) { self.rect = rect; }
fn focusable(&self) -> bool { false }
fn style(&self) -> Style { Style::default() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_diff() {
let diff = "--- a.txt\n+++ b.txt\n@@ -1 +1 @@\n-old\n+new";
let dv = DiffView::new(diff);
assert_eq!(dv.line_count(), 5);
}
#[test]
fn test_line_types() {
let dv = DiffView::new("-deleted\n+added\n unchanged\n@@ -1 +1 @@");
assert_eq!(dv.line_count(), 4);
}
}