mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
fix: simplify text processing to let UI handle wrapping naturally
- Removed manual word wrapping logic from text processing - Text is now only split by existing newlines, preserving structure - Markdown parsing remains for bold text support - UI widgets handle text wrapping automatically as designed This fixes the issue where agent messages were being split with unexpected line breaks. The UI already handles text wrapping properly, so manual wrapping was unnecessary and causing problems. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
@@ -1,110 +1,21 @@
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
|
||||
/// Process text with basic markdown support and proper word wrapping
|
||||
pub(crate) fn process_markdown_text(text: &str, width: u16) -> Vec<Line<'static>> {
|
||||
/// Process text with basic markdown support
|
||||
pub(crate) fn process_markdown_text(text: &str, _width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for paragraph in text.split("\n\n") {
|
||||
if paragraph.trim().is_empty() {
|
||||
lines.push(Line::from(""));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle each paragraph with word wrapping
|
||||
let paragraph_lines = wrap_markdown_paragraph(paragraph.trim(), width);
|
||||
lines.extend(paragraph_lines);
|
||||
lines.push(Line::from(""));
|
||||
}
|
||||
|
||||
// Remove trailing empty line if present
|
||||
if lines.last().map_or(false, |l| l.spans.is_empty()) {
|
||||
lines.pop();
|
||||
// Split by existing newlines to preserve intentional line breaks
|
||||
for line in text.lines() {
|
||||
lines.push(parse_markdown_line(line));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
/// Parse a paragraph with basic markdown and wrap it properly
|
||||
fn wrap_markdown_paragraph(paragraph: &str, width: u16) -> Vec<Line<'static>> {
|
||||
let mut lines = Vec::new();
|
||||
let mut current_line = Vec::new();
|
||||
let mut current_length = 0u16;
|
||||
let max_width = width.saturating_sub(4); // Account for padding
|
||||
|
||||
// Split paragraph into tokens (words and markdown elements)
|
||||
let tokens = parse_markdown_tokens(paragraph);
|
||||
|
||||
for token in tokens {
|
||||
let token_width = token.display_width();
|
||||
|
||||
// Check if we need to wrap to next line
|
||||
if current_length > 0 && current_length + token_width + 1 > max_width {
|
||||
// Finish current line
|
||||
if !current_line.is_empty() {
|
||||
lines.push(Line::from(current_line));
|
||||
current_line = Vec::new();
|
||||
current_length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Add space if not at start of line
|
||||
if current_length > 0 && !token.content().is_empty() {
|
||||
current_line.push(Span::raw(" "));
|
||||
current_length += 1;
|
||||
}
|
||||
|
||||
// Add the token
|
||||
current_line.push(token.to_span());
|
||||
current_length += token_width;
|
||||
}
|
||||
|
||||
// Add the last line if not empty
|
||||
if !current_line.is_empty() {
|
||||
lines.push(Line::from(current_line));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum MarkdownToken {
|
||||
Text(String),
|
||||
Bold(String),
|
||||
}
|
||||
|
||||
impl MarkdownToken {
|
||||
fn display_width(&self) -> u16 {
|
||||
match self {
|
||||
MarkdownToken::Text(s) | MarkdownToken::Bold(s) => s.chars().count() as u16,
|
||||
}
|
||||
}
|
||||
|
||||
fn content(&self) -> &str {
|
||||
match self {
|
||||
MarkdownToken::Text(s) | MarkdownToken::Bold(s) => s,
|
||||
}
|
||||
}
|
||||
|
||||
fn to_span(&self) -> Span<'static> {
|
||||
match self {
|
||||
MarkdownToken::Text(s) => Span::styled(
|
||||
s.clone(),
|
||||
Style::default().fg(crate::colors::text())
|
||||
),
|
||||
MarkdownToken::Bold(s) => Span::styled(
|
||||
s.clone(),
|
||||
Style::default()
|
||||
.fg(crate::colors::text_bright())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a paragraph into markdown tokens
|
||||
fn parse_markdown_tokens(text: &str) -> Vec<MarkdownToken> {
|
||||
let mut tokens = Vec::new();
|
||||
/// Parse a single line with markdown formatting
|
||||
fn parse_markdown_line(text: &str) -> Line<'static> {
|
||||
let mut spans = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut chars = text.chars().peekable();
|
||||
|
||||
@@ -115,10 +26,10 @@ fn parse_markdown_tokens(text: &str) -> Vec<MarkdownToken> {
|
||||
|
||||
// Save any accumulated text
|
||||
if !current_text.is_empty() {
|
||||
// Split by whitespace to handle word boundaries properly
|
||||
for word in current_text.split_whitespace() {
|
||||
tokens.push(MarkdownToken::Text(word.to_string()));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
current_text.clone(),
|
||||
Style::default().fg(crate::colors::text())
|
||||
));
|
||||
current_text.clear();
|
||||
}
|
||||
|
||||
@@ -136,10 +47,12 @@ fn parse_markdown_tokens(text: &str) -> Vec<MarkdownToken> {
|
||||
}
|
||||
|
||||
if found_closing && !bold_text.is_empty() {
|
||||
// Split bold text by whitespace too
|
||||
for word in bold_text.split_whitespace() {
|
||||
tokens.push(MarkdownToken::Bold(word.to_string()));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
bold_text,
|
||||
Style::default()
|
||||
.fg(crate::colors::text_bright())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
));
|
||||
} else {
|
||||
// No closing **, treat as regular text
|
||||
current_text.push_str("**");
|
||||
@@ -152,39 +65,38 @@ fn parse_markdown_tokens(text: &str) -> Vec<MarkdownToken> {
|
||||
|
||||
// Handle remaining text
|
||||
if !current_text.is_empty() {
|
||||
for word in current_text.split_whitespace() {
|
||||
tokens.push(MarkdownToken::Text(word.to_string()));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
current_text,
|
||||
Style::default().fg(crate::colors::text())
|
||||
));
|
||||
}
|
||||
|
||||
tokens
|
||||
Line::from(spans)
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_bold_parsing() {
|
||||
let tokens = parse_markdown_tokens("Hello **world** and **bold text** here");
|
||||
assert_eq!(tokens.len(), 6); // "Hello", "world", "and", "bold", "text", "here"
|
||||
|
||||
match &tokens[1] {
|
||||
MarkdownToken::Bold(text) => assert_eq!(text, "world"),
|
||||
_ => panic!("Expected bold token"),
|
||||
}
|
||||
let line = parse_markdown_line("Hello **world** and **bold text** here");
|
||||
// Check that we have multiple spans with different styles
|
||||
assert!(line.spans.len() > 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_word_wrapping() {
|
||||
let lines = process_markdown_text("This is a very long line that should be wrapped properly when it exceeds the maximum width", 20);
|
||||
assert!(lines.len() > 1);
|
||||
fn test_preserve_lines() {
|
||||
let lines = process_markdown_text("Line one\nLine two\nLine three", 80);
|
||||
assert_eq!(lines.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_paragraph_separation() {
|
||||
let lines = process_markdown_text("First paragraph.\n\nSecond paragraph.", 80);
|
||||
// Should have: first para line, empty line, second para line
|
||||
assert!(lines.len() >= 3);
|
||||
fn test_markdown_text() {
|
||||
let lines = process_markdown_text("Normal **bold** text", 80);
|
||||
assert_eq!(lines.len(), 1);
|
||||
// The first line should have spans for normal, bold, and text
|
||||
assert!(lines[0].spans.len() >= 3);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user