use crate::agent::ui::theme::current_theme;
pub fn render_diff(diff_text: &str) -> Vec<String> {
let mut lines: Vec<String> = Vec::new();
let mut prev_removed: Option<String> = None;
for line in diff_text.lines() {
if line.starts_with("---") || line.starts_with("+++") || line.starts_with("@@") {
prev_removed = None;
continue;
}
if line.is_empty() {
prev_removed = None;
continue;
}
let (prefix, content) = line.split_at(1);
let content = content.trim_end_matches('\r');
match prefix {
"-" => {
if prev_removed.is_some() {
if let Some(prev) = prev_removed.take() {
let styled = color_line(&prev, "toolDiffRemoved");
lines.push(styled);
}
}
prev_removed = Some(line.to_string());
}
"+" => {
if let Some(ref removed_full) = prev_removed.take() {
let removed_content = &removed_full[1..]; render_intra_line_diff(removed_content, content, &mut lines);
} else {
let styled = color_line(line, "toolDiffAdded");
lines.push(styled);
}
}
_ => {
prev_removed = None;
let styled = color_line(line, "toolDiffContext");
lines.push(styled);
}
}
}
if let Some(prev) = prev_removed.take() {
let styled = color_line(&prev, "toolDiffRemoved");
lines.push(styled);
}
lines
}
fn color_line(line: &str, color: &str) -> String {
let theme = current_theme();
let ansi = theme.fg_ansi(color).to_string();
drop(theme);
format!("{}{}\x1b[39m", ansi, line)
}
fn render_intra_line_diff(old: &str, new: &str, output: &mut Vec<String>) {
let changes: Vec<Change> = compute_word_diff(old, new);
let theme = current_theme();
let added_ansi = theme.fg_ansi("toolDiffAdded").to_string();
let removed_ansi = theme.fg_ansi("toolDiffRemoved").to_string();
let inverse_on = "\x1b[7m"; let inverse_off = "\x1b[27m"; let reset = "\x1b[39m";
drop(theme);
let mut removed_line = String::new();
let mut added_line = String::new();
for change in &changes {
match change {
Change::Equal(text) => {
removed_line.push_str(text);
added_line.push_str(text);
}
Change::Removed(text) => {
let trimmed = text.trim_start();
if trimmed.len() < text.len() {
let ws = &text[..text.len() - trimmed.len()];
removed_line.push_str(ws);
}
removed_line.push_str(&format!("{}{}{}", inverse_on, trimmed, inverse_off));
}
Change::Added(text) => {
let trimmed = text.trim_start();
if trimmed.len() < text.len() {
let ws = &text[..text.len() - trimmed.len()];
added_line.push_str(ws);
}
added_line.push_str(&format!("{}{}{}", inverse_on, trimmed, inverse_off));
}
}
}
output.push(format!("-{}{}{}", removed_ansi, removed_line, reset));
output.push(format!("+{}{}{}", added_ansi, added_line, reset));
}
#[derive(Debug)]
enum Change {
Equal(String),
Removed(String),
Added(String),
}
fn compute_word_diff(old: &str, new: &str) -> Vec<Change> {
let changeset = diff::chars(old, new);
let mut merged: Vec<Change> = Vec::new();
for change in &changeset {
let (tag, ch) = match change {
diff::Result::Left(c) => ("-", *c),
diff::Result::Right(c) => ("+", *c),
diff::Result::Both(c, _) => ("=", *c),
};
if let Some(last) = merged.last_mut() {
let last_tag = match last {
Change::Equal(_) => "=",
Change::Removed(_) => "-",
Change::Added(_) => "+",
};
if last_tag == tag {
match last {
Change::Equal(t) => t.push(ch),
Change::Removed(t) => t.push(ch),
Change::Added(t) => t.push(ch),
}
continue;
}
}
let change = match tag {
"=" => Change::Equal(ch.to_string()),
"-" => Change::Removed(ch.to_string()),
"+" => Change::Added(ch.to_string()),
_ => unreachable!(),
};
merged.push(change);
}
merged
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty_diff() {
let result = render_diff("");
assert!(result.is_empty());
}
#[test]
fn test_skips_headers() {
let diff = "--- a/file.rs\n+++ b/file.rs\n@@ -1,3 +1,4 @@\n";
let result = render_diff(diff);
assert!(result.is_empty(), "should skip all headers");
}
#[test]
fn test_context_lines() {
crate::agent::ui::theme::init_theme(Some("dark"), false);
let diff = " line1\n line2\n";
let result = render_diff(diff);
assert_eq!(result.len(), 2);
assert!(result[0].contains("line1"));
assert!(result[0].starts_with("\x1b")); assert!(result[0].contains("\x1b[39m")); }
#[test]
fn test_removed_line() {
crate::agent::ui::theme::init_theme(Some("dark"), false);
let diff = "-old_line\n";
let result = render_diff(diff);
assert_eq!(result.len(), 1);
assert!(result[0].contains('-')); assert!(result[0].contains("old_line"));
}
#[test]
fn test_added_line() {
crate::agent::ui::theme::init_theme(Some("dark"), false);
let diff = "+new_line\n";
let result = render_diff(diff);
assert_eq!(result.len(), 1);
assert!(result[0].contains('+'));
assert!(result[0].contains("new_line"));
}
#[test]
fn test_single_line_modification() {
crate::agent::ui::theme::init_theme(Some("dark"), false);
let diff = "-foo\n+bar\n";
let result = render_diff(diff);
assert_eq!(result.len(), 2);
assert!(result[0].contains('-'));
assert!(result[1].contains('+'));
assert!(
result[0].contains("\x1b[7m"),
"should have inverse on removed"
);
assert!(
result[1].contains("\x1b[7m"),
"should have inverse on added"
);
}
#[test]
fn test_multi_line_removes() {
crate::agent::ui::theme::init_theme(Some("dark"), false);
let diff = "-a\n-b\n+c\n";
let result = render_diff(diff);
assert!(result.len() >= 2);
assert!(result[0].contains("-a") || result[0].contains("-a"));
}
#[test]
fn test_compute_word_diff_basic() {
let changes = compute_word_diff("abc", "abd");
assert!(!changes.is_empty());
}
#[test]
fn test_compute_word_diff_identical() {
let changes = compute_word_diff("hello", "hello");
assert_eq!(changes.len(), 1);
assert!(matches!(changes[0], Change::Equal(_)));
}
}