|
1 | 1 | use crate::app::App; |
| 2 | +use once_cell::sync::Lazy; |
2 | 3 | use ratatui::{ |
3 | 4 | layout::{Constraint, Direction, Layout}, |
4 | 5 | style::{Color, Style, Stylize}, |
5 | 6 | text::{Line, Span}, |
6 | 7 | widgets::{Block, Borders, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState}, |
7 | 8 | Frame, |
8 | 9 | }; |
| 10 | +use syntect::easy::HighlightLines; |
| 11 | +use syntect::highlighting::{Style as SyntectStyle, ThemeSet}; |
| 12 | +use syntect::parsing::SyntaxSet; |
| 13 | + |
| 14 | +// Global syntax highlighting resources |
| 15 | +static SYNTAX_SET: Lazy<SyntaxSet> = Lazy::new(SyntaxSet::load_defaults_newlines); |
| 16 | +static THEME_SET: Lazy<ThemeSet> = Lazy::new(ThemeSet::load_defaults); |
| 17 | + |
| 18 | +// Convert syntect color to ratatui color |
| 19 | +fn syntect_to_ratatui_color(color: syntect::highlighting::Color) -> Color { |
| 20 | + Color::Rgb(color.r, color.g, color.b) |
| 21 | +} |
| 22 | + |
| 23 | +// Parse a message and extract code blocks with syntax highlighting |
| 24 | +fn parse_message_with_highlighting(text: &str, max_width: usize) -> Vec<Line<'static>> { |
| 25 | + let mut lines = Vec::new(); |
| 26 | + let mut in_code_block = false; |
| 27 | + let mut code_buffer: Vec<String> = Vec::new(); |
| 28 | + let mut code_language = String::new(); |
| 29 | + |
| 30 | + for line in text.lines() { |
| 31 | + if line.starts_with("```") { |
| 32 | + if in_code_block { |
| 33 | + // End of code block - highlight and flush |
| 34 | + let theme = &THEME_SET.themes["base16-ocean.dark"]; |
| 35 | + let syntax = SYNTAX_SET |
| 36 | + .find_syntax_by_token(&code_language) |
| 37 | + .unwrap_or_else(|| SYNTAX_SET.find_syntax_plain_text()); |
| 38 | + |
| 39 | + let mut highlighter = HighlightLines::new(syntax, theme); |
| 40 | + |
| 41 | + for code_line in &code_buffer { |
| 42 | + let ranges: Vec<(SyntectStyle, &str)> = highlighter |
| 43 | + .highlight_line(code_line, &SYNTAX_SET) |
| 44 | + .unwrap_or_default(); |
| 45 | + |
| 46 | + let mut spans = Vec::new(); |
| 47 | + for (style, text) in ranges { |
| 48 | + spans.push(Span::styled( |
| 49 | + text.to_string(), |
| 50 | + Style::default().fg(syntect_to_ratatui_color(style.foreground)), |
| 51 | + )); |
| 52 | + } |
| 53 | + lines.push(Line::from(spans)); |
| 54 | + } |
| 55 | + |
| 56 | + code_buffer.clear(); |
| 57 | + in_code_block = false; |
| 58 | + } else { |
| 59 | + // Start of code block |
| 60 | + in_code_block = true; |
| 61 | + code_language = line.trim_start_matches("```").trim().to_string(); |
| 62 | + if code_language.is_empty() { |
| 63 | + code_language = "txt".to_string(); |
| 64 | + } |
| 65 | + } |
| 66 | + } else if in_code_block { |
| 67 | + code_buffer.push(line.to_string()); |
| 68 | + } else { |
| 69 | + // Regular text - wrap it |
| 70 | + for wrapped_line in wrap_text(line, max_width) { |
| 71 | + lines.push(Line::from(vec![Span::styled( |
| 72 | + wrapped_line, |
| 73 | + Style::default().fg(Color::White), |
| 74 | + )])); |
| 75 | + } |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + // Handle unclosed code block |
| 80 | + if in_code_block && !code_buffer.is_empty() { |
| 81 | + for code_line in &code_buffer { |
| 82 | + lines.push(Line::from(vec![Span::styled( |
| 83 | + code_line.clone(), |
| 84 | + Style::default().fg(Color::Gray), |
| 85 | + )])); |
| 86 | + } |
| 87 | + } |
| 88 | + |
| 89 | + lines |
| 90 | +} |
9 | 91 |
|
10 | 92 | // Enhanced helper function to wrap text to fit within a given width |
11 | 93 | fn wrap_text(text: &str, max_width: usize) -> Vec<String> { |
@@ -116,36 +198,30 @@ pub fn ui(f: &mut Frame, app: &App) { |
116 | 198 | // Add all conversation messages |
117 | 199 | for message in &app.conversation { |
118 | 200 | if let Some(content) = message.strip_prefix("User: ") { |
119 | | - let wrapped_content = wrap_text(content, max_width.saturating_sub(6)); // Account for "User: " prefix |
120 | | - |
121 | | - for (i, line) in wrapped_content.iter().enumerate() { |
122 | | - if i == 0 { |
123 | | - conversation_lines.push(Line::from(vec![ |
124 | | - Span::styled("User: ", Style::default().fg(Color::Blue).bold()), |
125 | | - Span::styled(line.clone(), Style::default().fg(Color::White)), |
126 | | - ])); |
127 | | - } else { |
128 | | - conversation_lines.push(Line::from(vec![ |
129 | | - Span::styled(" ", Style::default().fg(Color::Blue)), // Indent continuation |
130 | | - Span::styled(line.clone(), Style::default().fg(Color::White)), |
131 | | - ])); |
132 | | - } |
| 201 | + // Add user prefix |
| 202 | + conversation_lines.push(Line::from(vec![Span::styled( |
| 203 | + "User: ", |
| 204 | + Style::default().fg(Color::Blue).bold(), |
| 205 | + )])); |
| 206 | + |
| 207 | + // Parse and highlight the content |
| 208 | + let content_lines = |
| 209 | + parse_message_with_highlighting(content, max_width.saturating_sub(6)); |
| 210 | + for line in content_lines { |
| 211 | + conversation_lines.push(line); |
133 | 212 | } |
134 | 213 | } else if let Some(content) = message.strip_prefix("Agent: ") { |
135 | | - let wrapped_content = wrap_text(content, max_width.saturating_sub(7)); // Account for "Agent: " prefix |
136 | | - |
137 | | - for (i, line) in wrapped_content.iter().enumerate() { |
138 | | - if i == 0 { |
139 | | - conversation_lines.push(Line::from(vec![ |
140 | | - Span::styled("Agent: ", Style::default().fg(Color::Green).bold()), |
141 | | - Span::styled(line.clone(), Style::default().fg(Color::White)), |
142 | | - ])); |
143 | | - } else { |
144 | | - conversation_lines.push(Line::from(vec![ |
145 | | - Span::styled(" ", Style::default().fg(Color::Green)), // Indent continuation |
146 | | - Span::styled(line.clone(), Style::default().fg(Color::White)), |
147 | | - ])); |
148 | | - } |
| 214 | + // Add agent prefix |
| 215 | + conversation_lines.push(Line::from(vec![Span::styled( |
| 216 | + "Agent: ", |
| 217 | + Style::default().fg(Color::Green).bold(), |
| 218 | + )])); |
| 219 | + |
| 220 | + // Parse and highlight the content |
| 221 | + let content_lines = |
| 222 | + parse_message_with_highlighting(content, max_width.saturating_sub(7)); |
| 223 | + for line in content_lines { |
| 224 | + conversation_lines.push(line); |
149 | 225 | } |
150 | 226 | } else { |
151 | 227 | let wrapped_message = wrap_text(message, max_width); |
|
0 commit comments