use nu_ansi_term::Color::{Green, Red};
use nu_ansi_term::Style;
use similar::{ChangeTag, TextDiff};
#[macro_export]
macro_rules! paint {
($f:ident, $with_color:ident, $colour:expr, $fmt:expr, $($args:tt)*) => (
if $with_color {
write!($f, "{}", $colour.paint(format!($fmt, $($args)*)))?;
} else {
write!($f, "{}", format!($fmt, $($args)*))?;
}
)
}
const SIGN_RIGHT: char = '+'; const SIGN_LEFT: char = '-';
type Run = (ChangeTag, String);
pub fn diff_lines(old: &str, new: &str) -> Vec<Run> {
let diff = TextDiff::from_lines(old, new);
let changes: Vec<_> = diff.iter_all_changes().collect();
changes
.chunk_by(|a, b| a.tag() == b.tag())
.map(|run| {
let text = run
.iter()
.map(|change| change.value().trim_end_matches('\n'))
.collect::<Vec<_>>()
.join("\n");
(run[0].tag(), text)
})
.collect()
}
fn lines_of(d: &Run) -> impl Iterator<Item = &str> {
d.1.split('\n')
}
fn print_context<'a>(
lines: Option<(
Option<String>,
impl Iterator<Item = &'a str>,
Option<String>,
)>,
f: &mut impl std::io::Write,
use_color: bool,
) -> std::io::Result<()> {
match lines {
None => {}
Some((before, lines, after)) => {
if let Some(before) = before {
paint!(f, use_color, Style::new().dimmed(), "{}\n", before,);
}
for line in lines {
writeln!(f, "{}", line)?;
}
if let Some(after) = after {
paint!(f, use_color, Style::new().dimmed(), "{}\n", after,);
}
}
};
Ok(())
}
pub fn format_changeset(
mut t: impl std::io::Write,
use_color: bool,
diffs: &[Run],
) -> std::io::Result<()> {
if use_color {
writeln!(
t,
"{} {} / {} :",
Style::new().bold().paint("Diff"),
Red.paint(format!("{SIGN_LEFT} removed")),
Green.paint(format!("added {SIGN_RIGHT}"))
)?;
} else {
writeln!(t, "Diff {SIGN_LEFT} removed / added {SIGN_RIGHT} :",)?;
}
let is_different = |d: &Run| d.0 != ChangeTag::Equal;
let first_changed_hunk = diffs.iter().position(is_different);
let last_changed_hunk = diffs.iter().rposition(is_different);
let context = 2;
{
let hunk_before_first_change = first_changed_hunk.and_then(|l| l.checked_sub(1));
print_context(
hunk_before_first_change.map(|l| {
let lines_outside_of_context =
lines_of(&diffs[l]).by_ref().count().saturating_sub(context);
(
if lines_outside_of_context > 0 {
Some(format!("[…skipped {} lines…]", lines_outside_of_context))
} else {
None
},
lines_of(&diffs[l]).skip(lines_outside_of_context),
None,
)
}),
&mut t,
use_color,
)?;
}
if let Some((first_changed_hunk, last_changed_hunk)) = first_changed_hunk.zip(last_changed_hunk)
{
for diff in &diffs[first_changed_hunk..=last_changed_hunk] {
match diff {
(ChangeTag::Equal, x) => {
writeln!(t, " {}", x)?;
}
(ChangeTag::Insert, x) => {
paint!(t, use_color, Green, "{}{}\n", SIGN_RIGHT, x);
}
(ChangeTag::Delete, x) => {
paint!(t, use_color, Red, "{}{}\n", SIGN_LEFT, x);
}
}
}
}
{
let hunk_after_last_change = last_changed_hunk.and_then(|l| diffs.get(l + 1));
print_context(
hunk_after_last_change.map(|diff| {
let skipped_lines_note = lines_of(diff)
.count()
.checked_sub(context)
.filter(|&skipped| skipped > 0)
.map(|skipped| format!("[…skipped {} lines…]", skipped));
(None, lines_of(diff).take(context), skipped_lines_note)
}),
&mut t,
use_color,
)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn format_diff(old: &str, new: &str) -> String {
let mut output = Vec::new();
format_changeset(&mut output, false, &diff_lines(old, new)).unwrap();
String::from_utf8(output).unwrap()
}
#[test]
fn does_not_repeat_an_insertion_at_eof_as_context() {
let output = format_diff(
"[package]
name = \"example\"
",
"[package]
name = \"example\"
include = [\"src/**/*\"]
",
);
assert_eq!(
output,
"Diff - removed / added + :
[package]
name = \"example\"
+include = [\"src/**/*\"]
"
);
}
#[test]
fn does_not_repeat_a_deletion_at_eof_as_context() {
let output = format_diff(
"[package]
name = \"example\"
include = [\"src/**/*\"]
",
"[package]
name = \"example\"
",
);
assert_eq!(
output,
"Diff - removed / added + :
[package]
name = \"example\"
-include = [\"src/**/*\"]
"
);
}
}