use crate::ui::theme;
use agent_client_protocol as acp;
use ratatui::style::{Color, Modifier, Style};
use ratatui::text::{Line, Span};
use similar::TextDiff;
pub fn render_diff(diff: &acp::Diff) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();
let name = diff.path.file_name().map_or_else(
|| diff.path.to_string_lossy().into_owned(),
|f| f.to_string_lossy().into_owned(),
);
lines.push(Line::from(Span::styled(
name,
Style::default().fg(Color::White).add_modifier(Modifier::BOLD),
)));
let old = diff.old_text.as_deref().unwrap_or("");
let new = &diff.new_text;
let text_diff = TextDiff::from_lines(old, new);
let udiff = text_diff.unified_diff();
for hunk in udiff.iter_hunks() {
let hunk_str = hunk.to_string();
if let Some(header) = hunk_str.lines().next()
&& header.starts_with("@@")
{
lines.push(Line::from(Span::styled(
header.to_owned(),
Style::default().fg(Color::Cyan),
)));
}
for change in hunk.iter_changes() {
let value = change.as_str().unwrap_or("").trim_end_matches('\n');
let (prefix, style) = match change.tag() {
similar::ChangeTag::Delete => ("-", Style::default().fg(Color::Red)),
similar::ChangeTag::Insert => ("+", Style::default().fg(Color::Green)),
similar::ChangeTag::Equal => (" ", Style::default().fg(theme::DIM)),
};
lines.push(Line::from(Span::styled(format!("{prefix} {value}"), style)));
}
}
lines
}
#[allow(clippy::case_sensitive_file_extension_comparisons)]
pub fn is_markdown_file(title: &str) -> bool {
let lower = title.to_lowercase();
lower.ends_with(".md") || lower.ends_with(".mdx") || lower.ends_with(".markdown")
}
pub fn lang_from_title(title: &str) -> String {
title
.split_whitespace()
.rev()
.find_map(|token| {
let ext = token.rsplit('.').next()?;
if ext.len() < token.len() { Some(ext.to_lowercase()) } else { None }
})
.unwrap_or_default()
}
pub fn strip_outer_code_fence(text: &str) -> String {
let trimmed = text.trim();
if trimmed.starts_with("```") {
if let Some(first_newline) = trimmed.find('\n') {
let after_opening = &trimmed[first_newline + 1..];
if let Some(body) = after_opening.strip_suffix("```") {
return body.trim_end().to_owned();
}
let after_trimmed = after_opening.trim_end();
if let Some(stripped) = after_trimmed.strip_suffix("```") {
return stripped.trim_end().to_owned();
}
}
}
text.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn strip_fenced_code() {
let input = "```rust\nfn main() {}\n```";
let result = strip_outer_code_fence(input);
assert_eq!(result, "fn main() {}");
}
#[test]
fn strip_fenced_no_lang_tag() {
let input = "```\nhello world\n```";
let result = strip_outer_code_fence(input);
assert_eq!(result, "hello world");
}
#[test]
fn strip_not_fenced_passthrough() {
let input = "just plain text";
let result = strip_outer_code_fence(input);
assert_eq!(result, "just plain text");
}
#[test]
fn strip_fenced_with_trailing_whitespace() {
let input = "```\ncontent\n``` \n";
let result = strip_outer_code_fence(input);
assert_eq!(result, "content");
}
#[test]
fn strip_nested_fences_only_outer() {
let input = "```\ninner ```\nstuff\n```";
let result = strip_outer_code_fence(input);
assert!(result.contains("inner ```"));
}
#[test]
fn strip_only_opening_fence() {
let input = "```rust\nfn main() {}";
let result = strip_outer_code_fence(input);
assert_eq!(result, input);
}
#[test]
fn strip_empty_fenced_block() {
let input = "```\n```";
let result = strip_outer_code_fence(input);
assert_eq!(result, "");
}
#[test]
fn strip_multiline_content() {
let input = "```python\nline1\nline2\nline3\n```";
let result = strip_outer_code_fence(input);
assert_eq!(result, "line1\nline2\nline3");
}
#[test]
fn strip_quadruple_backtick_fence() {
let input = "````\ncontent here\n````";
let result = strip_outer_code_fence(input);
assert!(result.contains("content here"));
}
#[test]
fn strip_tilde_fence_passthrough() {
let input = "~~~\ncontent\n~~~";
let result = strip_outer_code_fence(input);
assert_eq!(result, input);
}
#[test]
fn strip_inner_fence_in_content() {
let input = "```\nsome code\n```\nmore code\n```";
let result = strip_outer_code_fence(input);
assert!(result.contains("some code"));
}
#[test]
fn strip_large_fenced_content() {
let big: String = (0..10_000).fold(String::new(), |mut s, i| {
use std::fmt::Write;
writeln!(s, "line {i}").unwrap();
s
});
let input = format!("```\n{big}```");
let result = strip_outer_code_fence(&input);
assert!(result.contains("line 0"));
assert!(result.contains("line 9999"));
}
#[test]
fn strip_fence_with_blank_lines() {
let input = "```\n\n\n\n```";
let result = strip_outer_code_fence(input);
assert!(result.is_empty() || result.chars().all(|c| c == '\n'));
}
#[test]
fn strip_fence_with_leading_whitespace() {
let input = " ```\ncontent\n```";
let result = strip_outer_code_fence(input);
assert_eq!(result, "content");
}
#[test]
fn lang_rust_file() {
assert_eq!(lang_from_title("src/main.rs"), "rs");
}
#[test]
fn lang_python_with_prefix() {
assert_eq!(lang_from_title("Read foo.py"), "py");
}
#[test]
fn lang_toml_file() {
assert_eq!(lang_from_title("Cargo.toml"), "toml");
}
#[test]
fn lang_no_extension() {
assert_eq!(lang_from_title("Makefile"), "");
}
#[test]
fn lang_empty_title() {
assert_eq!(lang_from_title(""), "");
}
#[test]
fn lang_mixed_case() {
assert_eq!(lang_from_title("file.RS"), "rs");
}
#[test]
fn lang_multiple_dots() {
assert_eq!(lang_from_title("archive.tar.gz"), "gz");
}
#[test]
fn lang_path_with_spaces() {
assert_eq!(lang_from_title("Read some/dir/file.tsx"), "tsx");
}
#[test]
fn lang_hidden_file() {
assert_eq!(lang_from_title(".gitignore"), "gitignore");
}
#[test]
fn lang_chained_extensions() {
assert_eq!(lang_from_title("Read a.test.spec.ts"), "ts");
}
#[test]
fn lang_dot_at_end() {
assert_eq!(lang_from_title("file."), "");
}
#[test]
fn lang_whitespace_only() {
assert_eq!(lang_from_title(" "), "");
}
#[test]
fn lang_windows_backslash_path() {
assert_eq!(lang_from_title("Read src\\main.rs"), "rs");
}
#[test]
fn is_md_file() {
assert!(is_markdown_file("README.md"));
}
#[test]
fn is_mdx_file() {
assert!(is_markdown_file("component.mdx"));
}
#[test]
fn is_markdown_ext() {
assert!(is_markdown_file("doc.markdown"));
}
#[test]
fn is_markdown_case_insensitive() {
assert!(is_markdown_file("README.MD"));
assert!(is_markdown_file("file.Md"));
}
#[test]
fn is_not_markdown() {
assert!(!is_markdown_file("main.rs"));
assert!(!is_markdown_file("style.css"));
assert!(!is_markdown_file(""));
}
#[test]
fn is_not_markdown_partial() {
assert!(!is_markdown_file("somemdx"));
}
#[test]
fn is_not_markdown_md_in_middle() {
assert!(!is_markdown_file("file.md.bak"));
}
#[test]
fn is_markdown_with_path() {
assert!(is_markdown_file("docs/getting-started.md"));
assert!(is_markdown_file("Read /home/user/notes.md"));
}
#[test]
fn is_markdown_uppercase_full() {
assert!(is_markdown_file("FILE.MARKDOWN"));
}
}