use console::Style;
use std::path::Path;
pub struct DiffStyles;
impl DiffStyles {
pub fn file_header() -> Style {
Style::new().bold().blue()
}
pub fn file_path() -> Style {
Style::new().bold().cyan()
}
pub fn stats_header() -> Style {
Style::new().bold().magenta()
}
pub fn additions_count() -> Style {
Style::new().bold().green()
}
pub fn deletions_count() -> Style {
Style::new().bold().red()
}
pub fn changes_count() -> Style {
Style::new().bold().yellow()
}
pub fn summary_header() -> Style {
Style::new().bold().cyan()
}
pub fn added_line() -> Style {
Style::new().green()
}
pub fn removed_line() -> Style {
Style::new().red()
}
pub fn context_line() -> Style {
Style::new().white().dim()
}
pub fn header_line() -> Style {
Style::new().bold().blue()
}
pub fn apply_style(style: &Style, text: &str) -> String {
style.apply_to(text).to_string()
}
}
#[derive(Debug, Clone)]
pub struct DiffLine {
pub line_type: DiffLineType,
pub content: String,
pub line_number_old: Option<usize>,
pub line_number_new: Option<usize>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DiffLineType {
Added,
Removed,
Context,
Header,
}
#[derive(Debug)]
pub struct FileDiff {
pub file_path: String,
pub old_content: String,
pub new_content: String,
pub lines: Vec<DiffLine>,
pub stats: DiffStats,
}
#[derive(Debug)]
pub struct DiffStats {
pub additions: usize,
pub deletions: usize,
pub changes: usize,
}
pub struct DiffRenderer {
show_line_numbers: bool,
context_lines: usize,
use_colors: bool,
}
impl DiffRenderer {
pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
Self {
show_line_numbers,
context_lines,
use_colors,
}
}
pub fn render_diff(&self, diff: &FileDiff) -> String {
let mut output = String::new();
output.push_str(&self.render_header(&diff.file_path, &diff.stats));
for line in &diff.lines {
output.push_str(&self.render_line(line));
output.push('\n');
}
output.push_str(&self.render_footer(&diff.stats));
output
}
fn render_header(&self, file_path: &str, stats: &DiffStats) -> String {
let file_header_style = if self.use_colors {
DiffStyles::file_header()
} else {
Style::new()
};
let file_path_style = if self.use_colors {
DiffStyles::file_path()
} else {
Style::new()
};
let mut header = format!(
"\n{}{} File: {}{}\n",
file_header_style.apply_to("FILE"),
if self.use_colors { "\x1b[0m" } else { "" },
file_path_style.apply_to(file_path),
if self.use_colors { "\x1b[0m" } else { "" }
);
let stats_header_style = if self.use_colors {
DiffStyles::stats_header()
} else {
Style::new()
};
let additions_style = if self.use_colors {
DiffStyles::additions_count()
} else {
Style::new()
};
let deletions_style = if self.use_colors {
DiffStyles::deletions_count()
} else {
Style::new()
};
let changes_style = if self.use_colors {
DiffStyles::changes_count()
} else {
Style::new()
};
header.push_str(&format!(
"{}{} Changes: {}{} additions, {}{} deletions, {}{} modifications\n",
stats_header_style.apply_to("STATS"),
if self.use_colors { "\x1b[0m" } else { "" },
additions_style.apply_to(&stats.additions.to_string()),
if self.use_colors { "\x1b[0m" } else { "" },
deletions_style.apply_to(&stats.deletions.to_string()),
if self.use_colors { "\x1b[0m" } else { "" },
changes_style.apply_to(&stats.changes.to_string()),
if self.use_colors { "\x1b[0m" } else { "" }
));
if self.show_line_numbers {
header.push_str("┌─────┬─────────────────────────────────────────────────\n");
} else {
header.push_str("┌───────────────────────────────────────────────────────\n");
}
header
}
fn render_line(&self, line: &DiffLine) -> String {
let prefix = match line.line_type {
DiffLineType::Added => "+",
DiffLineType::Removed => "-",
DiffLineType::Context => " ",
DiffLineType::Header => "@",
};
let style = match line.line_type {
DiffLineType::Added => DiffStyles::added_line(),
DiffLineType::Removed => DiffStyles::removed_line(),
DiffLineType::Context => DiffStyles::context_line(),
DiffLineType::Header => DiffStyles::header_line(),
};
let mut result = String::new();
if self.show_line_numbers {
let old_num = line
.line_number_old
.map_or("".to_string(), |n| format!("{:4}", n));
let new_num = line
.line_number_new
.map_or("".to_string(), |n| format!("{:4}", n));
result.push_str(&format!("│{}/{}│", old_num, new_num));
}
if self.use_colors {
let styled_prefix = self.colorize(prefix, &style);
let styled_content = self.colorize(&line.content, &style);
result.push_str(&format!("{}{}", styled_prefix, styled_content));
} else {
result.push_str(&format!("{}{}", prefix, line.content));
}
result
}
fn render_footer(&self, stats: &DiffStats) -> String {
let mut footer = String::new();
if self.show_line_numbers {
footer.push_str("└─────┴─────────────────────────────────────────────────\n");
} else {
footer.push_str("└───────────────────────────────────────────────────────\n");
}
let summary_header_style = if self.use_colors {
DiffStyles::summary_header()
} else {
Style::new()
};
let summary_additions_style = if self.use_colors {
DiffStyles::additions_count()
} else {
Style::new()
};
let summary_deletions_style = if self.use_colors {
DiffStyles::deletions_count()
} else {
Style::new()
};
let summary_changes_style = if self.use_colors {
DiffStyles::changes_count()
} else {
Style::new()
};
footer.push_str(&format!(
"{}{} Summary: {}{} lines added, {}{} lines removed, {}{} lines changed\n\n",
summary_header_style.apply_to("SUMMARY"),
if self.use_colors { "\x1b[0m" } else { "" },
summary_additions_style.apply_to(&stats.additions.to_string()),
if self.use_colors { "\x1b[0m" } else { "" },
summary_deletions_style.apply_to(&stats.deletions.to_string()),
if self.use_colors { "\x1b[0m" } else { "" },
summary_changes_style.apply_to(&stats.changes.to_string()),
if self.use_colors { "\x1b[0m" } else { "" }
));
footer
}
fn colorize(&self, text: &str, style: &Style) -> String {
if self.use_colors {
DiffStyles::apply_style(style, text)
} else {
text.to_string()
}
}
pub fn generate_diff(&self, old_content: &str, new_content: &str, file_path: &str) -> FileDiff {
let old_lines: Vec<&str> = old_content.lines().collect();
let new_lines: Vec<&str> = new_content.lines().collect();
let mut lines = Vec::new();
let mut additions = 0;
let mut deletions = 0;
let _changes = 0;
let mut old_idx = 0;
let mut new_idx = 0;
while old_idx < old_lines.len() || new_idx < new_lines.len() {
if old_idx < old_lines.len() && new_idx < new_lines.len() {
if old_lines[old_idx] == new_lines[new_idx] {
lines.push(DiffLine {
line_type: DiffLineType::Context,
content: old_lines[old_idx].to_string(),
line_number_old: Some(old_idx + 1),
line_number_new: Some(new_idx + 1),
});
old_idx += 1;
new_idx += 1;
} else {
let (old_end, new_end) =
self.find_difference(&old_lines, &new_lines, old_idx, new_idx);
for i in old_idx..old_end {
lines.push(DiffLine {
line_type: DiffLineType::Removed,
content: old_lines[i].to_string(),
line_number_old: Some(i + 1),
line_number_new: None,
});
deletions += 1;
}
for i in new_idx..new_end {
lines.push(DiffLine {
line_type: DiffLineType::Added,
content: new_lines[i].to_string(),
line_number_old: None,
line_number_new: Some(i + 1),
});
additions += 1;
}
old_idx = old_end;
new_idx = new_end;
}
} else if old_idx < old_lines.len() {
lines.push(DiffLine {
line_type: DiffLineType::Removed,
content: old_lines[old_idx].to_string(),
line_number_old: Some(old_idx + 1),
line_number_new: None,
});
deletions += 1;
old_idx += 1;
} else if new_idx < new_lines.len() {
lines.push(DiffLine {
line_type: DiffLineType::Added,
content: new_lines[new_idx].to_string(),
line_number_old: None,
line_number_new: Some(new_idx + 1),
});
additions += 1;
new_idx += 1;
}
}
let changes = additions + deletions;
FileDiff {
file_path: file_path.to_string(),
old_content: old_content.to_string(),
new_content: new_content.to_string(),
lines,
stats: DiffStats {
additions,
deletions,
changes,
},
}
}
fn find_difference(
&self,
old_lines: &[&str],
new_lines: &[&str],
start_old: usize,
start_new: usize,
) -> (usize, usize) {
let mut old_end = start_old;
let mut new_end = start_new;
while old_end < old_lines.len() && new_end < new_lines.len() {
if old_lines[old_end] == new_lines[new_end] {
return (old_end, new_end);
}
let mut found = false;
for i in 1..=self.context_lines {
if old_end + i < old_lines.len() && new_end + i < new_lines.len() {
if old_lines[old_end + i] == new_lines[new_end + i] {
old_end += i;
new_end += i;
found = true;
break;
}
}
}
if !found {
old_end += 1;
new_end += 1;
}
}
(old_end, new_end)
}
}
pub struct DiffChatRenderer {
diff_renderer: DiffRenderer,
}
impl DiffChatRenderer {
pub fn new(show_line_numbers: bool, context_lines: usize, use_colors: bool) -> Self {
Self {
diff_renderer: DiffRenderer::new(show_line_numbers, context_lines, use_colors),
}
}
pub fn render_file_change(
&self,
file_path: &Path,
old_content: &str,
new_content: &str,
) -> String {
let diff = self.diff_renderer.generate_diff(
old_content,
new_content,
&file_path.to_string_lossy(),
);
self.diff_renderer.render_diff(&diff)
}
pub fn render_multiple_changes(&self, changes: Vec<(String, String, String)>) -> String {
let mut output = format!("\nMultiple File Changes ({} files)\n", changes.len());
output.push_str("═".repeat(60).as_str());
output.push_str("\n\n");
for (file_path, old_content, new_content) in changes {
let diff = self
.diff_renderer
.generate_diff(&old_content, &new_content, &file_path);
output.push_str(&self.diff_renderer.render_diff(&diff));
}
output
}
pub fn render_operation_summary(
&self,
operation: &str,
files_affected: usize,
success: bool,
) -> String {
let status = if success { "[Success]" } else { "[Failure]" };
let mut summary = format!("\n{} {}\n", status, operation);
summary.push_str(&format!(" Files affected: {}\n", files_affected));
if success {
summary.push_str("Operation completed successfully!\n");
} else {
summary.push_str(" Operation completed with errors\n");
}
summary
}
}
pub fn generate_unified_diff(old_content: &str, new_content: &str, filename: &str) -> String {
let mut diff = format!("--- a/{}\n+++ b/{}\n", filename, filename);
let old_lines: Vec<&str> = old_content.lines().collect();
let new_lines: Vec<&str> = new_content.lines().collect();
let mut old_idx = 0;
let mut new_idx = 0;
while old_idx < old_lines.len() || new_idx < new_lines.len() {
let start_old = old_idx;
let start_new = new_idx;
while old_idx < old_lines.len()
&& new_idx < new_lines.len()
&& old_lines[old_idx] == new_lines[new_idx]
{
old_idx += 1;
new_idx += 1;
}
if old_idx == old_lines.len() && new_idx == new_lines.len() {
break; }
let mut end_old = old_idx;
let mut end_new = new_idx;
let mut context_found = false;
for i in 0..3 {
if end_old + i < old_lines.len() && end_new + i < new_lines.len() {
if old_lines[end_old + i] == new_lines[end_new + i] {
end_old += i;
end_new += i;
context_found = true;
break;
}
}
}
if !context_found {
end_old = old_lines.len();
end_new = new_lines.len();
}
let old_count = end_old - start_old;
let new_count = end_new - start_new;
diff.push_str(&format!(
"@@ -{},{} +{},{} @@\n",
start_old + 1,
old_count,
start_new + 1,
new_count
));
for i in (start_old.saturating_sub(3))..start_old {
if i < old_lines.len() {
diff.push_str(&format!(" {}\n", old_lines[i]));
}
}
for i in start_old..end_old {
if i < old_lines.len() {
diff.push_str(&format!("-{}\n", old_lines[i]));
}
}
for i in start_new..end_new {
if i < new_lines.len() {
diff.push_str(&format!("+{}\n", new_lines[i]));
}
}
for i in end_old..(end_old + 3) {
if i < old_lines.len() {
diff.push_str(&format!(" {}\n", old_lines[i]));
}
}
old_idx = end_old;
new_idx = end_new;
}
diff
}