ocelot_base/
render_source_diagnostics.rs1use crate::diagnostic_level::DiagnosticLevel;
2use crate::shared_string::SharedString;
3use crate::source_annotation::SourceAnnotation;
4use crate::source_diagnostic::SourceDiagnostic;
5use crate::source_excerpt::SourceExcerpt;
6use annotate_snippets::Annotation;
7use annotate_snippets::AnnotationKind;
8use annotate_snippets::Group;
9use annotate_snippets::Level;
10use annotate_snippets::Renderer;
11use annotate_snippets::Snippet;
12use annotate_snippets::renderer::DecorStyle;
13
14pub fn render_source_diagnostics(diagnostics: &[SourceDiagnostic]) -> SharedString {
16 render_source_diagnostics_with_renderer(
17 diagnostics,
18 Renderer::styled().decor_style(DecorStyle::Unicode),
19 )
20}
21
22fn render_source_diagnostics_with_renderer(
23 diagnostics: &[SourceDiagnostic],
24 renderer: Renderer,
25) -> SharedString {
26 let groups: Vec<_> = diagnostics.iter().map(render_group).collect();
27 renderer.render(&groups).into()
28}
29
30fn render_group<'a>(diagnostic: &'a SourceDiagnostic) -> Group<'a> {
31 let mut group =
32 Group::with_title(level_for(diagnostic.level).primary_title(diagnostic.message.as_str()));
33
34 for excerpt in &diagnostic.excerpts {
35 group = group.element(render_excerpt(excerpt));
36 }
37
38 group
39}
40
41fn render_excerpt<'a>(excerpt: &'a SourceExcerpt) -> Snippet<'a, Annotation<'a>> {
42 Snippet::source(excerpt.source_line.as_str())
43 .line_start(excerpt.line_number)
44 .path(excerpt.file_path.as_str())
45 .fold(false)
46 .annotations(excerpt.annotations.iter().map(render_annotation))
47}
48
49fn render_annotation<'a>(annotation: &'a SourceAnnotation) -> Annotation<'a> {
50 AnnotationKind::Primary
51 .span(annotation.span.as_range())
52 .label(annotation.message.as_str())
53}
54
55fn level_for(level: DiagnosticLevel) -> Level<'static> {
56 match level {
57 DiagnosticLevel::Error => Level::ERROR,
58 DiagnosticLevel::Warning => Level::WARNING,
59 }
60}
61
62#[cfg(test)]
63mod tests {
64 use super::render_source_diagnostics;
65 use super::render_source_diagnostics_with_renderer;
66 use crate::diagnostic_level::DiagnosticLevel;
67 use crate::source_annotation::SourceAnnotation;
68 use crate::source_diagnostic::SourceDiagnostic;
69 use crate::source_excerpt::SourceExcerpt;
70 use crate::span::Span;
71 use crate::unansi;
72 use annotate_snippets::Renderer;
73 use annotate_snippets::renderer::DecorStyle;
74 use expect_test::expect;
75
76 #[test]
77 fn renders_plain_source_diagnostic_with_excerpt() {
78 let diagnostic = SourceDiagnostic::new(
79 DiagnosticLevel::Error,
80 "examples/test.ocelot",
81 "unresolved identifier `value`",
82 )
83 .with_excerpt(
84 SourceExcerpt::new("examples/test.ocelot", 3, "println(value);")
85 .with_annotation(SourceAnnotation::new(Span::new(8, 13), "not found")),
86 );
87
88 let rendered = render_source_diagnostics_with_renderer(
89 &[diagnostic],
90 Renderer::plain().decor_style(DecorStyle::Unicode),
91 )
92 .to_string();
93
94 expect![[r#"
95 error: unresolved identifier `value`
96 ╭▸ examples/test.ocelot:3:9
97 │
98 3 │ println(value);
99 ╰╴ ━━━━━ not found"#]]
100 .assert_eq(&rendered);
101 }
102
103 #[test]
104 fn renders_styled_source_diagnostic() {
105 let diagnostic = SourceDiagnostic::new(
106 DiagnosticLevel::Warning,
107 "examples/test.ocelot",
108 "unused value",
109 )
110 .with_excerpt(
111 SourceExcerpt::new("examples/test.ocelot", 2, "println(value);")
112 .with_annotation(SourceAnnotation::new(Span::new(8, 13), "never read")),
113 );
114
115 let rendered = unansi(&render_source_diagnostics(&[diagnostic]));
116
117 expect![[r#"
118 warning: unused value
119 ╭▸ examples/test.ocelot:2:9
120 │
121 2 │ println(value);
122 ╰╴ ━━━━━ never read"#]]
123 .assert_eq(&rendered);
124 }
125}