mirror of
https://github.com/denoland/deno.git
synced 2025-03-03 17:34:47 -05:00
fix(diff): better handling of text with only line ending differences (#11212)
Additionally fixes: * It not displaying a diff when one text had a trailing newline and the other didn't. * Edge case where the line number width could be incorrect if the original text had say 99 lines and the edit text had 100 lines.
This commit is contained in:
parent
7dd4090c2a
commit
68b4d60fd7
1 changed files with 181 additions and 131 deletions
312
cli/diff.rs
312
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<Chunk<'a>>) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue