ocelot-base 0.1.2

Shared base utilities for the ocelot project
Documentation
use crate::diagnostic_level::DiagnosticLevel;
use crate::shared_string::SharedString;
use crate::source_annotation::SourceAnnotation;
use crate::source_diagnostic::SourceDiagnostic;
use crate::source_excerpt::SourceExcerpt;
use annotate_snippets::Annotation;
use annotate_snippets::AnnotationKind;
use annotate_snippets::Group;
use annotate_snippets::Level;
use annotate_snippets::Renderer;
use annotate_snippets::Snippet;
use annotate_snippets::renderer::DecorStyle;

/// Renders source diagnostics into a displayable string.
pub fn render_source_diagnostics(diagnostics: &[SourceDiagnostic]) -> SharedString {
    render_source_diagnostics_with_renderer(
        diagnostics,
        Renderer::styled().decor_style(DecorStyle::Unicode),
    )
}

fn render_source_diagnostics_with_renderer(
    diagnostics: &[SourceDiagnostic],
    renderer: Renderer,
) -> SharedString {
    let groups: Vec<_> = diagnostics.iter().map(render_group).collect();
    renderer.render(&groups).into()
}

fn render_group<'a>(diagnostic: &'a SourceDiagnostic) -> Group<'a> {
    let mut group =
        Group::with_title(level_for(diagnostic.level).primary_title(diagnostic.message.as_str()));

    for excerpt in &diagnostic.excerpts {
        group = group.element(render_excerpt(excerpt));
    }

    group
}

fn render_excerpt<'a>(excerpt: &'a SourceExcerpt) -> Snippet<'a, Annotation<'a>> {
    Snippet::source(excerpt.source_line.as_str())
        .line_start(excerpt.line_number)
        .path(excerpt.file_path.as_str())
        .fold(false)
        .annotations(excerpt.annotations.iter().map(render_annotation))
}

fn render_annotation<'a>(annotation: &'a SourceAnnotation) -> Annotation<'a> {
    AnnotationKind::Primary
        .span(annotation.span.as_range())
        .label(annotation.message.as_str())
}

fn level_for(level: DiagnosticLevel) -> Level<'static> {
    match level {
        DiagnosticLevel::Error => Level::ERROR,
        DiagnosticLevel::Warning => Level::WARNING,
    }
}

#[cfg(test)]
mod tests {
    use super::render_source_diagnostics;
    use super::render_source_diagnostics_with_renderer;
    use crate::diagnostic_level::DiagnosticLevel;
    use crate::source_annotation::SourceAnnotation;
    use crate::source_diagnostic::SourceDiagnostic;
    use crate::source_excerpt::SourceExcerpt;
    use crate::span::Span;
    use crate::unansi;
    use annotate_snippets::Renderer;
    use annotate_snippets::renderer::DecorStyle;
    use expect_test::expect;

    #[test]
    fn renders_plain_source_diagnostic_with_excerpt() {
        let diagnostic = SourceDiagnostic::new(
            DiagnosticLevel::Error,
            "examples/test.ocelot",
            "unresolved identifier `value`",
        )
        .with_excerpt(
            SourceExcerpt::new("examples/test.ocelot", 3, "println(value);")
                .with_annotation(SourceAnnotation::new(Span::new(8, 13), "not found")),
        );

        let rendered = render_source_diagnostics_with_renderer(
            &[diagnostic],
            Renderer::plain().decor_style(DecorStyle::Unicode),
        )
        .to_string();

        expect![[r#"
            error: unresolved identifier `value`
              ╭▸ examples/test.ocelot:3:9
              │
            3 │ println(value);
              ╰╴        ━━━━━ not found"#]]
        .assert_eq(&rendered);
    }

    #[test]
    fn renders_styled_source_diagnostic() {
        let diagnostic = SourceDiagnostic::new(
            DiagnosticLevel::Warning,
            "examples/test.ocelot",
            "unused value",
        )
        .with_excerpt(
            SourceExcerpt::new("examples/test.ocelot", 2, "println(value);")
                .with_annotation(SourceAnnotation::new(Span::new(8, 13), "never read")),
        );

        let rendered = unansi(&render_source_diagnostics(&[diagnostic]));

        expect![[r#"
            warning: unused value
              ╭▸ examples/test.ocelot:2:9
              │
            2 │ println(value);
              ╰╴        ━━━━━ never read"#]]
        .assert_eq(&rendered);
    }
}