1use super::parsing::{flush_current_line, push_blank_line};
2use super::{CODE_LINE_NUMBER_MIN_WIDTH, MarkdownLine, MarkdownSegment, RenderMarkdownOptions};
3use crate::config::loader::SyntaxHighlightingConfig;
4use crate::ui::syntax_highlight;
5use crate::ui::theme::ThemeStyles;
6use crate::utils::diff_styles::DiffColorPalette;
7use anstyle::{Effects, Style};
8use pulldown_cmark::{Event, Options, Parser, Tag, TagEnd};
9use syntect::util::LinesWithEndings;
10use vtcode_commons::diff_paths::{
11 format_start_only_hunk_header, is_diff_addition_line, is_diff_deletion_line,
12 is_diff_header_line, is_diff_new_file_marker_line, looks_like_diff_content,
13 parse_diff_git_path, parse_diff_marker_path,
14};
15
16const DIFF_SUMMARY_PREFIX: &str = "• Diff ";
17
18#[derive(Clone, Debug)]
19pub(crate) struct CodeBlockState {
20 pub(crate) language: Option<String>,
21 pub(crate) buffer: String,
22}
23
24pub(crate) struct CodeBlockRenderEnv<'a> {
25 pub(crate) lines: &'a mut Vec<MarkdownLine>,
26 pub(crate) current_line: &'a mut MarkdownLine,
27 pub(crate) blockquote_depth: usize,
28 pub(crate) list_continuation_prefix: &'a str,
29 pub(crate) pending_list_prefix: &'a mut Option<String>,
30 pub(crate) base_style: Style,
31 pub(crate) theme_styles: &'a ThemeStyles,
32 pub(crate) highlight_config: Option<&'a SyntaxHighlightingConfig>,
33 pub(crate) render_options: RenderMarkdownOptions,
34}
35
36pub(crate) fn handle_code_block_event(
37 event: &Event<'_>,
38 code_block: &mut Option<CodeBlockState>,
39 env: &mut CodeBlockRenderEnv<'_>,
40) -> bool {
41 if code_block.is_none() {
42 return false;
43 }
44
45 match event {
46 Event::Text(text) => {
47 if let Some(state) = code_block.as_mut() {
48 state.buffer.push_str(text);
49 }
50 true
51 }
52 Event::End(TagEnd::CodeBlock) => {
53 finalize_code_block(env, code_block, true, true);
54 true
55 }
56 _ => false,
57 }
58}
59
60pub(crate) fn finalize_unclosed_code_block(
61 code_block: &mut Option<CodeBlockState>,
62 env: &mut CodeBlockRenderEnv<'_>,
63) {
64 finalize_code_block(env, code_block, false, false);
65}
66
67fn finalize_code_block(
68 env: &mut CodeBlockRenderEnv<'_>,
69 code_block: &mut Option<CodeBlockState>,
70 allow_table_reparse: bool,
71 append_trailing_blank_line: bool,
72) {
73 flush_current_line(
74 env.lines,
75 env.current_line,
76 env.blockquote_depth,
77 env.list_continuation_prefix,
78 env.pending_list_prefix,
79 env.base_style,
80 );
81 if let Some(state) = code_block.take() {
82 let rendered = render_code_block_state(&state, env, allow_table_reparse);
83 env.lines.extend(rendered);
84 if append_trailing_blank_line {
85 push_blank_line(env.lines);
86 }
87 }
88}
89
90fn render_code_block_state(
91 state: &CodeBlockState,
92 env: &CodeBlockRenderEnv<'_>,
93 allow_table_reparse: bool,
94) -> Vec<MarkdownLine> {
95 if allow_table_reparse
96 && !env.render_options.disable_code_block_table_reparse
97 && code_block_contains_table(&state.buffer, state.language.as_deref())
98 {
99 return render_markdown_code_block_table(
100 &state.buffer,
101 env.base_style,
102 env.theme_styles,
103 env.highlight_config,
104 env.render_options,
105 );
106 }
107
108 let prefix = build_prefix_segments(
109 env.blockquote_depth,
110 env.list_continuation_prefix,
111 env.base_style,
112 );
113 highlight_code_block(
114 &state.buffer,
115 state.language.as_deref(),
116 env.highlight_config,
117 env.theme_styles,
118 env.base_style,
119 &prefix,
120 env.render_options.preserve_code_indentation,
121 )
122}
123
124fn render_markdown_code_block_table(
125 source: &str,
126 base_style: Style,
127 theme_styles: &ThemeStyles,
128 highlight_config: Option<&SyntaxHighlightingConfig>,
129 render_options: RenderMarkdownOptions,
130) -> Vec<MarkdownLine> {
131 let mut nested_options = render_options;
132 nested_options.disable_code_block_table_reparse = true;
133 super::render_markdown_to_lines_with_options(
134 source,
135 base_style,
136 theme_styles,
137 highlight_config,
138 nested_options,
139 )
140}
141
142fn build_prefix_segments(
143 blockquote_depth: usize,
144 list_continuation_prefix: &str,
145 base_style: Style,
146) -> Vec<MarkdownSegment> {
147 let mut segments =
148 Vec::with_capacity(blockquote_depth + usize::from(!list_continuation_prefix.is_empty()));
149 for _ in 0..blockquote_depth {
150 segments.push(MarkdownSegment::new(base_style.dimmed().italic(), "│ "));
151 }
152 if !list_continuation_prefix.is_empty() {
153 segments.push(MarkdownSegment::new(base_style, list_continuation_prefix));
154 }
155 segments
156}
157
158fn highlight_code_block(
159 code: &str,
160 language: Option<&str>,
161 highlight_config: Option<&SyntaxHighlightingConfig>,
162 theme_styles: &ThemeStyles,
163 base_style: Style,
164 prefix_segments: &[MarkdownSegment],
165 preserve_code_indentation: bool,
166) -> Vec<MarkdownLine> {
167 let mut lines = Vec::new();
168
169 let normalized_code = normalize_code_indentation(code, language, preserve_code_indentation);
170 let code_to_display = &normalized_code;
171 if is_diff_language(language)
172 || (language.is_none() && looks_like_diff_content(code_to_display))
173 {
174 return render_diff_code_block(code_to_display, theme_styles, base_style, prefix_segments);
175 }
176 let use_line_numbers =
177 language.is_some_and(|lang| !lang.trim().is_empty()) && !is_diff_language(language);
178
179 if let Some(config) = highlight_config.filter(|cfg| cfg.enabled)
180 && let Some(highlighted) = try_highlight(code_to_display, language, config)
181 {
182 let source_lines: Vec<&str> = code_to_display.lines().collect();
183 let line_count = source_line_count(code_to_display);
184 let number_width = line_number_width(line_count);
185 let gutter_style = line_number_style(theme_styles, base_style);
186 let mut line_number = 1usize;
187 for (index, segments) in highlighted.into_iter().enumerate() {
188 let src = source_lines.get(index).copied().unwrap_or("");
189 let is_omitted = parse_omitted_line_count(src).is_some();
190 let (gutter_text, omitted) = if use_line_numbers {
191 let (text, om) = format_gutter_text(line_number, number_width, src);
192 (Some(text), om)
193 } else {
194 (None, 1)
195 };
196 let mut line =
197 code_line_with_prefix(prefix_segments, gutter_text.as_deref(), gutter_style);
198 if is_omitted {
199 line.push_segment(gutter_style, src);
200 } else {
201 for (style, text) in segments {
202 line.push_segment(style, &text);
203 }
204 }
205 line_number = line_number.saturating_add(omitted);
206 lines.push(line);
207 }
208 return lines;
209 }
210
211 let mut line_number = 1usize;
212 let line_count = source_line_count(code_to_display);
213 let number_width = line_number_width(line_count);
214 let gutter_style = line_number_style(theme_styles, base_style);
215
216 for raw_line in LinesWithEndings::from(code_to_display) {
217 let trimmed = raw_line.trim_end_matches('\n');
218 let is_omitted = parse_omitted_line_count(trimmed).is_some();
219 let (gutter_text, omitted) = if use_line_numbers {
220 let (text, om) = format_gutter_text(line_number, number_width, trimmed);
221 (Some(text), om)
222 } else {
223 (None, 1)
224 };
225 let mut line = code_line_with_prefix(prefix_segments, gutter_text.as_deref(), gutter_style);
226 if !trimmed.is_empty() {
227 if is_omitted {
228 line.push_segment(gutter_style, trimmed);
229 } else {
230 line.push_segment(code_block_style(theme_styles, base_style), trimmed);
231 }
232 }
233 lines.push(line);
234 line_number = line_number.saturating_add(omitted);
235 }
236
237 if code_to_display.ends_with('\n') {
238 let (gutter_text, _) = format_gutter_text(line_number, number_width, "");
239 let line = code_line_with_prefix(
240 prefix_segments,
241 use_line_numbers.then_some(gutter_text.as_str()),
242 gutter_style,
243 );
244 lines.push(line);
245 }
246
247 lines
248}
249
250pub(crate) fn normalize_diff_lines(code: &str) -> Vec<String> {
251 #[derive(Default)]
252 struct DiffBlock {
253 header: String,
254 path: String,
255 lines: Vec<String>,
256 additions: usize,
257 deletions: usize,
258 }
259
260 let mut preface = Vec::new();
261 let mut blocks = Vec::new();
262 let mut current: Option<DiffBlock> = None;
263 let mut fallback_additions = 0usize;
264 let mut fallback_deletions = 0usize;
265 let mut fallback_path: Option<String> = None;
266 let mut summary_insert_index: Option<usize> = None;
267
268 for line in code.lines() {
269 if fallback_path.is_none() {
270 fallback_path = parse_diff_marker_path(line);
271 }
272 if summary_insert_index.is_none() && is_diff_new_file_marker_line(line.trim_start()) {
273 summary_insert_index = Some(preface.len());
274 }
275 bump_diff_counters(line, &mut fallback_additions, &mut fallback_deletions);
276
277 if let Some(path) = parse_diff_git_path(line) {
278 if let Some(block) = current.take() {
279 blocks.push(block);
280 }
281 current = Some(DiffBlock {
282 header: line.to_string(),
283 path,
284 lines: Vec::new(),
285 additions: 0,
286 deletions: 0,
287 });
288 continue;
289 }
290
291 let rewritten = rewrite_diff_line(line);
292 if let Some(block) = current.as_mut() {
293 bump_diff_counters(line, &mut block.additions, &mut block.deletions);
294 block.lines.push(rewritten);
295 } else {
296 preface.push(rewritten);
297 }
298 }
299
300 if let Some(block) = current {
301 blocks.push(block);
302 }
303
304 if blocks.is_empty() {
305 let path = fallback_path.unwrap_or_else(|| "file".to_string());
306 let summary = format_diff_summary(path.as_str(), fallback_additions, fallback_deletions);
307
308 let mut output = Vec::with_capacity(preface.len() + 1);
309 if let Some(idx) = summary_insert_index {
310 output.extend(preface[..=idx].iter().cloned());
311 output.push(summary);
312 output.extend(preface[idx + 1..].iter().cloned());
313 } else {
314 output.push(summary);
315 output.extend(preface);
316 }
317 return output;
318 }
319
320 let mut output = Vec::new();
321 output.extend(preface);
322 for block in blocks {
323 output.push(block.header);
324 output.push(format_diff_summary(
325 block.path.as_str(),
326 block.additions,
327 block.deletions,
328 ));
329 output.extend(block.lines);
330 }
331 output
332}
333
334fn render_diff_code_block(
335 code: &str,
336 theme_styles: &ThemeStyles,
337 base_style: Style,
338 prefix_segments: &[MarkdownSegment],
339) -> Vec<MarkdownLine> {
340 let mut lines = Vec::new();
341 let palette = DiffColorPalette::default();
342 let context_style = code_block_style(theme_styles, base_style);
343 let header_style = palette.header_style();
344 let added_style = palette.added_style();
345 let removed_style = palette.removed_style();
346
347 for line in normalize_diff_lines(code) {
348 let trimmed = line.trim_end_matches('\n');
349 let trimmed_start = trimmed.trim_start();
350 if let Some((path, additions, deletions)) = parse_diff_summary_line(trimmed_start) {
351 let leading_len = trimmed.len().saturating_sub(trimmed_start.len());
352 let leading = &trimmed[..leading_len];
353 let mut line = prefixed_line(prefix_segments);
354 if !leading.is_empty() {
355 line.push_segment(context_style, leading);
356 }
357 line.push_segment(context_style, &format!("{DIFF_SUMMARY_PREFIX}{path} ("));
358 line.push_segment(added_style, &format!("+{additions}"));
359 line.push_segment(context_style, " ");
360 line.push_segment(removed_style, &format!("-{deletions}"));
361 line.push_segment(context_style, ")");
362 lines.push(line);
363 continue;
364 }
365 let style = if trimmed.is_empty() {
366 context_style
367 } else if is_diff_header_line(trimmed_start) {
368 header_style
369 } else if is_diff_addition_line(trimmed_start) {
370 added_style
371 } else if is_diff_deletion_line(trimmed_start) {
372 removed_style
373 } else {
374 context_style
375 };
376
377 let mut line = prefixed_line(prefix_segments);
378 if !trimmed.is_empty() {
379 line.push_segment(style, trimmed);
380 }
381 lines.push(line);
382 }
383
384 if code.ends_with('\n') {
385 lines.push(prefixed_line(prefix_segments));
386 }
387
388 lines
389}
390
391fn parse_diff_summary_line(line: &str) -> Option<(&str, usize, usize)> {
392 let summary = line.strip_prefix(DIFF_SUMMARY_PREFIX)?;
393 let (path, counts) = summary.rsplit_once(" (")?;
394 let counts = counts.strip_suffix(')')?;
395 let mut parts = counts.split_whitespace();
396 let additions = parts.next()?.strip_prefix('+')?.parse().ok()?;
397 let deletions = parts.next()?.strip_prefix('-')?.parse().ok()?;
398 Some((path, additions, deletions))
399}
400
401fn format_diff_summary(path: &str, additions: usize, deletions: usize) -> String {
402 format!("{DIFF_SUMMARY_PREFIX}{path} (+{additions} -{deletions})")
403}
404
405fn append_prefix_segments(line: &mut MarkdownLine, prefix_segments: &[MarkdownSegment]) {
406 for segment in prefix_segments {
407 line.push_segment(segment.style, &segment.text);
408 }
409}
410
411fn prefixed_line(prefix_segments: &[MarkdownSegment]) -> MarkdownLine {
412 let mut line = MarkdownLine::default();
413 append_prefix_segments(&mut line, prefix_segments);
414 line
415}
416
417fn line_number_style(theme_styles: &ThemeStyles, base_style: Style) -> Style {
418 if base_style == theme_styles.tool_output {
419 theme_styles.tool_detail.dimmed()
420 } else {
421 base_style.dimmed()
422 }
423}
424
425fn format_gutter_text(line_num: usize, width: usize, line_text: &str) -> (String, usize) {
427 if let Some(omitted) = parse_omitted_line_count(line_text) {
428 let range_end = line_num.saturating_add(omitted.saturating_sub(1));
429 let range = format!("{line_num}-{range_end}");
430 (format!("{range:>width$} "), omitted)
431 } else {
432 (format!("{line_num:>width$} "), 1)
433 }
434}
435
436fn code_line_with_prefix(
437 prefix_segments: &[MarkdownSegment],
438 gutter_text: Option<&str>,
439 gutter_style: Style,
440) -> MarkdownLine {
441 let mut line = MarkdownLine::default();
442 append_prefix_segments(&mut line, prefix_segments);
443 if let Some(text) = gutter_text {
444 line.push_segment(gutter_style, text);
445 }
446 line
447}
448
449fn line_number_width(line_count: usize) -> usize {
450 let digits = line_count.max(1).to_string().len();
451 digits.max(CODE_LINE_NUMBER_MIN_WIDTH)
452}
453
454fn source_line_count(code: &str) -> usize {
455 let mut count = 0usize;
456 for raw_line in LinesWithEndings::from(code) {
457 let trimmed = raw_line.trim_end_matches('\n');
458 count = count.saturating_add(parse_omitted_line_count(trimmed).unwrap_or(1));
459 }
460 if code.ends_with('\n') {
461 count = count.saturating_add(1);
462 }
463 count
464}
465
466fn parse_omitted_line_count(text: &str) -> Option<usize> {
469 let trimmed = text.trim();
470 let after = trimmed.strip_prefix("… [+")?;
471 let end = after.find(' ')?;
472 let count_str = &after[..end];
473 count_str.parse::<usize>().ok()
474}
475
476fn code_block_contains_table(content: &str, language: Option<&str>) -> bool {
477 if let Some(lang) = language {
478 let lang_lower = lang.to_ascii_lowercase();
479 if !matches!(
480 lang_lower.as_str(),
481 "markdown" | "md" | "text" | "txt" | "plaintext" | "plain"
482 ) {
483 return false;
484 }
485 }
486
487 let trimmed = content.trim();
488 if trimmed.is_empty() {
489 return false;
490 }
491
492 let mut has_pipe_line = false;
493 let mut has_separator = false;
494 for line in trimmed.lines().take(4) {
495 let line = line.trim();
496 if line.contains('|') {
497 has_pipe_line = true;
498 }
499 if line.starts_with('|') && line.chars().all(|c| matches!(c, '|' | '-' | ':' | ' ')) {
500 has_separator = true;
501 }
502 }
503 if !has_pipe_line || !has_separator {
504 return false;
505 }
506
507 let options = Options::ENABLE_TABLES;
508 let parser = Parser::new_ext(trimmed, options);
509 for event in parser {
510 match event {
511 Event::Start(Tag::Table(_)) => return true,
512 Event::Start(Tag::Paragraph) | Event::Text(_) | Event::SoftBreak => continue,
513 _ => return false,
514 }
515 }
516 false
517}
518
519fn rewrite_diff_line(line: &str) -> String {
520 format_start_only_hunk_header(line).unwrap_or_else(|| line.to_string())
521}
522
523fn bump_diff_counters(line: &str, additions: &mut usize, deletions: &mut usize) {
524 let trimmed = line.trim_start();
525 if is_diff_addition_line(trimmed) {
526 *additions += 1;
527 } else if is_diff_deletion_line(trimmed) {
528 *deletions += 1;
529 }
530}
531
532fn is_diff_language(language: Option<&str>) -> bool {
533 language.is_some_and(|lang| {
534 matches!(
535 lang.to_ascii_lowercase().as_str(),
536 "diff" | "patch" | "udiff" | "git"
537 )
538 })
539}
540
541fn code_block_style(theme_styles: &ThemeStyles, base_style: Style) -> Style {
542 let base_fg = base_style.get_fg_color();
543 let theme_fg = theme_styles.output.get_fg_color();
544 let fg = if base_style.get_effects().contains(Effects::DIMMED) {
545 base_fg.or(theme_fg)
546 } else {
547 theme_fg.or(base_fg)
548 };
549 let mut style = base_style;
550 if let Some(color) = fg {
551 style = style.fg_color(Some(color));
552 }
553 style
554}
555
556pub(crate) fn normalize_code_indentation(
557 code: &str,
558 language: Option<&str>,
559 preserve_indentation: bool,
560) -> String {
561 if preserve_indentation {
562 return code.to_string();
563 }
564 let has_language_hint = language.is_some_and(|hint| {
565 matches!(
566 hint.to_lowercase().as_str(),
567 "rust"
568 | "rs"
569 | "python"
570 | "py"
571 | "javascript"
572 | "js"
573 | "jsx"
574 | "typescript"
575 | "ts"
576 | "tsx"
577 | "go"
578 | "golang"
579 | "java"
580 | "cpp"
581 | "c"
582 | "php"
583 | "html"
584 | "css"
585 | "sql"
586 | "csharp"
587 | "bash"
588 | "sh"
589 | "swift"
590 )
591 });
592
593 if !has_language_hint && language.is_some() {
594 return code.to_string();
595 }
596
597 let lines: Vec<&str> = code.lines().collect();
598 let min_indent = lines
599 .iter()
600 .filter(|line| !line.trim().is_empty())
601 .map(|line| &line[..line.len() - line.trim_start().len()])
602 .reduce(|acc, p| {
603 let mut len = 0;
604 for (c1, c2) in acc.chars().zip(p.chars()) {
605 if c1 != c2 {
606 break;
607 }
608 len += c1.len_utf8();
609 }
610 &acc[..len]
611 })
612 .map(|s| s.len())
613 .unwrap_or(0);
614
615 let normalized = lines
616 .iter()
617 .map(|line| {
618 if line.trim().is_empty() {
619 line
620 } else if line.len() >= min_indent {
621 &line[min_indent..]
622 } else {
623 line
624 }
625 })
626 .collect::<Vec<_>>()
627 .join("\n");
628
629 if code.ends_with('\n') {
630 format!("{normalized}\n")
631 } else {
632 normalized
633 }
634}
635
636pub fn highlight_line_for_diff(line: &str, language: Option<&str>) -> Option<Vec<(Style, String)>> {
637 syntax_highlight::highlight_line_to_anstyle_segments(
638 line,
639 language,
640 syntax_highlight::get_active_syntax_theme(),
641 true,
642 )
643 .map(|segments| {
644 segments
645 .into_iter()
646 .map(|(style, text)| {
647 let fg = style.get_fg_color().map(|c| match c {
648 anstyle::Color::Rgb(rgb) => {
649 let brighten = |v: u8| (v as u16 * 120 / 100).min(255) as u8;
650 anstyle::Color::Rgb(anstyle::RgbColor(
651 brighten(rgb.0),
652 brighten(rgb.1),
653 brighten(rgb.2),
654 ))
655 }
656 anstyle::Color::Ansi(ansi) => match ansi {
657 anstyle::AnsiColor::Black => {
658 anstyle::Color::Ansi(anstyle::AnsiColor::BrightWhite)
659 }
660 anstyle::AnsiColor::Red => {
661 anstyle::Color::Ansi(anstyle::AnsiColor::BrightRed)
662 }
663 anstyle::AnsiColor::Green => {
664 anstyle::Color::Ansi(anstyle::AnsiColor::BrightGreen)
665 }
666 anstyle::AnsiColor::Yellow => {
667 anstyle::Color::Ansi(anstyle::AnsiColor::BrightYellow)
668 }
669 anstyle::AnsiColor::Blue => {
670 anstyle::Color::Ansi(anstyle::AnsiColor::BrightBlue)
671 }
672 anstyle::AnsiColor::Magenta => {
673 anstyle::Color::Ansi(anstyle::AnsiColor::BrightMagenta)
674 }
675 anstyle::AnsiColor::Cyan => {
676 anstyle::Color::Ansi(anstyle::AnsiColor::BrightCyan)
677 }
678 anstyle::AnsiColor::White => {
679 anstyle::Color::Ansi(anstyle::AnsiColor::BrightWhite)
680 }
681 other => anstyle::Color::Ansi(other),
682 },
683 other => other,
684 });
685 let bg = style.get_bg_color();
686 let new_style = style.fg_color(fg).bg_color(bg);
687 (new_style, text)
688 })
689 .collect()
690 })
691}
692
693fn try_highlight(
694 code: &str,
695 language: Option<&str>,
696 config: &SyntaxHighlightingConfig,
697) -> Option<Vec<Vec<(Style, String)>>> {
698 let max_bytes = config.max_file_size_mb.saturating_mul(1024 * 1024);
699 if max_bytes > 0 && code.len() > max_bytes {
700 return None;
701 }
702
703 if let Some(lang) = language
704 && !config.enabled_languages.is_empty()
705 {
706 let direct_match = config
707 .enabled_languages
708 .iter()
709 .any(|entry| entry.eq_ignore_ascii_case(lang));
710 if !direct_match {
711 let syntax_ref = syntax_highlight::find_syntax_by_token(lang);
712 let resolved_match = config
713 .enabled_languages
714 .iter()
715 .any(|entry| entry.eq_ignore_ascii_case(&syntax_ref.name));
716 if !resolved_match {
717 return None;
718 }
719 }
720 }
721
722 let rendered = syntax_highlight::highlight_code_to_anstyle_line_segments(
723 code,
724 language,
725 &config.theme,
726 true,
727 );
728
729 Some(rendered)
730}
731
732#[derive(Clone, Debug)]
733pub struct HighlightedSegment {
734 pub style: Style,
735 pub text: String,
736}
737
738pub fn highlight_code_to_segments(
739 code: &str,
740 language: Option<&str>,
741 theme_name: &str,
742) -> Vec<Vec<HighlightedSegment>> {
743 syntax_highlight::highlight_code_to_anstyle_line_segments(code, language, theme_name, true)
744 .into_iter()
745 .map(|segments| {
746 segments
747 .into_iter()
748 .map(|(style, text)| HighlightedSegment { style, text })
749 .collect()
750 })
751 .collect()
752}
753
754pub fn highlight_code_to_ansi(code: &str, language: Option<&str>, theme_name: &str) -> Vec<String> {
755 let segments = highlight_code_to_segments(code, language, theme_name);
756 segments
757 .into_iter()
758 .map(|line_segments| {
759 let mut ansi_line = String::new();
760 for seg in line_segments {
761 let rendered = seg.style.render();
762 ansi_line.push_str(&format!(
763 "{rendered}{text}{reset}",
764 text = seg.text,
765 reset = anstyle::Reset
766 ));
767 }
768 ansi_line
769 })
770 .collect()
771}