lv_tui/widgets/
diffview.rs1use crate::component::{Component, EventCx, MeasureCx};
2use crate::event::Event;
3use crate::geom::{Pos, Rect, Size};
4use crate::layout::Constraint;
5use crate::render::RenderCx;
6use crate::style::{Color, Style};
7
8pub struct DiffView {
14 lines: Vec<DiffLine>,
15 scroll: u16,
16 rect: Rect,
17}
18
19#[derive(Debug, Clone)]
20enum DiffLine {
21 Header(String),
22 Hunk(String),
23 Add(u64, String), Del(u64, String), Ctx(u64, String), }
27
28impl DiffView {
29 pub fn new(diff_text: &str) -> Self {
31 let mut lines = Vec::new();
32 let mut old_num: u64 = 0;
33 let mut new_num: u64 = 0;
34
35 for line_str in diff_text.lines() {
36 if line_str.starts_with("@@") {
37 if let Some((o, n)) = parse_hunk(line_str) {
38 old_num = o;
39 new_num = n;
40 }
41 lines.push(DiffLine::Hunk(line_str.to_string()));
42 } else if line_str.starts_with("---") || line_str.starts_with("+++") {
43 lines.push(DiffLine::Header(line_str.to_string()));
44 } else if line_str.starts_with('+') {
45 let text = &line_str[1..];
46 lines.push(DiffLine::Add(new_num, text.to_string()));
47 new_num += 1;
48 } else if line_str.starts_with('-') {
49 let text = &line_str[1..];
50 lines.push(DiffLine::Del(old_num, text.to_string()));
51 old_num += 1;
52 } else {
53 let text = if line_str.starts_with(' ') { &line_str[1..] } else { line_str };
54 lines.push(DiffLine::Ctx(new_num, text.to_string()));
55 old_num += 1;
56 new_num += 1;
57 }
58 }
59 Self { lines, scroll: 0, rect: Rect::default() }
60 }
61
62 pub fn line_count(&self) -> usize { self.lines.len() }
63}
64
65fn parse_hunk(header: &str) -> Option<(u64, u64)> {
66 let parts: Vec<&str> = header.split_whitespace().collect();
67 if parts.len() >= 3 {
68 let old = parts[1].trim_start_matches('-').split(',').next()?.parse().ok()?;
69 let new = parts[2].trim_start_matches('+').split(',').next()?.parse().ok()?;
70 Some((old, new))
71 } else {
72 None
73 }
74}
75
76const NUM_W: u16 = 4;
80const GUTTER_W: u16 = NUM_W + 1; impl Component for DiffView {
83 fn render(&self, cx: &mut RenderCx) {
84 let vp = self.rect;
85 let visible = vp.height.max(1) as usize;
86 let start = (self.scroll as usize).min(self.lines.len().saturating_sub(visible));
87 let end = (start + visible).min(self.lines.len());
88
89 let gray = Style::default().fg(Color::Gray);
90 let add_style = Style::default().fg(Color::Green);
91 let del_style = Style::default().fg(Color::Red);
92 let ctx_style = Style::default();
93
94 for i in start..end {
95 let line = &self.lines[i];
96 let y = vp.y.saturating_add((i - start) as u16);
97
98 match line {
99 DiffLine::Header(s) | DiffLine::Hunk(s) => {
100 cx.buffer.write_text(Pos { x: vp.x, y }, vp, s, &ctx_style);
101 }
102 DiffLine::Add(n, text) => {
103 let num = format!("{:>4}", n);
104 cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &add_style);
105 cx.buffer.write_text(Pos { x: vp.x + 4, y }, vp, " +", &add_style);
106 let code_x = vp.x + GUTTER_W + 2;
107 cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &add_style);
108 }
109 DiffLine::Del(n, text) => {
110 let num = format!("{:>4}", n);
111 cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &del_style);
112 cx.buffer.write_text(Pos { x: vp.x + 4, y }, vp, " -", &del_style);
113 let code_x = vp.x + GUTTER_W + 2;
114 cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &del_style);
115 }
116 DiffLine::Ctx(n, text) => {
117 let num = format!("{:>4}", n);
118 cx.buffer.write_text(Pos { x: vp.x, y }, vp, &num, &gray);
119 let code_x = vp.x + GUTTER_W + 2;
120 cx.buffer.write_text(Pos { x: code_x, y }, vp, text, &ctx_style);
121 }
122 }
123 }
124 }
125
126 fn measure(&self, _c: Constraint, _cx: &mut MeasureCx) -> Size {
127 Size { width: 80, height: self.lines.len().max(1) as u16 }
128 }
129
130 fn event(&mut self, event: &Event, cx: &mut EventCx) {
131 if let Event::Key(key) = event {
132 let visible = self.rect.height.max(1) as u16;
133 let total = self.lines.len().max(1) as u16;
134 let max_scroll = total.saturating_sub(visible);
135 match &key.key {
136 crate::event::Key::Up => { self.scroll = self.scroll.saturating_sub(1); cx.invalidate_paint(); }
137 crate::event::Key::Down => { self.scroll = (self.scroll + 1).min(max_scroll); cx.invalidate_paint(); }
138 crate::event::Key::PageUp => { self.scroll = self.scroll.saturating_sub(visible); cx.invalidate_paint(); }
139 crate::event::Key::PageDown => { self.scroll = (self.scroll + visible).min(max_scroll); cx.invalidate_paint(); }
140 _ => {}
141 }
142 }
143 }
144
145 fn layout(&mut self, rect: Rect, _cx: &mut crate::component::LayoutCx) { self.rect = rect; }
146 fn focusable(&self) -> bool { false }
147 fn style(&self) -> Style { Style::default() }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_parse_diff() {
156 let diff = "--- a.txt\n+++ b.txt\n@@ -1 +1 @@\n-old\n+new";
157 let dv = DiffView::new(diff);
158 assert_eq!(dv.line_count(), 5);
159 }
160
161 #[test]
162 fn test_line_types() {
163 let dv = DiffView::new("-deleted\n+added\n unchanged\n@@ -1 +1 @@");
164 assert_eq!(dv.line_count(), 4);
165 }
166}
167