@@ -5,6 +5,7 @@ use ratatui::prelude::Stylize;
55use ratatui:: style:: { Modifier , Style } ;
66use ratatui:: text:: { Line , Span } ;
77
8+ use crate :: studio:: components:: syntax:: SyntaxHighlighter ;
89use crate :: studio:: state:: { ChatRole , ChatState } ;
910use crate :: studio:: theme;
1011
@@ -200,6 +201,9 @@ pub fn format_markdown(content: &str, max_width: usize, base_style: Style) -> Ve
200201 let mut code_lang = String :: new ( ) ;
201202 let mut list_depth: usize = 0 ;
202203 let mut ordered_list_num: Option < u64 > = None ;
204+ let mut _in_link = false ;
205+ let mut in_table = false ;
206+ let mut table_row: Vec < String > = Vec :: new ( ) ;
203207
204208 // Enable all markdown options
205209 let options = Options :: ENABLE_STRIKETHROUGH
@@ -256,14 +260,26 @@ pub fn format_markdown(content: &str, max_width: usize, base_style: Style) -> Ve
256260 }
257261 }
258262 Tag :: Emphasis => {
263+ // Add space before if needed
264+ if needs_space_before ( & current_spans) {
265+ current_spans. push ( Span :: styled ( " " , base_style) ) ;
266+ }
259267 let current = style_stack. last ( ) . copied ( ) . unwrap_or ( base_style) ;
260268 style_stack. push ( current. add_modifier ( Modifier :: ITALIC ) ) ;
261269 }
262270 Tag :: Strong => {
271+ // Add space before if needed
272+ if needs_space_before ( & current_spans) {
273+ current_spans. push ( Span :: styled ( " " , base_style) ) ;
274+ }
263275 let current = style_stack. last ( ) . copied ( ) . unwrap_or ( base_style) ;
264276 style_stack. push ( current. add_modifier ( Modifier :: BOLD ) ) ;
265277 }
266278 Tag :: Strikethrough => {
279+ // Add space before if needed
280+ if needs_space_before ( & current_spans) {
281+ current_spans. push ( Span :: styled ( " " , base_style) ) ;
282+ }
267283 let current = style_stack. last ( ) . copied ( ) . unwrap_or ( base_style) ;
268284 style_stack. push ( current. add_modifier ( Modifier :: CROSSED_OUT ) ) ;
269285 }
@@ -272,7 +288,22 @@ pub fn format_markdown(content: &str, max_width: usize, base_style: Style) -> Ve
272288 current_spans. push ( Span :: styled ( " │ " , Style :: default ( ) . fg ( theme:: TEXT_DIM ) ) ) ;
273289 }
274290 Tag :: Link { .. } | Tag :: Image { .. } => {
275- style_stack. push ( Style :: default ( ) . fg ( theme:: NEON_CYAN ) ) ;
291+ // Add space before link if needed
292+ if needs_space_before ( & current_spans) {
293+ current_spans. push ( Span :: styled ( " " , base_style) ) ;
294+ }
295+ _in_link = true ;
296+ style_stack. push (
297+ Style :: default ( )
298+ . fg ( theme:: NEON_CYAN )
299+ . add_modifier ( Modifier :: UNDERLINED ) ,
300+ ) ;
301+ }
302+ Tag :: Table ( _) | Tag :: TableHead | Tag :: TableRow => {
303+ in_table = true ;
304+ }
305+ Tag :: TableCell => {
306+ // Cell content handled in text
276307 }
277308 _ => { }
278309 } ,
@@ -304,53 +335,49 @@ pub fn format_markdown(content: &str, max_width: usize, base_style: Style) -> Ve
304335 }
305336 TagEnd :: Emphasis | TagEnd :: Strong | TagEnd :: Strikethrough => {
306337 style_stack. pop ( ) ;
338+ // Add space after if next text might need it
339+ // (will be trimmed if next char is punctuation)
307340 }
308341 TagEnd :: BlockQuote ( _) => {
309342 flush_line ( & mut lines, & mut current_spans, list_depth) ;
310343 }
311344 TagEnd :: Link | TagEnd :: Image => {
312345 style_stack. pop ( ) ;
346+ _in_link = false ;
347+ // Add space after link
348+ current_spans. push ( Span :: styled ( " " , base_style) ) ;
349+ }
350+ TagEnd :: Table => {
351+ in_table = false ;
352+ flush_line ( & mut lines, & mut current_spans, list_depth) ;
353+ lines. push ( Line :: from ( "" ) ) ; // Space after table
354+ }
355+ TagEnd :: TableHead | TagEnd :: TableRow => {
356+ // Render the row
357+ if !table_row. is_empty ( ) {
358+ let row_text = table_row. join ( " │ " ) ;
359+ flush_line ( & mut lines, & mut current_spans, list_depth) ;
360+ current_spans. push ( Span :: styled (
361+ format ! ( " │ {} │" , row_text) ,
362+ Style :: default ( ) . fg ( theme:: TEXT_SECONDARY ) ,
363+ ) ) ;
364+ flush_line ( & mut lines, & mut current_spans, list_depth) ;
365+ table_row. clear ( ) ;
366+ }
367+ }
368+ TagEnd :: TableCell => {
369+ // Cell completed - handled in text
313370 }
314371 _ => { }
315372 } ,
316373 Event :: Text ( text) => {
317374 if in_code_block {
318375 code_block_lines. extend ( text. lines ( ) . map ( String :: from) ) ;
376+ } else if in_table {
377+ table_row. push ( text. to_string ( ) ) ;
319378 } else {
320379 let style = style_stack. last ( ) . copied ( ) . unwrap_or ( base_style) ;
321-
322- // Add space after inline code if text doesn't start with punctuation/space
323- if let Some ( last_span) = current_spans. last ( ) {
324- let last_content = last_span. content . as_ref ( ) ;
325- if last_content. ends_with ( '`' ) {
326- let first_char = text. chars ( ) . next ( ) . unwrap_or ( ' ' ) ;
327- if !first_char. is_whitespace ( )
328- && !matches ! (
329- first_char,
330- '.' | ',' | ':' | ';' | ')' | ']' | '-' | '!' | '?'
331- )
332- {
333- current_spans. push ( Span :: styled ( " " , style) ) ;
334- }
335- }
336- }
337-
338- // Word wrap the text
339- let effective_width = max_width. saturating_sub ( 4 + list_depth * 2 ) ;
340- for chunk in wrap_text ( & text, effective_width) {
341- if !current_spans. is_empty ( ) && !chunk. is_empty ( ) {
342- current_spans. push ( Span :: styled ( chunk, style) ) ;
343- } else if !chunk. is_empty ( ) {
344- // Base indent + list depth indent (aligns with bullet text)
345- let indent = if list_depth > 0 {
346- // 2 spaces per depth level + 2 for bullet alignment
347- " " . repeat ( list_depth) + " "
348- } else {
349- " " . to_string ( )
350- } ;
351- current_spans. push ( Span :: styled ( format ! ( "{}{}" , indent, chunk) , style) ) ;
352- }
353- }
380+ process_text ( & text, style, & mut current_spans, list_depth, max_width) ;
354381 }
355382 }
356383 Event :: Code ( code) => {
@@ -417,6 +444,63 @@ fn flush_line(lines: &mut Vec<Line<'static>>, spans: &mut Vec<Span<'static>>, _l
417444 }
418445}
419446
447+ /// Check if we need to add a space before a new styled element
448+ fn needs_space_before ( spans : & [ Span < ' static > ] ) -> bool {
449+ if let Some ( last_span) = spans. last ( ) {
450+ let last_content = last_span. content . as_ref ( ) ;
451+ !last_content. is_empty ( )
452+ && !last_content. ends_with ( ' ' )
453+ && !last_content. ends_with ( '\n' )
454+ && !last_content. ends_with ( '(' )
455+ && !last_content. ends_with ( '[' )
456+ && !last_content. ends_with ( '"' )
457+ && !last_content. ends_with ( '\'' )
458+ } else {
459+ false
460+ }
461+ }
462+
463+ /// Process text content with proper spacing and word wrapping
464+ fn process_text (
465+ text : & str ,
466+ style : Style ,
467+ current_spans : & mut Vec < Span < ' static > > ,
468+ list_depth : usize ,
469+ max_width : usize ,
470+ ) {
471+ // Add space after inline code if text doesn't start with punctuation/space
472+ if let Some ( last_span) = current_spans. last ( ) {
473+ let last_content = last_span. content . as_ref ( ) ;
474+ if last_content. ends_with ( '`' ) {
475+ let first_char = text. chars ( ) . next ( ) . unwrap_or ( ' ' ) ;
476+ if !first_char. is_whitespace ( )
477+ && !matches ! (
478+ first_char,
479+ '.' | ',' | ':' | ';' | ')' | ']' | '-' | '!' | '?'
480+ )
481+ {
482+ current_spans. push ( Span :: styled ( " " , style) ) ;
483+ }
484+ }
485+ }
486+
487+ // Word wrap the text
488+ let effective_width = max_width. saturating_sub ( 4 + list_depth * 2 ) ;
489+ for chunk in wrap_text ( text, effective_width) {
490+ if !current_spans. is_empty ( ) && !chunk. is_empty ( ) {
491+ current_spans. push ( Span :: styled ( chunk, style) ) ;
492+ } else if !chunk. is_empty ( ) {
493+ // Base indent + list depth indent (aligns with bullet text)
494+ let indent = if list_depth > 0 {
495+ " " . repeat ( list_depth) + " "
496+ } else {
497+ " " . to_string ( )
498+ } ;
499+ current_spans. push ( Span :: styled ( format ! ( "{}{}" , indent, chunk) , style) ) ;
500+ }
501+ }
502+ }
503+
420504fn render_code_block (
421505 lines : & mut Vec < Line < ' static > > ,
422506 code_lines : & [ String ] ,
@@ -429,16 +513,33 @@ fn render_code_block(
429513 Style :: default ( ) . fg ( theme:: TEXT_DIM ) ,
430514 ) ) ) ;
431515
516+ // Create syntax highlighter for the language
517+ let highlighter = SyntaxHighlighter :: for_extension ( lang) ;
518+
432519 for code_line in code_lines {
433520 let truncated = if code_line. len ( ) > max_width. saturating_sub ( 4 ) {
434521 format ! ( "{}…" , & code_line[ ..max_width. saturating_sub( 5 ) ] )
435522 } else {
436523 code_line. clone ( )
437524 } ;
438- lines. push ( Line :: from ( vec ! [
439- Span :: styled( " │ " , Style :: default ( ) . fg( theme:: TEXT_DIM ) ) ,
440- Span :: styled( truncated, Style :: default ( ) . fg( theme:: SUCCESS_GREEN ) ) ,
441- ] ) ) ;
525+
526+ // Build spans for this line
527+ let mut line_spans = vec ! [ Span :: styled( " │ " , Style :: default ( ) . fg( theme:: TEXT_DIM ) ) ] ;
528+
529+ if highlighter. is_available ( ) {
530+ // Syntax highlighted
531+ for ( style, text) in highlighter. highlight_line ( & truncated) {
532+ line_spans. push ( Span :: styled ( text, style) ) ;
533+ }
534+ } else {
535+ // Fallback to plain green
536+ line_spans. push ( Span :: styled (
537+ truncated,
538+ Style :: default ( ) . fg ( theme:: SUCCESS_GREEN ) ,
539+ ) ) ;
540+ }
541+
542+ lines. push ( Line :: from ( line_spans) ) ;
442543 }
443544
444545 lines. push ( Line :: from ( Span :: styled (
@@ -453,19 +554,32 @@ fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
453554 return vec ! [ text. to_string( ) ] ;
454555 }
455556
557+ // Preserve leading/trailing whitespace
558+ let has_leading_space = text. starts_with ( char:: is_whitespace) ;
559+ let has_trailing_space = text. ends_with ( char:: is_whitespace) ;
560+
456561 let mut lines = Vec :: new ( ) ;
457562 let mut current_line = String :: new ( ) ;
563+ let mut first_word = true ;
458564
459565 for word in text. split_whitespace ( ) {
566+ // Add leading space to first word if original had it
567+ let word_to_add = if first_word && has_leading_space {
568+ first_word = false ;
569+ format ! ( " {}" , word)
570+ } else {
571+ first_word = false ;
572+ word. to_string ( )
573+ } ;
460574 // Handle words longer than max_width by breaking them
461- if word . len ( ) > max_width {
575+ if word_to_add . len ( ) > max_width {
462576 // Push current line if not empty
463577 if !current_line. is_empty ( ) {
464578 lines. push ( current_line) ;
465579 current_line = String :: new ( ) ;
466580 }
467581 // Break the long word into chunks
468- let mut remaining = word ;
582+ let mut remaining = word_to_add . as_str ( ) ;
469583 while remaining. len ( ) > max_width {
470584 let ( chunk, rest) = remaining. split_at ( max_width) ;
471585 lines. push ( chunk. to_string ( ) ) ;
@@ -475,16 +589,21 @@ fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
475589 current_line = remaining. to_string ( ) ;
476590 }
477591 } else if current_line. is_empty ( ) {
478- current_line = word . to_string ( ) ;
479- } else if current_line. len ( ) + 1 + word . len ( ) <= max_width {
592+ current_line = word_to_add ;
593+ } else if current_line. len ( ) + 1 + word_to_add . len ( ) <= max_width {
480594 current_line. push ( ' ' ) ;
481- current_line. push_str ( word ) ;
595+ current_line. push_str ( & word_to_add ) ;
482596 } else {
483597 lines. push ( current_line) ;
484- current_line = word . to_string ( ) ;
598+ current_line = word_to_add ;
485599 }
486600 }
487601
602+ // Add trailing space if original had it
603+ if has_trailing_space && !current_line. is_empty ( ) {
604+ current_line. push ( ' ' ) ;
605+ }
606+
488607 if !current_line. is_empty ( ) {
489608 lines. push ( current_line) ;
490609 }
0 commit comments