1use console::{style, Style};
5use similar::{udiff::UnifiedHunkHeader, Algorithm, ChangeTag, TextDiff};
6use std::{
7 io::{self, Write},
8 time::Duration,
9};
10
11pub fn dump<Output: Write>(mut o: Output, old: &str, new: &str) -> io::Result<()> {
12 let diff = TextDiff::configure()
13 .timeout(Duration::from_millis(200))
14 .algorithm(Algorithm::Patience)
15 .diff_lines(old, new);
16
17 for group in diff.grouped_ops(4).into_iter() {
18 if group.is_empty() {
19 continue;
20 }
21
22 let line = diff.iter_changes(&group[0]).next().unwrap().value();
24 let scope = find_scope(old, line);
25
26 let header = style(UnifiedHunkHeader::new(&group)).cyan();
27
28 if scope != line {
29 writeln!(o, "{header} {scope}")?;
30 } else {
31 writeln!(o, "{header}")?;
32 }
33
34 for op in group {
35 for change in diff.iter_inline_changes(&op) {
36 let (marker, style) = match change.tag() {
37 ChangeTag::Delete => ('-', Style::new().red()),
38 ChangeTag::Insert => ('+', Style::new().green()),
39 ChangeTag::Equal => (' ', Style::new().dim()),
40 };
41 write!(o, "{}", style.apply_to(marker).dim().bold())?;
42 for &(emphasized, value) in change.values() {
43 if emphasized {
44 write!(o, "{}", style.clone().underlined().bold().apply_to(value))?;
45 } else {
46 write!(o, "{}", style.apply_to(value))?;
47 }
48 }
49 }
50 }
51 }
52
53 Ok(())
54}
55
56fn find_scope<'a>(old: &'a str, mut line: &'a str) -> &'a str {
58 let base = old.as_ptr() as usize;
59
60 while line.is_empty() || line.starts_with(char::is_whitespace) {
61 let len = (line.as_ptr() as usize).saturating_sub(base);
62
63 let Some(subject) = old[..len].lines().next_back() else {
64 break;
65 };
66
67 line = subject;
68 }
69
70 line
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_find_scope() {
79 let text = r#"
80header
81foo
82
83 bar
84
85 baz
86"#
87 .trim_start();
88
89 assert_eq!(find_scope(text, text.lines().next().unwrap()), "header");
90 assert_eq!(find_scope(text, text.lines().nth(1).unwrap()), "foo");
91 assert_eq!(find_scope(text, text.lines().last().unwrap()), "foo");
92 }
93}