Skip to main content

ocelot_base/
render_source_diagnostics.rs

1use 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
14/// Renders source diagnostics into a displayable string.
15pub 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
9798            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
120121            2 │ println(value);
122              ╰╴        ━━━━━ never read"#]]
123        .assert_eq(&rendered);
124    }
125}