diff --git a/cli/diff.rs b/cli/diff.rs index 1adb3da4aa..33af8bf0eb 100644 --- a/cli/diff.rs +++ b/cli/diff.rs @@ -3,6 +3,142 @@ use crate::colors; use dissimilar::{diff as difference, Chunk}; +/// Print diff of the same file_path, before and after formatting. +/// +/// Diff format is loosely based on Github diff formatting. +pub fn diff(orig_text: &str, edit_text: &str) -> String { + if orig_text == edit_text { + return String::new(); + } + + // normalize newlines as it adds too much noise if they differ + let orig_text = orig_text.replace("\r\n", "\n"); + let edit_text = edit_text.replace("\r\n", "\n"); + + if orig_text == edit_text { + return " | Text differed by line endings.\n".to_string(); + } + + DiffBuilder::build(&orig_text, &edit_text) +} + +struct DiffBuilder { + output: String, + line_number_width: usize, + orig_line: usize, + edit_line: usize, + orig: String, + edit: String, + has_changes: bool, +} + +impl DiffBuilder { + pub fn build(orig_text: &str, edit_text: &str) -> String { + let mut diff_builder = DiffBuilder { + output: String::new(), + orig_line: 1, + edit_line: 1, + orig: String::new(), + edit: String::new(), + has_changes: false, + line_number_width: { + let line_count = std::cmp::max( + orig_text.split('\n').count(), + edit_text.split('\n').count(), + ); + line_count.to_string().chars().count() + }, + }; + + let chunks = difference(orig_text, edit_text); + diff_builder.handle_chunks(chunks); + diff_builder.output + } + + fn handle_chunks<'a>(&'a mut self, chunks: Vec>) { + for chunk in chunks { + match chunk { + Chunk::Delete(s) => { + let split = s.split('\n').enumerate(); + for (i, s) in split { + if i > 0 { + self.orig.push('\n'); + } + self.orig.push_str(&fmt_rem_text_highlight(s)); + } + self.has_changes = true + } + Chunk::Insert(s) => { + let split = s.split('\n').enumerate(); + for (i, s) in split { + if i > 0 { + self.edit.push('\n'); + } + self.edit.push_str(&fmt_add_text_highlight(s)); + } + self.has_changes = true + } + Chunk::Equal(s) => { + let split = s.split('\n').enumerate(); + for (i, s) in split { + if i > 0 { + self.flush_changes(); + } + self.orig.push_str(&fmt_rem_text(s)); + self.edit.push_str(&fmt_add_text(s)); + } + } + } + } + + self.flush_changes(); + } + + fn flush_changes(&mut self) { + if self.has_changes { + self.write_line_diff(); + + self.orig_line += self.orig.split('\n').count(); + self.edit_line += self.edit.split('\n').count(); + self.has_changes = false; + } else { + self.orig_line += 1; + self.edit_line += 1; + } + + self.orig.clear(); + self.edit.clear(); + } + + fn write_line_diff(&mut self) { + let split = self.orig.split('\n').enumerate(); + for (i, s) in split { + self.output.push_str(&format!( + "{:width$}{} ", + self.orig_line + i, + colors::gray(" |"), + width = self.line_number_width + )); + self.output.push_str(&fmt_rem()); + self.output.push_str(s); + self.output.push('\n'); + } + + let split = self.edit.split('\n').enumerate(); + for (i, s) in split { + self.output.push_str(&format!( + "{:width$}{} ", + self.edit_line + i, + colors::gray(" |"), + width = self.line_number_width + )); + self.output.push_str(&fmt_add()); + self.output.push_str(s); + self.output.push('\n'); + } + } +} + fn fmt_add() -> String { colors::green_bold("+").to_string() } @@ -27,146 +163,60 @@ fn fmt_rem_text_highlight(x: &str) -> String { colors::white_on_red(x).to_string() } -fn write_line_diff( - diff: &mut String, - orig_line: &mut usize, - edit_line: &mut usize, - line_number_width: usize, - orig: &mut String, - edit: &mut String, -) { - let split = orig.split('\n').enumerate(); - for (i, s) in split { - diff.push_str(&format!( - "{:width$}{} ", - *orig_line + i, - colors::gray(" |"), - width = line_number_width - )); - diff.push_str(&fmt_rem()); - diff.push_str(s); - diff.push('\n'); - } - - let split = edit.split('\n').enumerate(); - for (i, s) in split { - diff.push_str(&format!( - "{:width$}{} ", - *edit_line + i, - colors::gray(" |"), - width = line_number_width - )); - diff.push_str(&fmt_add()); - diff.push_str(s); - diff.push('\n'); - } - - *orig_line += orig.split('\n').count(); - *edit_line += edit.split('\n').count(); - - orig.clear(); - edit.clear(); -} - -/// Print diff of the same file_path, before and after formatting. -/// -/// Diff format is loosely based on Github diff formatting. -pub fn diff(orig_text: &str, edit_text: &str) -> String { - let lines = edit_text.split('\n').count(); - let line_number_width = lines.to_string().chars().count(); - - let mut diff = String::new(); - - let mut text1 = orig_text.to_string(); - let mut text2 = edit_text.to_string(); - - if !text1.ends_with('\n') { - text1.push('\n'); - } - if !text2.ends_with('\n') { - text2.push('\n'); - } - - let mut orig_line: usize = 1; - let mut edit_line: usize = 1; - let mut orig: String = String::new(); - let mut edit: String = String::new(); - let mut changes = false; - - let chunks = difference(&text1, &text2); - for chunk in chunks { - match chunk { - Chunk::Delete(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - orig.push('\n'); - } - orig.push_str(&fmt_rem_text_highlight(s)); - } - changes = true - } - Chunk::Insert(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - edit.push('\n'); - } - edit.push_str(&fmt_add_text_highlight(s)); - } - changes = true - } - Chunk::Equal(s) => { - let split = s.split('\n').enumerate(); - for (i, s) in split { - if i > 0 { - if changes { - write_line_diff( - &mut diff, - &mut orig_line, - &mut edit_line, - line_number_width, - &mut orig, - &mut edit, - ); - changes = false - } else { - orig.clear(); - edit.clear(); - orig_line += 1; - edit_line += 1; - } - } - orig.push_str(&fmt_rem_text(s)); - edit.push_str(&fmt_add_text(s)); - } - } - } - } - diff -} - #[cfg(test)] mod tests { use super::*; #[test] fn test_diff() { - let simple_console_log_unfmt = "console.log('Hello World')"; - let simple_console_log_fmt = "console.log(\"Hello World\");"; - assert_eq!( - colors::strip_ansi_codes(&diff( - simple_console_log_unfmt, - simple_console_log_fmt - )), - "1 | -console.log('Hello World')\n1 | +console.log(\"Hello World\");\n" + run_test( + "console.log('Hello World')", + "console.log(\"Hello World\");", + concat!( + "1 | -console.log('Hello World')\n", + "1 | +console.log(\"Hello World\");\n", + ), ); - let line_number_unfmt = "\n\n\n\nconsole.log(\n'Hello World'\n)"; - let line_number_fmt = "console.log(\n\"Hello World\"\n);"; + run_test( + "\n\n\n\nconsole.log(\n'Hello World'\n)", + "console.log(\n\"Hello World\"\n);", + concat!( + "1 | -\n", + "2 | -\n", + "3 | -\n", + "4 | -\n", + "5 | -console.log(\n", + "1 | +console.log(\n", + "6 | -'Hello World'\n", + "2 | +\"Hello World\"\n", + "7 | -)\n3 | +);\n", + ), + ); + } + + #[test] + fn test_eof_newline_missing() { + run_test( + "test\nsome line text test", + "test\nsome line text test\n", + concat!( + "2 | -some line text test\n", + "2 | +some line text test\n", + "3 | +\n", + ), + ); + } + + #[test] + fn test_newlines_differing() { + run_test("test\n", "test\r\n", " | Text differed by line endings.\n"); + } + + fn run_test(diff_text1: &str, diff_text2: &str, expected_output: &str) { assert_eq!( - colors::strip_ansi_codes(&diff(line_number_unfmt, line_number_fmt)), - "1 | -\n2 | -\n3 | -\n4 | -\n5 | -console.log(\n1 | +console.log(\n6 | -'Hello World'\n2 | +\"Hello World\"\n7 | -)\n3 | +);\n" + colors::strip_ansi_codes(&diff(diff_text1, diff_text2,)), + expected_output, ); } }