Skip to main content

vtcode_core/utils/ansi/
markdown.rs

1//! Markdown rendering for tool output.
2
3use 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    /// Render markdown content with proper syntax highlighting and indentation normalization.
18    /// Use this for tool output that contains markdown code blocks.
19    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        // Strip ANSI codes from agent response to prevent interference with markdown rendering
43        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            // Read terminal width fresh so tables adapt to resizes.
53            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        // Pre-allocate buffer for markdown output if rendering many lines
84        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}