diff --git a/Cargo.toml b/Cargo.toml index d880808..d20df6b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ readme = "README.md" travis-ci = { repository = "dtolnay/trybuild" } [dependencies] +diffr-lib = "=0.1.3" glob = "0.3" lazy_static = "1.3" serde = { version = "1.0.103", features = ["derive"] } diff --git a/src/diff.rs b/src/diff.rs new file mode 100644 index 0000000..346987a --- /dev/null +++ b/src/diff.rs @@ -0,0 +1,112 @@ +use diffr_lib::{diff, tokenize, DiffInput, HashedSpan, Snake, Tokenization}; +use std::cmp; +use std::iter::Peekable; +use std::slice; + +pub struct Diff<'a> { + pub worth_printing: bool, + expected: &'a str, + expected_tokens: Vec, + actual: &'a str, + actual_tokens: Vec, + common: Vec, +} + +impl<'a> Diff<'a> { + pub fn compute(expected: &'a str, actual: &'a str) -> Self { + let mut actual_tokens = Vec::new(); + tokenize(actual.as_bytes(), 0, &mut actual_tokens); + let added = Tokenization::new(actual.as_bytes(), &actual_tokens); + + let mut expected_tokens = Vec::new(); + tokenize(expected.as_bytes(), 0, &mut expected_tokens); + let removed = Tokenization::new(expected.as_bytes(), &expected_tokens); + + let input = DiffInput { added, removed }; + let mut scratch = Vec::new(); + let mut common = Vec::new(); + diff(&input, &mut scratch, &mut common); + + let min_len = cmp::max(expected_tokens.len(), actual_tokens.len()); + let common_len = common.iter().map(|snake| snake.len).sum::() as usize; + let worth_printing = common_len / 4 >= min_len / 5; + + Diff { + worth_printing, + expected, + expected_tokens, + actual, + actual_tokens, + common, + } + } + + pub fn iter(&self, input: &str) -> Iter { + if input == self.expected { + Iter { + pos: 0, + input: self.expected, + tokens: &self.expected_tokens, + common: self.common.iter().peekable(), + token_index: |snake| snake.x0, + } + } else if input == self.actual { + Iter { + pos: 0, + input: self.actual, + tokens: &self.actual_tokens, + common: self.common.iter().peekable(), + token_index: |snake| snake.y0, + } + } else { + panic!("unrecognized input"); + } + } +} + +pub struct Iter<'a> { + pos: usize, + input: &'a str, + tokens: &'a [HashedSpan], + common: Peekable>, + token_index: fn(&Snake) -> isize, +} + +pub enum Chunk<'a> { + Common(&'a str), + Unique(&'a str), +} + +impl<'a> Iterator for Iter<'a> { + type Item = Chunk<'a>; + + fn next(&mut self) -> Option { + match self.common.peek() { + Some(common) => { + let index = (self.token_index)(common); + let begin = &self.tokens[index as usize]; + if self.pos < begin.lo { + let chunk = &self.input[self.pos..begin.lo]; + self.pos = begin.lo; + Some(Chunk::Unique(chunk)) + } else { + let index = (self.token_index)(common) + common.len - 1; + let end = &self.tokens[index as usize]; + let chunk = &self.input[begin.lo..end.hi]; + self.common.next().unwrap(); + self.pos = end.hi; + Some(Chunk::Common(chunk)) + } + } + None => { + if self.pos < self.input.len() { + let chunk = &self.input[self.pos..]; + self.pos = self.input.len(); + Some(Chunk::Unique(chunk)) + } else { + None + } + } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 8548065..7d63b8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -208,6 +208,7 @@ mod path; mod cargo; mod dependencies; +mod diff; mod env; mod error; mod features; diff --git a/src/message.rs b/src/message.rs index 8e4903e..baaab04 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,6 +1,7 @@ use termcolor::Color::{self, *}; use super::{Expected, Test}; +use crate::diff::{Chunk, Diff}; use crate::error::Error; use crate::normalize; use crate::term; @@ -129,13 +130,14 @@ pub(crate) fn mismatch(expected: &str, actual: &str) { println!("mismatch"); term::reset(); println!(); + let diff = Diff::compute(expected, actual); term::bold_color(Blue); println!("EXPECTED:"); - snippet(Blue, expected); + snippet_diff(Blue, expected, Some(&diff)); println!(); term::bold_color(Red); println!("ACTUAL OUTPUT:"); - snippet(Red, actual); + snippet_diff(Red, actual, Some(&diff)); println!(); } @@ -203,13 +205,36 @@ pub(crate) fn warnings(warnings: &str) { } fn snippet(color: Color, content: &str) { + snippet_diff(color, content, None); +} + +fn snippet_diff(color: Color, content: &str, diff: Option<&Diff>) { fn dotted_line() { println!("{}", "┈".repeat(60)); } term::color(color); dotted_line(); - print!("{}", content); + + match diff { + Some(diff) if diff.worth_printing => { + for chunk in diff.iter(content) { + match chunk { + Chunk::Common(s) => { + term::color(color); + print!("{}", s); + } + Chunk::Unique(s) => { + term::bold_color(color); + print!("\x1B[7m{}", s); + } + } + } + } + _ => print!("{}", content), + } + + term::color(color); dotted_line(); term::reset(); }