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 _ = 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 §ion.notes {
270 render_note(&mut output, note, capabilities, theme);
271 }
272 }
273 }
274
275 output
276}
277
278fn 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
439fn 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 ¬e.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
465fn 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
698fn 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
806fn 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}