| use std::collections::VecDeque; | |
| use std::fmt; | |
| use std::io; | |
| use std::io::Write; | |
| use crate::config::{Color, Config, Verbosity}; | |
| #[derive(Debug, PartialEq)] | |
| pub(crate) enum DiffLine { | |
| Context(String), | |
| Expected(String), | |
| Resulting(String), | |
| } | |
| #[derive(Debug, PartialEq)] | |
| pub(crate) struct Mismatch { | |
| /// The line number in the formatted version. | |
| pub(crate) line_number: u32, | |
| /// The line number in the original version. | |
| pub(crate) line_number_orig: u32, | |
| /// The set of lines (context and old/new) in the mismatch. | |
| pub(crate) 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| matches!(line, DiffLine::Resulting(_))) | |
| .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.len() | |
| )?; | |
| 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::{Mismatch, make_diff}; | |
| 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(())); | |
| } | |
| } |