1mod algorithm;
10mod cancellation;
11mod inline;
12mod line_builder;
13mod output;
14mod provenance;
15
16pub use algorithm::{compute_four_way_diff, DiffInput};
17pub use inline::InlineSpan;
18
19pub(crate) use inline::compute_inline_diff_merged;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum LineSource {
23 Base,
24 Committed,
25 Staged,
26 Unstaged,
27 DeletedBase,
28 DeletedCommitted,
29 DeletedStaged,
30 CanceledCommitted,
31 CanceledStaged,
32 FileHeader,
33 Elided,
34}
35
36impl LineSource {
37 pub fn is_change(self) -> bool {
39 matches!(
40 self,
41 Self::Committed
42 | Self::Staged
43 | Self::Unstaged
44 | Self::DeletedBase
45 | Self::DeletedCommitted
46 | Self::DeletedStaged
47 | Self::CanceledCommitted
48 | Self::CanceledStaged
49 )
50 }
51
52 pub fn is_addition(self) -> bool {
54 matches!(self, Self::Committed | Self::Staged | Self::Unstaged)
55 }
56
57 pub fn is_deletion(self) -> bool {
59 matches!(
60 self,
61 Self::DeletedBase | Self::DeletedCommitted | Self::DeletedStaged
62 )
63 }
64
65 pub fn is_unstaged(self) -> bool {
67 matches!(self, Self::Unstaged)
68 }
69
70 pub fn is_header(self) -> bool {
72 matches!(self, Self::FileHeader)
73 }
74
75 pub fn is_current_commit(self) -> bool {
79 matches!(
80 self,
81 Self::Staged | Self::DeletedCommitted | Self::CanceledStaged
82 )
83 }
84}
85
86#[derive(Debug, Clone)]
87pub struct DiffLine {
88 pub source: LineSource,
89 pub content: String,
90 pub prefix: char,
91 pub line_number: Option<usize>,
92 pub file_path: Option<String>,
93 pub inline_spans: Vec<InlineSpan>,
94 pub old_content: Option<String>,
95 pub change_source: Option<LineSource>,
96 pub in_current_bookmark: Option<bool>,
99}
100
101impl DiffLine {
102 pub fn new(source: LineSource, content: String, prefix: char, line_number: Option<usize>) -> Self {
103 Self {
104 source,
105 content,
106 prefix,
107 line_number,
108 file_path: None,
109 inline_spans: Vec::new(),
110 old_content: None,
111 change_source: None,
112 in_current_bookmark: None,
113 }
114 }
115
116 pub fn with_old_content(mut self, old: &str) -> Self {
117 self.old_content = Some(old.to_string());
118 self
119 }
120
121 pub fn with_change_source(mut self, change_source: LineSource) -> Self {
122 self.change_source = Some(change_source);
123 self
124 }
125
126 pub fn is_change(&self) -> bool {
127 self.source.is_change() || self.change_source.is_some_and(|cs| cs.is_change())
128 }
129
130 pub fn is_current_commit(&self) -> bool {
133 self.source.is_current_commit()
134 || self.change_source.is_some_and(|cs| cs.is_current_commit())
135 }
136
137 pub fn is_current_bookmark(&self) -> bool {
139 self.in_current_bookmark == Some(true)
140 }
141
142 pub fn ensure_inline_spans(&mut self) {
143 if self.inline_spans.is_empty()
144 && let Some(ref old) = self.old_content
145 {
146 let source = self.change_source.unwrap_or(self.source);
147 let result = compute_inline_diff_merged(old, &self.content, source);
148 self.inline_spans = result.spans;
149 }
150 }
151
152 pub fn with_file_path(mut self, path: &str) -> Self {
153 self.file_path = Some(path.to_string());
154 self
155 }
156
157 pub fn is_image_marker(&self) -> bool {
159 self.content == "[image]" && self.file_path.is_some()
160 }
161}
162#[derive(Debug)]
172pub struct FileDiff {
173 pub lines: Vec<DiffLine>,
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179
180 #[test]
182 fn test_line_source_is_change() {
183 assert!(LineSource::Committed.is_change());
185 assert!(LineSource::Staged.is_change());
186 assert!(LineSource::Unstaged.is_change());
187 assert!(LineSource::DeletedBase.is_change());
188 assert!(LineSource::DeletedCommitted.is_change());
189 assert!(LineSource::DeletedStaged.is_change());
190 assert!(LineSource::CanceledCommitted.is_change());
191 assert!(LineSource::CanceledStaged.is_change());
192
193 assert!(!LineSource::Base.is_change());
195 assert!(!LineSource::FileHeader.is_change());
196 assert!(!LineSource::Elided.is_change());
197 }
198
199 #[test]
200 fn test_line_source_is_addition() {
201 assert!(LineSource::Committed.is_addition());
202 assert!(LineSource::Staged.is_addition());
203 assert!(LineSource::Unstaged.is_addition());
204
205 assert!(!LineSource::Base.is_addition());
206 assert!(!LineSource::DeletedBase.is_addition());
207 assert!(!LineSource::CanceledCommitted.is_addition());
208 }
209
210 #[test]
211 fn test_line_source_is_deletion() {
212 assert!(LineSource::DeletedBase.is_deletion());
213 assert!(LineSource::DeletedCommitted.is_deletion());
214 assert!(LineSource::DeletedStaged.is_deletion());
215
216 assert!(!LineSource::Base.is_deletion());
217 assert!(!LineSource::Committed.is_deletion());
218 assert!(!LineSource::CanceledCommitted.is_deletion());
219 }
220
221 #[test]
222 fn test_line_source_is_unstaged() {
223 assert!(LineSource::Unstaged.is_unstaged());
224
225 assert!(!LineSource::Base.is_unstaged());
226 assert!(!LineSource::Committed.is_unstaged());
227 assert!(!LineSource::Staged.is_unstaged());
228 }
229
230 #[test]
231 fn test_line_source_is_header() {
232 assert!(LineSource::FileHeader.is_header());
233
234 assert!(!LineSource::Base.is_header());
235 assert!(!LineSource::Committed.is_header());
236 assert!(!LineSource::Elided.is_header());
237 }
238
239 #[test]
240 fn test_line_source_is_current_commit() {
241 assert!(LineSource::Staged.is_current_commit());
242 assert!(LineSource::DeletedCommitted.is_current_commit());
243 assert!(LineSource::CanceledStaged.is_current_commit());
244
245 assert!(!LineSource::Base.is_current_commit());
246 assert!(!LineSource::Committed.is_current_commit());
247 assert!(!LineSource::Unstaged.is_current_commit());
248 assert!(!LineSource::DeletedBase.is_current_commit());
249 assert!(!LineSource::DeletedStaged.is_current_commit());
250 assert!(!LineSource::CanceledCommitted.is_current_commit());
251 assert!(!LineSource::FileHeader.is_current_commit());
252 assert!(!LineSource::Elided.is_current_commit());
253 }
254
255 #[test]
256 fn test_diff_line_is_current_commit_via_source() {
257 let staged = DiffLine::new(LineSource::Staged, "added".to_string(), '+', None);
258 assert!(staged.is_current_commit());
259
260 let del = DiffLine::new(LineSource::DeletedCommitted, "removed".to_string(), '-', None);
261 assert!(del.is_current_commit());
262
263 let base = DiffLine::new(LineSource::Base, "context".to_string(), ' ', None);
264 assert!(!base.is_current_commit());
265
266 let committed = DiffLine::new(LineSource::Committed, "earlier".to_string(), '+', None);
267 assert!(!committed.is_current_commit());
268 }
269
270 #[test]
271 fn test_diff_line_is_current_commit_via_change_source() {
272 let mut base_with_staged_mod = DiffLine::new(LineSource::Base, "modified".to_string(), ' ', Some(1));
273 base_with_staged_mod.change_source = Some(LineSource::Staged);
274 assert!(base_with_staged_mod.is_current_commit());
275
276 let mut base_with_committed_mod = DiffLine::new(LineSource::Base, "modified".to_string(), ' ', Some(1));
277 base_with_committed_mod.change_source = Some(LineSource::Committed);
278 assert!(!base_with_committed_mod.is_current_commit());
279
280 let mut base_with_unstaged_mod = DiffLine::new(LineSource::Base, "modified".to_string(), ' ', Some(1));
281 base_with_unstaged_mod.change_source = Some(LineSource::Unstaged);
282 assert!(!base_with_unstaged_mod.is_current_commit());
283 }
284
285 #[test]
286 fn test_is_current_bookmark() {
287 let mut line = DiffLine::new(LineSource::Committed, "test".to_string(), '+', None);
288 assert!(!line.is_current_bookmark(), "None should be false");
289
290 line.in_current_bookmark = Some(true);
291 assert!(line.is_current_bookmark(), "Some(true) should be true");
292
293 line.in_current_bookmark = Some(false);
294 assert!(!line.is_current_bookmark(), "Some(false) should be false");
295 }
296
297 fn compute_diff_with_inline(
298 path: &str,
299 base: Option<&str>,
300 head: Option<&str>,
301 index: Option<&str>,
302 working: Option<&str>,
303 ) -> FileDiff {
304 let mut diff = compute_four_way_diff(DiffInput {
305 path,
306 base,
307 head,
308 index,
309 working,
310 old_path: None,
311 });
312 for line in &mut diff.lines {
313 line.ensure_inline_spans();
314 }
315 diff
316 }
317
318 fn content_lines(diff: &FileDiff) -> Vec<&DiffLine> {
319 diff.lines.iter().filter(|l| !l.source.is_header()).collect()
320 }
321
322 #[test]
323 fn test_compute_file_diff_with_rename() {
324 let content = "line1\nline2";
325 let diff = compute_four_way_diff(DiffInput {
326 path: "new/path.rs",
327 base: Some(content),
328 head: Some(content),
329 index: Some(content),
330 working: Some(content),
331 old_path: Some("old/path.rs"),
332 });
333 assert_eq!(diff.lines[0].source, LineSource::FileHeader);
334 assert_eq!(diff.lines[0].content, "old/path.rs → new/path.rs");
335 }
336
337 #[test]
338 fn test_renamed_file_with_content_change() {
339 let original = "line 1\nline 2\nline 3\nline 4\nline 5";
342 let modified = "line 1\nline 2 modified\nline 3\nline 4\nline 5";
343
344 let diff = compute_four_way_diff(DiffInput {
345 path: "renamed.txt",
346 base: Some(original), head: Some(original), index: Some(original), working: Some(modified), old_path: Some("original.txt"),
351 });
352
353 let modified_lines: Vec<_> = diff
355 .lines
356 .iter()
357 .filter(|l| l.old_content.is_some() || l.change_source.is_some())
358 .collect();
359
360 assert!(
361 !modified_lines.is_empty(),
362 "Expected at least one modified line"
363 );
364
365 let mod_line = modified_lines
367 .iter()
368 .find(|l| l.content.contains("line 2 modified"))
369 .expect("Should have modification for line 2");
370
371 assert_eq!(
372 mod_line.old_content.as_deref(),
373 Some("line 2"),
374 "Should track original content"
375 );
376 assert_eq!(
377 mod_line.change_source,
378 Some(LineSource::Unstaged),
379 "Should mark as unstaged modification"
380 );
381 }
382
383 #[test]
384 fn test_committed_rename_with_content_change() {
385 let original = "line 1\nline 2\nline 3";
388 let modified = "line 1\nline 2 modified\nline 3";
389
390 let diff = compute_four_way_diff(DiffInput {
391 path: "renamed.txt",
392 base: Some(original), head: Some(modified), index: Some(modified), working: Some(modified), old_path: Some("original.txt"),
397 });
398
399 let modified_lines: Vec<_> = diff
401 .lines
402 .iter()
403 .filter(|l| l.old_content.is_some() || l.change_source.is_some())
404 .collect();
405
406 assert!(
407 !modified_lines.is_empty(),
408 "Expected modification to be tracked"
409 );
410
411 let mod_line = modified_lines
412 .iter()
413 .find(|l| l.content.contains("line 2 modified"))
414 .expect("Should have modification for line 2");
415
416 assert_eq!(
417 mod_line.change_source,
418 Some(LineSource::Committed),
419 "Should mark as committed modification"
420 );
421 }
422
423 #[test]
424 fn test_staged_rename_with_content_change() {
425 let original = "line 1\nline 2\nline 3";
428 let modified = "line 1\nline 2 modified\nline 3";
429
430 let diff = compute_four_way_diff(DiffInput {
431 path: "renamed.txt",
432 base: Some(original), head: Some(original), index: Some(modified), working: Some(modified), old_path: Some("original.txt"),
437 });
438
439 let modified_lines: Vec<_> = diff
441 .lines
442 .iter()
443 .filter(|l| l.old_content.is_some() || l.change_source.is_some())
444 .collect();
445
446 assert!(
447 !modified_lines.is_empty(),
448 "Expected modification to be tracked"
449 );
450
451 let mod_line = modified_lines
452 .iter()
453 .find(|l| l.content.contains("line 2 modified"))
454 .expect("Should have modification for line 2");
455
456 assert_eq!(
457 mod_line.change_source,
458 Some(LineSource::Staged),
459 "Should mark as staged modification"
460 );
461 }
462
463 #[test]
464 fn test_pure_rename_no_content_change() {
465 let content = "line 1\nline 2\nline 3";
467
468 let diff = compute_four_way_diff(DiffInput {
469 path: "renamed.txt",
470 base: Some(content),
471 head: Some(content),
472 index: Some(content),
473 working: Some(content),
474 old_path: Some("original.txt"),
475 });
476
477 let modified_lines: Vec<_> = diff
479 .lines
480 .iter()
481 .filter(|l| l.old_content.is_some() || l.change_source.is_some())
482 .collect();
483
484 assert!(
485 modified_lines.is_empty(),
486 "Pure rename should have no modifications, got {:?}",
487 modified_lines
488 );
489
490 assert_eq!(diff.lines[0].source, LineSource::FileHeader);
492 assert_eq!(diff.lines[0].content, "original.txt → renamed.txt");
493 }
494
495 #[test]
496 fn test_canceled_committed_line() {
497 let base = "line1\nline2";
498 let head = "line1\nline2\ncommitted_line";
499 let working = "line1\nline2";
500
501 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(working));
502
503 let canceled_lines: Vec<_> = diff.lines.iter()
504 .filter(|l| l.source == LineSource::CanceledCommitted)
505 .collect();
506
507 assert_eq!(canceled_lines.len(), 1);
508 assert_eq!(canceled_lines[0].content, "committed_line");
509 assert_eq!(canceled_lines[0].prefix, '±');
510 }
511
512 #[test]
513 fn test_canceled_staged_line() {
514 let base = "line1\nline2";
515 let index = "line1\nline2\nstaged_line";
516 let working = "line1\nline2";
517
518 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(index), Some(working));
519
520 let canceled_lines: Vec<_> = diff.lines.iter()
521 .filter(|l| l.source == LineSource::CanceledStaged)
522 .collect();
523
524 assert_eq!(canceled_lines.len(), 1);
525 assert_eq!(canceled_lines[0].content, "staged_line");
526 assert_eq!(canceled_lines[0].prefix, '±');
527 }
528
529 #[test]
530 fn test_committed_then_modified_not_canceled() {
531 let base = "line1\nline2";
532 let head = "line1\nline2\nversion1";
533 let working = "line1\nline2\nversion2";
534
535 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(working));
536
537 let canceled_lines: Vec<_> = diff.lines.iter()
538 .filter(|l| l.source == LineSource::CanceledCommitted)
539 .collect();
540
541 assert_eq!(canceled_lines.len(), 0, "modified line should not be canceled");
542 }
543
544 #[test]
545 fn test_staged_then_modified_not_canceled() {
546 let base = "line1\nline2";
547 let index = "line1\nline2\nversion1";
548 let working = "line1\nline2\nversion2";
549
550 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(index), Some(working));
551
552 let canceled_lines: Vec<_> = diff.lines.iter()
553 .filter(|l| l.source == LineSource::CanceledStaged)
554 .collect();
555
556 assert_eq!(canceled_lines.len(), 0, "modified line should not be canceled");
557 }
558
559 #[test]
560 fn test_modified_line_shows_merged_with_inline_spans() {
561 let base = "line1\nold content\nline3";
562 let working = "line1\nnew content\nline3";
563
564 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
565 let lines = content_lines(&diff);
566
567 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
568
569 assert_eq!(with_spans.len(), 1);
570 assert_eq!(with_spans[0].content, "new content");
571 assert!(with_spans[0].prefix == ' ');
572 }
573
574 #[test]
575 fn test_modified_line_position_preserved() {
576 let base = "before\nprocess_data(input)\nafter";
577 let working = "before\nprocess_data(input, options)\nafter";
578
579 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
580 let lines = content_lines(&diff);
581
582 let contents: Vec<_> = lines.iter().map(|l| l.content.as_str()).collect();
583 assert_eq!(contents, vec!["before", "process_data(input, options)", "after"]);
584
585 let modified = lines.iter().find(|l| l.content == "process_data(input, options)").unwrap();
586 assert!(!modified.inline_spans.is_empty());
587 }
588
589 #[test]
590 fn test_multiple_modifications_show_merged() {
591 let base = "line1\nprocess_item(data1)\nline3\nprocess_item(data2)\nline5";
592 let working = "line1\nprocess_item(data1, options)\nline3\nprocess_item(data2, options)\nline5";
593
594 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
595 let lines = content_lines(&diff);
596
597 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
598
599 assert_eq!(with_spans.len(), 2);
600 assert_eq!(with_spans[0].content, "process_item(data1, options)");
601 assert_eq!(with_spans[1].content, "process_item(data2, options)");
602 }
603
604 #[test]
605 fn test_committed_modification_shows_merged() {
606 let base = "line1\nfunction getData()\nline3";
607 let head = "line1\nfunction getData(params)\nline3";
608
609 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
610 let lines = content_lines(&diff);
611
612 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
613 assert_eq!(with_spans.len(), 1);
614 assert_eq!(with_spans[0].content, "function getData(params)");
615
616 let changed: Vec<_> = with_spans[0].inline_spans.iter()
617 .filter(|s| s.source == Some(LineSource::Committed))
618 .collect();
619 assert!(!changed.is_empty());
620 }
621
622 #[test]
623 fn test_committed_modification_with_additions_before() {
624 let base = r#"layout {
627 pane size=1 borderless=true {
628 plugin location="tab-bar"
629 }
630 pane split_direction="vertical" {
631 pane split_direction="horizontal" size="50%" {
632 pane size="70%" {
633 command "claude"
634 }
635 pane size="30%" {
636 }
637 }
638 pane size="50%" {
639 command "branchdiff"
640 }
641 }
642 pane size=1 borderless=true {
643 plugin location="status-bar"
644 }
645}"#;
646
647 let head = r#"keybinds {
648 unbind "Alt f"
649}
650
651layout {
652 pane size=1 borderless=true {
653 plugin location="tab-bar"
654 }
655 pane split_direction="vertical" {
656 pane split_direction="horizontal" size="50%" {
657 pane size="80%" {
658 command "claude"
659 }
660 pane size="20%" {
661 }
662 }
663 pane size="50%" {
664 command "branchdiff"
665 }
666 }
667 pane size=1 borderless=true {
668 plugin location="status-bar"
669 }
670}"#;
671
672 let diff = compute_diff_with_inline("workon.kdl", Some(base), Some(head), Some(head), Some(head));
673 let lines = content_lines(&diff);
674
675 let additions: Vec<_> = lines.iter()
677 .filter(|l| l.source == LineSource::Committed && l.prefix == '+')
678 .collect();
679 assert!(additions.iter().any(|l| l.content.contains("keybinds")),
680 "Should have committed addition for 'keybinds', got additions: {:?}",
681 additions.iter().map(|l| &l.content).collect::<Vec<_>>());
682
683 let modified_line = lines.iter()
686 .find(|l| l.content.contains("80%"));
687 assert!(modified_line.is_some(), "Should have a line containing '80%'");
688
689 let modified = modified_line.unwrap();
690 assert_eq!(modified.source, LineSource::Base,
692 "Modified line '{}' should be Base (with inline highlighting), not {:?}",
693 modified.content, modified.source);
694 assert!(modified.old_content.is_some(),
696 "Modified line should have old_content set");
697 assert!(!modified.inline_spans.is_empty(),
698 "Modified line '{}' should have inline spans showing the change from 70% to 80%",
699 modified.content);
700
701 let modified_line_2 = lines.iter()
703 .find(|l| l.content.contains("20%"));
704 assert!(modified_line_2.is_some(), "Should have a line containing '20%'");
705
706 let modified2 = modified_line_2.unwrap();
707 assert_eq!(modified2.source, LineSource::Base,
708 "Modified line '{}' should be Base (with inline highlighting), not {:?}",
709 modified2.content, modified2.source);
710 assert!(modified2.old_content.is_some(),
711 "Modified line should have old_content set");
712 assert!(!modified2.inline_spans.is_empty(),
713 "Modified line '{}' should have inline spans showing the change from 30% to 20%",
714 modified2.content);
715 }
716
717 #[test]
718 fn test_staged_modification_shows_merged() {
719 let base = "line1\nfunction getData()\nline3";
720 let index = "line1\nfunction getData(params)\nline3";
721
722 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(index), Some(index));
723 let lines = content_lines(&diff);
724
725 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
726 assert_eq!(with_spans.len(), 1);
727 assert_eq!(with_spans[0].content, "function getData(params)");
728
729 let changed: Vec<_> = with_spans[0].inline_spans.iter()
730 .filter(|s| s.source == Some(LineSource::Staged))
731 .collect();
732 assert!(!changed.is_empty());
733 }
734
735 #[test]
736 fn test_context_lines_preserved() {
737 let base = "line1\nline2\nline3\nline4\nline5";
738 let working = "line1\nline2\nmodified\nline4\nline5";
739
740 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
741 let lines = content_lines(&diff);
742
743 let pure_context: Vec<_> = lines.iter()
744 .filter(|l| l.source == LineSource::Base && l.inline_spans.is_empty())
745 .collect();
746
747 assert_eq!(pure_context.len(), 4);
748 assert!(pure_context.iter().all(|l| l.prefix == ' '));
749 }
750
751 #[test]
752 fn test_line_numbers_correct_after_deletion() {
753 let base = "line1\nto_delete\nline3";
754 let working = "line1\nline3";
755
756 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
757 let lines = content_lines(&diff);
758
759 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
760 assert!(deleted.iter().all(|l| l.line_number.is_none()));
761
762 let with_numbers: Vec<_> = lines.iter()
763 .filter(|l| l.line_number.is_some())
764 .collect();
765
766 assert_eq!(with_numbers.len(), 2);
767 assert_eq!(with_numbers[0].line_number, Some(1));
768 assert_eq!(with_numbers[1].line_number, Some(2));
769 }
770
771 #[test]
772 fn test_line_numbers_correct_after_addition() {
773 let base = "line1\nline2";
774 let working = "line1\nnew_line\nline2";
775
776 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
777 let lines = content_lines(&diff);
778
779 let with_numbers: Vec<_> = lines.iter()
780 .filter(|l| l.line_number.is_some())
781 .map(|l| (l.content.as_str(), l.line_number.unwrap()))
782 .collect();
783
784 assert_eq!(with_numbers, vec![
785 ("line1", 1),
786 ("new_line", 2),
787 ("line2", 3),
788 ]);
789 }
790
791 #[test]
792 fn test_modify_committed_line_in_working_tree() {
793 let base = "line1\n";
794 let head = "line1\ncommitted line\n";
795 let working = "line1\ncommitted line # with comment\n";
796
797 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(working));
798 let lines = content_lines(&diff);
799
800 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
801 assert_eq!(merged.len(), 1);
802 assert_eq!(merged[0].content, "committed line # with comment");
803
804 let unchanged_spans: Vec<_> = merged[0].inline_spans.iter()
805 .filter(|s| s.source.is_none() && !s.is_deletion)
806 .collect();
807 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
808 .filter(|s| s.source == Some(LineSource::Unstaged))
809 .collect();
810
811 assert!(!unchanged_spans.is_empty());
812 assert!(!unstaged_spans.is_empty());
813 }
814
815 #[test]
816 fn test_modify_staged_line_in_working_tree() {
817 let base = "line1\n";
818 let head = "line1\n";
819 let index = "line1\nstaged line\n";
820 let working = "line1\nstaged line modified\n";
821
822 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
823 let lines = content_lines(&diff);
824
825 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
826 assert_eq!(merged.len(), 1);
827 assert_eq!(merged[0].content, "staged line modified");
828
829 let unchanged_spans: Vec<_> = merged[0].inline_spans.iter()
830 .filter(|s| s.source.is_none() && !s.is_deletion)
831 .collect();
832 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
833 .filter(|s| s.source == Some(LineSource::Unstaged))
834 .collect();
835
836 assert!(!unchanged_spans.is_empty());
837 assert!(!unstaged_spans.is_empty());
838 }
839
840 #[test]
841 fn test_modify_base_line_in_commit() {
842 let base = "do_thing(data)\n";
843 let head = "do_thing(data, params)\n";
844
845 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
846 let lines = content_lines(&diff);
847
848 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
849 assert_eq!(with_spans.len(), 1);
850 assert_eq!(with_spans[0].content, "do_thing(data, params)");
851
852 let changed: Vec<_> = with_spans[0].inline_spans.iter()
853 .filter(|s| s.source == Some(LineSource::Committed))
854 .collect();
855 assert!(!changed.is_empty());
856 }
857
858 #[test]
859 fn test_chain_of_modifications() {
860 let base = "original\n";
861 let head = "committed version\n";
862 let index = "staged version\n";
863 let working = "working version\n";
864
865 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
866 let lines = content_lines(&diff);
867
868 let with_spans: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
869 assert_eq!(with_spans.len(), 1);
870 assert_eq!(with_spans[0].content, "working version");
871
872 let changed: Vec<_> = with_spans[0].inline_spans.iter()
873 .filter(|s| s.source == Some(LineSource::Unstaged))
874 .collect();
875 assert!(!changed.is_empty());
876 }
877
878 #[test]
879 fn test_committed_line_unchanged_through_stages() {
880 let base = "line1\n";
881 let head = "line1\ncommitted line\n";
882
883 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
884 let lines = content_lines(&diff);
885
886 let added: Vec<_> = lines.iter().filter(|l| l.prefix == '+').collect();
887
888 assert_eq!(added.len(), 1);
889 assert_eq!(added[0].content, "committed line");
890 assert_eq!(added[0].source, LineSource::Committed);
891 }
892
893 #[test]
894 fn test_staged_line_unchanged_in_working() {
895 let base = "line1\n";
896 let head = "line1\n";
897 let index = "line1\nstaged line\n";
898
899 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(index));
900 let lines = content_lines(&diff);
901
902 let added: Vec<_> = lines.iter().filter(|l| l.prefix == '+').collect();
903
904 assert_eq!(added.len(), 1);
905 assert_eq!(added[0].content, "staged line");
906 assert_eq!(added[0].source, LineSource::Staged);
907 }
908
909 #[test]
910 fn test_inline_diff_not_meaningful_falls_back_to_pair() {
911 let base = "abcdefgh\n";
912 let working = "xyz12345\n";
913
914 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
915 let lines = content_lines(&diff);
916
917 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
918 let added: Vec<_> = lines.iter().filter(|l| l.prefix == '+').collect();
919
920 assert_eq!(deleted.len(), 1);
921 assert_eq!(added.len(), 1);
922 assert_eq!(deleted[0].content, "abcdefgh");
923 assert_eq!(added[0].content, "xyz12345");
924 }
925
926 #[test]
927 fn test_block_of_changes_no_inline_merge() {
928 let base = "context\nalpha: aaa,\nbeta: bbb,\ngamma: ccc,\nend";
929 let working = "context\nxray: xxx,\nyankee: yyy,\nzulu: zzz,\nend";
930
931 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
932 let lines = content_lines(&diff);
933
934 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
935 let added: Vec<_> = lines.iter().filter(|l| l.prefix == '+').collect();
936 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
937
938 assert_eq!(deleted.len(), 3);
939 assert_eq!(added.len(), 3);
940 assert_eq!(merged.len(), 0);
941
942 for line in &added {
943 assert!(line.inline_spans.is_empty());
944 }
945 }
946
947 #[test]
948 fn test_single_line_modification_with_context_shows_inline() {
949 let base = "before\ndescribed_class.new(bond).execute\nafter";
950 let working = "before\ndescribed_class.new(bond).execute # and add some color commentary\nafter";
951
952 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
953 let lines = content_lines(&diff);
954
955 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
956 assert_eq!(deleted.len(), 0);
957
958 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
959 assert_eq!(merged.len(), 1);
960
961 assert!(merged[0].content.contains("# and add some color commentary"));
962
963 let unchanged: Vec<_> = merged[0].inline_spans.iter()
964 .filter(|s| s.source.is_none())
965 .collect();
966 let changed: Vec<_> = merged[0].inline_spans.iter()
967 .filter(|s| s.source.is_some())
968 .collect();
969
970 assert!(!unchanged.is_empty());
971 assert!(!changed.is_empty());
972
973 let unchanged_text: String = unchanged.iter().map(|s| s.text.as_str()).collect();
974 assert!(unchanged_text.contains("described_class.new(bond).execute"));
975 }
976
977 #[test]
978 fn test_single_line_committed_modification_shows_inline() {
979 let base = "before\ndescribed_class.new(bond).execute\nafter";
980 let head = "before\ndescribed_class.new(bond).execute # and add some color commentary\nafter";
981
982 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
983 let lines = content_lines(&diff);
984
985 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
986 assert_eq!(deleted.len(), 0);
987
988 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
989 assert_eq!(merged.len(), 1);
990
991 assert!(merged[0].content.contains("# and add some color commentary"));
992
993 let unchanged: Vec<_> = merged[0].inline_spans.iter()
994 .filter(|s| s.source.is_none())
995 .collect();
996 let changed: Vec<_> = merged[0].inline_spans.iter()
997 .filter(|s| s.source.is_some())
998 .collect();
999
1000 assert!(!unchanged.is_empty());
1001 assert!(!changed.is_empty());
1002
1003 let committed_spans: Vec<_> = merged[0].inline_spans.iter()
1004 .filter(|s| s.source == Some(LineSource::Committed))
1005 .collect();
1006 assert!(!committed_spans.is_empty());
1007
1008 let unchanged_text: String = unchanged.iter().map(|s| s.text.as_str()).collect();
1009 assert!(unchanged_text.contains("described_class.new(bond).execute"));
1010 }
1011
1012 #[test]
1013 fn test_modification_with_adjacent_empty_line_inserts_shows_inline() {
1014 let base = "before\ndescribed_class.new(bond).execute\nafter";
1015 let head = "before\n\ndescribed_class.new(bond).execute # comment\n\nafter";
1016
1017 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
1018 let lines = content_lines(&diff);
1019
1020 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1021 assert_eq!(merged.len(), 1);
1022
1023 assert!(merged[0].content.contains("# comment"));
1024
1025 let unchanged: Vec<_> = merged[0].inline_spans.iter()
1026 .filter(|s| s.source.is_none())
1027 .collect();
1028 let changed: Vec<_> = merged[0].inline_spans.iter()
1029 .filter(|s| s.source.is_some())
1030 .collect();
1031
1032 assert!(!unchanged.is_empty());
1033 assert!(!changed.is_empty());
1034 }
1035
1036 #[test]
1037 fn test_unstaged_modification_of_committed_line_shows_inline() {
1038 let base = "before\nafter";
1039 let head = "before\ndescribed_class.new(bond).execute\nafter";
1040 let index = "before\ndescribed_class.new(bond).execute\nafter";
1041 let working = "before\ndescribed_class.new(bond).execute # and add some color commentary\nafter";
1042
1043 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1044 let lines = content_lines(&diff);
1045
1046 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1047 assert_eq!(merged.len(), 1);
1048
1049 assert_eq!(merged[0].content, "described_class.new(bond).execute # and add some color commentary");
1050
1051 let unchanged_spans: Vec<_> = merged[0].inline_spans.iter()
1052 .filter(|s| s.source.is_none() && !s.is_deletion)
1053 .collect();
1054 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
1055 .filter(|s| s.source == Some(LineSource::Unstaged))
1056 .collect();
1057
1058 assert!(!unchanged_spans.is_empty());
1059 let unchanged_text: String = unchanged_spans.iter().map(|s| s.text.as_str()).collect();
1060 assert!(unchanged_text.contains("described_class.new(bond).execute"));
1061
1062 assert!(!unstaged_spans.is_empty());
1063 let unstaged_text: String = unstaged_spans.iter().map(|s| s.text.as_str()).collect();
1064 assert!(unstaged_text.contains("# and add some color commentary"));
1065 }
1066
1067 #[test]
1068 fn test_unstaged_modification_of_base_line_shows_gray_and_yellow() {
1069 let base = "before\noriginal_code()\nafter";
1070 let head = "before\noriginal_code()\nafter";
1071 let index = "before\noriginal_code()\nafter";
1072 let working = "before\noriginal_code() # added comment\nafter";
1073
1074 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1075 let lines = content_lines(&diff);
1076
1077 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1078 assert_eq!(merged.len(), 1);
1079
1080 let base_spans: Vec<_> = merged[0].inline_spans.iter()
1081 .filter(|s| s.source.is_none() || s.source == Some(LineSource::Base))
1082 .collect();
1083 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
1084 .filter(|s| s.source == Some(LineSource::Unstaged))
1085 .collect();
1086
1087 let committed_spans: Vec<_> = merged[0].inline_spans.iter()
1088 .filter(|s| s.source == Some(LineSource::Committed))
1089 .collect();
1090 assert!(committed_spans.is_empty(), "line from master shouldn't be Committed");
1091
1092 assert!(!base_spans.is_empty());
1093 assert!(!unstaged_spans.is_empty());
1094
1095 let unstaged_text: String = unstaged_spans.iter().map(|s| s.text.as_str()).collect();
1096 assert!(unstaged_text.contains("# added comment"));
1097 }
1098
1099 #[test]
1100 fn test_duplicate_lines_correct_source_attribution() {
1101 let base = "context 'first' do\n it 'test' do\n end\nend\n";
1102 let head = "context 'first' do\n it 'test' do\n end\n it 'new test' do\n end\nend\n";
1103 let index = head;
1104 let working = "context 'first' do\n it 'test' do\n end\n it 'new test' do\n end # added comment\nend\n";
1105
1106 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1107 let lines = content_lines(&diff);
1108
1109 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1110 assert_eq!(merged.len(), 1);
1111
1112 let unchanged_spans: Vec<_> = merged[0].inline_spans.iter()
1113 .filter(|s| s.source.is_none() && !s.is_deletion)
1114 .collect();
1115 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
1116 .filter(|s| s.source == Some(LineSource::Unstaged))
1117 .collect();
1118
1119 assert!(!unchanged_spans.is_empty());
1120 assert!(!unstaged_spans.is_empty());
1121 }
1122
1123 #[test]
1124 fn test_duplicate_lines_earlier_base_line_doesnt_affect_committed_line() {
1125 let base = "context 'existing' do
1126 it 'existing test' do
1127 described_class.new(bond).execute
1128 end
1129end
1130";
1131 let head = "context 'existing' do
1132 it 'existing test' do
1133 described_class.new(bond).execute
1134 end
1135end
1136
1137context 'new' do
1138 it 'new test' do
1139 described_class.new(bond).execute
1140 end
1141end
1142";
1143 let index = head;
1144 let working = "context 'existing' do
1146 it 'existing test' do
1147 described_class.new(bond).execute
1148 end
1149end
1150
1151context 'new' do
1152 it 'new test' do
1153 described_class.new(bond).execute # added comment
1154 end
1155end
1156";
1157
1158 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1159 let lines = content_lines(&diff);
1160
1161 let merged: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1162 assert_eq!(merged.len(), 1);
1163
1164 assert!(merged[0].content.contains("described_class.new(bond).execute # added comment"));
1165
1166 let unchanged_spans: Vec<_> = merged[0].inline_spans.iter()
1167 .filter(|s| s.source.is_none() && !s.is_deletion)
1168 .collect();
1169 let unstaged_spans: Vec<_> = merged[0].inline_spans.iter()
1170 .filter(|s| s.source == Some(LineSource::Unstaged))
1171 .collect();
1172
1173 assert!(!unchanged_spans.is_empty());
1174
1175 assert!(!unstaged_spans.is_empty());
1176 let unstaged_text: String = unstaged_spans.iter().map(|s| s.text.as_str()).collect();
1177 assert!(unstaged_text.contains("# added comment"));
1178 }
1179
1180 #[test]
1181 fn test_last_test_in_committed_block_shows_committed_not_base() {
1182 let base = "context 'existing' do
1183 it 'existing test' do
1184 described_class.new(bond).execute
1185 end
1186end
1187";
1188 let head = "context 'existing' do
1189 it 'existing test' do
1190 described_class.new(bond).execute
1191 end
1192end
1193
1194context 'first new' do
1195 it 'first new test' do
1196 described_class.new(bond).execute
1197 end
1198end
1199
1200context 'second new' do
1201 it 'second new test' do
1202 described_class.new(bond).execute
1203 end
1204end
1205
1206context 'third new' do
1207 it 'third new test' do
1208 described_class.new(bond).execute
1209 end
1210end
1211";
1212 let index = head;
1213 let working = head;
1214
1215 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1216 let lines = content_lines(&diff);
1217
1218 let execute_lines: Vec<_> = lines.iter()
1219 .filter(|l| l.content == " described_class.new(bond).execute")
1220 .collect();
1221
1222 assert_eq!(execute_lines.len(), 4);
1223
1224 assert_eq!(execute_lines[0].source, LineSource::Base);
1225
1226 for line in execute_lines.iter().skip(1) {
1227 assert_eq!(line.source, LineSource::Committed);
1228 }
1229 }
1230
1231 #[test]
1232 fn test_committed_block_with_shared_end_line() {
1233 let base = "context 'existing' do
1234 it 'test' do
1235 described_class.new(bond).execute
1236 end
1237end
1238";
1239 let head = "context 'existing' do
1240 it 'test' do
1241 described_class.new(bond).execute
1242 end
1243end
1244
1245context 'new' do
1246 it 'uses bond data' do
1247 expected_address = bond.principal_mailing_address
1248
1249 described_class.new(bond).execute
1250
1251 notice = Commercial::PremiumDueNotice.new(bond)
1252 expect(notice.pdf_fields_hash[:address]).to eq(expected_address)
1253 end
1254end
1255";
1256 let index = head;
1257 let working = head;
1258
1259 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1260 let lines = content_lines(&diff);
1261
1262 let new_test_lines: Vec<_> = lines.iter()
1263 .enumerate()
1264 .filter(|(_, l)| {
1265 l.content.contains("uses bond data") ||
1266 l.content.contains("expected_address") ||
1267 l.content.contains("notice = Commercial") ||
1268 l.content.contains("expect(notice")
1269 })
1270 .collect();
1271
1272 for (_, line) in &new_test_lines {
1273 assert_eq!(line.source, LineSource::Committed);
1274 }
1275
1276 let execute_lines: Vec<_> = lines.iter()
1277 .enumerate()
1278 .filter(|(_, l)| l.content.trim() == "described_class.new(bond).execute")
1279 .collect();
1280
1281 assert_eq!(execute_lines.len(), 2);
1282 assert_eq!(execute_lines[0].1.source, LineSource::Base);
1283 assert_eq!(execute_lines[1].1.source, LineSource::Committed);
1284 }
1285
1286 #[test]
1287 fn test_blank_line_in_committed_block_shows_committed() {
1288 let base = "context 'existing' do
1289 it 'test' do
1290 existing_code
1291
1292 described_class.new(bond).execute
1293 end
1294end
1295";
1296 let head = "context 'existing' do
1297 it 'test' do
1298 existing_code
1299
1300 described_class.new(bond).execute
1301 end
1302end
1303
1304context 'new' do
1305 it 'new test' do
1306 expected_address = bond.principal_mailing_address
1307
1308 described_class.new(bond).execute
1309
1310 notice = Commercial::PremiumDueNotice.new(bond)
1311 end
1312end
1313";
1314 let index = head;
1315 let working = head;
1316
1317 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1318 let lines = content_lines(&diff);
1319
1320 let new_context_idx = lines.iter().position(|l| l.content.contains("context 'new'"));
1321 assert!(new_context_idx.is_some());
1322
1323 let new_context_idx = new_context_idx.unwrap();
1324
1325 for (i, line) in lines.iter().enumerate().skip(new_context_idx) {
1326 if line.content.trim().is_empty() {
1327 assert_eq!(line.source, LineSource::Committed,
1328 "Blank line at {} should be Committed", i);
1329 }
1330 }
1331 }
1332
1333 #[test]
1334 fn test_third_test_in_block_of_three_shows_committed() {
1335 let base = "context 'existing' do
1336 it 'existing test' do
1337 described_class.new(bond).execute
1338 end
1339end
1340";
1341 let head = "context 'existing' do
1342 it 'existing test' do
1343 described_class.new(bond).execute
1344 end
1345end
1346
1347context 'first new' do
1348 it 'first test' do
1349 expected = bond.first_attribute
1350
1351 described_class.new(bond).execute
1352
1353 expect(result).to eq(expected)
1354 end
1355end
1356
1357context 'second new' do
1358 it 'second test' do
1359 expected = bond.second_attribute
1360
1361 described_class.new(bond).execute
1362
1363 expect(result).to eq(expected)
1364 end
1365end
1366
1367context 'third new' do
1368 it 'third test' do
1369 expected = bond.third_attribute
1370
1371 described_class.new(bond).execute
1372
1373 expect(result).to eq(expected)
1374 end
1375end
1376";
1377 let index = head;
1378 let working = head;
1379
1380 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(index), Some(working));
1381 let lines = content_lines(&diff);
1382
1383 let third_context_idx = lines.iter().position(|l| l.content.contains("context 'third new'"));
1384 assert!(third_context_idx.is_some());
1385 let third_context_idx = third_context_idx.unwrap();
1386
1387 for (_, line) in lines.iter().enumerate().skip(third_context_idx) {
1388 if line.content.contains("third test") ||
1389 line.content.contains("third_attribute") ||
1390 line.content.contains("described_class") ||
1391 line.content.contains("expect(result)") {
1392 assert_eq!(line.source, LineSource::Committed);
1393 }
1394 }
1395
1396 let execute_lines: Vec<_> = lines.iter().enumerate()
1397 .filter(|(_, l)| l.content.trim() == "described_class.new(bond).execute")
1398 .collect();
1399
1400 assert_eq!(execute_lines.len(), 4);
1401 assert_eq!(execute_lines[0].1.source, LineSource::Base);
1402 assert_eq!(execute_lines[1].1.source, LineSource::Committed);
1403 assert_eq!(execute_lines[2].1.source, LineSource::Committed);
1404 assert_eq!(execute_lines[3].1.source, LineSource::Committed);
1405 }
1406
1407 #[test]
1408 fn test_modified_line_shows_as_single_merged_line() {
1409 let base = "do_thing(data)\n";
1410 let working = "do_thing(data, parameters)\n";
1411
1412 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1413 let lines = content_lines(&diff);
1414
1415 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
1416 let modified: Vec<_> = lines.iter().filter(|l| !l.inline_spans.is_empty()).collect();
1417
1418 assert_eq!(deleted.len(), 0);
1419 assert_eq!(modified.len(), 1);
1420 assert_eq!(modified[0].content, "do_thing(data, parameters)");
1421
1422 let changed: Vec<_> = modified[0].inline_spans.iter()
1423 .filter(|s| s.source == Some(LineSource::Unstaged))
1424 .collect();
1425 assert!(!changed.is_empty());
1426
1427 let changed_text: String = changed.iter().map(|s| s.text.as_str()).collect();
1428 assert!(changed_text.contains(", parameters"));
1429 }
1430
1431 #[test]
1432 fn test_new_line_addition_no_inline_spans() {
1433 let base = "line1\n";
1434 let working = "line1\nnew line\n";
1435
1436 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1437 let lines = content_lines(&diff);
1438
1439 let added: Vec<_> = lines.iter().filter(|l| l.prefix == '+').collect();
1440 assert_eq!(added.len(), 1);
1441 assert!(added[0].inline_spans.is_empty());
1442 }
1443
1444 #[test]
1445 fn test_pure_deletion_still_shows_minus() {
1446 let base = "line1\nto_delete\nline3\n";
1447 let working = "line1\nline3\n";
1448
1449 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1450 let lines = content_lines(&diff);
1451
1452 let deleted: Vec<_> = lines.iter().filter(|l| l.prefix == '-').collect();
1453 assert_eq!(deleted.len(), 1);
1454 assert_eq!(deleted[0].content, "to_delete");
1455 }
1456
1457 #[test]
1458 fn test_two_adjacent_committed_modifications() {
1459 let base = r#" principal_zip: "00000",
1460 effective_date: "2022-08-30",
1461 expiration_date: "2024-08-30",
1462"#;
1463 let head = r#" principal_zip: "00000",
1464 effective_date: "2023-08-30",
1465 expiration_date: "2025-08-30",
1466"#;
1467
1468 let diff = compute_diff_with_inline("test.txt", Some(base), Some(head), Some(head), Some(head));
1469 let lines = content_lines(&diff);
1470
1471 let effective_lines: Vec<_> = lines.iter()
1472 .filter(|l| l.content.contains("effective_date"))
1473 .collect();
1474 let expiration_lines: Vec<_> = lines.iter()
1475 .filter(|l| l.content.contains("expiration_date"))
1476 .collect();
1477
1478 assert_eq!(effective_lines.len(), 1);
1479 assert!(effective_lines[0].content.contains("2023"));
1480 assert!(!effective_lines[0].inline_spans.is_empty());
1481
1482 assert_eq!(expiration_lines.len(), 1);
1483 assert!(expiration_lines[0].content.contains("2025"));
1484 assert!(!expiration_lines[0].inline_spans.is_empty());
1485
1486 let effective_has_committed = effective_lines[0].inline_spans.iter()
1487 .any(|s| s.source == Some(LineSource::Committed));
1488 let expiration_has_committed = expiration_lines[0].inline_spans.iter()
1489 .any(|s| s.source == Some(LineSource::Committed));
1490
1491 assert!(effective_has_committed);
1492 assert!(expiration_has_committed);
1493 }
1494
1495 #[test]
1496 fn test_deletion_positioned_correctly_with_insertions_before() {
1497 let base = "line1\nline2\nline3\nto_delete\nline5";
1498 let working = "line1\nline2\nNEW_LINE\nline3\nline5";
1499
1500 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1501 let lines = content_lines(&diff);
1502
1503 let line_contents: Vec<&str> = lines.iter().map(|l| l.content.as_str()).collect();
1504
1505 let new_line_pos = line_contents.iter().position(|&c| c == "NEW_LINE").unwrap();
1506 let to_delete_pos = line_contents.iter().position(|&c| c == "to_delete").unwrap();
1507 let line3_pos = line_contents.iter().position(|&c| c == "line3").unwrap();
1508
1509 assert!(to_delete_pos > line3_pos);
1510 assert!(to_delete_pos > new_line_pos);
1511
1512 let deleted_line = &lines[to_delete_pos];
1513 assert_eq!(deleted_line.prefix, '-');
1514 }
1515
1516 #[test]
1517 fn test_deletion_before_insertion_at_same_position() {
1518 let base = "def principal_mailing_address\n commercial_renewal.principal_mailing_address\nend";
1519 let working = "def principal_mailing_address\n \"new content\"\nend";
1520
1521 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1522 let lines = content_lines(&diff);
1523
1524 let line_contents: Vec<&str> = lines.iter().map(|l| l.content.as_str()).collect();
1525 let prefixes: Vec<char> = lines.iter().map(|l| l.prefix).collect();
1526
1527 let deleted_pos = line_contents.iter().position(|&c| c.contains("commercial_renewal")).unwrap();
1528 let inserted_pos = line_contents.iter().position(|&c| c.contains("new content")).unwrap();
1529
1530 assert!(deleted_pos < inserted_pos);
1531 assert_eq!(prefixes[deleted_pos], '-');
1532 assert_eq!(prefixes[inserted_pos], '+');
1533 }
1534
1535 #[test]
1536 fn test_deletion_appears_after_preceding_context_line() {
1537 let base = "def foo\n body_line\nend";
1538 let working = "def foo\n \"new body\"\nend";
1539
1540 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1541 let lines = content_lines(&diff);
1542
1543 let line_contents: Vec<&str> = lines.iter().map(|l| l.content.as_str()).collect();
1544
1545 let def_pos = line_contents.iter().position(|&c| c.contains("def foo")).unwrap();
1546 let deleted_pos = line_contents.iter().position(|&c| c.contains("body_line")).unwrap();
1547 let inserted_pos = line_contents.iter().position(|&c| c.contains("new body")).unwrap();
1548 let end_pos = line_contents.iter().position(|&c| c == "end").unwrap();
1549
1550 assert!(deleted_pos > def_pos);
1551 assert!(deleted_pos < inserted_pos);
1552 assert!(deleted_pos < end_pos);
1553
1554 assert_eq!(def_pos, 0);
1555 assert_eq!(deleted_pos, 1);
1556 assert_eq!(inserted_pos, 2);
1557 assert_eq!(end_pos, 3);
1558 }
1559
1560 #[test]
1561 fn test_deletion_after_modified_line() {
1562 let base = "def principal_mailing_address\n commercial_renewal.principal_mailing_address\nend";
1563 let working = "def pribond_descripal_mailtiong_address\n \"new content\"\nend";
1564
1565 let diff = compute_diff_with_inline("test.txt", Some(base), Some(base), Some(base), Some(working));
1566 let lines = content_lines(&diff);
1567
1568 let line_contents: Vec<&str> = lines.iter().map(|l| l.content.as_str()).collect();
1569 let prefixes: Vec<char> = lines.iter().map(|l| l.prefix).collect();
1570
1571 let deleted_principal_pos = line_contents.iter().position(|&c| c.contains("principal_mailing_address")).unwrap();
1572 let inserted_pribond_pos = line_contents.iter().position(|&c| c.contains("pribond")).unwrap();
1573 let deleted_commercial_pos = line_contents.iter().position(|&c| c.contains("commercial_renewal")).unwrap();
1574 let inserted_new_content_pos = line_contents.iter().position(|&c| c.contains("new content")).unwrap();
1575 let end_pos = line_contents.iter().position(|&c| c == "end").unwrap();
1576
1577 assert_eq!(prefixes[deleted_principal_pos], '-', "principal should be deleted");
1578 assert_eq!(prefixes[inserted_pribond_pos], '+', "pribond should be inserted");
1579 assert_eq!(prefixes[deleted_commercial_pos], '-', "commercial_renewal should be deleted");
1580 assert_eq!(prefixes[inserted_new_content_pos], '+', "new content should be inserted");
1581 assert_eq!(prefixes[end_pos], ' ', "end should be unchanged context");
1582
1583 assert!(deleted_principal_pos < deleted_commercial_pos, "both deletions should come together");
1584 assert!(deleted_commercial_pos < inserted_pribond_pos, "deletions should come before insertions");
1585 assert!(inserted_pribond_pos < inserted_new_content_pos, "both insertions should come together");
1586 }
1587
1588 #[test]
1589 fn test_unstaged_modification_of_newly_committed_method_appears_in_correct_position() {
1590 let base = "def abeyance_required?
1591 abeyance? || active?
1592end
1593
1594def from_domino?
1595 legacy_unid.present?
1596end
1597";
1598 let head = "def abeyance_required?
1599 abeyance? || active?
1600end
1601
1602def can_request_letter_of_bondability?
1603 !commercial? && (active? || abeyance?)
1604end
1605
1606def from_domino?
1607 legacy_unid.present?
1608end
1609";
1610 let index = head;
1611 let working = "def abeyance_required?
1612 abeyance? || active?
1613end
1614
1615def can_request_letter_of_bondability?
1616 !inactive? && status != \"Destroy\"
1617end
1618
1619def from_domino?
1620 legacy_unid.present?
1621end
1622";
1623
1624 let diff = compute_diff_with_inline("principal.rb", Some(base), Some(head), Some(index), Some(working));
1625 let lines = content_lines(&diff);
1626
1627 let abeyance_pos = lines.iter().position(|l| l.content.contains("abeyance_required")).unwrap();
1628 let can_request_pos = lines.iter().position(|l| l.content.contains("can_request_letter_of_bondability")).unwrap();
1629 let from_domino_pos = lines.iter().position(|l| l.content.contains("from_domino")).unwrap();
1630
1631 let commercial_line_pos = lines.iter().position(|l| l.content.contains("!commercial?"));
1632 let inactive_line_pos = lines.iter().position(|l| l.content.contains("!inactive?"));
1633
1634 assert!(can_request_pos > abeyance_pos, "can_request method should come after abeyance_required");
1635 assert!(can_request_pos < from_domino_pos, "can_request method should come before from_domino");
1636
1637 if let Some(commercial_pos) = commercial_line_pos {
1638 assert!(
1639 commercial_pos > abeyance_pos && commercial_pos < from_domino_pos,
1640 "!commercial? line should appear between abeyance_required and from_domino, not at pos {} (abeyance={}, from_domino={})",
1641 commercial_pos, abeyance_pos, from_domino_pos
1642 );
1643 }
1644
1645 if let Some(inactive_pos) = inactive_line_pos {
1646 assert!(
1647 inactive_pos > abeyance_pos && inactive_pos < from_domino_pos,
1648 "!inactive? line should appear between abeyance_required and from_domino, not at pos {} (abeyance={}, from_domino={})",
1649 inactive_pos, abeyance_pos, from_domino_pos
1650 );
1651 }
1652 }
1653
1654 #[test]
1655 fn test_trailing_context_after_addition() {
1656 let base = "def foo\nend\nend";
1657 let working = "def foo\nnew_line\nend\nend";
1658
1659 let diff = compute_diff_with_inline("test.rb", Some(base), Some(base), Some(base), Some(working));
1660 let lines = content_lines(&diff);
1661
1662 assert_eq!(lines.len(), 4);
1663
1664 assert_eq!(lines[0].content, "def foo");
1665 assert_eq!(lines[0].source, LineSource::Base);
1666
1667 assert_eq!(lines[1].content, "new_line");
1668 assert_eq!(lines[1].prefix, '+');
1669
1670 assert_eq!(lines[2].content, "end");
1671 assert_eq!(lines[2].source, LineSource::Base);
1672
1673 assert_eq!(lines[3].content, "end");
1674 assert_eq!(lines[3].source, LineSource::Base);
1675 }
1676
1677 #[test]
1678 fn test_trailing_context_after_committed_addition() {
1679 let base = "def foo\nend\nend";
1680 let head = "def foo\nnew_line\nend\nend";
1681
1682 let diff = compute_diff_with_inline("test.rb", Some(base), Some(head), Some(head), Some(head));
1683 let lines = content_lines(&diff);
1684 assert_eq!(lines.len(), 4);
1685
1686 assert_eq!(lines[0].content, "def foo");
1687 assert_eq!(lines[0].source, LineSource::Base);
1688
1689 assert_eq!(lines[1].content, "new_line");
1690 assert_eq!(lines[1].source, LineSource::Committed);
1691 assert_eq!(lines[1].prefix, '+');
1692
1693 assert_eq!(lines[2].content, "end");
1694 assert_eq!(lines[2].source, LineSource::Base);
1695
1696 assert_eq!(lines[3].content, "end");
1697 assert_eq!(lines[3].source, LineSource::Base);
1698 }
1699
1700 #[test]
1701 fn test_addition_at_end_of_file_with_trailing_context() {
1702 let base = "class Foo\n def bar\n end\nend";
1703 let head = "class Foo\n def bar\n new_line\n end\nend";
1704
1705 let diff = compute_diff_with_inline("test.rb", Some(base), Some(head), Some(head), Some(head));
1706 let lines = content_lines(&diff);
1707
1708 assert_eq!(lines.len(), 5);
1709
1710 assert_eq!(lines[0].content, "class Foo");
1711 assert_eq!(lines[1].content, " def bar");
1712 assert_eq!(lines[2].content, " new_line");
1713 assert_eq!(lines[2].source, LineSource::Committed);
1714 assert_eq!(lines[3].content, " end");
1715 assert_eq!(lines[3].source, LineSource::Base);
1716 assert_eq!(lines[4].content, "end");
1717 assert_eq!(lines[4].source, LineSource::Base);
1718 }
1719
1720 #[test]
1721 fn test_addition_before_two_trailing_ends() {
1722 let base = "do\n body\nend\nend";
1723 let head = "do\n body\n new_end\nend\nend";
1724
1725 let diff = compute_diff_with_inline("test.rb", Some(base), Some(head), Some(head), Some(head));
1726 let lines = content_lines(&diff);
1727
1728 assert_eq!(lines.len(), 5);
1729
1730 assert_eq!(lines[0].content, "do");
1731 assert_eq!(lines[0].source, LineSource::Base);
1732
1733 assert_eq!(lines[1].content, " body");
1734 assert_eq!(lines[1].source, LineSource::Base);
1735
1736 assert_eq!(lines[2].content, " new_end");
1737 assert_eq!(lines[2].source, LineSource::Committed);
1738 assert_eq!(lines[2].prefix, '+');
1739
1740 assert_eq!(lines[3].content, "end");
1741 assert_eq!(lines[3].source, LineSource::Base);
1742
1743 assert_eq!(lines[4].content, "end");
1744 assert_eq!(lines[4].source, LineSource::Base);
1745 }
1746
1747 #[test]
1748 fn test_final_file_ends_with_addition() {
1749 let base = "do\n body\nend";
1750 let head = "do\n body\nend\n extra";
1751
1752 let diff = compute_diff_with_inline("test.rb", Some(base), Some(head), Some(head), Some(head));
1753 let lines = content_lines(&diff);
1754
1755 assert_eq!(lines.len(), 4);
1756
1757 assert_eq!(lines[0].content, "do");
1758 assert_eq!(lines[0].source, LineSource::Base);
1759
1760 assert_eq!(lines[1].content, " body");
1761 assert_eq!(lines[1].source, LineSource::Base);
1762
1763 assert_eq!(lines[2].content, "end");
1764 assert_eq!(lines[2].source, LineSource::Base);
1765
1766 assert_eq!(lines[3].content, " extra");
1767 assert_eq!(lines[3].source, LineSource::Committed);
1768 assert_eq!(lines[3].prefix, '+');
1769 }
1770
1771 #[test]
1772 fn test_file_without_trailing_newline() {
1773 let base = "line1\nline2\nend\nend\nend";
1774 let head = "line1\nline2\nnew_line\nend\nend\nend";
1775
1776 let diff = compute_diff_with_inline("test.rb", Some(base), Some(head), Some(head), Some(head));
1777 let lines = content_lines(&diff);
1778
1779 assert_eq!(lines.len(), 6);
1780 assert_eq!(lines[0].content, "line1");
1781 assert_eq!(lines[1].content, "line2");
1782 assert_eq!(lines[2].content, "new_line");
1783 assert_eq!(lines[2].source, LineSource::Committed);
1784 assert_eq!(lines[3].content, "end");
1785 assert_eq!(lines[3].source, LineSource::Base);
1786 assert_eq!(lines[4].content, "end");
1787 assert_eq!(lines[4].source, LineSource::Base);
1788 assert_eq!(lines[5].content, "end");
1789 assert_eq!(lines[5].source, LineSource::Base);
1790 }
1791
1792 #[test]
1793 fn test_exact_scenario_from_bug_report() {
1794 let base = r##" describe "#method" do
1810 it "test 1" do
1811 expect(true).to be(true)
1812 end
1813 end
1814end"##;
1815
1816 let head = r##" describe "#method" do
1817 it "test 1" do
1818 expect(true).to be(true)
1819 end
1820
1821 it "new test" do
1822 expect(false).to be(false)
1823 end
1824 end
1825end"##;
1826
1827 let diff = compute_diff_with_inline("spec.rb", Some(base), Some(head), Some(head), Some(head));
1828 let lines = content_lines(&diff);
1829
1830 assert!(lines.len() >= 10, "Should have at least 10 lines, got {}", lines.len());
1843
1844 let last_two: Vec<_> = lines.iter().rev().take(2).collect();
1846 assert_eq!(last_two[0].content, "end", "Last line should be 'end'");
1847 assert_eq!(last_two[0].source, LineSource::Base, "Last line should be Base");
1848 assert_eq!(last_two[1].content, " end", "Second to last should be ' end'");
1850 assert_eq!(last_two[1].source, LineSource::Base, "Second to last should be Base");
1851
1852 let added_end = lines.iter().find(|l| l.content == " end" && l.source == LineSource::Committed);
1854 assert!(added_end.is_some(), "Should have a committed ' end' line");
1855
1856 assert_eq!(lines.len(), 10, "All 10 lines should be in the diff output");
1858 }
1859
1860 #[test]
1861 fn test_staging_changes_line_source_from_unstaged_to_staged() {
1862 let base = "line1\nline2";
1863 let modified = "line1\nline2\nline3";
1864
1865 let diff_before = compute_four_way_diff(DiffInput {
1867 path: "test.txt",
1868 base: Some(base),
1869 head: Some(base),
1870 index: Some(base), working: Some(modified), old_path: None,
1873 });
1874
1875 let unstaged_lines: Vec<_> = diff_before.lines.iter()
1876 .filter(|l| l.source == LineSource::Unstaged && l.content == "line3")
1877 .collect();
1878 assert_eq!(unstaged_lines.len(), 1, "line3 should be Unstaged before staging");
1879
1880 let diff_after = compute_four_way_diff(DiffInput {
1882 path: "test.txt",
1883 base: Some(base),
1884 head: Some(base),
1885 index: Some(modified), working: Some(modified), old_path: None,
1888 });
1889
1890 let staged_lines: Vec<_> = diff_after.lines.iter()
1891 .filter(|l| l.source == LineSource::Staged && l.content == "line3")
1892 .collect();
1893 assert_eq!(staged_lines.len(), 1, "line3 should be Staged after staging");
1894
1895 let still_unstaged: Vec<_> = diff_after.lines.iter()
1897 .filter(|l| l.source == LineSource::Unstaged && l.content == "line3")
1898 .collect();
1899 assert_eq!(still_unstaged.len(), 0, "line3 should NOT be Unstaged after staging");
1900 }
1901
1902 #[test]
1903 fn test_unstaged_import_modification_shows_inline() {
1904 let base = r#"use ratatui::{
1907 layout::Rect,
1908 style::{Color, Style},
1909 text::{Line, Span},
1910 widgets::{Block, Borders, Paragraph},
1911 Frame,
1912};"#;
1913
1914 let working = r#"use ratatui::{
1915 layout::Rect,
1916 style::{Color, Style},
1917 text::{Line, Span},
1918 widgets::{Block, Borders, Clear, Paragraph},
1919 Frame,
1920};"#;
1921
1922 let diff = compute_diff_with_inline(
1923 "test.rs",
1924 Some(base),
1925 Some(base),
1926 Some(base),
1927 Some(working),
1928 );
1929 let lines = content_lines(&diff);
1930
1931 let modified_line = lines.iter()
1933 .find(|l| l.content.contains("Clear"));
1934 assert!(modified_line.is_some(), "Should have a line containing 'Clear'");
1935
1936 let modified = modified_line.unwrap();
1937
1938 assert!(modified.old_content.is_some(),
1940 "Modified import line should have old_content set, but it was None. \
1941 Source is {:?}, change_source is {:?}",
1942 modified.source, modified.change_source);
1943
1944 assert!(!modified.inline_spans.is_empty(),
1945 "Modified import line should have inline spans showing 'Clear, ' insertion. \
1946 Source is {:?}, change_source is {:?}",
1947 modified.source, modified.change_source);
1948
1949 assert_eq!(modified.change_source, Some(LineSource::Unstaged),
1951 "Modification should be marked as Unstaged");
1952 }
1953
1954 #[test]
1955 fn test_unstaged_modification_plus_addition() {
1956 let base = r#"line 1
1960line 2
1961widgets::{Block, Borders, Paragraph},
1962line 4
1963line 5
1964line 6
1965line 7
1966line 8
1967render_widget(paragraph)"#;
1968
1969 let working = r#"line 1
1970line 2
1971widgets::{Block, Borders, Clear, Paragraph},
1972line 4
1973line 5
1974line 6
1975line 7
1976line 8
1977render_widget(Clear);
1978render_widget(paragraph)"#;
1979
1980 let diff = compute_diff_with_inline(
1981 "test.rs",
1982 Some(base),
1983 Some(base),
1984 Some(base),
1985 Some(working),
1986 );
1987 let lines = content_lines(&diff);
1988
1989 let modified_line = lines.iter()
1991 .find(|l| l.content.contains("Clear, Paragraph"));
1992 assert!(modified_line.is_some(), "Should have modified line with 'Clear, Paragraph'");
1993
1994 let modified = modified_line.unwrap();
1995 assert!(modified.old_content.is_some(),
1996 "Modified line should have old_content set. Source: {:?}, change_source: {:?}",
1997 modified.source, modified.change_source);
1998 assert_eq!(modified.change_source, Some(LineSource::Unstaged),
1999 "Modification should be marked as Unstaged");
2000
2001 let added_line = lines.iter()
2003 .find(|l| l.content == "render_widget(Clear);");
2004 assert!(added_line.is_some(), "Should have added line 'render_widget(Clear);'");
2005
2006 let added = added_line.unwrap();
2007 assert_eq!(added.source, LineSource::Unstaged,
2008 "Pure addition should have source Unstaged");
2009 assert_eq!(added.prefix, '+', "Addition should have + prefix");
2010 }
2011
2012 #[test]
2013 fn test_exact_diff_view_scenario() {
2014 let base = r#"//! Diff view rendering with pure data model separation.
2018//!
2019//! The DiffViewModel provides a pure view model for rendering, enabling
2020//! easier unit testing without requiring a full App instance.
2021
2022use std::collections::HashSet;
2023
2024use ratatui::{
2025 layout::Rect,
2026 style::{Color, Style},
2027 text::{Line, Span},
2028 widgets::{Block, Borders, Paragraph},
2029 Frame,
2030};
2031
2032use crate::app::{App, DisplayableItem, FrameContext, Selection};
2033
2034// ... 140 lines of code ...
2035
2036 let block = Block::default()
2037 .title(title)
2038 .borders(Borders::ALL)
2039 .border_style(Style::default().fg(Color::DarkGray));
2040
2041 let paragraph = Paragraph::new(all_lines).block(block);
2042 frame.render_widget(paragraph, self.area);"#;
2043
2044 let working = r#"//! Diff view rendering with pure data model separation.
2045//!
2046//! The DiffViewModel provides a pure view model for rendering, enabling
2047//! easier unit testing without requiring a full App instance.
2048
2049use std::collections::HashSet;
2050
2051use ratatui::{
2052 layout::Rect,
2053 style::{Color, Style},
2054 text::{Line, Span},
2055 widgets::{Block, Borders, Clear, Paragraph},
2056 Frame,
2057};
2058
2059use crate::app::{App, DisplayableItem, FrameContext, Selection};
2060
2061// ... 140 lines of code ...
2062
2063 let block = Block::default()
2064 .title(title)
2065 .borders(Borders::ALL)
2066 .border_style(Style::default().fg(Color::DarkGray));
2067
2068 frame.render_widget(Clear, self.area);
2069 let paragraph = Paragraph::new(all_lines).block(block);
2070 frame.render_widget(paragraph, self.area);"#;
2071
2072 let diff = compute_diff_with_inline(
2073 "diff_view.rs",
2074 Some(base),
2075 Some(base),
2076 Some(base),
2077 Some(working),
2078 );
2079 let lines = content_lines(&diff);
2080
2081 for (i, line) in lines.iter().enumerate() {
2083 if line.content.contains("Clear") || line.content.contains("widgets") {
2084 eprintln!("Line {}: source={:?}, change_source={:?}, old_content={}, content='{}'",
2085 i, line.source, line.change_source, line.old_content.is_some(), line.content);
2086 }
2087 }
2088
2089 let modified_import = lines.iter()
2091 .find(|l| l.content.contains("Clear, Paragraph"));
2092 assert!(modified_import.is_some(), "Should have modified import line");
2093
2094 let modified = modified_import.unwrap();
2095 assert!(modified.old_content.is_some(),
2096 "Modified import should have old_content. Source: {:?}, change_source: {:?}, prefix: '{}'",
2097 modified.source, modified.change_source, modified.prefix);
2098
2099 let added_line = lines.iter()
2101 .find(|l| l.content.contains("render_widget(Clear"));
2102 assert!(added_line.is_some(), "Should have added render_widget line");
2103
2104 let added = added_line.unwrap();
2105 assert_eq!(added.prefix, '+', "Addition should have + prefix");
2106 }
2107}