use std::collections::HashMap;
#[derive(Debug, PartialEq)]
pub enum DiffEntry {
Added(String), Removed(String), Moved(String), Unchanged(String), }
#[derive(Debug)]
pub struct PathDiff {
pub entries: Vec<DiffEntry>,
}
impl PathDiff {
#[allow(dead_code)]
#[must_use]
pub fn is_empty(&self) -> bool {
self.entries
.iter()
.all(|e| matches!(e, DiffEntry::Unchanged(_)))
}
}
pub fn compute_diff(current: &str, initial: &str, _full: bool) -> PathDiff {
let current_entries: Vec<String> = current
.split(':')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
let initial_entries: Vec<String> = initial
.split(':')
.filter(|s| !s.is_empty())
.map(String::from)
.collect();
let mut initial_positions: HashMap<String, usize> = HashMap::new();
for (idx, entry) in initial_entries.iter().enumerate() {
initial_positions.entry(entry.clone()).or_insert(idx);
}
let mut current_positions: HashMap<String, usize> = HashMap::new();
for (idx, entry) in current_entries.iter().enumerate() {
current_positions.entry(entry.clone()).or_insert(idx);
}
let initial_set: std::collections::HashSet<String> = initial_entries.iter().cloned().collect();
let current_set: std::collections::HashSet<String> = current_entries.iter().cloned().collect();
let mut diff_entries = Vec::new();
for entry in &initial_entries {
if !current_set.contains(entry) {
diff_entries.push(DiffEntry::Removed(entry.clone()));
}
}
for entry in ¤t_entries {
if !initial_set.contains(entry) {
diff_entries.push(DiffEntry::Added(entry.clone()));
continue;
}
let initial_pos = initial_positions[entry];
let current_pos = current_positions[entry];
if initial_pos == current_pos {
diff_entries.push(DiffEntry::Unchanged(entry.clone()));
} else {
diff_entries.push(DiffEntry::Moved(entry.clone()));
}
}
PathDiff {
entries: diff_entries,
}
}
#[must_use]
pub fn format_diff(diff: &PathDiff, use_color: bool) -> String {
format_diff_with_limit(diff, use_color, false)
}
#[must_use]
pub fn format_diff_with_limit(diff: &PathDiff, use_color: bool, full: bool) -> String {
const MAX_ENTRIES: usize = 15;
let has_changes = diff
.entries
.iter()
.any(|e| !matches!(e, DiffEntry::Unchanged(_)));
if !has_changes {
return "No differences".to_string();
}
let mut output = Vec::new();
let (red, green, cyan, gray, reset) = if use_color {
("\x1b[31m", "\x1b[32m", "\x1b[36m", "\x1b[90m", "\x1b[0m")
} else {
("", "", "", "", "")
};
let mut added = 0;
let mut removed = 0;
let mut moved = 0;
let mut unchanged = 0;
for entry in &diff.entries {
match entry {
DiffEntry::Added(_) => added += 1,
DiffEntry::Removed(_) => removed += 1,
DiffEntry::Moved(_) => moved += 1,
DiffEntry::Unchanged(_) => unchanged += 1,
}
}
let mut summary_parts = Vec::new();
if added > 0 {
summary_parts.push(format!("{green}+{added}{reset}"));
}
if removed > 0 {
summary_parts.push(format!("{red}-{removed}{reset}"));
}
if moved > 0 {
summary_parts.push(format!("{cyan}M{moved}{reset}"));
}
if unchanged > 0 {
summary_parts.push(format!("{gray}U{unchanged}{reset}"));
}
if !summary_parts.is_empty() {
output.push(summary_parts.join(" | "));
output.push(String::new()); }
let mut removal_lines = Vec::new();
let mut current_path_lines = Vec::new();
for entry in &diff.entries {
match entry {
DiffEntry::Removed(path) => {
removal_lines.push(format!("{red}- {path}{reset}"));
}
DiffEntry::Added(path) => {
current_path_lines.push(format!("{green}+ {path}{reset}"));
}
DiffEntry::Moved(path) => {
current_path_lines.push(format!("{cyan}M {path}{reset}"));
}
DiffEntry::Unchanged(path) => {
current_path_lines.push(format!("{gray}U {path}{reset}"));
}
}
}
output.extend(removal_lines);
let total_current = current_path_lines.len();
if !full && total_current > MAX_ENTRIES {
output.extend(current_path_lines.into_iter().take(MAX_ENTRIES));
let remaining = total_current - MAX_ENTRIES;
output.push(format!(
"{gray}... and {remaining} more entries. Run 'whi diff full' to see all.{reset}"
));
} else {
output.extend(current_path_lines);
}
output.join("\n")
}