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