vtcode_core/utils/ansi/
markdown.rs1use super::*;
4
5use std::io::Write;
6
7use crate::utils::transcript;
8
9use crate::ui::markdown::{
10 MarkdownLine, MarkdownSegment, RenderMarkdownOptions, render_markdown_to_lines_with_options,
11};
12use crate::ui::theme;
13use anstyle::Reset;
14use anyhow::Result;
15
16impl AnsiRenderer {
17 pub fn render_markdown_output(&mut self, style: MessageStyle, text: &str) -> Result<()> {
20 self.flush_compact_command_group();
21 self.render_markdown(style, text)
22 }
23
24 pub(super) fn render_markdown(&mut self, style: MessageStyle, text: &str) -> Result<()> {
25 if !self.should_render_style(style) {
26 return Ok(());
27 }
28 let styles = theme::active_styles();
29 let base_style = style.style();
30 let indent = self.indent_for_style(style);
31 let preserve_code_indentation = matches!(
32 style,
33 MessageStyle::Output
34 | MessageStyle::ToolOutput
35 | MessageStyle::ToolDetail
36 | MessageStyle::Response
37 | MessageStyle::Reasoning
38 | MessageStyle::ReasoningEmphasis
39 | MessageStyle::User
40 );
41
42 let text_storage;
44 let text = if matches!(style, MessageStyle::Response) {
45 text_storage = crate::utils::ansi_parser::strip_ansi(text);
46 &text_storage
47 } else {
48 text
49 };
50
51 if let Some(sink) = &mut self.sink {
52 if sink.table_max_width_override.is_none()
54 && let Ok((w, _)) = crossterm::terminal::size()
55 {
56 sink.table_max_width = Some(w as usize);
57 }
58 let last_empty =
59 sink.write_markdown(text, indent, base_style, Self::message_kind(style), preserve_code_indentation)?;
60 self.last_line_was_empty = last_empty;
61 return Ok(());
62 }
63 let highlight_cfg = if self.highlight_config.enabled {
64 Some(&self.highlight_config)
65 } else {
66 None
67 };
68 let mut lines = render_markdown_to_lines_with_options(
69 text,
70 base_style,
71 &styles,
72 highlight_cfg,
73 RenderMarkdownOptions {
74 preserve_code_indentation,
75 disable_code_block_table_reparse: false,
76 table_max_width: terminal_table_content_width(""),
77 },
78 );
79 if lines.is_empty() {
80 lines.push(MarkdownLine::default());
81 }
82
83 if lines.len() > 10 {
85 self.buffer.reserve(lines.len() * 80);
86 }
87
88 for line in lines {
89 self.write_markdown_line(style, indent, line)?;
90 }
91 Ok(())
92 }
93
94 pub(super) fn write_markdown_line(
95 &mut self,
96 style: MessageStyle,
97 indent: &str,
98 mut line: MarkdownLine,
99 ) -> Result<()> {
100 if !indent.is_empty() && !line.segments.is_empty() {
101 line.segments.insert(
102 0,
103 MarkdownSegment {
104 style: style.style(),
105 text: indent.to_string(),
106 link_target: None,
107 },
108 );
109 }
110
111 if let Some(sink) = &mut self.sink {
112 sink.write_segments(&line.segments, Self::message_kind(style))?;
113 self.last_line_was_empty = line.is_empty();
114 return Ok(());
115 }
116
117 let mut plain = String::new();
118 if self.color {
119 for segment in &line.segments {
120 let clickable_target = segment.link_target.as_deref().and_then(make_clickable_target);
121 if let Some(target) = clickable_target.as_deref() {
122 write!(self.writer, "\u{1b}]8;;{target}\u{1b}\\")?;
123 }
124 write!(self.writer, "{style}{}{Reset}", segment.text, style = segment.style)?;
125 if clickable_target.is_some() {
126 write!(self.writer, "\u{1b}]8;;\u{1b}\\")?;
127 }
128 plain.push_str(&segment.text);
129 }
130 writeln!(self.writer)?;
131 } else {
132 for segment in &line.segments {
133 let clickable_target = segment.link_target.as_deref().and_then(make_clickable_target);
134 if let Some(target) = clickable_target.as_deref() {
135 write!(self.writer, "\u{1b}]8;;{target}\u{1b}\\")?;
136 }
137 write!(self.writer, "{}", segment.text)?;
138 if clickable_target.is_some() {
139 write!(self.writer, "\u{1b}]8;;\u{1b}\\")?;
140 }
141 plain.push_str(&segment.text);
142 }
143 writeln!(self.writer)?;
144 }
145 self.writer.flush()?;
146 transcript::append(&plain);
147 self.last_line_was_empty = plain.trim().is_empty();
148 Ok(())
149 }
150}