1use crate::app::App;
4use crate::diff::{DiffLineType, DiffViewType, DiffWidget};
5use crate::style::Theme;
6use ratatui::prelude::*;
7
8pub struct Renderer;
10
11impl Renderer {
12 pub fn new() -> Self {
14 Self
15 }
16
17 pub fn render(&self, _app: &App) -> anyhow::Result<()> {
19 Ok(())
21 }
22
23 pub fn render_diff_unified(
25 &self,
26 diff: &DiffWidget,
27 _area: Rect,
28 _theme: &Theme,
29 ) -> Vec<Line<'static>> {
30 let mut lines = Vec::new();
31
32 let total_lines: usize = diff.hunks.iter().map(|h| h.lines.len()).sum();
34 let added_count = diff
35 .hunks
36 .iter()
37 .flat_map(|h| &h.lines)
38 .filter(|l| l.line_type == DiffLineType::Added)
39 .count();
40 let removed_count = diff
41 .hunks
42 .iter()
43 .flat_map(|h| &h.lines)
44 .filter(|l| l.line_type == DiffLineType::Removed)
45 .count();
46
47 let header = format!(
48 "Unified Diff View | {} lines | +{} -{} | Approved: {}",
49 total_lines,
50 added_count,
51 removed_count,
52 diff.approved_hunks().len()
53 );
54 lines.push(Line::from(header));
55 lines.push(Line::from(""));
56
57 for (hunk_idx, hunk) in diff.hunks.iter().enumerate() {
59 let is_selected = diff.selected_hunk == Some(hunk_idx);
60 let is_approved = diff.approvals.get(hunk_idx).copied().unwrap_or(false);
61
62 let header_style = if is_selected {
64 Style::default().fg(Color::Cyan).bold()
65 } else if is_approved {
66 Style::default().fg(Color::Green)
67 } else {
68 Style::default().fg(Color::Yellow)
69 };
70
71 let approval_indicator = if is_approved { "✓" } else { " " };
72 let collapse_indicator = if hunk.collapsed { "▶" } else { "▼" };
73
74 let hunk_header = format!(
75 "{} {} {} {}",
76 approval_indicator, collapse_indicator, hunk.header, ""
77 );
78 lines.push(Line::from(Span::styled(hunk_header, header_style)));
79
80 if !hunk.collapsed {
82 for line in &hunk.lines {
83 let (prefix, style) = match line.line_type {
84 DiffLineType::Added => ("+", Style::default().fg(Color::Green)),
85 DiffLineType::Removed => ("-", Style::default().fg(Color::Red)),
86 DiffLineType::Context => (" ", Style::default()),
87 DiffLineType::Unchanged => (" ", Style::default()),
88 };
89
90 let line_num_str = match (line.old_line_num, line.new_line_num) {
91 (Some(old), Some(new)) => format!("{:4} {:4}", old, new),
92 (Some(old), None) => format!("{:4} ", old),
93 (None, Some(new)) => format!(" {:4}", new),
94 (None, None) => " ".to_string(),
95 };
96
97 let content = format!("{} {} {}", prefix, line_num_str, line.content);
98 lines.push(Line::from(Span::styled(content, style)));
99 }
100 }
101
102 lines.push(Line::from(""));
103 }
104
105 lines
106 }
107
108 pub fn render_diff_side_by_side(
110 &self,
111 diff: &DiffWidget,
112 area: Rect,
113 _theme: &Theme,
114 ) -> Vec<Line<'static>> {
115 let mut lines = Vec::new();
116
117 let total_lines: usize = diff.hunks.iter().map(|h| h.lines.len()).sum();
119 let added_count = diff
120 .hunks
121 .iter()
122 .flat_map(|h| &h.lines)
123 .filter(|l| l.line_type == DiffLineType::Added)
124 .count();
125 let removed_count = diff
126 .hunks
127 .iter()
128 .flat_map(|h| &h.lines)
129 .filter(|l| l.line_type == DiffLineType::Removed)
130 .count();
131
132 let header = format!(
133 "Side-by-Side Diff View | {} lines | +{} -{} | Approved: {}",
134 total_lines,
135 added_count,
136 removed_count,
137 diff.approved_hunks().len()
138 );
139 lines.push(Line::from(header));
140 lines.push(Line::from(""));
141
142 let col_width = (area.width as usize).saturating_sub(20) / 2;
144 let header_left = format!("Original ({:width$})", "", width = col_width);
145 let header_right = format!("Modified ({:width$})", "", width = col_width);
146 lines.push(Line::from(format!("{} | {}", header_left, header_right)));
147 lines.push(Line::from("─".repeat(area.width as usize)));
148
149 for (hunk_idx, hunk) in diff.hunks.iter().enumerate() {
151 let is_selected = diff.selected_hunk == Some(hunk_idx);
152 let is_approved = diff.approvals.get(hunk_idx).copied().unwrap_or(false);
153
154 let header_style = if is_selected {
156 Style::default().fg(Color::Cyan).bold()
157 } else if is_approved {
158 Style::default().fg(Color::Green)
159 } else {
160 Style::default().fg(Color::Yellow)
161 };
162
163 let approval_indicator = if is_approved { "✓" } else { " " };
164 let collapse_indicator = if hunk.collapsed { "▶" } else { "▼" };
165
166 let hunk_header = format!(
167 "{} {} {}",
168 approval_indicator, collapse_indicator, hunk.header
169 );
170 lines.push(Line::from(Span::styled(hunk_header, header_style)));
171
172 if !hunk.collapsed {
174 for line in &hunk.lines {
175 let (prefix, style) = match line.line_type {
176 DiffLineType::Added => ("+", Style::default().fg(Color::Green)),
177 DiffLineType::Removed => ("-", Style::default().fg(Color::Red)),
178 DiffLineType::Context => (" ", Style::default()),
179 DiffLineType::Unchanged => (" ", Style::default()),
180 };
181
182 let line_num = line.new_line_num.map(|n| n.to_string()).unwrap_or_default();
183 let content = format!("{} {:4} {}", prefix, line_num, line.content);
184
185 let padded = format!("{:<width$} | {}", "", content, width = col_width);
188 lines.push(Line::from(Span::styled(padded, style)));
189 }
190 }
191
192 lines.push(Line::from(""));
193 }
194
195 lines
196 }
197
198 pub fn render_diff(&self, diff: &DiffWidget, area: Rect, theme: &Theme) -> Vec<Line<'static>> {
200 match diff.view_type {
201 DiffViewType::Unified => self.render_diff_unified(diff, area, theme),
202 DiffViewType::SideBySide => self.render_diff_side_by_side(diff, area, theme),
203 }
204 }
205}
206
207impl Default for Renderer {
208 fn default() -> Self {
209 Self::new()
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::diff::{DiffHunk, DiffLine};
217
218 #[test]
219 fn test_renderer_creation() {
220 let renderer = Renderer::new();
221 let default_renderer = Renderer::default();
222 let _ = renderer;
224 let _ = default_renderer;
225 }
226
227 #[test]
228 fn test_render_diff_unified_empty() {
229 let renderer = Renderer::new();
230 let diff = DiffWidget::new();
231 let theme = Theme::default();
232 let area = Rect {
233 x: 0,
234 y: 0,
235 width: 80,
236 height: 24,
237 };
238
239 let lines = renderer.render_diff_unified(&diff, area, &theme);
240 assert!(!lines.is_empty());
241 assert!(lines.len() >= 2);
243 }
244
245 #[test]
246 fn test_render_diff_unified_with_hunks() {
247 let renderer = Renderer::new();
248 let mut diff = DiffWidget::new();
249
250 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
251 hunk.add_line(
252 DiffLine::new(DiffLineType::Unchanged, "let x = 5;")
253 .with_old_line_num(1)
254 .with_new_line_num(1),
255 );
256 hunk.add_line(DiffLine::new(DiffLineType::Added, "let y = 10;").with_new_line_num(2));
257 hunk.add_line(DiffLine::new(DiffLineType::Removed, "let z = 15;").with_old_line_num(2));
258
259 diff.add_hunk(hunk);
260
261 let theme = Theme::default();
262 let area = Rect {
263 x: 0,
264 y: 0,
265 width: 80,
266 height: 24,
267 };
268
269 let lines = renderer.render_diff_unified(&diff, area, &theme);
270 assert!(!lines.is_empty());
271 assert!(lines.len() > 3);
273 }
274
275 #[test]
276 fn test_render_diff_unified_with_collapsed_hunk() {
277 let renderer = Renderer::new();
278 let mut diff = DiffWidget::new();
279
280 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
281 hunk.add_line(DiffLine::new(DiffLineType::Added, "new line"));
282 hunk.toggle_collapsed();
283
284 diff.add_hunk(hunk);
285
286 let theme = Theme::default();
287 let area = Rect {
288 x: 0,
289 y: 0,
290 width: 80,
291 height: 24,
292 };
293
294 let lines = renderer.render_diff_unified(&diff, area, &theme);
295 assert!(!lines.is_empty());
296 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
298 assert!(!content.contains("new line"));
299 }
300
301 #[test]
302 fn test_render_diff_unified_with_approval() {
303 let renderer = Renderer::new();
304 let mut diff = DiffWidget::new();
305
306 let hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
307 diff.add_hunk(hunk);
308 diff.approve_all();
309
310 let theme = Theme::default();
311 let area = Rect {
312 x: 0,
313 y: 0,
314 width: 80,
315 height: 24,
316 };
317
318 let lines = renderer.render_diff_unified(&diff, area, &theme);
319 assert!(!lines.is_empty());
320 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
322 assert!(content.contains("✓"));
323 }
324
325 #[test]
326 fn test_render_diff_side_by_side_empty() {
327 let renderer = Renderer::new();
328 let diff = DiffWidget::new();
329 let theme = Theme::default();
330 let area = Rect {
331 x: 0,
332 y: 0,
333 width: 160,
334 height: 24,
335 };
336
337 let lines = renderer.render_diff_side_by_side(&diff, area, &theme);
338 assert!(!lines.is_empty());
339 assert!(lines.len() >= 3);
341 }
342
343 #[test]
344 fn test_render_diff_side_by_side_with_hunks() {
345 let renderer = Renderer::new();
346 let mut diff = DiffWidget::new();
347
348 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
349 hunk.add_line(DiffLine::new(DiffLineType::Added, "new line").with_new_line_num(1));
350 hunk.add_line(DiffLine::new(DiffLineType::Removed, "old line").with_old_line_num(1));
351
352 diff.add_hunk(hunk);
353
354 let theme = Theme::default();
355 let area = Rect {
356 x: 0,
357 y: 0,
358 width: 160,
359 height: 24,
360 };
361
362 let lines = renderer.render_diff_side_by_side(&diff, area, &theme);
363 assert!(!lines.is_empty());
364 assert!(lines.len() > 4);
366 }
367
368 #[test]
369 fn test_render_diff_unified_view_type() {
370 let renderer = Renderer::new();
371 let mut diff = DiffWidget::new();
372
373 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
374 hunk.add_line(DiffLine::new(DiffLineType::Added, "line"));
375 diff.add_hunk(hunk);
376
377 let theme = Theme::default();
378 let area = Rect {
379 x: 0,
380 y: 0,
381 width: 80,
382 height: 24,
383 };
384
385 assert_eq!(diff.view_type, DiffViewType::Unified);
386 let lines = renderer.render_diff(&diff, area, &theme);
387 assert!(!lines.is_empty());
388 }
389
390 #[test]
391 fn test_render_diff_side_by_side_view_type() {
392 let renderer = Renderer::new();
393 let mut diff = DiffWidget::new();
394
395 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
396 hunk.add_line(DiffLine::new(DiffLineType::Added, "line"));
397 diff.add_hunk(hunk);
398 diff.toggle_view_type();
399
400 let theme = Theme::default();
401 let area = Rect {
402 x: 0,
403 y: 0,
404 width: 160,
405 height: 24,
406 };
407
408 assert_eq!(diff.view_type, DiffViewType::SideBySide);
409 let lines = renderer.render_diff(&diff, area, &theme);
410 assert!(!lines.is_empty());
411 }
412
413 #[test]
414 fn test_render_diff_with_multiple_hunks() {
415 let renderer = Renderer::new();
416 let mut diff = DiffWidget::new();
417
418 for i in 0..3 {
419 let mut hunk = DiffHunk::new(&format!("@@ -{},{} +{},{} @@", i * 5, 5, i * 5, 5));
420 hunk.add_line(DiffLine::new(DiffLineType::Added, format!("line {}", i)));
421 diff.add_hunk(hunk);
422 }
423
424 let theme = Theme::default();
425 let area = Rect {
426 x: 0,
427 y: 0,
428 width: 80,
429 height: 24,
430 };
431
432 let lines = renderer.render_diff_unified(&diff, area, &theme);
433 assert!(!lines.is_empty());
434 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
436 assert!(content.contains("@@"));
437 }
438
439 #[test]
440 fn test_render_diff_line_numbers() {
441 let renderer = Renderer::new();
442 let mut diff = DiffWidget::new();
443
444 let mut hunk = DiffHunk::new("@@ -10,5 +20,6 @@");
445 hunk.add_line(
446 DiffLine::new(DiffLineType::Unchanged, "code")
447 .with_old_line_num(10)
448 .with_new_line_num(20),
449 );
450 diff.add_hunk(hunk);
451
452 let theme = Theme::default();
453 let area = Rect {
454 x: 0,
455 y: 0,
456 width: 80,
457 height: 24,
458 };
459
460 let lines = renderer.render_diff_unified(&diff, area, &theme);
461 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
462 assert!(content.contains("10") || content.contains("20"));
464 }
465
466 #[test]
467 fn test_render_diff_added_removed_lines() {
468 let renderer = Renderer::new();
469 let mut diff = DiffWidget::new();
470
471 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
472 hunk.add_line(DiffLine::new(DiffLineType::Added, "added line"));
473 hunk.add_line(DiffLine::new(DiffLineType::Removed, "removed line"));
474 hunk.add_line(DiffLine::new(DiffLineType::Unchanged, "unchanged line"));
475 diff.add_hunk(hunk);
476
477 let theme = Theme::default();
478 let area = Rect {
479 x: 0,
480 y: 0,
481 width: 80,
482 height: 24,
483 };
484
485 let lines = renderer.render_diff_unified(&diff, area, &theme);
486 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
487 assert!(content.contains("added line"));
488 assert!(content.contains("removed line"));
489 assert!(content.contains("unchanged line"));
490 }
491
492 #[test]
493 fn test_render_diff_selected_hunk() {
494 let renderer = Renderer::new();
495 let mut diff = DiffWidget::new();
496
497 let hunk1 = DiffHunk::new("@@ -1,5 +1,6 @@");
498 let hunk2 = DiffHunk::new("@@ -10,5 +11,6 @@");
499 diff.add_hunk(hunk1);
500 diff.add_hunk(hunk2);
501
502 diff.select_next_hunk();
503 assert_eq!(diff.selected_hunk, Some(0));
504
505 let theme = Theme::default();
506 let area = Rect {
507 x: 0,
508 y: 0,
509 width: 80,
510 height: 24,
511 };
512
513 let lines = renderer.render_diff_unified(&diff, area, &theme);
514 assert!(!lines.is_empty());
515 }
516
517 #[test]
518 fn test_render_diff_stats() {
519 let renderer = Renderer::new();
520 let mut diff = DiffWidget::new();
521
522 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
523 hunk.add_line(DiffLine::new(DiffLineType::Added, "line1"));
524 hunk.add_line(DiffLine::new(DiffLineType::Added, "line2"));
525 hunk.add_line(DiffLine::new(DiffLineType::Removed, "line3"));
526 diff.add_hunk(hunk);
527
528 let theme = Theme::default();
529 let area = Rect {
530 x: 0,
531 y: 0,
532 width: 80,
533 height: 24,
534 };
535
536 let lines = renderer.render_diff_unified(&diff, area, &theme);
537 let content = lines.iter().map(|l| l.to_string()).collect::<String>();
538 assert!(content.contains("+2") || content.contains("-1"));
540 }
541}