use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
text::Span,
widgets::Widget,
};
use crate::widgets::ai_chat::components::theme::ChatColors;
use super::block_tool::BlockTool;
use super::inline_tool::ToolStatus;
#[derive(Debug, Clone, PartialEq)]
pub struct Diagnostic {
pub severity: DiagnosticSeverity,
pub message: String,
pub line: Option<u32>,
pub column: Option<u32>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Info,
}
pub struct ToolWrite<'a> {
file_path: &'a str,
content: &'a str,
diagnostics: Vec<Diagnostic>,
status: ToolStatus,
expanded: bool,
}
impl<'a> ToolWrite<'a> {
pub fn new(file_path: &'a str, content: &'a str) -> Self {
Self {
file_path,
content,
diagnostics: Vec::new(),
status: ToolStatus::Pending,
expanded: false,
}
}
pub fn add_diagnostic(mut self, diagnostic: Diagnostic) -> Self {
self.diagnostics.push(diagnostic);
self
}
pub fn status(mut self, status: ToolStatus) -> Self {
self.status = status;
self
}
pub fn expanded(mut self, expanded: bool) -> Self {
self.expanded = expanded;
self
}
fn border_color(&self, colors: &ChatColors) -> Color {
match self.status {
ToolStatus::Pending => colors.warning,
ToolStatus::Complete => colors.success,
ToolStatus::Error => colors.error,
ToolStatus::PermissionPending => Color::Rgb(255, 165, 0),
}
}
pub fn render(&self, area: Rect, buf: &mut Buffer, colors: &ChatColors) {
if area.height < 1 {
return;
}
let max_y = area.y + area.height;
let mut y = area.y;
let border_color = self.border_color(colors);
for y_pos in area.y..max_y {
buf.get_mut(area.x, y_pos)
.set_char('│')
.set_style(Style::default().fg(border_color));
}
let header = format!("← Write {}", if self.expanded { "▼" } else { "▶" });
let header_span = Span::styled(
header,
Style::default()
.fg(border_color)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &header_span, area.width.saturating_sub(3));
y += 1;
if y < max_y {
buf.set_span(
area.x + 2,
y,
&Span::styled(
"─".repeat(30),
Style::default()
.fg(border_color)
.add_modifier(Modifier::DIM),
),
area.width.saturating_sub(3),
);
y += 1;
}
if y < max_y {
let path_span = Span::styled(
format!("📄 {}", self.file_path),
Style::default()
.fg(colors.primary)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &path_span, area.width.saturating_sub(3));
y += 1;
}
let content_lines: Vec<&str> = self.content.lines().collect();
let max_lines = if self.expanded {
content_lines
.len()
.min(area.height.saturating_sub(y - area.y) as usize)
} else {
content_lines.len().min(10)
};
for (i, line) in content_lines.iter().take(max_lines).enumerate() {
if y >= max_y {
break;
}
let line_num = format!("{:4} │ ", i + 1);
let line_num_span = Span::styled(
line_num,
Style::default()
.fg(colors.text_muted)
.add_modifier(Modifier::DIM),
);
buf.set_span(area.x + 2, y, &line_num_span, 7);
let line_style = self.syntax_style_for_line(line, colors);
let display_line = if line.len() > area.width as usize - 10 {
format!("{}...", &line[..area.width as usize - 13])
} else {
line.to_string()
};
buf.set_span(
area.x + 9,
y,
&Span::styled(display_line, line_style),
area.width.saturating_sub(10),
);
y += 1;
}
if !self.expanded && content_lines.len() > 10 && y < max_y {
buf.set_span(
area.x + 2,
y,
&Span::styled(
format!(
"... {} more lines (click to expand)",
content_lines.len() - 10
),
Style::default()
.fg(colors.text_muted)
.add_modifier(Modifier::ITALIC),
),
area.width.saturating_sub(3),
);
y += 1;
}
for diag in &self.diagnostics {
if y >= max_y {
break;
}
let (symbol, diag_color) = match diag.severity {
DiagnosticSeverity::Error => ("✗", colors.error),
DiagnosticSeverity::Warning => ("⚠", colors.warning),
DiagnosticSeverity::Info => ("ℹ", colors.primary),
};
let location = if let (Some(line), Some(col)) = (diag.line, diag.column) {
format!(":{}:{}", line, col)
} else {
String::new()
};
let diag_text = format!("{} {}{}", symbol, diag.message, location);
let diag_span = Span::styled(diag_text, Style::default().fg(diag_color));
buf.set_span(area.x + 2, y, &diag_span, area.width.saturating_sub(3));
y += 1;
}
if y < max_y {
let status_text = match self.status {
ToolStatus::Pending => "⏳ Pending...",
ToolStatus::Complete => "✓ Written successfully",
ToolStatus::Error => "✗ Write failed",
ToolStatus::PermissionPending => "⚠ Permission required",
};
let status_span = Span::styled(
status_text,
Style::default()
.fg(border_color)
.add_modifier(Modifier::BOLD),
);
buf.set_span(area.x + 2, y, &status_span, area.width.saturating_sub(3));
}
}
fn syntax_style_for_line(&self, line: &str, colors: &ChatColors) -> Style {
let trimmed = line.trim();
if trimmed.starts_with("//") || trimmed.starts_with("#") || trimmed.starts_with("/*") {
return Style::default()
.fg(colors.text_muted)
.add_modifier(Modifier::ITALIC);
}
if trimmed.starts_with('"') || trimmed.starts_with('\'') {
return Style::default().fg(colors.diff_added);
}
let keywords = [
"fn", "let", "const", "mut", "pub", "struct", "enum", "impl", "use", "mod", "if",
"else", "for", "while", "return", "async", "await",
];
for kw in keywords {
if trimmed.split_whitespace().next() == Some(kw) {
return Style::default()
.fg(colors.secondary)
.add_modifier(Modifier::BOLD);
}
}
Style::default().fg(colors.text)
}
pub fn toggle_expanded(&mut self) {
self.expanded = !self.expanded;
}
}
impl Widget for ToolWrite<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
let colors = ChatColors::default();
self.render(area, buf, &colors);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tool_write_basic() {
let write = ToolWrite::new("src/main.rs", "fn main() {}");
assert_eq!(write.file_path, "src/main.rs");
}
#[test]
fn test_tool_write_with_diagnostics() {
let write = ToolWrite::new("src/main.rs", "fn main() {}").add_diagnostic(Diagnostic {
severity: DiagnosticSeverity::Warning,
message: "Unused variable".to_string(),
line: Some(1),
column: Some(5),
});
assert_eq!(write.diagnostics.len(), 1);
}
#[test]
fn test_tool_write_status() {
let pending = ToolWrite::new("test.rs", "content").status(ToolStatus::Pending);
assert_eq!(pending.status, ToolStatus::Pending);
let complete = ToolWrite::new("test.rs", "content").status(ToolStatus::Complete);
assert_eq!(complete.status, ToolStatus::Complete);
}
}