1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47pub enum DiffLineType {
48 Unchanged,
50 Added,
52 Removed,
54 Context,
56}
57
58#[derive(Debug, Clone)]
60pub struct DiffLine {
61 pub line_type: DiffLineType,
63 pub old_line_num: Option<usize>,
65 pub new_line_num: Option<usize>,
67 pub content: String,
69}
70
71impl DiffLine {
72 pub fn new(line_type: DiffLineType, content: impl Into<String>) -> Self {
74 Self {
75 line_type,
76 old_line_num: None,
77 new_line_num: None,
78 content: content.into(),
79 }
80 }
81
82 pub fn with_old_line_num(mut self, num: usize) -> Self {
84 self.old_line_num = Some(num);
85 self
86 }
87
88 pub fn with_new_line_num(mut self, num: usize) -> Self {
90 self.new_line_num = Some(num);
91 self
92 }
93}
94
95#[derive(Debug, Clone)]
97pub struct DiffHunk {
98 pub header: String,
100 pub lines: Vec<DiffLine>,
102 pub collapsed: bool,
104}
105
106impl DiffHunk {
107 pub fn new(header: impl Into<String>) -> Self {
109 Self {
110 header: header.into(),
111 lines: Vec::new(),
112 collapsed: false,
113 }
114 }
115
116 pub fn add_line(&mut self, line: DiffLine) {
118 self.lines.push(line);
119 }
120
121 pub fn toggle_collapsed(&mut self) {
123 self.collapsed = !self.collapsed;
124 }
125
126 pub fn visible_lines(&self) -> Vec<&DiffLine> {
128 if self.collapsed {
129 Vec::new()
130 } else {
131 self.lines.iter().collect()
132 }
133 }
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq)]
138pub enum DiffViewType {
139 Unified,
141 SideBySide,
143}
144
145#[derive(Debug)]
147pub struct DiffWidget {
148 pub hunks: Vec<DiffHunk>,
150 pub view_type: DiffViewType,
152 pub selected_hunk: Option<usize>,
154 pub scroll: usize,
156 pub approvals: Vec<bool>,
158}
159
160impl DiffWidget {
161 pub fn new() -> Self {
163 Self {
164 hunks: Vec::new(),
165 view_type: DiffViewType::Unified,
166 selected_hunk: None,
167 scroll: 0,
168 approvals: Vec::new(),
169 }
170 }
171
172 pub fn add_hunk(&mut self, hunk: DiffHunk) {
174 self.hunks.push(hunk);
175 self.approvals.push(false);
176 }
177
178 pub fn toggle_view_type(&mut self) {
180 self.view_type = match self.view_type {
181 DiffViewType::Unified => DiffViewType::SideBySide,
182 DiffViewType::SideBySide => DiffViewType::Unified,
183 };
184 }
185
186 pub fn select_next_hunk(&mut self) {
188 if self.hunks.is_empty() {
189 return;
190 }
191 match self.selected_hunk {
192 None => self.selected_hunk = Some(0),
193 Some(idx) if idx < self.hunks.len() - 1 => self.selected_hunk = Some(idx + 1),
194 _ => {}
195 }
196 }
197
198 pub fn select_prev_hunk(&mut self) {
200 match self.selected_hunk {
201 None => {}
202 Some(0) => self.selected_hunk = None,
203 Some(idx) => self.selected_hunk = Some(idx - 1),
204 }
205 }
206
207 pub fn toggle_selected_hunk(&mut self) {
209 if let Some(idx) = self.selected_hunk {
210 if let Some(hunk) = self.hunks.get_mut(idx) {
211 hunk.toggle_collapsed();
212 }
213 }
214 }
215
216 pub fn approve_all(&mut self) {
218 for approval in &mut self.approvals {
219 *approval = true;
220 }
221 }
222
223 pub fn reject_all(&mut self) {
225 for approval in &mut self.approvals {
226 *approval = false;
227 }
228 }
229
230 pub fn approve_hunk(&mut self) {
232 if let Some(idx) = self.selected_hunk {
233 if let Some(approval) = self.approvals.get_mut(idx) {
234 *approval = true;
235 }
236 }
237 }
238
239 pub fn reject_hunk(&mut self) {
241 if let Some(idx) = self.selected_hunk {
242 if let Some(approval) = self.approvals.get_mut(idx) {
243 *approval = false;
244 }
245 }
246 }
247
248 pub fn approved_hunks(&self) -> Vec<usize> {
250 self.approvals
251 .iter()
252 .enumerate()
253 .filter_map(|(idx, &approved)| if approved { Some(idx) } else { None })
254 .collect()
255 }
256
257 pub fn rejected_hunks(&self) -> Vec<usize> {
259 self.approvals
260 .iter()
261 .enumerate()
262 .filter_map(|(idx, &approved)| if !approved { Some(idx) } else { None })
263 .collect()
264 }
265
266 pub fn scroll_up(&mut self) {
268 if self.scroll > 0 {
269 self.scroll -= 1;
270 }
271 }
272
273 pub fn scroll_down(&mut self, height: usize) {
275 let total_lines: usize = self.hunks.iter().map(|h| h.visible_lines().len()).sum();
276 let max_scroll = total_lines.saturating_sub(height);
277 if self.scroll < max_scroll {
278 self.scroll += 1;
279 }
280 }
281}
282
283impl Default for DiffWidget {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn test_diff_line_creation() {
295 let line = DiffLine::new(DiffLineType::Added, "let x = 5;");
296 assert_eq!(line.line_type, DiffLineType::Added);
297 assert_eq!(line.content, "let x = 5;");
298 }
299
300 #[test]
301 fn test_diff_line_numbers() {
302 let line = DiffLine::new(DiffLineType::Unchanged, "code")
303 .with_old_line_num(1)
304 .with_new_line_num(1);
305
306 assert_eq!(line.old_line_num, Some(1));
307 assert_eq!(line.new_line_num, Some(1));
308 }
309
310 #[test]
311 fn test_diff_hunk_creation() {
312 let hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
313 assert_eq!(hunk.header, "@@ -1,5 +1,6 @@");
314 assert!(!hunk.collapsed);
315 }
316
317 #[test]
318 fn test_diff_hunk_add_line() {
319 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
320 hunk.add_line(DiffLine::new(DiffLineType::Added, "new line"));
321
322 assert_eq!(hunk.lines.len(), 1);
323 }
324
325 #[test]
326 fn test_diff_hunk_toggle_collapsed() {
327 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
328 assert!(!hunk.collapsed);
329
330 hunk.toggle_collapsed();
331 assert!(hunk.collapsed);
332
333 hunk.toggle_collapsed();
334 assert!(!hunk.collapsed);
335 }
336
337 #[test]
338 fn test_diff_hunk_visible_lines() {
339 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
340 hunk.add_line(DiffLine::new(DiffLineType::Added, "line 1"));
341 hunk.add_line(DiffLine::new(DiffLineType::Removed, "line 2"));
342
343 assert_eq!(hunk.visible_lines().len(), 2);
344
345 hunk.toggle_collapsed();
346 assert_eq!(hunk.visible_lines().len(), 0);
347 }
348
349 #[test]
350 fn test_diff_widget_creation() {
351 let widget = DiffWidget::new();
352 assert!(widget.hunks.is_empty());
353 assert_eq!(widget.view_type, DiffViewType::Unified);
354 }
355
356 #[test]
357 fn test_diff_widget_add_hunk() {
358 let mut widget = DiffWidget::new();
359 let hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
360 widget.add_hunk(hunk);
361
362 assert_eq!(widget.hunks.len(), 1);
363 assert_eq!(widget.approvals.len(), 1);
364 }
365
366 #[test]
367 fn test_diff_widget_toggle_view_type() {
368 let mut widget = DiffWidget::new();
369 assert_eq!(widget.view_type, DiffViewType::Unified);
370
371 widget.toggle_view_type();
372 assert_eq!(widget.view_type, DiffViewType::SideBySide);
373
374 widget.toggle_view_type();
375 assert_eq!(widget.view_type, DiffViewType::Unified);
376 }
377
378 #[test]
379 fn test_diff_widget_hunk_selection() {
380 let mut widget = DiffWidget::new();
381 widget.add_hunk(DiffHunk::new("@@ -1,5 +1,6 @@"));
382 widget.add_hunk(DiffHunk::new("@@ -10,5 +11,6 @@"));
383
384 widget.select_next_hunk();
385 assert_eq!(widget.selected_hunk, Some(0));
386
387 widget.select_next_hunk();
388 assert_eq!(widget.selected_hunk, Some(1));
389
390 widget.select_prev_hunk();
391 assert_eq!(widget.selected_hunk, Some(0));
392 }
393
394 #[test]
395 fn test_diff_widget_approval() {
396 let mut widget = DiffWidget::new();
397 widget.add_hunk(DiffHunk::new("@@ -1,5 +1,6 @@"));
398 widget.add_hunk(DiffHunk::new("@@ -10,5 +11,6 @@"));
399
400 widget.approve_all();
401 assert_eq!(widget.approved_hunks().len(), 2);
402 assert_eq!(widget.rejected_hunks().len(), 0);
403
404 widget.reject_all();
405 assert_eq!(widget.approved_hunks().len(), 0);
406 assert_eq!(widget.rejected_hunks().len(), 2);
407 }
408
409 #[test]
410 fn test_diff_widget_hunk_approval() {
411 let mut widget = DiffWidget::new();
412 widget.add_hunk(DiffHunk::new("@@ -1,5 +1,6 @@"));
413 widget.add_hunk(DiffHunk::new("@@ -10,5 +11,6 @@"));
414
415 widget.select_next_hunk();
416 widget.approve_hunk();
417
418 assert_eq!(widget.approved_hunks().len(), 1);
419 assert_eq!(widget.approved_hunks()[0], 0);
420 }
421
422 #[test]
423 fn test_diff_widget_scroll() {
424 let mut widget = DiffWidget::new();
425 let mut hunk = DiffHunk::new("@@ -1,5 +1,6 @@");
426 for i in 0..20 {
427 hunk.add_line(DiffLine::new(
428 DiffLineType::Unchanged,
429 format!("line {}", i),
430 ));
431 }
432 widget.add_hunk(hunk);
433
434 assert_eq!(widget.scroll, 0);
435
436 widget.scroll_down(10);
437 assert_eq!(widget.scroll, 1);
438
439 widget.scroll_up();
440 assert_eq!(widget.scroll, 0);
441
442 widget.scroll_up();
443 assert_eq!(widget.scroll, 0);
444 }
445}