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