| use std::collections::VecDeque; |
| use std::fmt; |
| use std::io; |
| use std::io::Write; |
| |
| use diff; |
| |
| use crate::config::{Color, Config, Verbosity}; |
| |
| #[derive(Debug, PartialEq)] |
| pub enum DiffLine { |
| Context(String), |
| Expected(String), |
| Resulting(String), |
| } |
| |
| #[derive(Debug, PartialEq)] |
| pub struct Mismatch { |
| /// The line number in the formatted version. |
| pub line_number: u32, |
| /// The line number in the original version. |
| pub line_number_orig: u32, |
| /// The set of lines (context and old/new) in the mismatch. |
| pub lines: Vec<DiffLine>, |
| } |
| |
| impl Mismatch { |
| fn new(line_number: u32, line_number_orig: u32) -> Mismatch { |
| Mismatch { |
| line_number, |
| line_number_orig, |
| lines: Vec::new(), |
| } |
| } |
| } |
| |
| /// A single span of changed lines, with 0 or more removed lines |
| /// and a vector of 0 or more inserted lines. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct ModifiedChunk { |
| /// The first to be removed from the original text |
| pub line_number_orig: u32, |
| /// The number of lines which have been replaced |
| pub lines_removed: u32, |
| /// The new lines |
| pub lines: Vec<String>, |
| } |
| |
| /// Set of changed sections of a file. |
| #[derive(Debug, PartialEq, Eq)] |
| pub struct ModifiedLines { |
| /// The set of changed chunks. |
| pub chunks: Vec<ModifiedChunk>, |
| } |
| |
| impl From<Vec<Mismatch>> for ModifiedLines { |
| fn from(mismatches: Vec<Mismatch>) -> ModifiedLines { |
| let chunks = mismatches.into_iter().map(|mismatch| { |
| let lines = mismatch.lines.iter(); |
| let num_removed = lines |
| .filter(|line| match line { |
| DiffLine::Resulting(_) => true, |
| _ => false, |
| }) |
| .count(); |
| |
| let new_lines = mismatch.lines.into_iter().filter_map(|line| match line { |
| DiffLine::Context(_) | DiffLine::Resulting(_) => None, |
| DiffLine::Expected(str) => Some(str), |
| }); |
| |
| ModifiedChunk { |
| line_number_orig: mismatch.line_number_orig, |
| lines_removed: num_removed as u32, |
| lines: new_lines.collect(), |
| } |
| }); |
| |
| ModifiedLines { |
| chunks: chunks.collect(), |
| } |
| } |
| } |
| |
| // Converts a `Mismatch` into a serialized form, which just includes |
| // enough information to modify the original file. |
| // Each section starts with a line with three integers, space separated: |
| // lineno num_removed num_added |
| // followed by (`num_added`) lines of added text. The line numbers are |
| // relative to the original file. |
| impl fmt::Display for ModifiedLines { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| for chunk in &self.chunks { |
| writeln!( |
| f, |
| "{} {} {}", |
| chunk.line_number_orig, |
| chunk.lines_removed, |
| chunk.lines.iter().count() |
| )?; |
| |
| for line in &chunk.lines { |
| writeln!(f, "{}", line)?; |
| } |
| } |
| |
| Ok(()) |
| } |
| } |
| |
| // Allows to convert `Display`ed `ModifiedLines` back to the structural data. |
| impl std::str::FromStr for ModifiedLines { |
| type Err = (); |
| |
| fn from_str(s: &str) -> Result<ModifiedLines, ()> { |
| let mut chunks = vec![]; |
| |
| let mut lines = s.lines(); |
| while let Some(header) = lines.next() { |
| let mut header = header.split_whitespace(); |
| let (orig, rem, new_lines) = match (header.next(), header.next(), header.next()) { |
| (Some(orig), Some(removed), Some(added)) => (orig, removed, added), |
| _ => return Err(()), |
| }; |
| let (orig, rem, new_lines): (u32, u32, usize) = |
| match (orig.parse(), rem.parse(), new_lines.parse()) { |
| (Ok(a), Ok(b), Ok(c)) => (a, b, c), |
| _ => return Err(()), |
| }; |
| let lines = lines.by_ref().take(new_lines); |
| let lines: Vec<_> = lines.map(ToOwned::to_owned).collect(); |
| if lines.len() != new_lines { |
| return Err(()); |
| } |
| |
| chunks.push(ModifiedChunk { |
| line_number_orig: orig, |
| lines_removed: rem, |
| lines, |
| }); |
| } |
| |
| Ok(ModifiedLines { chunks }) |
| } |
| } |
| |
| // This struct handles writing output to stdout and abstracts away the logic |
| // of printing in color, if it's possible in the executing environment. |
| pub(crate) struct OutputWriter { |
| terminal: Option<Box<dyn term::Terminal<Output = io::Stdout>>>, |
| } |
| |
| impl OutputWriter { |
| // Create a new OutputWriter instance based on the caller's preference |
| // for colorized output and the capabilities of the terminal. |
| pub(crate) fn new(color: Color) -> Self { |
| if let Some(t) = term::stdout() { |
| if color.use_colored_tty() && t.supports_color() { |
| return OutputWriter { terminal: Some(t) }; |
| } |
| } |
| OutputWriter { terminal: None } |
| } |
| |
| // Write output in the optionally specified color. The output is written |
| // in the specified color if this OutputWriter instance contains a |
| // Terminal in its `terminal` field. |
| pub(crate) fn writeln(&mut self, msg: &str, color: Option<term::color::Color>) { |
| match &mut self.terminal { |
| Some(ref mut t) => { |
| if let Some(color) = color { |
| t.fg(color).unwrap(); |
| } |
| writeln!(t, "{}", msg).unwrap(); |
| if color.is_some() { |
| t.reset().unwrap(); |
| } |
| } |
| None => println!("{}", msg), |
| } |
| } |
| } |
| |
| // Produces a diff between the expected output and actual output of rustfmt. |
| pub(crate) fn make_diff(expected: &str, actual: &str, context_size: usize) -> Vec<Mismatch> { |
| let mut line_number = 1; |
| let mut line_number_orig = 1; |
| let mut context_queue: VecDeque<&str> = VecDeque::with_capacity(context_size); |
| let mut lines_since_mismatch = context_size + 1; |
| let mut results = Vec::new(); |
| let mut mismatch = Mismatch::new(0, 0); |
| |
| for result in diff::lines(expected, actual) { |
| match result { |
| diff::Result::Left(str) => { |
| if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { |
| results.push(mismatch); |
| mismatch = Mismatch::new( |
| line_number - context_queue.len() as u32, |
| line_number_orig - context_queue.len() as u32, |
| ); |
| } |
| |
| while let Some(line) = context_queue.pop_front() { |
| mismatch.lines.push(DiffLine::Context(line.to_owned())); |
| } |
| |
| mismatch.lines.push(DiffLine::Resulting(str.to_owned())); |
| line_number_orig += 1; |
| lines_since_mismatch = 0; |
| } |
| diff::Result::Right(str) => { |
| if lines_since_mismatch >= context_size && lines_since_mismatch > 0 { |
| results.push(mismatch); |
| mismatch = Mismatch::new( |
| line_number - context_queue.len() as u32, |
| line_number_orig - context_queue.len() as u32, |
| ); |
| } |
| |
| while let Some(line) = context_queue.pop_front() { |
| mismatch.lines.push(DiffLine::Context(line.to_owned())); |
| } |
| |
| mismatch.lines.push(DiffLine::Expected(str.to_owned())); |
| line_number += 1; |
| lines_since_mismatch = 0; |
| } |
| diff::Result::Both(str, _) => { |
| if context_queue.len() >= context_size { |
| let _ = context_queue.pop_front(); |
| } |
| |
| if lines_since_mismatch < context_size { |
| mismatch.lines.push(DiffLine::Context(str.to_owned())); |
| } else if context_size > 0 { |
| context_queue.push_back(str); |
| } |
| |
| line_number += 1; |
| line_number_orig += 1; |
| lines_since_mismatch += 1; |
| } |
| } |
| } |
| |
| results.push(mismatch); |
| results.remove(0); |
| |
| results |
| } |
| |
| pub(crate) fn print_diff<F>(diff: Vec<Mismatch>, get_section_title: F, config: &Config) |
| where |
| F: Fn(u32) -> String, |
| { |
| let color = config.color(); |
| let line_terminator = if config.verbose() == Verbosity::Verbose { |
| "⏎" |
| } else { |
| "" |
| }; |
| |
| let mut writer = OutputWriter::new(color); |
| |
| for mismatch in diff { |
| let title = get_section_title(mismatch.line_number_orig); |
| writer.writeln(&title, None); |
| |
| for line in mismatch.lines { |
| match line { |
| DiffLine::Context(ref str) => { |
| writer.writeln(&format!(" {}{}", str, line_terminator), None) |
| } |
| DiffLine::Expected(ref str) => writer.writeln( |
| &format!("+{}{}", str, line_terminator), |
| Some(term::color::GREEN), |
| ), |
| DiffLine::Resulting(ref str) => writer.writeln( |
| &format!("-{}{}", str, line_terminator), |
| Some(term::color::RED), |
| ), |
| } |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::DiffLine::*; |
| use super::{make_diff, Mismatch}; |
| use super::{ModifiedChunk, ModifiedLines}; |
| |
| #[test] |
| fn diff_simple() { |
| let src = "one\ntwo\nthree\nfour\nfive\n"; |
| let dest = "one\ntwo\ntrois\nfour\nfive\n"; |
| let diff = make_diff(src, dest, 1); |
| assert_eq!( |
| diff, |
| vec![Mismatch { |
| line_number: 2, |
| line_number_orig: 2, |
| lines: vec![ |
| Context("two".to_owned()), |
| Resulting("three".to_owned()), |
| Expected("trois".to_owned()), |
| Context("four".to_owned()), |
| ], |
| }] |
| ); |
| } |
| |
| #[test] |
| fn diff_simple2() { |
| let src = "one\ntwo\nthree\nfour\nfive\nsix\nseven\n"; |
| let dest = "one\ntwo\ntrois\nfour\ncinq\nsix\nseven\n"; |
| let diff = make_diff(src, dest, 1); |
| assert_eq!( |
| diff, |
| vec![ |
| Mismatch { |
| line_number: 2, |
| line_number_orig: 2, |
| lines: vec![ |
| Context("two".to_owned()), |
| Resulting("three".to_owned()), |
| Expected("trois".to_owned()), |
| Context("four".to_owned()), |
| ], |
| }, |
| Mismatch { |
| line_number: 5, |
| line_number_orig: 5, |
| lines: vec![ |
| Resulting("five".to_owned()), |
| Expected("cinq".to_owned()), |
| Context("six".to_owned()), |
| ], |
| }, |
| ] |
| ); |
| } |
| |
| #[test] |
| fn diff_zerocontext() { |
| let src = "one\ntwo\nthree\nfour\nfive\n"; |
| let dest = "one\ntwo\ntrois\nfour\nfive\n"; |
| let diff = make_diff(src, dest, 0); |
| assert_eq!( |
| diff, |
| vec![Mismatch { |
| line_number: 3, |
| line_number_orig: 3, |
| lines: vec![Resulting("three".to_owned()), Expected("trois".to_owned())], |
| }] |
| ); |
| } |
| |
| #[test] |
| fn diff_trailing_newline() { |
| let src = "one\ntwo\nthree\nfour\nfive"; |
| let dest = "one\ntwo\nthree\nfour\nfive\n"; |
| let diff = make_diff(src, dest, 1); |
| assert_eq!( |
| diff, |
| vec![Mismatch { |
| line_number: 5, |
| line_number_orig: 5, |
| lines: vec![Context("five".to_owned()), Expected("".to_owned())], |
| }] |
| ); |
| } |
| |
| #[test] |
| fn modified_lines_from_str() { |
| use std::str::FromStr; |
| |
| let src = "1 6 2\nfn some() {}\nfn main() {}\n25 3 1\n struct Test {}"; |
| let lines = ModifiedLines::from_str(src).unwrap(); |
| assert_eq!( |
| lines, |
| ModifiedLines { |
| chunks: vec![ |
| ModifiedChunk { |
| line_number_orig: 1, |
| lines_removed: 6, |
| lines: vec!["fn some() {}".to_owned(), "fn main() {}".to_owned(),] |
| }, |
| ModifiedChunk { |
| line_number_orig: 25, |
| lines_removed: 3, |
| lines: vec![" struct Test {}".to_owned()] |
| } |
| ] |
| } |
| ); |
| |
| let src = "1 5 3"; |
| assert_eq!(ModifiedLines::from_str(src), Err(())); |
| |
| let src = "1 5 3\na\nb"; |
| assert_eq!(ModifiedLines::from_str(src), Err(())); |
| } |
| } |