blob: af3e3e6a92cd59bac2fad191d3ec9c66bcb65b91 [file] [log] [blame]
#[cfg(feature = "alloc")]
use alloc::format;
use ansi_term::{
Colour::{Fixed, Green, Red},
Style,
};
use core::fmt;
macro_rules! paint {
($f:expr, $colour:expr, $fmt:expr, $($args:tt)*) => (
write!($f, "{}", $colour.paint(format!($fmt, $($args)*)))
)
}
const SIGN_RIGHT: char = '>'; // + > →
const SIGN_LEFT: char = '<'; // - < ←
/// Present the diff output for two mutliline strings in a pretty, colorised manner.
pub(crate) fn write_header(f: &mut fmt::Formatter) -> fmt::Result {
writeln!(
f,
"{} {} / {} :",
Style::new().bold().paint("Diff"),
Red.paint(format!("{} left", SIGN_LEFT)),
Green.paint(format!("right {}", SIGN_RIGHT))
)
}
/// Delay formatting this deleted chunk until later.
///
/// It can be formatted as a whole chunk by calling `flush`, or the inner value
/// obtained with `take` for further processing (such as an inline diff).
#[derive(Default)]
struct LatentDeletion<'a> {
// The most recent deleted line we've seen
value: Option<&'a str>,
// The number of deleted lines we've seen, including the current value
count: usize,
}
impl<'a> LatentDeletion<'a> {
/// Set the chunk value.
fn set(&mut self, value: &'a str) {
self.value = Some(value);
self.count += 1;
}
/// Take the underlying chunk value, if it's suitable for inline diffing.
///
/// If there is no value or we've seen more than one line, return `None`.
fn take(&mut self) -> Option<&'a str> {
if self.count == 1 {
self.value.take()
} else {
None
}
}
/// If a value is set, print it as a whole chunk, using the given formatter.
///
/// If a value is not set, reset the count to zero (as we've called `flush` twice,
/// without seeing another deletion. Therefore the line in the middle was something else).
fn flush<TWrite: fmt::Write>(&mut self, f: &mut TWrite) -> fmt::Result {
if let Some(value) = self.value {
paint!(f, Red, "{}{}", SIGN_LEFT, value)?;
writeln!(f)?;
self.value = None;
} else {
self.count = 0;
}
Ok(())
}
}
// Adapted from:
// https://github.com/johannhof/difference.rs/blob/c5749ad7d82aa3d480c15cb61af9f6baa08f116f/examples/github-style.rs
// Credits johannhof (MIT License)
/// Present the diff output for two mutliline strings in a pretty, colorised manner.
pub(crate) fn write_lines<TWrite: fmt::Write>(
f: &mut TWrite,
left: &str,
right: &str,
) -> fmt::Result {
let diff = ::diff::lines(left, right);
let mut changes = diff.into_iter().peekable();
let mut previous_deletion = LatentDeletion::default();
while let Some(change) = changes.next() {
match (change, changes.peek()) {
// If the text is unchanged, just print it plain
(::diff::Result::Both(value, _), _) => {
previous_deletion.flush(f)?;
writeln!(f, " {}", value)?;
}
// Defer any deletions to next loop
(::diff::Result::Left(deleted), _) => {
previous_deletion.flush(f)?;
previous_deletion.set(deleted);
}
// If we're being followed by more insertions, don't inline diff
(::diff::Result::Right(inserted), Some(::diff::Result::Right(_))) => {
previous_deletion.flush(f)?;
paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
writeln!(f)?;
}
// Otherwise, check if we need to inline diff with the previous line (if it was a deletion)
(::diff::Result::Right(inserted), _) => {
if let Some(deleted) = previous_deletion.take() {
write_inline_diff(f, deleted, inserted)?;
} else {
previous_deletion.flush(f)?;
paint!(f, Green, "{}{}", SIGN_RIGHT, inserted)?;
writeln!(f)?;
}
}
};
}
previous_deletion.flush(f)?;
Ok(())
}
/// Group character styling for an inline diff, to prevent wrapping each single
/// character in terminal styling codes.
///
/// Styles are applied automatically each time a new style is given in `write_with_style`.
struct InlineWriter<'a, Writer> {
f: &'a mut Writer,
style: Style,
}
impl<'a, Writer> InlineWriter<'a, Writer>
where
Writer: fmt::Write,
{
fn new(f: &'a mut Writer) -> Self {
InlineWriter {
f,
style: Style::new(),
}
}
/// Push a new character into the buffer, specifying the style it should be written in.
fn write_with_style(&mut self, c: &char, style: Style) -> fmt::Result {
// If the style is the same as previously, just write character
if style == self.style {
write!(self.f, "{}", c)?;
} else {
// Close out previous style
write!(self.f, "{}", self.style.suffix())?;
// Store new style and start writing it
write!(self.f, "{}{}", style.prefix(), c)?;
self.style = style;
}
Ok(())
}
/// Finish any existing style and reset to default state.
fn finish(&mut self) -> fmt::Result {
// Close out previous style
writeln!(self.f, "{}", self.style.suffix())?;
self.style = Default::default();
Ok(())
}
}
/// Format a single line to show an inline diff of the two strings given.
///
/// The given strings should not have a trailing newline.
///
/// The output of this function will be two lines, each with a trailing newline.
fn write_inline_diff<TWrite: fmt::Write>(f: &mut TWrite, left: &str, right: &str) -> fmt::Result {
let diff = ::diff::chars(left, right);
let mut writer = InlineWriter::new(f);
// Print the left string on one line, with differences highlighted
let light = Red.into();
let heavy = Red.on(Fixed(52)).bold();
writer.write_with_style(&SIGN_LEFT, light)?;
for change in diff.iter() {
match change {
::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
::diff::Result::Left(value) => writer.write_with_style(value, heavy)?,
_ => (),
}
}
writer.finish()?;
// Print the right string on one line, with differences highlighted
let light = Green.into();
let heavy = Green.on(Fixed(22)).bold();
writer.write_with_style(&SIGN_RIGHT, light)?;
for change in diff.iter() {
match change {
::diff::Result::Both(value, _) => writer.write_with_style(value, light)?,
::diff::Result::Right(value) => writer.write_with_style(value, heavy)?,
_ => (),
}
}
writer.finish()
}
#[cfg(test)]
mod test {
use super::*;
#[cfg(feature = "alloc")]
use alloc::string::String;
// ANSI terminal codes used in our outputs.
//
// Interpolate these into test strings to make expected values easier to read.
const RED_LIGHT: &str = "\u{1b}[31m";
const GREEN_LIGHT: &str = "\u{1b}[32m";
const RED_HEAVY: &str = "\u{1b}[1;48;5;52;31m";
const GREEN_HEAVY: &str = "\u{1b}[1;48;5;22;32m";
const RESET: &str = "\u{1b}[0m";
/// Given that both of our diff printing functions have the same
/// type signature, we can reuse the same test code for them.
///
/// This could probably be nicer with traits!
fn check_printer<TPrint>(printer: TPrint, left: &str, right: &str, expected: &str)
where
TPrint: Fn(&mut String, &str, &str) -> fmt::Result,
{
let mut actual = String::new();
printer(&mut actual, left, right).expect("printer function failed");
// Cannot use IO without stdlib
#[cfg(feature = "std")]
println!(
"## left ##\n\
{}\n\
## right ##\n\
{}\n\
## actual diff ##\n\
{}\n\
## expected diff ##\n\
{}",
left, right, actual, expected
);
assert_eq!(actual, expected);
}
#[test]
fn write_inline_diff_empty() {
let left = "";
let right = "";
let expected = format!(
"{red_light}<{reset}\n\
{green_light}>{reset}\n",
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_inline_diff, left, right, &expected);
}
#[test]
fn write_inline_diff_added() {
let left = "";
let right = "polymerase";
let expected = format!(
"{red_light}<{reset}\n\
{green_light}>{reset}{green_heavy}polymerase{reset}\n",
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_inline_diff, left, right, &expected);
}
#[test]
fn write_inline_diff_removed() {
let left = "polyacrylamide";
let right = "";
let expected = format!(
"{red_light}<{reset}{red_heavy}polyacrylamide{reset}\n\
{green_light}>{reset}\n",
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
red_heavy = RED_HEAVY,
reset = RESET,
);
check_printer(write_inline_diff, left, right, &expected);
}
#[test]
fn write_inline_diff_changed() {
let left = "polymerase";
let right = "polyacrylamide";
let expected = format!(
"{red_light}<poly{reset}{red_heavy}me{reset}{red_light}ra{reset}{red_heavy}s{reset}{red_light}e{reset}\n\
{green_light}>poly{reset}{green_heavy}ac{reset}{green_light}r{reset}{green_heavy}yl{reset}{green_light}a{reset}{green_heavy}mid{reset}{green_light}e{reset}\n",
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
red_heavy = RED_HEAVY,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_inline_diff, left, right, &expected);
}
/// If one of our strings is empty, it should not be shown at all in the output.
#[test]
fn write_lines_empty_string() {
let left = "";
let right = "content";
let expected = format!(
"{green_light}>content{reset}\n",
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Realistic multiline struct diffing case.
#[test]
fn write_lines_struct() {
let left = r#"Some(
Foo {
lorem: "Hello World!",
ipsum: 42,
dolor: Ok(
"hey",
),
},
)"#;
let right = r#"Some(
Foo {
lorem: "Hello Wrold!",
ipsum: 42,
dolor: Ok(
"hey ho!",
),
},
)"#;
let expected = format!(
r#" Some(
Foo {{
{red_light}< lorem: "Hello W{reset}{red_heavy}o{reset}{red_light}rld!",{reset}
{green_light}> lorem: "Hello Wr{reset}{green_heavy}o{reset}{green_light}ld!",{reset}
ipsum: 42,
dolor: Ok(
{red_light}< "hey",{reset}
{green_light}> "hey{reset}{green_heavy} ho!{reset}{green_light}",{reset}
),
}},
)
"#,
red_light = RED_LIGHT,
red_heavy = RED_HEAVY,
green_light = GREEN_LIGHT,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Relistic multiple line chunks
///
/// We can't support realistic line diffing in large blocks
/// (also, it's unclear how usefult this is)
///
/// So if we have more than one line in a single removal chunk, disable inline diffing.
#[test]
fn write_lines_multiline_block() {
let left = r#"Proboscis
Cabbage"#;
let right = r#"Probed
Caravaggio"#;
let expected = format!(
r#"{red_light}<Proboscis{reset}
{red_light}<Cabbage{reset}
{green_light}>Probed{reset}
{green_light}>Caravaggio{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Single deletion line, multiple insertions - no inline diffing.
#[test]
fn write_lines_multiline_insert() {
let left = r#"Cabbage"#;
let right = r#"Probed
Caravaggio"#;
let expected = format!(
r#"{red_light}<Cabbage{reset}
{green_light}>Probed{reset}
{green_light}>Caravaggio{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Multiple deletion, single insertion - no inline diffing.
#[test]
fn write_lines_multiline_delete() {
let left = r#"Proboscis
Cabbage"#;
let right = r#"Probed"#;
let expected = format!(
r#"{red_light}<Proboscis{reset}
{red_light}<Cabbage{reset}
{green_light}>Probed{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Regression test for multiline highlighting issue
#[test]
fn write_lines_issue12() {
let left = r#"[
0,
0,
0,
128,
10,
191,
5,
64,
]"#;
let right = r#"[
84,
248,
45,
64,
]"#;
let expected = format!(
r#" [
{red_light}< 0,{reset}
{red_light}< 0,{reset}
{red_light}< 0,{reset}
{red_light}< 128,{reset}
{red_light}< 10,{reset}
{red_light}< 191,{reset}
{red_light}< 5,{reset}
{green_light}> 84,{reset}
{green_light}> 248,{reset}
{green_light}> 45,{reset}
64,
]
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
mod write_lines_edge_newlines {
use super::*;
#[test]
fn both_trailing() {
let left = "fan\n";
let right = "mug\n";
// Note the additional space at the bottom is caused by a trailing newline
// adding an additional line with zero content to both sides of the diff
let expected = format!(
r#"{red_light}<{reset}{red_heavy}fan{reset}
{green_light}>{reset}{green_heavy}mug{reset}
"#,
red_light = RED_LIGHT,
red_heavy = RED_HEAVY,
green_light = GREEN_LIGHT,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
#[test]
fn both_leading() {
let left = "\nfan";
let right = "\nmug";
// Note the additional space at the top is caused by a leading newline
// adding an additional line with zero content to both sides of the diff
let expected = format!(
r#"
{red_light}<{reset}{red_heavy}fan{reset}
{green_light}>{reset}{green_heavy}mug{reset}
"#,
red_light = RED_LIGHT,
red_heavy = RED_HEAVY,
green_light = GREEN_LIGHT,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
#[test]
fn leading_added() {
let left = "fan";
let right = "\nmug";
let expected = format!(
r#"{red_light}<fan{reset}
{green_light}>{reset}
{green_light}>mug{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
#[test]
fn leading_deleted() {
let left = "\nfan";
let right = "mug";
let expected = format!(
r#"{red_light}<{reset}
{red_light}<fan{reset}
{green_light}>mug{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
#[test]
fn trailing_added() {
let left = "fan";
let right = "mug\n";
let expected = format!(
r#"{red_light}<fan{reset}
{green_light}>mug{reset}
{green_light}>{reset}
"#,
red_light = RED_LIGHT,
green_light = GREEN_LIGHT,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
/// Regression test for double abort
///
/// See: https://github.com/colin-kiegel/rust-pretty-assertions/issues/96
#[test]
fn trailing_deleted() {
// The below inputs caused an abort via double panic
// we panicked at 'insertion followed by deletion'
let left = "fan\n";
let right = "mug";
let expected = format!(
r#"{red_light}<{reset}{red_heavy}fan{reset}
{green_light}>{reset}{green_heavy}mug{reset}
{red_light}<{reset}
"#,
red_light = RED_LIGHT,
red_heavy = RED_HEAVY,
green_light = GREEN_LIGHT,
green_heavy = GREEN_HEAVY,
reset = RESET,
);
check_printer(write_lines, left, right, &expected);
}
}
}