use similar::{ChangeTag, TextDiff};
use std::io::IsTerminal;
pub fn should_use_color() -> bool {
match std::env::var("NO_COLOR") {
Ok(_) => false, Err(_) => std::io::stdout().is_terminal(),
}
}
pub fn format_unified_diff(old: &str, new: &str, path: &str, context_lines: usize) -> String {
if old == new {
return String::new();
}
let diff = TextDiff::from_lines(old, new);
let header_old = format!("a/{}", path);
let header_new = format!("b/{}", path);
diff.unified_diff()
.context_radius(context_lines)
.header(&header_old, &header_new)
.to_string()
}
pub fn format_colored_diff(old: &str, new: &str, use_color: bool) -> String {
let diff = TextDiff::from_lines(old, new);
let mut result = String::new();
for change in diff.iter_all_changes() {
let tag = change.tag();
let line = change.value();
let formatted = if use_color {
match tag {
ChangeTag::Delete => {
format!("{}{}", nu_ansi_term::Color::Red.paint("-"), line)
}
ChangeTag::Insert => {
format!("{}{}", nu_ansi_term::Color::Green.paint("+"), line)
}
ChangeTag::Equal => {
format!(" {}", line)
}
}
} else {
match tag {
ChangeTag::Delete => format!("-{}", line),
ChangeTag::Insert => format!("+{}", line),
ChangeTag::Equal => format!(" {}", line),
}
};
result.push_str(&formatted);
}
result
}
pub fn format_diff_summary(files: usize, insertions: usize, deletions: usize) -> String {
if files == 0 {
return String::new();
}
let mut parts = Vec::new();
if files == 1 {
parts.push("1 file changed".to_string());
} else {
parts.push(format!("{} files changed", files));
}
if insertions > 0 {
if insertions == 1 {
parts.push("1 insertion(+)".to_string());
} else {
parts.push(format!("{} insertions(+)", insertions));
}
}
if deletions > 0 {
if deletions == 1 {
parts.push("1 deletion(-)".to_string());
} else {
parts.push(format!("{} deletions(-)", deletions));
}
}
if insertions == 0 && deletions == 0 {
parts.push("0 insertions(+)".to_string());
parts.push("0 deletions(-)".to_string());
}
format!(" {}", parts.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
#[test]
fn test_should_use_color_with_no_color_set() {
env::set_var("NO_COLOR", "1");
let result = should_use_color();
env::remove_var("NO_COLOR");
assert!(
!result,
"should_use_color should return false when NO_COLOR is set"
);
}
#[test]
fn test_should_use_color_without_no_color() {
env::remove_var("NO_COLOR");
let result = should_use_color();
let _ = result;
}
#[test]
fn test_format_unified_diff_basic() {
let old = "line 1\nline 2\nline 3\n";
let new = "line 1\nline 2 modified\nline 3\n";
let diff = format_unified_diff(old, new, "test.txt", 3);
assert!(diff.contains("--- a/test.txt"));
assert!(diff.contains("+++ b/test.txt"));
assert!(diff.contains("-line 2"));
assert!(diff.contains("+line 2 modified"));
}
#[test]
fn test_format_unified_diff_no_changes() {
let old = "line 1\nline 2\nline 3\n";
let new = "line 1\nline 2\nline 3\n";
let diff = format_unified_diff(old, new, "test.txt", 3);
assert!(diff.is_empty(), "Diff should be empty when old == new");
}
#[test]
fn test_format_unified_diff_context_lines() {
let old = "line 1\nline 2\nline 3\nline 4\nline 5\n";
let new = "line 1\nline 2\nline 3 modified\nline 4\nline 5\n";
let diff = format_unified_diff(old, new, "test.txt", 2);
assert!(diff.contains("line 1")); assert!(diff.contains("line 4")); assert!(diff.contains("-line 3"));
assert!(diff.contains("+line 3 modified"));
}
#[test]
fn test_format_colored_diff_plain_text() {
let old = "line 1\nline 2\nline 3\n";
let new = "line 1\nline 2 modified\nline 3\n";
let diff = format_colored_diff(old, new, false);
assert!(!diff.contains("\x1b[")); assert!(diff.contains("-line 2"));
assert!(diff.contains("+line 2 modified"));
assert!(diff.contains(" line 1")); }
#[test]
fn test_format_colored_diff_with_color() {
let old = "line 1\nline 2\nline 3\n";
let new = "line 1\nline 2 modified\nline 3\n";
let diff = format_colored_diff(old, new, true);
assert!(!diff.is_empty());
assert!(diff.contains("line 2 modified"));
}
#[test]
fn test_format_colored_diff_tags() {
let old = "keep\nremove\nkeep2\n";
let new = "keep\nadd\nkeep2\n";
let diff_plain = format_colored_diff(old, new, false);
assert!(diff_plain.contains(" keep\n")); assert!(diff_plain.contains("-remove\n")); assert!(diff_plain.contains("+add\n")); assert!(diff_plain.contains(" keep2\n"));
let diff_colored = format_colored_diff(old, new, true);
assert!(!diff_colored.is_empty());
}
#[test]
fn test_format_unified_diff_with_rust_code() {
let old = "fn hello() {\n println!(\"Hello\");\n}\n";
let new = "fn hello() {\n println!(\"Hello, World!\");\n}\n";
let diff = format_unified_diff(old, new, "src/main.rs", 3);
assert!(diff.contains("--- a/src/main.rs"));
assert!(diff.contains("+++ b/src/main.rs"));
assert!(diff.contains("- println!(\"Hello\");"));
assert!(diff.contains("+ println!(\"Hello, World!\");"));
}
#[test]
fn test_format_diff_summary_single_file_with_insertions_and_deletions() {
let summary = format_diff_summary(1, 5, 2);
assert_eq!(summary, " 1 file changed, 5 insertions(+), 2 deletions(-)");
}
#[test]
fn test_format_diff_summary_multiple_files() {
let summary = format_diff_summary(2, 10, 3);
assert_eq!(
summary,
" 2 files changed, 10 insertions(+), 3 deletions(-)"
);
}
#[test]
fn test_format_diff_summary_only_insertions() {
let summary = format_diff_summary(1, 3, 0);
assert_eq!(summary, " 1 file changed, 3 insertions(+)");
}
#[test]
fn test_format_diff_summary_only_deletions() {
let summary = format_diff_summary(1, 0, 3);
assert_eq!(summary, " 1 file changed, 3 deletions(-)");
}
#[test]
fn test_format_diff_summary_singular_insertion() {
let summary = format_diff_summary(1, 1, 0);
assert_eq!(summary, " 1 file changed, 1 insertion(+)");
}
#[test]
fn test_format_diff_summary_singular_deletion() {
let summary = format_diff_summary(1, 0, 1);
assert_eq!(summary, " 1 file changed, 1 deletion(-)");
}
#[test]
fn test_format_diff_summary_no_changes() {
let summary = format_diff_summary(1, 0, 0);
assert_eq!(summary, " 1 file changed, 0 insertions(+), 0 deletions(-)");
}
#[test]
fn test_format_diff_summary_no_files() {
let summary = format_diff_summary(0, 0, 0);
assert!(summary.is_empty());
}
}