duvet_core/
diff.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4use 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        // find the previous text that doesn't have an indent
23        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
56/// Finds the most recent non-empty line with no indentation
57fn 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}