use dissimilar::*;
use crossterm::style::Colorize;
use dprint_core::types::ErrBox;
pub fn get_difference(text1: &str, text2: &str) -> Result<String, ErrBox> {
debug_assert!(text1 != text2);
let text1 = text1.replace("\r\n", "\n");
let text2 = text2.replace("\r\n", "\n");
if text1 == text2 {
return Ok(String::from(" | Text differed by line endings."));
}
let grouped_changes = get_grouped_changes(&text1, &text2);
let mut text = String::new();
for (i, grouped_change) in grouped_changes.into_iter().enumerate() {
if i > 0 {
text.push_str("\n...\n");
}
let max_line_num_width = grouped_change.end_line_number.to_string().chars().count();
text.push_str(&format!("{:width$}| ", grouped_change.start_line_number, width = max_line_num_width));
text.push_str(&annotate_whitespace(get_line_start_text(&text1, grouped_change.start_index)?));
let mut last_index = grouped_change.start_index;
for change in grouped_change.changes {
for (i, line) in text1[last_index..change.start_index()].split('\n').enumerate() {
if i > 0 {
text.push_str(&format!("\n{}| ", " ".repeat(max_line_num_width)));
}
text.push_str(&annotate_whitespace(line));
}
last_index = change.end_index();
match change {
Change::Addition(addition) => {
for (i, line) in addition.new_text.split('\n').enumerate() {
if i > 0 {
text.push_str("\n");
text.push_str(&get_addition_text(&format!("{}| ", " ".repeat(max_line_num_width))));
}
if !line.is_empty() {
text.push_str(&get_addition_text(&annotate_whitespace(line)));
}
}
},
Change::Removal(removal) => {
for (i, line) in removal.removed_text.split('\n').enumerate() {
if i > 0 {
text.push_str("\n");
let line_text = format!("{}| ", " ".repeat(max_line_num_width));
if line.is_empty() {
text.push_str(&get_removal_text(&line_text));
} else {
text.push_str(&line_text);
}
}
if !line.is_empty() {
text.push_str(&get_removal_text(&annotate_whitespace(line)));
}
}
}
}
}
text.push_str(&annotate_whitespace(&get_line_end_text(&text1, grouped_change.end_index)));
}
Ok(text)
}
fn get_line_start_text<'a>(text: &'a str, index: usize) -> Result<&'a str, ErrBox> {
let new_line_byte = '\n' as u8;
let text_bytes = text.as_bytes();
let mut start_index = 0;
let mut length = 0;
if index > text.len() {
return err!("The byte index was {}, but the text byte length is {}.", index, text.len());
}
for i in (0..index).rev() {
if text_bytes[i] == new_line_byte || length > 50 {
start_index = i + 1;
break;
}
length += 1
}
Ok(&text[start_index..index])
}
fn get_line_end_text<'a>(text: &'a str, index: usize) -> &'a str {
let new_line_byte = '\n' as u8;
let text_bytes = text.as_bytes();
let mut end_index = index;
let mut length = 0;
for i in index..text.len() {
if text_bytes[i] == new_line_byte || length > 50 {
end_index = i;
break;
}
length += 1;
}
&text[index..end_index]
}
#[derive(Debug)]
struct GroupedChange<'a> {
start_index: usize,
end_index: usize,
start_line_number: usize,
end_line_number: usize,
changes: Vec<Change<'a>>,
}
fn get_grouped_changes<'a>(text1: &'a str, text2: &'a str) -> Vec<GroupedChange<'a>> {
let changes = get_changes(text1, text2);
let mut grouped_changes: Vec<GroupedChange<'a>> = Vec::new();
for change in changes {
if let Some(grouped_change) = grouped_changes.last_mut() {
const GROUPED_LINE_COUNT: usize = 2;
let should_group = change.start_line_number() < GROUPED_LINE_COUNT || grouped_change.end_line_number >= change.start_line_number() - GROUPED_LINE_COUNT;
if should_group {
grouped_change.end_index = change.end_index();
grouped_change.end_line_number = change.end_line_number();
grouped_change.changes.push(change);
continue;
}
}
grouped_changes.push(GroupedChange {
start_index: change.start_index(),
end_index: change.end_index(),
start_line_number: change.start_line_number(),
end_line_number: change.end_line_number(),
changes: vec![change],
})
}
grouped_changes
}
#[derive(Debug)]
enum Change<'a> {
Addition(Addition<'a>),
Removal(Removal<'a>),
}
impl<'a> Change<'a> {
fn start_index(&self) -> usize {
match self {
Change::Addition(addition) => addition.insert_index,
Change::Removal(removal) => removal.start_index,
}
}
fn end_index(&self) -> usize {
match self {
Change::Addition(addition) => addition.insert_index,
Change::Removal(removal) => removal.end_index,
}
}
fn start_line_number(&self) -> usize {
match self {
Change::Addition(addition) => addition.insert_line_number,
Change::Removal(removal) => removal.start_line_number,
}
}
fn end_line_number(&self) -> usize {
match self {
Change::Addition(addition) => addition.insert_line_number,
Change::Removal(removal) => removal.end_line_number,
}
}
}
#[derive(Debug)]
struct Addition<'a> {
new_text: &'a str,
insert_index: usize,
insert_line_number: usize,
}
#[derive(Debug)]
struct Removal<'a> {
removed_text: &'a str,
start_index: usize,
end_index: usize,
start_line_number: usize,
end_line_number: usize,
}
fn get_changes<'a>(text1: &'a str, text2: &'a str) -> Vec<Change<'a>> {
let chunks = get_pre_processed_chunks(text1, text2);
let mut changes: Vec<Change<'a>> = Vec::new();
let mut line_number = 1;
let mut byte_index = 0;
for i in 0..chunks.len() {
let chunk = chunks[i];
match chunk {
Chunk::Insert(inserted_text) => {
changes.push(Change::Addition(Addition {
new_text: inserted_text,
insert_index: byte_index,
insert_line_number: line_number,
}));
},
Chunk::Delete(deleted_text) => {
let line_count = deleted_text.split('\n').count() - 1;
changes.push(Change::Removal(Removal {
removed_text: deleted_text,
start_index: byte_index,
end_index: byte_index + deleted_text.len(),
start_line_number: line_number,
end_line_number: line_number + line_count,
}));
byte_index += deleted_text.len();
line_number += line_count;
},
Chunk::Equal(equal_text) => {
byte_index += equal_text.len();
line_number += equal_text.split('\n').count() - 1;
},
}
}
changes
}
fn get_pre_processed_chunks<'a>(text1: &'a str, text2: &'a str) -> Vec<dissimilar::Chunk<'a>> {
let chunks = dissimilar::diff(text1, text2);
let mut final_chunks = Vec::new();
let mut i = 0;
while i < chunks.len() {
let chunk = chunks[i];
let mut was_deleted_with_newline = false;
if let Chunk::Delete(delete_text) = chunk {
if let Some(Chunk::Equal(last_change)) = final_chunks.last() {
was_deleted_with_newline = delete_text.find('\n').is_some() && !last_change.ends_with('\n');
}
}
if was_deleted_with_newline {
let add_delete_at_end = if let Some(Chunk::Insert(insert_text)) = chunks.get(i + 1) {
!insert_text.contains('\n')
} else {
true
};
let delete_text = if let Chunk::Delete(delete_text) = chunk { delete_text } else { unreachable!() };
let delete_text_new_line_index = delete_text.find('\n').unwrap();
if delete_text_new_line_index > 0 {
final_chunks.push(dissimilar::Chunk::Delete(&delete_text[0..delete_text_new_line_index]));
}
let mut next_line_chunks = Vec::new();
if delete_text_new_line_index + 1 < delete_text.len() {
next_line_chunks.push(dissimilar::Chunk::Delete(&delete_text[delete_text_new_line_index + 1..]));
}
i += 1;
while i < chunks.len() {
let chunk = chunks[i];
match chunk {
Chunk::Equal(equal_text) => {
let new_line_index = equal_text.find('\n');
if let Some(new_line_index) = new_line_index {
final_chunks.push(dissimilar::Chunk::Insert(&equal_text[0..new_line_index]));
next_line_chunks.push(dissimilar::Chunk::Delete(&equal_text[0..new_line_index]));
let remainder_equal_text = &equal_text[new_line_index + 1..];
if remainder_equal_text.len() > 0 {
next_line_chunks.push(dissimilar::Chunk::Equal(remainder_equal_text));
}
break;
} else {
final_chunks.push(dissimilar::Chunk::Insert(equal_text));
next_line_chunks.push(dissimilar::Chunk::Delete(equal_text));
}
},
Chunk::Insert(insert_text) => {
let new_line_index = insert_text.find('\n');
if let Some(new_line_index) = new_line_index {
final_chunks.push(dissimilar::Chunk::Insert(&insert_text[0..new_line_index]));
let remainder_text = &insert_text[new_line_index + 1..];
if remainder_text.len() > 0 {
next_line_chunks.push(dissimilar::Chunk::Insert(remainder_text));
}
break;
} else {
final_chunks.push(dissimilar::Chunk::Insert(insert_text));
}
}
Chunk::Delete(delete_text) => {
next_line_chunks.push(dissimilar::Chunk::Delete(delete_text));
}
}
i += 1;
}
final_chunks.push(dissimilar::Chunk::Equal(&delete_text[delete_text_new_line_index..delete_text_new_line_index+1]));
final_chunks.extend(next_line_chunks);
if add_delete_at_end {
final_chunks.push(dissimilar::Chunk::Delete(&delete_text[delete_text_new_line_index..delete_text_new_line_index+1]));
}
} else {
final_chunks.push(chunk);
}
i += 1;
}
final_chunks
}
fn get_addition_text(text: &str) -> String {
text.white().on_green().to_string()
}
fn get_removal_text(text: &str) -> String {
let text = text.replace("\t", "\u{21E5}");
text.white().on_red().to_string()
}
fn annotate_whitespace(text: &str) -> String {
text.replace("\t", "\u{2192}")
.replace(" ", "\u{00B7}")
}
#[cfg(test)]
mod test {
use pretty_assertions::assert_eq;
use super::*;
#[test]
fn it_should_get_when_differs_by_line_endings() {
assert_eq!(get_difference("test\r\n", "test\n").unwrap(), " | Text differed by line endings.");
}
#[test]
fn it_should_get_difference_on_one_line() {
assert_eq!(get_difference("test1\n", "test2\n").unwrap(), format!("1| test{}{}", get_removal_text("1"), get_addition_text("2")));
}
#[test]
fn it_should_show_the_addition_of_last_line() {
assert_eq!(
get_difference("testing\ntesting", "testing\ntesting\n").unwrap(),
format!(
"{}\n{}",
"2| testing",
get_addition_text(&format!(" | "))
)
);
}
#[test]
fn it_should_get_difference_for_removed_line() {
assert_eq!(
get_difference("class Test\n{\n\n}", "class Test {\n}\n").unwrap(),
format!(
"{}\n{}\n{}\n{}\n{}",
format!("1| class\u{00B7}Test{}{}", get_addition_text("\u{00B7}"), get_addition_text("{")),
format!(" | {}", get_removal_text("{")),
get_removal_text(" | "),
format!("{}{}", get_removal_text(" | "), "}"),
get_addition_text(" | "),
)
);
}
#[test]
fn it_should_show_multiple_removals_on_different_lines() {
assert_eq!(
get_difference("let t ;\n\n\nlet u ;\n", "let t;\n\n\nlet u;\n").unwrap(),
format!(
"{}\n...\n{}",
format!("1| let\u{00B7}t{};", get_removal_text("\u{00B7}")),
format!("4| let\u{00B7}u{};", get_removal_text("\u{00B7}")),
)
);
}
#[test]
fn it_should_keep_grouped_when_changes_only_separated_by_one_line() {
assert_eq!(
get_difference("let t ;\ntest;\nlet u ;\n", "let t;\ntest;\nlet u;\n").unwrap(),
format!(
"{}\n{}\n{}",
format!("1| let\u{00B7}t{};", get_removal_text("\u{00B7}")),
" | test;",
format!(" | let\u{00B7}u{};", get_removal_text("\u{00B7}")),
)
);
}
#[test]
fn it_should_annotate_whitespace_end_line_text() {
assert_eq!(
get_difference("t t t\n", "tt t\n").unwrap(),
format!(
"1| t{}t\u{00B7}t",
get_removal_text("\u{00B7}")
)
);
}
#[test]
fn it_should_handle_replacements() {
assert_eq!(
get_difference("use::asdf\nuse::test", "use::other\nsomething").unwrap(),
format!(
"1| use::{}{}\n | {}{}",
get_removal_text("asdf"),
get_addition_text("other"),
get_removal_text("use::test"),
get_addition_text("something")
)
);
}
}