Skip to main content

margin_term/
lib.rs

1//! Terminal rendering for `margin` layout plans.
2//!
3//! d[impl package.primary-split]
4
5use std::fmt::Write;
6
7use arborium_theme::{Theme as ArboriumTheme, ThemeSlot, slot_to_highlight_index};
8use margin::{
9    AnnotationRole, Diagnostics, LayoutError, LayoutOptions, LayoutPlan, Note, NoteKind,
10    PlacementMode, ResolvedSpan, Severity, SourceWindow, SyntaxClass, WindowAnnotation, plan,
11};
12use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
13
14/// d[impl test.fixture-first]
15/// d[impl test.width-matrix]
16/// d[impl test.capability-matrix]
17/// d[impl test.unicode]
18#[cfg(test)]
19mod tests;
20
21/// d[impl glyph.unicode-ascii]
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum GlyphMode {
24    Unicode,
25    Ascii,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum ColorLevel {
30    None,
31    Ansi16,
32    Rgb24,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum HyperlinkMode {
37    None,
38    Osc8,
39}
40
41/// d[impl api.renderer-options]
42/// d[impl term.explicit-capabilities]
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub struct TerminalCapabilities {
45    pub width: usize,
46    pub glyph_mode: GlyphMode,
47    pub color_level: ColorLevel,
48    pub hyperlink_mode: HyperlinkMode,
49    pub tab_width: usize,
50}
51
52/// d[impl theme.roles]
53#[derive(Debug, Clone, Copy, PartialEq, Eq)]
54pub struct Theme {
55    pub severity_error: Style,
56    pub severity_warning: Style,
57    pub severity_advice: Style,
58    pub primary_label: Style,
59    pub secondary_label: Style,
60    pub syntax_token: Style,
61    pub note: Style,
62    pub help: Style,
63    pub gutter: Style,
64    pub connector: Style,
65    pub emphasis: Style,
66    syntax_styles: [Style; SYNTAX_CLASS_COUNT],
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub struct Style {
71    ansi16: Option<u8>,
72    fg_rgb24: Option<Rgb24>,
73    bg_rgb24: Option<Rgb24>,
74    modifiers: TextModifiers,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78struct Rgb24 {
79    r: u8,
80    g: u8,
81    b: u8,
82}
83
84#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
85struct TextModifiers {
86    bold: bool,
87    italic: bool,
88    underline: bool,
89    strikethrough: bool,
90}
91
92const SYNTAX_CLASS_COUNT: usize = 27;
93
94impl Default for TerminalCapabilities {
95    fn default() -> Self {
96        Self {
97            width: 80,
98            glyph_mode: GlyphMode::Unicode,
99            color_level: ColorLevel::None,
100            hyperlink_mode: HyperlinkMode::None,
101            tab_width: 4,
102        }
103    }
104}
105
106impl Default for Theme {
107    fn default() -> Self {
108        Self {
109            severity_error: Style::ansi16(31),
110            severity_warning: Style::ansi16(33),
111            severity_advice: Style::ansi16(36),
112            primary_label: Style::ansi16(31),
113            secondary_label: Style::ansi16(33),
114            syntax_token: Style::ansi16(36),
115            note: Style::ansi16(36),
116            help: Style::ansi16(32),
117            gutter: Style::ansi16(90),
118            connector: Style::ansi16(90),
119            emphasis: Style::ansi16(35),
120            syntax_styles: std::array::from_fn(|index| {
121                fallback_syntax_style(syntax_class_from_index(index))
122            }),
123        }
124    }
125}
126
127impl Style {
128    const fn ansi16(code: u8) -> Self {
129        Self {
130            ansi16: Some(code),
131            fg_rgb24: None,
132            bg_rgb24: None,
133            modifiers: TextModifiers {
134                bold: false,
135                italic: false,
136                underline: false,
137                strikethrough: false,
138            },
139        }
140    }
141
142    const fn plain() -> Self {
143        Self {
144            ansi16: None,
145            fg_rgb24: None,
146            bg_rgb24: None,
147            modifiers: TextModifiers {
148                bold: false,
149                italic: false,
150                underline: false,
151                strikethrough: false,
152            },
153        }
154    }
155
156    fn from_arborium(theme: &ArboriumTheme, slot: ThemeSlot) -> Option<Self> {
157        let index = slot_to_highlight_index(slot)?;
158        let style = theme.style(index)?;
159        if style.is_empty() {
160            return None;
161        }
162
163        Some(Self {
164            ansi16: None,
165            fg_rgb24: style.fg.map(Rgb24::from),
166            bg_rgb24: style.bg.map(Rgb24::from),
167            modifiers: TextModifiers {
168                bold: style.modifiers.bold,
169                italic: style.modifiers.italic,
170                underline: style.modifiers.underline,
171                strikethrough: style.modifiers.strikethrough,
172            },
173        })
174    }
175
176    /// d[impl theme.capability-fallback]
177    fn has_effect(self, color_level: ColorLevel) -> bool {
178        match color_level {
179            ColorLevel::None => false,
180            ColorLevel::Ansi16 => {
181                self.ansi16.is_some()
182                    || self.modifiers.bold
183                    || self.modifiers.italic
184                    || self.modifiers.underline
185                    || self.modifiers.strikethrough
186            }
187            ColorLevel::Rgb24 => {
188                self.ansi16.is_some()
189                    || self.fg_rgb24.is_some()
190                    || self.bg_rgb24.is_some()
191                    || self.modifiers.bold
192                    || self.modifiers.italic
193                    || self.modifiers.underline
194                    || self.modifiers.strikethrough
195            }
196        }
197    }
198}
199
200pub fn layout(
201    diagnostics: &Diagnostics,
202    capabilities: TerminalCapabilities,
203) -> Result<LayoutPlan, LayoutError> {
204    plan(
205        diagnostics,
206        &LayoutOptions {
207            width: capabilities.width,
208            tab_width: capabilities.tab_width,
209            ..LayoutOptions::default()
210        },
211    )
212}
213
214pub fn render(
215    diagnostics: &Diagnostics,
216    capabilities: TerminalCapabilities,
217) -> Result<String, LayoutError> {
218    let plan = layout(diagnostics, capabilities)?;
219    Ok(render_plan(&plan, capabilities))
220}
221
222/// d[impl api.layout-render-separation]
223pub fn render_plan(plan: &LayoutPlan, capabilities: TerminalCapabilities) -> String {
224    render_plan_with_theme(plan, capabilities, Theme::default())
225}
226
227pub fn render_plan_with_theme(
228    plan: &LayoutPlan,
229    capabilities: TerminalCapabilities,
230    theme: Theme,
231) -> String {
232    let mut output = String::new();
233
234    for (report_index, report) in plan.reports.iter().enumerate() {
235        if report_index > 0 {
236            output.push('\n');
237        }
238
239        let _ = writeln!(
240            output,
241            "{}: {}",
242            colorize(
243                severity_label(report.severity),
244                theme.style_for_severity(report.severity),
245                capabilities.color_level
246            ),
247            report.title
248        );
249
250        for window in &report.windows {
251            render_window(&mut output, window, capabilities, theme);
252        }
253
254        for note in &report.notes {
255            render_note(&mut output, note, capabilities, theme);
256        }
257
258        for section in &report.sections {
259            let _ = writeln!(
260                output,
261                "{} {}",
262                colorize(
263                    glyphs(capabilities.glyph_mode).branch,
264                    theme.connector,
265                    capabilities.color_level
266                ),
267                section.title
268            );
269            for note in &section.notes {
270                render_note(&mut output, note, capabilities, theme);
271            }
272        }
273    }
274
275    output
276}
277
278/// d[impl layout.ellipsis]
279fn render_window(
280    output: &mut String,
281    window: &SourceWindow,
282    capabilities: TerminalCapabilities,
283    theme: Theme,
284) {
285    let glyphs = glyphs(capabilities.glyph_mode);
286    let source_name = hyperlink_text(
287        window.source_name.as_str(),
288        window.source_hyperlink.as_deref(),
289        capabilities.hyperlink_mode,
290    );
291    let _ = writeln!(
292        output,
293        "{} {}",
294        colorize(glyphs.source, theme.connector, capabilities.color_level),
295        source_name
296    );
297    if window.omitted_before {
298        let _ = writeln!(
299            output,
300            "{} ...",
301            colorize(glyphs.separator, theme.gutter, capabilities.color_level)
302        );
303    }
304
305    for line in &window.lines {
306        let expanded = expand_tabs(&line.text, capabilities.tab_width);
307        let clipped = clip_to_width(&expanded, window.geometry.source_columns);
308        let styled = style_source_line(
309            clipped.as_str(),
310            line.line_number,
311            &window.annotations,
312            capabilities,
313            theme,
314        );
315        let separator = line_separator(&window.annotations, line.line_number, capabilities, theme);
316        let omission = if line.clipped { "..." } else { "" };
317        let _ = writeln!(
318            output,
319            "{:>width$} {} {}{}",
320            line.line_number,
321            separator,
322            styled,
323            omission,
324            width = window.geometry.line_number_width
325        );
326
327        for annotation in annotations_for_line(&window.annotations, line.line_number)
328            .into_iter()
329            .filter(|annotation| annotation.role != AnnotationRole::SyntaxToken)
330        {
331            render_annotation(output, annotation, window, capabilities, theme);
332        }
333    }
334
335    if window.omitted_after {
336        let _ = writeln!(
337            output,
338            "{} ...",
339            colorize(glyphs.separator, theme.gutter, capabilities.color_level)
340        );
341    }
342}
343
344fn style_source_line(
345    text: &str,
346    line_number: usize,
347    annotations: &[WindowAnnotation],
348    capabilities: TerminalCapabilities,
349    theme: Theme,
350) -> String {
351    if capabilities.color_level == ColorLevel::None {
352        return text.to_string();
353    }
354
355    let syntax_segments = annotations
356        .iter()
357        .filter(|annotation| annotation.role == AnnotationRole::SyntaxToken)
358        .flat_map(|annotation| {
359            annotation
360                .segments
361                .iter()
362                .copied()
363                .map(move |segment| (segment, annotation.syntax_class))
364        })
365        .filter(|(segment, _)| segment.line_number == line_number)
366        .collect::<Vec<_>>();
367
368    if syntax_segments.is_empty() {
369        return text.to_string();
370    }
371
372    let mut styled = String::new();
373    let mut run = String::new();
374    let mut current_style = None;
375    let mut column = 0;
376    for ch in text.chars() {
377        let width = UnicodeWidthChar::width(ch).unwrap_or(0);
378        let style = syntax_segments
379            .iter()
380            .find_map(|(segment, syntax_class)| {
381                let overlaps = if width == 0 {
382                    segment.start_column <= column && column < segment.end_column
383                } else {
384                    segment.start_column < column + width && column < segment.end_column
385                };
386                overlaps.then(|| theme.style_for_syntax_class(*syntax_class))
387            })
388            .filter(|style| style.has_effect(capabilities.color_level));
389
390        if style != current_style {
391            flush_styled_run(
392                &mut styled,
393                &mut run,
394                current_style,
395                capabilities.color_level,
396            );
397            current_style = style;
398        }
399        run.push(ch);
400
401        column += width;
402    }
403    flush_styled_run(
404        &mut styled,
405        &mut run,
406        current_style,
407        capabilities.color_level,
408    );
409
410    styled
411}
412
413fn flush_styled_run(
414    output: &mut String,
415    run: &mut String,
416    style: Option<Style>,
417    color_level: ColorLevel,
418) {
419    if run.is_empty() {
420        return;
421    }
422
423    match style {
424        Some(style) => output.push_str(&colorize(run.as_str(), style, color_level)),
425        None => output.push_str(run),
426    }
427    run.clear();
428}
429
430fn hyperlink_text(text: &str, target: Option<&str>, mode: HyperlinkMode) -> String {
431    match (mode, target) {
432        (HyperlinkMode::Osc8, Some(target)) => {
433            format!("\u{1b}]8;;{target}\u{1b}\\{text}\u{1b}]8;;\u{1b}\\")
434        }
435        _ => text.to_string(),
436    }
437}
438
439/// d[impl layout.notes-wrap]
440fn render_note(output: &mut String, note: &Note, capabilities: TerminalCapabilities, theme: Theme) {
441    let prefix = match note.kind {
442        NoteKind::Note => "note",
443        NoteKind::Help => "help",
444    };
445    let indent = format!("  = {prefix}: ");
446    let wrapped = wrap_text(
447        &note.text,
448        capabilities.width.saturating_sub(indent_width(&indent)),
449    );
450
451    for (index, line) in wrapped.iter().enumerate() {
452        if index == 0 {
453            let styled_indent = colorize(
454                indent.as_str(),
455                theme.style_for_note_kind(note.kind),
456                capabilities.color_level,
457            );
458            let _ = writeln!(output, "{styled_indent}{line}");
459        } else {
460            let _ = writeln!(output, "{}{line}", " ".repeat(indent_width(&indent)));
461        }
462    }
463}
464
465/// d[impl layout.multiline-labels]
466/// d[impl label.multiline-message-alignment]
467fn render_annotation(
468    output: &mut String,
469    annotation: RenderableAnnotation<'_>,
470    window: &SourceWindow,
471    capabilities: TerminalCapabilities,
472    theme: Theme,
473) {
474    let glyphs = glyphs(capabilities.glyph_mode);
475    let annotation_style = theme.style_for_annotation_role(annotation.role, annotation.severity);
476    if annotation.block {
477        if let Some(message) = annotation.message {
478            let gutter = format!(
479                "{:>width$} {} ",
480                "",
481                colorize(glyphs.block, annotation_style, capabilities.color_level),
482                width = window.geometry.line_number_width
483            );
484            let continuation_padding = " ".repeat(indent_width(glyphs.branch) + 1);
485            let available = window
486                .geometry
487                .source_columns
488                .saturating_sub(indent_width(glyphs.branch) + 1);
489            for (index, line) in wrap_text(message, available).into_iter().enumerate() {
490                let _ = writeln!(
491                    output,
492                    "{gutter}{}{line}",
493                    if index == 0 {
494                        format!(
495                            "{} ",
496                            colorize(glyphs.branch, annotation_style, capabilities.color_level)
497                        )
498                    } else {
499                        continuation_padding.clone()
500                    }
501                );
502            }
503        }
504        return;
505    }
506
507    let marker_line = marker_line(
508        annotation.segment,
509        annotation.placement,
510        capabilities.glyph_mode,
511    );
512    let colored = colorize(
513        marker_line.as_str(),
514        annotation_style,
515        capabilities.color_level,
516    );
517    let gutter = format!(
518        "{:>width$} {} ",
519        "",
520        colorize(glyphs.separator, theme.gutter, capabilities.color_level),
521        width = window.geometry.line_number_width
522    );
523
524    match annotation.placement {
525        PlacementMode::Side => {
526            let message = annotation
527                .message
528                .map(|message| format!(" {}", message))
529                .unwrap_or_default();
530            let _ = writeln!(output, "{gutter}{colored}{message}");
531        }
532        PlacementMode::BelowSpan => {
533            let _ = writeln!(output, "{gutter}{colored}");
534            if let Some(message) = annotation.message {
535                let anchor_padding = " ".repeat(annotation.segment.start_column);
536                let continuation_padding = " ".repeat(indent_width(glyphs.branch) + 1);
537                let available = window
538                    .geometry
539                    .source_columns
540                    .saturating_sub(annotation.segment.start_column + 2);
541                for (index, line) in wrap_text(message, available).into_iter().enumerate() {
542                    let _ = writeln!(
543                        output,
544                        "{gutter}{anchor_padding}{}{line}",
545                        if index == 0 {
546                            format!(
547                                "{} ",
548                                colorize(glyphs.branch, annotation_style, capabilities.color_level)
549                            )
550                        } else {
551                            continuation_padding.clone()
552                        }
553                    );
554                }
555            }
556        }
557        PlacementMode::Stacked => {
558            let _ = writeln!(output, "{gutter}{colored}");
559            if let Some(message) = annotation.message {
560                for line in wrap_text(message, window.geometry.source_columns.saturating_sub(4)) {
561                    let _ = writeln!(
562                        output,
563                        "{gutter}{} = {line}",
564                        colorize(glyphs.branch, annotation_style, capabilities.color_level)
565                    );
566                }
567            }
568        }
569    }
570}
571
572fn annotations_for_line<'a>(
573    annotations: &'a [WindowAnnotation],
574    line_number: usize,
575) -> Vec<RenderableAnnotation<'a>> {
576    let mut renderable = annotations
577        .iter()
578        .flat_map(|annotation| {
579            let block = is_gutter_block_annotation(annotation);
580            let message_owner = if block {
581                annotation.segments.last().copied()
582            } else {
583                annotation.segments.first().copied()
584            };
585            annotation
586                .segments
587                .iter()
588                .copied()
589                .filter(move |segment| {
590                    if block {
591                        message_owner == Some(*segment) && segment.line_number == line_number
592                    } else {
593                        segment.line_number == line_number
594                    }
595                })
596                .map(move |segment| RenderableAnnotation {
597                    block,
598                    message: (message_owner == Some(segment))
599                        .then_some(annotation.message.as_deref())
600                        .flatten(),
601                    placement: annotation.placement,
602                    role: annotation.role,
603                    severity: severity_for_role(annotation.role),
604                    priority: annotation.priority,
605                    segment,
606                })
607        })
608        .collect::<Vec<_>>();
609    renderable.sort_by_key(|item| std::cmp::Reverse(item.priority));
610    renderable
611}
612
613fn line_separator(
614    annotations: &[WindowAnnotation],
615    line_number: usize,
616    capabilities: TerminalCapabilities,
617    theme: Theme,
618) -> String {
619    let glyphs = glyphs(capabilities.glyph_mode);
620    if let Some(annotation) = annotations
621        .iter()
622        .filter(|annotation| is_gutter_block_annotation(annotation))
623        .filter(|annotation| {
624            annotation
625                .segments
626                .iter()
627                .any(|segment| segment.line_number == line_number)
628        })
629        .max_by_key(|annotation| annotation.priority)
630    {
631        let style =
632            theme.style_for_annotation_role(annotation.role, severity_for_role(annotation.role));
633        colorize(glyphs.block, style, capabilities.color_level)
634    } else {
635        colorize(glyphs.separator, theme.gutter, capabilities.color_level)
636    }
637}
638
639fn is_gutter_block_annotation(annotation: &WindowAnnotation) -> bool {
640    annotation.message.is_some()
641        && annotation.role != AnnotationRole::SyntaxToken
642        && annotation.segments.len() >= 3
643}
644
645fn marker_line(span: ResolvedSpan, placement: PlacementMode, glyph_mode: GlyphMode) -> String {
646    let (start, fill) = match (placement, glyph_mode) {
647        (PlacementMode::BelowSpan, GlyphMode::Unicode) => ('┬', '─'),
648        (_, GlyphMode::Unicode) => ('─', '─'),
649        (_, GlyphMode::Ascii) => ('^', '^'),
650    };
651    let width = span.end_column.saturating_sub(span.start_column).max(1);
652    let mut marker = String::with_capacity(span.start_column + width);
653    marker.push_str(&" ".repeat(span.start_column));
654    marker.push(start);
655    if width > 1 {
656        marker.push_str(&fill.to_string().repeat(width - 1));
657    }
658    marker
659}
660
661fn expand_tabs(text: &str, tab_width: usize) -> String {
662    let mut expanded = String::new();
663    let tab_width = tab_width.max(1);
664    let mut col = 0;
665
666    for ch in text.chars() {
667        if ch == '\t' {
668            let spaces = tab_width - (col % tab_width);
669            expanded.push_str(&" ".repeat(spaces));
670            col += spaces;
671        } else {
672            expanded.push(ch);
673            col += UnicodeWidthChar::width(ch).unwrap_or(0);
674        }
675    }
676
677    expanded
678}
679
680fn clip_to_width(text: &str, width: usize) -> String {
681    if width == 0 {
682        return String::new();
683    }
684
685    let mut clipped = String::new();
686    let mut used = 0;
687    for ch in text.chars() {
688        let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
689        if used + ch_width > width {
690            break;
691        }
692        clipped.push(ch);
693        used += ch_width;
694    }
695    clipped
696}
697
698/// d[impl layout.no-terminal-wrap]
699fn wrap_text(text: &str, width: usize) -> Vec<String> {
700    if width == 0 {
701        return vec![String::new()];
702    }
703
704    let mut lines = Vec::new();
705    for paragraph in text.split('\n') {
706        let mut current = String::new();
707        let mut saw_content = false;
708        for word in paragraph.split_whitespace() {
709            saw_content = true;
710            push_wrapped_word(&mut lines, &mut current, word, width);
711        }
712        if current.is_empty() && !saw_content {
713            lines.push(String::new());
714        } else if !current.is_empty() {
715            lines.push(current);
716        }
717    }
718    lines
719}
720
721fn indent_width(text: &str) -> usize {
722    UnicodeWidthStr::width(text)
723}
724
725fn push_wrapped_word(lines: &mut Vec<String>, current: &mut String, word: &str, width: usize) {
726    let mut remaining = word;
727
728    loop {
729        let separator = if current.is_empty() { "" } else { " " };
730        let candidate = format!("{current}{separator}{remaining}");
731        if UnicodeWidthStr::width(candidate.as_str()) <= width {
732            if !current.is_empty() {
733                current.push(' ');
734            }
735            current.push_str(remaining);
736            return;
737        }
738
739        if !current.is_empty() {
740            lines.push(std::mem::take(current));
741            continue;
742        }
743
744        let (head, tail) = split_long_token(remaining, width);
745        lines.push(head.to_string());
746        if tail.is_empty() {
747            return;
748        }
749        remaining = tail;
750    }
751}
752
753fn split_long_token(token: &str, width: usize) -> (&str, &str) {
754    let mut used = 0;
755    let mut furthest_end = 0;
756    let mut preferred_end = None;
757
758    for (index, ch) in token.char_indices() {
759        let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
760        if used + ch_width > width {
761            break;
762        }
763
764        used += ch_width;
765        furthest_end = index + ch.len_utf8();
766        if is_path_wrap_boundary(ch) {
767            preferred_end = Some(furthest_end);
768        }
769    }
770
771    let split_at = preferred_end.or_else(|| {
772        if furthest_end > 0 {
773            Some(furthest_end)
774        } else {
775            token.chars().next().map(char::len_utf8)
776        }
777    });
778    let split_at = split_at.unwrap_or(0);
779    (&token[..split_at], &token[split_at..])
780}
781
782fn is_path_wrap_boundary(ch: char) -> bool {
783    matches!(ch, '/' | '\\' | ':' | '.' | '-' | '_')
784}
785
786fn severity_label(severity: Severity) -> &'static str {
787    match severity {
788        Severity::Error => "error",
789        Severity::Warning => "warning",
790        Severity::Advice => "advice",
791    }
792}
793
794fn severity_for_role(role: AnnotationRole) -> Severity {
795    match role {
796        AnnotationRole::PrimaryLabel => Severity::Error,
797        AnnotationRole::SecondaryLabel => Severity::Warning,
798        AnnotationRole::RelatedLabel
799        | AnnotationRole::SyntaxToken
800        | AnnotationRole::SearchHighlight
801        | AnnotationRole::Selection
802        | AnnotationRole::Emphasis => Severity::Advice,
803    }
804}
805
806/// d[impl term.ansi-discipline]
807/// d[impl term.plaintext-mode]
808fn colorize(text: &str, style: Style, color_level: ColorLevel) -> String {
809    if color_level == ColorLevel::None {
810        return text.to_string();
811    }
812
813    let mut codes = Vec::<String>::new();
814    if style.modifiers.bold {
815        codes.push("1".to_string());
816    }
817    if style.modifiers.italic {
818        codes.push("3".to_string());
819    }
820    if style.modifiers.underline {
821        codes.push("4".to_string());
822    }
823    if style.modifiers.strikethrough {
824        codes.push("9".to_string());
825    }
826
827    match color_level {
828        ColorLevel::None => {}
829        ColorLevel::Ansi16 => {
830            if let Some(code) = style.ansi16 {
831                codes.push(code.to_string());
832            }
833        }
834        ColorLevel::Rgb24 => {
835            if let Some(fg) = style.fg_rgb24 {
836                codes.push(format!("38;2;{};{};{}", fg.r, fg.g, fg.b));
837            } else if let Some(code) = style.ansi16 {
838                codes.push(code.to_string());
839            }
840            if let Some(bg) = style.bg_rgb24 {
841                codes.push(format!("48;2;{};{};{}", bg.r, bg.g, bg.b));
842            }
843        }
844    }
845
846    if codes.is_empty() {
847        return text.to_string();
848    }
849    format!("\u{1b}[{}m{text}\u{1b}[0m", codes.join(";"))
850}
851
852fn glyphs(mode: GlyphMode) -> Glyphs {
853    match mode {
854        GlyphMode::Unicode => Glyphs {
855            source: "╭─>",
856            separator: "│",
857            branch: "╰─",
858            block: "▌",
859        },
860        GlyphMode::Ascii => Glyphs {
861            source: "-->",
862            separator: "|",
863            branch: "\\-",
864            block: "|",
865        },
866    }
867}
868
869#[derive(Debug, Clone, Copy)]
870struct Glyphs {
871    source: &'static str,
872    separator: &'static str,
873    branch: &'static str,
874    block: &'static str,
875}
876
877#[derive(Debug, Clone, Copy)]
878struct RenderableAnnotation<'a> {
879    block: bool,
880    message: Option<&'a str>,
881    placement: PlacementMode,
882    role: AnnotationRole,
883    severity: Severity,
884    priority: u16,
885    segment: ResolvedSpan,
886}
887
888impl Theme {
889    pub fn from_arborium(theme: &ArboriumTheme) -> Self {
890        let default = Self::default();
891
892        Self {
893            severity_error: Style::from_arborium(theme, ThemeSlot::Error)
894                .unwrap_or(default.severity_error),
895            severity_warning: Style::from_arborium(theme, ThemeSlot::Type)
896                .unwrap_or(default.secondary_label),
897            severity_advice: Style::from_arborium(theme, ThemeSlot::Function)
898                .unwrap_or(default.severity_advice),
899            primary_label: Style::from_arborium(theme, ThemeSlot::Error)
900                .unwrap_or(default.primary_label),
901            secondary_label: Style::from_arborium(theme, ThemeSlot::Type)
902                .unwrap_or(default.secondary_label),
903            syntax_token: Style::from_arborium(theme, ThemeSlot::Function)
904                .unwrap_or(default.syntax_token),
905            note: Style::from_arborium(theme, ThemeSlot::Comment).unwrap_or(default.note),
906            help: Style::from_arborium(theme, ThemeSlot::String).unwrap_or(default.help),
907            gutter: Style::from_arborium(theme, ThemeSlot::Comment).unwrap_or(default.gutter),
908            connector: Style::from_arborium(theme, ThemeSlot::Comment).unwrap_or(default.connector),
909            emphasis: Style::from_arborium(theme, ThemeSlot::Keyword).unwrap_or(default.emphasis),
910            syntax_styles: std::array::from_fn(|index| {
911                let class = syntax_class_from_index(index);
912                Style::from_arborium(theme, theme_slot_for_syntax_class(class))
913                    .unwrap_or(fallback_syntax_style(class))
914            }),
915        }
916    }
917
918    fn style_for_severity(self, severity: Severity) -> Style {
919        match severity {
920            Severity::Error => self.severity_error,
921            Severity::Warning => self.severity_warning,
922            Severity::Advice => self.severity_advice,
923        }
924    }
925
926    fn style_for_note_kind(self, kind: NoteKind) -> Style {
927        match kind {
928            NoteKind::Note => self.note,
929            NoteKind::Help => self.help,
930        }
931    }
932
933    fn style_for_annotation_role(self, role: AnnotationRole, severity: Severity) -> Style {
934        match role {
935            AnnotationRole::PrimaryLabel => self.primary_label,
936            AnnotationRole::SecondaryLabel => self.secondary_label,
937            AnnotationRole::SyntaxToken => self.syntax_token,
938            AnnotationRole::Emphasis => self.emphasis,
939            AnnotationRole::RelatedLabel
940            | AnnotationRole::SearchHighlight
941            | AnnotationRole::Selection => self.style_for_severity(severity),
942        }
943    }
944
945    fn style_for_syntax_class(self, syntax_class: Option<SyntaxClass>) -> Style {
946        syntax_class
947            .map(|class| self.syntax_styles[syntax_class_index(class)])
948            .unwrap_or(Style::plain())
949    }
950}
951
952impl From<arborium_theme::Color> for Rgb24 {
953    fn from(value: arborium_theme::Color) -> Self {
954        Self {
955            r: value.r,
956            g: value.g,
957            b: value.b,
958        }
959    }
960}
961
962fn fallback_syntax_style(class: SyntaxClass) -> Style {
963    match class {
964        SyntaxClass::Keyword
965        | SyntaxClass::Operator
966        | SyntaxClass::Macro
967        | SyntaxClass::Namespace
968        | SyntaxClass::Tag
969        | SyntaxClass::Title
970        | SyntaxClass::Strong
971        | SyntaxClass::Emphasis
972        | SyntaxClass::Link
973        | SyntaxClass::Literal
974        | SyntaxClass::Strikethrough => Style::ansi16(35),
975        SyntaxClass::Function | SyntaxClass::Constructor => Style::ansi16(36),
976        SyntaxClass::String | SyntaxClass::DiffAdd => Style::ansi16(32),
977        SyntaxClass::Comment | SyntaxClass::Punctuation => Style::ansi16(90),
978        SyntaxClass::Type | SyntaxClass::Attribute => Style::ansi16(33),
979        SyntaxClass::Constant
980        | SyntaxClass::Number
981        | SyntaxClass::Property
982        | SyntaxClass::Label
983        | SyntaxClass::Embedded => Style::ansi16(36),
984        SyntaxClass::DiffDelete | SyntaxClass::Error => Style::ansi16(31),
985        SyntaxClass::Variable => Style::plain(),
986    }
987}
988
989const fn syntax_class_index(class: SyntaxClass) -> usize {
990    match class {
991        SyntaxClass::Keyword => 0,
992        SyntaxClass::Function => 1,
993        SyntaxClass::String => 2,
994        SyntaxClass::Comment => 3,
995        SyntaxClass::Type => 4,
996        SyntaxClass::Variable => 5,
997        SyntaxClass::Constant => 6,
998        SyntaxClass::Number => 7,
999        SyntaxClass::Operator => 8,
1000        SyntaxClass::Punctuation => 9,
1001        SyntaxClass::Property => 10,
1002        SyntaxClass::Attribute => 11,
1003        SyntaxClass::Tag => 12,
1004        SyntaxClass::Macro => 13,
1005        SyntaxClass::Label => 14,
1006        SyntaxClass::Namespace => 15,
1007        SyntaxClass::Constructor => 16,
1008        SyntaxClass::Title => 17,
1009        SyntaxClass::Strong => 18,
1010        SyntaxClass::Emphasis => 19,
1011        SyntaxClass::Link => 20,
1012        SyntaxClass::Literal => 21,
1013        SyntaxClass::Strikethrough => 22,
1014        SyntaxClass::DiffAdd => 23,
1015        SyntaxClass::DiffDelete => 24,
1016        SyntaxClass::Embedded => 25,
1017        SyntaxClass::Error => 26,
1018    }
1019}
1020
1021const fn syntax_class_from_index(index: usize) -> SyntaxClass {
1022    match index {
1023        0 => SyntaxClass::Keyword,
1024        1 => SyntaxClass::Function,
1025        2 => SyntaxClass::String,
1026        3 => SyntaxClass::Comment,
1027        4 => SyntaxClass::Type,
1028        5 => SyntaxClass::Variable,
1029        6 => SyntaxClass::Constant,
1030        7 => SyntaxClass::Number,
1031        8 => SyntaxClass::Operator,
1032        9 => SyntaxClass::Punctuation,
1033        10 => SyntaxClass::Property,
1034        11 => SyntaxClass::Attribute,
1035        12 => SyntaxClass::Tag,
1036        13 => SyntaxClass::Macro,
1037        14 => SyntaxClass::Label,
1038        15 => SyntaxClass::Namespace,
1039        16 => SyntaxClass::Constructor,
1040        17 => SyntaxClass::Title,
1041        18 => SyntaxClass::Strong,
1042        19 => SyntaxClass::Emphasis,
1043        20 => SyntaxClass::Link,
1044        21 => SyntaxClass::Literal,
1045        22 => SyntaxClass::Strikethrough,
1046        23 => SyntaxClass::DiffAdd,
1047        24 => SyntaxClass::DiffDelete,
1048        25 => SyntaxClass::Embedded,
1049        _ => SyntaxClass::Error,
1050    }
1051}
1052
1053const fn theme_slot_for_syntax_class(class: SyntaxClass) -> ThemeSlot {
1054    match class {
1055        SyntaxClass::Keyword => ThemeSlot::Keyword,
1056        SyntaxClass::Function => ThemeSlot::Function,
1057        SyntaxClass::String => ThemeSlot::String,
1058        SyntaxClass::Comment => ThemeSlot::Comment,
1059        SyntaxClass::Type => ThemeSlot::Type,
1060        SyntaxClass::Variable => ThemeSlot::Variable,
1061        SyntaxClass::Constant => ThemeSlot::Constant,
1062        SyntaxClass::Number => ThemeSlot::Number,
1063        SyntaxClass::Operator => ThemeSlot::Operator,
1064        SyntaxClass::Punctuation => ThemeSlot::Punctuation,
1065        SyntaxClass::Property => ThemeSlot::Property,
1066        SyntaxClass::Attribute => ThemeSlot::Attribute,
1067        SyntaxClass::Tag => ThemeSlot::Tag,
1068        SyntaxClass::Macro => ThemeSlot::Macro,
1069        SyntaxClass::Label => ThemeSlot::Label,
1070        SyntaxClass::Namespace => ThemeSlot::Namespace,
1071        SyntaxClass::Constructor => ThemeSlot::Constructor,
1072        SyntaxClass::Title => ThemeSlot::Title,
1073        SyntaxClass::Strong => ThemeSlot::Strong,
1074        SyntaxClass::Emphasis => ThemeSlot::Emphasis,
1075        SyntaxClass::Link => ThemeSlot::Link,
1076        SyntaxClass::Literal => ThemeSlot::Literal,
1077        SyntaxClass::Strikethrough => ThemeSlot::Strikethrough,
1078        SyntaxClass::DiffAdd => ThemeSlot::DiffAdd,
1079        SyntaxClass::DiffDelete => ThemeSlot::DiffDelete,
1080        SyntaxClass::Embedded => ThemeSlot::Embedded,
1081        SyntaxClass::Error => ThemeSlot::Error,
1082    }
1083}