Repository: zkat/miette
Pull request #23 (2021-08-19 06:23:23)

feat(reporter): fancy new reporter with unicode, colors, and multiline
Fixes: https://github.com/zkat/miette/issues/3
Fixes: https://github.com/zkat/miette/issues/5


Patch:
diff --git a/Cargo.toml b/Cargo.toml
index 52d3e05..41afbf8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,8 @@ indenter = "0.3.3"
 thiserror = "1.0.26"
 miette-derive = { version = "=0.11.0", path = "miette-derive" }
 once_cell = "1.8.0"
+owo-colors = "2.0.0"
+atty = "0.2.14"
 
 [dev-dependencies]
 thiserror = "1.0.26"
diff --git a/README.md b/README.md
index f886891..f151b75 100644
--- a/README.md
+++ b/README.md
@@ -131,4 +131,5 @@ Error: Error[oops::my::bad]: oops it broke!
 
 It also includes some code taken from [`eyre`](https://github.com/yaahc/eyre),
 and some from [`thiserror`](https://github.com/dtolnay/thiserror), also under
-the Apache License.
+the Apache License. Some code is taken from
+[`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
diff --git a/miette-derive/src/fmt.rs b/miette-derive/src/fmt.rs
index d076e76..f26e08f 100644
--- a/miette-derive/src/fmt.rs
+++ b/miette-derive/src/fmt.rs
@@ -6,7 +6,7 @@ use proc_macro2::{Delimiter, Group, TokenStream, TokenTree};
 use quote::{format_ident, quote, quote_spanned, ToTokens};
 use syn::ext::IdentExt;
 use syn::parse::{ParseStream, Parser};
-use syn::{Ident, Index, LitStr, Member, Result, Token, braced, bracketed, parenthesized};
+use syn::{braced, bracketed, parenthesized, Ident, Index, LitStr, Member, Result, Token};
 
 #[derive(Clone)]
 pub struct Display {
diff --git a/src/lib.rs b/src/lib.rs
index 1400894..1c18995 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,13 +3,13 @@
 pub use miette_derive::*;
 
 pub use error::*;
+pub use printer::*;
 pub use protocol::*;
-pub use reporter::*;
 pub use utils::*;
 
 mod chain;
 mod error;
+mod printer;
 mod protocol;
-mod reporter;
 mod source_impls;
 mod utils;
diff --git a/src/printer/default_reporter.rs b/src/printer/default_reporter.rs
new file mode 100644
index 0000000..3ce7331
--- /dev/null
+++ b/src/printer/default_reporter.rs
@@ -0,0 +1,536 @@
+use std::fmt;
+
+use indenter::indented;
+use owo_colors::{OwoColorize, Style};
+
+use crate::chain::Chain;
+use crate::printer::theme::*;
+use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
+use crate::SourceSpan;
+
+/**
+Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
+good enough for simple use-cases, and is the default one installed with `miette`,
+but you might want to implement your own if you want custom reporting for your
+tool or app.
+*/
+pub struct DefaultReportPrinter {
+    pub(crate) theme: MietteTheme,
+}
+
+impl DefaultReportPrinter {
+    pub fn new() -> Self {
+        Self {
+            theme: MietteTheme::default(),
+        }
+    }
+
+    pub fn new_themed(theme: MietteTheme) -> Self {
+        Self { theme }
+    }
+}
+
+impl Default for DefaultReportPrinter {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl DefaultReportPrinter {
+    pub fn render_report(
+        &self,
+        f: &mut impl fmt::Write,
+        diagnostic: &(dyn Diagnostic),
+    ) -> fmt::Result {
+        self.render_header(f, diagnostic)?;
+        self.render_causes(f, diagnostic)?;
+        if let Some(snippets) = diagnostic.snippets() {
+            let mut pre = false;
+            for snippet in snippets {
+                if !pre {
+                    writeln!(f)?;
+                    pre = true;
+                }
+                self.render_snippet(f, &snippet)?;
+            }
+        }
+
+        self.render_footer(f, diagnostic)?;
+        Ok(())
+    }
+
+    fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+        let sev = match diagnostic.severity() {
+            Some(Severity::Error) | None => "Error".style(self.theme.styles.error),
+            Some(Severity::Warning) => "Warning".style(self.theme.styles.warning),
+            Some(Severity::Advice) => "Advice".style(self.theme.styles.advice),
+        }
+        .to_string();
+        let code = diagnostic.code();
+        let msg = diagnostic.to_string();
+        writeln!(
+            f,
+            "{} [{}]: {}",
+            sev,
+            code.style(self.theme.styles.code),
+            msg
+        )?;
+        Ok(())
+    }
+
+    fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+        use fmt::Write as _;
+
+        if let Some(cause) = diagnostic.source() {
+            writeln!(f)?;
+            write!(f, "Caused by:")?;
+
+            let multiple = cause.source().is_some();
+
+            for (n, error) in Chain::new(cause).enumerate() {
+                let msg = format!("{}", error);
+                writeln!(f)?;
+                if multiple {
+                    write!(indented(f).ind(n), "{}", msg)?;
+                } else {
+                    write!(indented(f), "{}", msg)?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+
+    fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
+        if let Some(help) = diagnostic.help() {
+            let help = help.style(self.theme.styles.help);
+            writeln!(f)?;
+            writeln!(f, "{} {}", self.theme.characters.eq, help)?;
+        }
+        Ok(())
+    }
+
+    fn render_snippet(&self, f: &mut impl fmt::Write, snippet: &DiagnosticSnippet) -> fmt::Result {
+        // Boring: The Header
+        if let Some(source_name) = snippet.context.label() {
+            let source_name = source_name.style(self.theme.styles.filename);
+            write!(f, "[{}]", source_name)?;
+        }
+        if let Some(msg) = &snippet.message {
+            write!(f, " {}:", msg)?;
+        }
+        writeln!(f)?;
+        writeln!(f)?;
+
+        // Fun time!
+
+        // Our actual code, line by line! Handy!
+        let lines = self.get_lines(snippet)?;
+
+        // Highlights are the bits we're going to underline in our overall
+        // snippet, and we need to do some analysis first to come up with
+        // gutter size.
+        let mut highlights = snippet.highlights.clone().unwrap_or_else(Vec::new);
+        // sorting is your friend.
+        highlights.sort_unstable_by_key(|h| h.offset());
+        let highlights = highlights
+            .into_iter()
+            .zip(self.theme.styles.highlights.iter().cloned().cycle())
+            .map(|(hl, st)| FancySpan::new(hl, st))
+            .collect::<Vec<_>>();
+
+        // The max number of gutter-lines that will be active at any given
+        // point. We need this to figure out indentation, so we do one loop
+        // over the lines to see what the damage is gonna be.
+        let mut max_gutter = 0usize;
+        for line in &lines {
+            let mut num_highlights = 0;
+            for hl in &highlights {
+                if !line.span_line_only(hl) && line.span_applies(hl) {
+                    num_highlights += 1;
+                }
+            }
+            max_gutter = std::cmp::max(max_gutter, num_highlights);
+        }
+
+        // Oh and one more thing: We need to figure out how much room our line numbers need!
+        let linum_width = lines[..]
+            .last()
+            .expect("get_lines should always return at least one line?")
+            .line_number
+            .to_string()
+            .len();
+
+        // Now it's time for the fun part--actually rendering everything!
+        for line in &lines {
+            // Line number, appropriately padded.
+            self.write_linum(f, linum_width, line.line_number)?;
+
+            // Then, we need to print the gutter, along with any fly-bys We
+            // have separate gutters depending on whether we're on the actual
+            // line, or on one of the "highlight lines" below it.
+            self.render_line_gutter(f, max_gutter, line, &highlights)?;
+
+            // And _now_ we can print out the line text itself!
+            writeln!(f, "{}", line.text)?;
+
+            // Next, we write all the highlights that apply to this particular line.
+            let (single_line, multi_line): (Vec<_>, Vec<_>) = highlights
+                .iter()
+                .filter(|hl| line.span_applies(hl))
+                .partition(|hl| line.span_line_only(hl));
+            if !single_line.is_empty() {
+                // no line number!
+                self.write_no_linum(f, linum_width)?;
+                // gutter _again_
+                self.render_highlight_gutter(f, max_gutter, line, &highlights)?;
+                self.render_single_line_highlights(
+                    f,
+                    line,
+                    linum_width,
+                    max_gutter,
+                    &single_line,
+                    &highlights,
+                )?;
+            }
+            for hl in multi_line {
+                if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
+                    // no line number!
+                    self.write_no_linum(f, linum_width)?;
+                    // gutter _again_
+                    self.render_highlight_gutter(f, max_gutter, line, &highlights)?;
+                    self.render_multi_line_end(f, hl)?;
+                }
+            }
+        }
+        Ok(())
+    }
+
+    fn render_line_gutter(
+        &self,
+        f: &mut impl fmt::Write,
+        max_gutter: usize,
+        line: &Line,
+        highlights: &[FancySpan],
+    ) -> fmt::Result {
+        if max_gutter == 0 {
+            return Ok(());
+        }
+        let chars = &self.theme.characters;
+        let mut gutter = String::new();
+        let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+        let mut arrow = false;
+        for (i, hl) in applicable.enumerate() {
+            if line.span_starts(hl) {
+                gutter.push_str(&chars.ltop.to_string().style(hl.style).to_string());
+                gutter.push_str(
+                    &chars
+                        .hbar
+                        .to_string()
+                        .repeat(max_gutter.saturating_sub(i))
+                        .style(hl.style)
+                        .to_string(),
+                );
+                gutter.push_str(&chars.rarrow.to_string().style(hl.style).to_string());
+                arrow = true;
+                break;
+            } else if line.span_ends(hl) {
+                if hl.label().is_some() {
+                    gutter.push_str(&chars.lcross.to_string().style(hl.style).to_string());
+                } else {
+                    gutter.push_str(&chars.lbot.to_string().style(hl.style).to_string());
+                }
+                gutter.push_str(
+                    &chars
+                        .hbar
+                        .to_string()
+                        .repeat(max_gutter.saturating_sub(i))
+                        .style(hl.style)
+                        .to_string(),
+                );
+                gutter.push_str(&chars.rarrow.to_string().style(hl.style).to_string());
+                arrow = true;
+                break;
+            } else if line.span_flyby(hl) {
+                gutter.push_str(&chars.vbar.to_string().style(hl.style).to_string());
+            } else {
+                gutter.push(' ');
+            }
+        }
+        write!(
+            f,
+            "{}{}",
+            gutter,
+            " ".repeat(
+                if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
+            )
+        )?;
+        Ok(())
+    }
+
+    fn render_highlight_gutter(
+        &self,
+        f: &mut impl fmt::Write,
+        max_gutter: usize,
+        line: &Line,
+        highlights: &[FancySpan],
+    ) -> fmt::Result {
+        if max_gutter == 0 {
+            return Ok(());
+        }
+        let chars = &self.theme.characters;
+        let mut gutter = String::new();
+        let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+        for (i, hl) in applicable.enumerate() {
+            if !line.span_line_only(hl) && line.span_ends(hl) {
+                gutter.push_str(&chars.lbot.to_string().style(hl.style).to_string());
+                gutter.push_str(
+                    &chars
+                        .hbar
+                        .to_string()
+                        .repeat(max_gutter.saturating_sub(i) + 2)
+                        .style(hl.style)
+                        .to_string(),
+                );
+                break;
+            } else {
+                gutter.push_str(&chars.vbar.to_string().style(hl.style).to_string());
+            }
+        }
+        write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
+        Ok(())
+    }
+
+    fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
+        write!(
+            f,
+            " {:width$} {} ",
+            linum,
+            self.theme.characters.vbar,
+            width = width
+        )?;
+        Ok(())
+    }
+
+    fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
+        write!(
+            f,
+            " {:width$} {} ",
+            "",
+            self.theme.characters.vbar_break,
+            width = width
+        )?;
+        Ok(())
+    }
+
+    fn render_single_line_highlights(
+        &self,
+        f: &mut impl fmt::Write,
+        line: &Line,
+        linum_width: usize,
+        max_gutter: usize,
+        single_liners: &[&FancySpan],
+        all_highlights: &[FancySpan],
+    ) -> fmt::Result {
+        let mut underlines = String::new();
+        let mut highest = 0;
+        let chars = &self.theme.characters;
+        for hl in single_liners {
+            let local_offset = hl.offset() - line.offset;
+            let vbar_offset = local_offset + (hl.len() / 2);
+            let num_left = vbar_offset - local_offset;
+            let num_right = local_offset + hl.len() - vbar_offset - 1;
+            let start = std::cmp::max(local_offset, highest);
+            let end = local_offset + hl.len();
+            if start < end {
+                underlines.push_str(
+                    &format!(
+                        "{:width$}{}{}{}",
+                        "",
+                        chars.underline.to_string().repeat(num_left),
+                        if hl.label().is_some() {
+                            chars.underbar
+                        } else {
+                            chars.underline
+                        },
+                        chars.underline.to_string().repeat(num_right),
+                        width = local_offset.saturating_sub(highest),
+                    )
+                    .style(hl.style)
+                    .to_string(),
+                );
+            }
+            highest = std::cmp::max(highest, end);
+        }
+        writeln!(f, "{}", underlines)?;
+
+        for hl in single_liners {
+            if let Some(label) = hl.label() {
+                self.write_no_linum(f, linum_width)?;
+                self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
+                let local_offset = hl.offset() - line.offset;
+                let vbar_offset = local_offset + (hl.len() / 2);
+                let num_right = local_offset + hl.len() - vbar_offset - 1;
+                let lines = format!(
+                    "{:width$}{}{} {}",
+                    " ",
+                    chars.lbot,
+                    chars.hbar.to_string().repeat(num_right + 1),
+                    label,
+                    width = vbar_offset
+                );
+                writeln!(f, "{}", lines.style(hl.style))?;
+            }
+        }
+        Ok(())
+    }
+
+    fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
+        writeln!(
+            f,
+            "{} {}",
+            self.theme.characters.hbar.to_string().style(hl.style),
+            hl.label().unwrap_or_else(|| "".into()),
+        )?;
+        Ok(())
+    }
+
+    fn get_lines(&self, snippet: &DiagnosticSnippet) -> Result<Vec<Line>, fmt::Error> {
+        let context_data = snippet
+            .source
+            .read_span(&snippet.context)
+            .map_err(|_| fmt::Error)?;
+        let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
+        let mut line = context_data.line();
+        let mut column = context_data.column();
+        let mut offset = snippet.context.offset();
+        let mut line_offset = offset;
+        let mut iter = context.chars().peekable();
+        let mut line_str = String::new();
+        let mut lines = Vec::new();
+        while let Some(char) = iter.next() {
+            offset += char.len_utf8();
+            match char {
+                '\r' => {
+                    if iter.next_if_eq(&'\n').is_some() {
+                        offset += 1;
+                        line += 1;
+                        column = 0;
+                    } else {
+                        line_str.push(char);
+                        column += 1;
+                    }
+                }
+                '\n' => {
+                    line += 1;
+                    column = 0;
+                }
+                _ => {
+                    line_str.push(char);
+                    column += 1;
+                }
+            }
+            if iter.peek().is_none() {
+                line += 1;
+            }
+
+            if column == 0 || iter.peek().is_none() {
+                lines.push(Line {
+                    line_number: line,
+                    offset: line_offset,
+                    length: offset - line_offset,
+                    text: line_str.clone(),
+                });
+                line_str.clear();
+                line_offset = offset;
+            }
+        }
+        Ok(lines)
+    }
+}
+
+impl DiagnosticReportPrinter for DefaultReportPrinter {
+    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            return fmt::Debug::fmt(diagnostic, f);
+        }
+
+        self.render_report(f, diagnostic)
+    }
+}
+
+/*
+Support types
+*/
+
+struct Line {
+    line_number: usize,
+    offset: usize,
+    length: usize,
+    text: String,
+}
+
+impl Line {
+    fn span_line_only(&self, span: &FancySpan) -> bool {
+        span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
+    }
+
+    fn span_applies(&self, span: &FancySpan) -> bool {
+        // Span starts in this line
+        (span.offset() >= self.offset && span.offset() <= self.offset +self.length)
+        // Span passes through this line
+        || (span.offset() < self.offset && span.offset() + span.len() > self.offset + self.length) //todo
+        // Span ends on this line
+        || (span.offset() + span.len() >= self.offset && span.offset() + span.len() <= self.offset + self.length)
+    }
+
+    // A "flyby" is a multi-line span that technically covers this line, but
+    // does not begin or end within the line itself. This method is used to
+    // calculate gutters.
+    fn span_flyby(&self, span: &FancySpan) -> bool {
+        // the span itself starts before this line's starting offset (so, in a prev line)
+        span.offset() < self.offset
+            // ...and it stops after this line's end.
+            && span.offset() + span.len() > self.offset + self.length
+    }
+
+    // Does this line contain the *beginning* of this multiline span?
+    // This assumes self.span_applies() is true already.
+    fn span_starts(&self, span: &FancySpan) -> bool {
+        span.offset() >= self.offset
+    }
+
+    // Does this line contain the *end* of this multiline span?
+    // This assumes self.span_applies() is true already.
+    fn span_ends(&self, span: &FancySpan) -> bool {
+        span.offset() + span.len() >= self.offset
+            && span.offset() + span.len() <= self.offset + self.length
+    }
+}
+
+struct FancySpan {
+    span: SourceSpan,
+    style: Style,
+}
+
+impl FancySpan {
+    fn new(span: SourceSpan, style: Style) -> Self {
+        FancySpan { span, style }
+    }
+
+    fn style(&self) -> Style {
+        self.style
+    }
+
+    fn label(&self) -> Option<String> {
+        self.span.label().map(|l| l.style(self.style()).to_string())
+    }
+
+    fn offset(&self) -> usize {
+        self.span.offset()
+    }
+
+    fn len(&self) -> usize {
+        self.span.len()
+    }
+}
diff --git a/src/printer/mod.rs b/src/printer/mod.rs
new file mode 100644
index 0000000..407a8fc
--- /dev/null
+++ b/src/printer/mod.rs
@@ -0,0 +1,77 @@
+/*!
+Basic reporter for Diagnostics. Probably good enough for most use-cases,
+but largely meant to be an example.
+*/
+use std::fmt;
+
+use once_cell::sync::OnceCell;
+
+use crate::protocol::{Diagnostic, DiagnosticReportPrinter, Severity};
+use crate::MietteError;
+
+pub use default_reporter::*;
+pub use theme::*;
+
+mod default_reporter;
+mod theme;
+
+static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
+    OnceCell::new();
+
+/// Set the global [DiagnosticReportPrinter] that will be used when you report
+/// using [DiagnosticReport].
+pub fn set_reporter(
+    reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
+) -> Result<(), MietteError> {
+    REPORTER
+        .set(Box::new(reporter))
+        .map_err(|_| MietteError::ReporterInstallFailed)
+}
+
+/// Used by [DiagnosticReport] to fetch the reporter that will be used to
+/// print stuff out.
+pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
+    &**REPORTER.get_or_init(|| {
+        Box::new(DefaultReportPrinter {
+            // TODO: color support detection here?
+            theme: MietteTheme::default(),
+        })
+    })
+}
+
+/// Literally what it says on the tin.
+pub struct JokeReporter;
+
+impl DiagnosticReportPrinter for JokeReporter {
+    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        if f.alternate() {
+            return fmt::Debug::fmt(diagnostic, f);
+        }
+
+        let sev = match diagnostic.severity() {
+            Some(Severity::Error) | None => "error",
+            Some(Severity::Warning) => "warning",
+            Some(Severity::Advice) => "advice",
+        };
+        writeln!(
+            f,
+            "me, with {} {}: {}",
+            sev,
+            diagnostic,
+            diagnostic
+                .help()
+                .unwrap_or_else(|| Box::new(&"have you tried not failing?"))
+        )?;
+        writeln!(
+            f,
+            "miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
+            diagnostic.code(),
+            diagnostic.snippets().map(|snippets| {
+                snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
+                .collect::<Option<Vec<String>>>()
+            }).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
+        )?;
+
+        Ok(())
+    }
+}
diff --git a/src/printer/theme.rs b/src/printer/theme.rs
new file mode 100644
index 0000000..0eff3be
--- /dev/null
+++ b/src/printer/theme.rs
@@ -0,0 +1,165 @@
+use atty::Stream;
+use owo_colors::Style;
+
+pub struct MietteTheme {
+    pub characters: MietteCharacters,
+    pub styles: MietteStyles,
+}
+
+impl MietteTheme {
+    pub fn basic() -> Self {
+        Self {
+            characters: MietteCharacters::ascii(),
+            styles: MietteStyles::ansi(),
+        }
+    }
+    pub fn unicode() -> Self {
+        Self {
+            characters: MietteCharacters::unicode(),
+            styles: MietteStyles::ansi(),
+        }
+    }
+    pub fn unicode_nocolor() -> Self {
+        Self {
+            characters: MietteCharacters::unicode(),
+            styles: MietteStyles::none(),
+        }
+    }
+    pub fn none() -> Self {
+        Self {
+            characters: MietteCharacters::ascii(),
+            styles: MietteStyles::none(),
+        }
+    }
+}
+
+impl Default for MietteTheme {
+    fn default() -> Self {
+        match std::env::var("NO_COLOR") {
+            _ if !atty::is(Stream::Stdout) || !atty::is(Stream::Stderr) => Self::basic(),
+            Ok(string) if string != "0" => Self::unicode_nocolor(),
+            _ => Self::unicode(),
+        }
+    }
+}
+
+pub struct MietteStyles {
+    pub error: Style,
+    pub warning: Style,
+    pub advice: Style,
+    pub code: Style,
+    pub help: Style,
+    pub filename: Style,
+    pub highlights: Vec<Style>,
+}
+
+fn style() -> Style {
+    Style::new()
+}
+
+impl MietteStyles {
+    pub fn ansi() -> Self {
+        Self {
+            error: style().red(),
+            warning: style().yellow(),
+            advice: style().cyan(),
+            code: style().yellow(),
+            help: style().cyan(),
+            filename: style().green(),
+            highlights: vec![style().red(), style().magenta(), style().cyan()],
+        }
+    }
+
+    pub fn none() -> Self {
+        Self {
+            error: style(),
+            warning: style(),
+            advice: style(),
+            code: style(),
+            help: style(),
+            filename: style(),
+            highlights: vec![style()],
+        }
+    }
+}
+
+// ---------------------------------------
+// All code below here was taken from ariadne here:
+// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
+pub struct MietteCharacters {
+    pub hbar: char,
+    pub vbar: char,
+    pub xbar: char,
+    pub vbar_break: char,
+
+    pub uarrow: char,
+    pub rarrow: char,
+
+    pub ltop: char,
+    pub mtop: char,
+    pub rtop: char,
+    pub lbot: char,
+    pub rbot: char,
+    pub mbot: char,
+
+    pub lbox: char,
+    pub rbox: char,
+
+    pub lcross: char,
+    pub rcross: char,
+
+    pub underbar: char,
+    pub underline: char,
+
+    pub eq: char,
+}
+
+impl MietteCharacters {
+    pub fn unicode() -> Self {
+        Self {
+            hbar: '─',
+            vbar: '│',
+            xbar: '┼',
+            vbar_break: '·',
+            uarrow: '▲',
+            rarrow: '▶',
+            ltop: '╭',
+            mtop: '┬',
+            rtop: '╮',
+            lbot: '╰',
+            mbot: '┴',
+            rbot: '╯',
+            lbox: '[',
+            rbox: ']',
+            lcross: '├',
+            rcross: '┤',
+            underbar: '┬',
+            underline: '─',
+            eq: '﹦',
+        }
+    }
+
+    pub fn ascii() -> Self {
+        Self {
+            hbar: '-',
+            vbar: '|',
+            xbar: '+',
+            vbar_break: ':',
+            uarrow: '^',
+            rarrow: '>',
+            ltop: ',',
+            mtop: 'v',
+            rtop: '.',
+            lbot: '`',
+            mbot: '^',
+            rbot: '\'',
+            lbox: '[',
+            rbox: ']',
+            lcross: '|',
+            rcross: '|',
+            underbar: '|',
+            underline: '^',
+            eq: '=',
+        }
+    }
+}
diff --git a/src/protocol.rs b/src/protocol.rs
index 941ec49..73cc387 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -4,7 +4,11 @@ that you can implement to get access to miette's (and related library's) full
 reporting and such features.
 */
 
-use std::{fmt::Display, fs, panic::Location};
+use std::{
+    fmt::{self, Display},
+    fs,
+    panic::Location,
+};
 
 use crate::MietteError;
 
@@ -63,6 +67,36 @@ impl<T: Diagnostic + Send + Sync + 'static> From<T> for Box<dyn Diagnostic + 'st
     }
 }
 
+/**
+When used with `?`/`From`, this will wrap any Diagnostics and, when
+formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
+use it to format the inner [Diagnostic].
+*/
+pub struct DiagnosticReport {
+    diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
+}
+
+impl DiagnosticReport {
+    /// Return a reference to the inner [Diagnostic].
+    pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
+        &*self.diagnostic
+    }
+}
+
+impl std::fmt::Debug for DiagnosticReport {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        crate::get_reporter().debug(&*self.diagnostic, f)
+    }
+}
+
+impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
+    fn from(diagnostic: T) -> Self {
+        DiagnosticReport {
+            diagnostic: Box::new(diagnostic),
+        }
+    }
+}
+
 /**
 Protocol for [Diagnostic] handlers, which are responsible for actually printing out Diagnostics.
 
diff --git a/src/reporter.rs b/src/reporter.rs
deleted file mode 100644
index 3b8299e..0000000
--- a/src/reporter.rs
+++ /dev/null
@@ -1,243 +0,0 @@
-/*!
-Basic reporter for Diagnostics. Probably good enough for most use-cases,
-but largely meant to be an example.
-*/
-use std::fmt;
-
-use indenter::indented;
-use once_cell::sync::OnceCell;
-
-use crate::chain::Chain;
-use crate::protocol::{Diagnostic, DiagnosticReportPrinter, DiagnosticSnippet, Severity};
-use crate::MietteError;
-
-static REPORTER: OnceCell<Box<dyn DiagnosticReportPrinter + Send + Sync + 'static>> =
-    OnceCell::new();
-
-/// Set the global [DiagnosticReportPrinter] that will be used when you report
-/// using [DiagnosticReport].
-pub fn set_reporter(
-    reporter: impl DiagnosticReportPrinter + Send + Sync + 'static,
-) -> Result<(), MietteError> {
-    REPORTER
-        .set(Box::new(reporter))
-        .map_err(|_| MietteError::ReporterInstallFailed)
-}
-
-/// Used by [DiagnosticReport] to fetch the reporter that will be used to
-/// print stuff out.
-pub fn get_reporter() -> &'static (dyn DiagnosticReportPrinter + Send + Sync + 'static) {
-    &**REPORTER.get_or_init(|| Box::new(DefaultReportPrinter))
-}
-
-/// Convenience alias. This is intended to be used as the return type for `main()`
-pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
-
-/// When used with `?`/`From`, this will wrap any Diagnostics and, when
-/// formatted with `Debug`, will fetch the current [DiagnosticReportPrinter] and
-/// use it to format the inner [Diagnostic].
-pub struct DiagnosticReport {
-    diagnostic: Box<dyn Diagnostic + Send + Sync + 'static>,
-}
-
-impl DiagnosticReport {
-    /// Return a reference to the inner [Diagnostic].
-    pub fn inner(&self) -> &(dyn Diagnostic + Send + Sync + 'static) {
-        &*self.diagnostic
-    }
-}
-
-impl std::fmt::Debug for DiagnosticReport {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        get_reporter().debug(&*self.diagnostic, f)
-    }
-}
-
-impl<T: Diagnostic + Send + Sync + 'static> From<T> for DiagnosticReport {
-    fn from(diagnostic: T) -> Self {
-        DiagnosticReport {
-            diagnostic: Box::new(diagnostic),
-        }
-    }
-}
-
-/**
-Reference implementation of the [DiagnosticReportPrinter] trait. This is generally
-good enough for simple use-cases, and is the default one installed with `miette`,
-but you might want to implement your own if you want custom reporting for your
-tool or app.
-*/
-pub struct DefaultReportPrinter;
-
-impl DefaultReportPrinter {
-    fn render_snippet(
-        &self,
-        f: &mut fmt::Formatter<'_>,
-        snippet: &DiagnosticSnippet,
-    ) -> fmt::Result {
-        use fmt::Write as _;
-        if let Some(source_name) = snippet.context.label() {
-            write!(f, "[{}]", source_name)?;
-        }
-        if let Some(msg) = &snippet.message {
-            write!(f, " {}:", msg)?;
-        }
-        writeln!(f)?;
-        writeln!(f)?;
-        let context_data = snippet
-            .source
-            .read_span(&snippet.context)
-            .map_err(|_| fmt::Error)?;
-        let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
-        let mut line = context_data.line();
-        let mut column = context_data.column();
-        let mut offset = snippet.context.offset();
-        let mut line_offset = offset;
-        let mut iter = context.chars().peekable();
-        let mut line_str = String::new();
-        let highlights = snippet.highlights.as_ref();
-        while let Some(char) = iter.next() {
-            offset += char.len_utf8();
-            match char {
-                '\r' => {
-                    if iter.next_if_eq(&'\n').is_some() {
-                        offset += 1;
-                        line += 1;
-                        column = 0;
-                    } else {
-                        line_str.push(char);
-                        column += 1;
-                    }
-                }
-                '\n' => {
-                    line += 1;
-                    column = 0;
-                }
-                _ => {
-                    line_str.push(char);
-                    column += 1;
-                }
-            }
-            if iter.peek().is_none() {
-                line += 1;
-            }
-
-            if column == 0 || iter.peek().is_none() {
-                writeln!(indented(f), "{: <2} | {}", line, line_str)?;
-                line_str.clear();
-                if let Some(highlights) = highlights {
-                    for span in highlights {
-                        if span.offset() >= line_offset && (span.offset() + span.len()) < offset {
-                            // Highlight only covers one line.
-                            write!(indented(f), "{: <2} | ", "⫶")?;
-                            write!(
-                                f,
-                                "{}{} ",
-                                " ".repeat(span.offset() - line_offset),
-                                "^".repeat(span.len())
-                            )?;
-                            if let Some(label) = span.label() {
-                                writeln!(f, "{}", label)?;
-                            }
-                        } else if span.offset() < offset
-                            && span.offset() >= line_offset
-                            && (span.offset() + span.len()) >= offset
-                        {
-                            // Multiline highlight.
-                            todo!("Multiline highlights.");
-                        }
-                    }
-                }
-                line_offset = offset;
-            }
-        }
-        Ok(())
-    }
-}
-
-impl DiagnosticReportPrinter for DefaultReportPrinter {
-    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        use fmt::Write as _;
-
-        if f.alternate() {
-            return fmt::Debug::fmt(diagnostic, f);
-        }
-
-        let sev = match diagnostic.severity() {
-            Some(Severity::Error) | None => "Error",
-            Some(Severity::Warning) => "Warning",
-            Some(Severity::Advice) => "Advice",
-        };
-        writeln!(f, "{}[{}]: {}", sev, diagnostic.code(), diagnostic)?;
-
-        if let Some(cause) = diagnostic.source() {
-            writeln!(f)?;
-            write!(f, "Caused by:")?;
-            let multiple = cause.source().is_some();
-
-            for (n, error) in Chain::new(cause).enumerate() {
-                writeln!(f)?;
-                if multiple {
-                    write!(indented(f).ind(n), "{}", error)?;
-                } else {
-                    write!(indented(f), "{}", error)?;
-                }
-            }
-        }
-
-        if let Some(snippets) = diagnostic.snippets() {
-            let mut pre = false;
-            for snippet in snippets {
-                if !pre {
-                    writeln!(f)?;
-                    pre = true;
-                }
-                self.render_snippet(f, &snippet)?;
-            }
-        }
-
-        if let Some(help) = diagnostic.help() {
-            writeln!(f)?;
-            writeln!(f, "﹦{}", help)?;
-        }
-
-        Ok(())
-    }
-}
-
-/// Literally what it says on the tin.
-pub struct JokeReporter;
-
-impl DiagnosticReportPrinter for JokeReporter {
-    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        if f.alternate() {
-            return fmt::Debug::fmt(diagnostic, f);
-        }
-
-        let sev = match diagnostic.severity() {
-            Some(Severity::Error) | None => "error",
-            Some(Severity::Warning) => "warning",
-            Some(Severity::Advice) => "advice",
-        };
-        writeln!(
-            f,
-            "me, with {} {}: {}",
-            sev,
-            diagnostic,
-            diagnostic
-                .help()
-                .unwrap_or_else(|| Box::new(&"have you tried not failing?"))
-        )?;
-        writeln!(
-            f,
-            "miette, her eyes enormous: you {} miette? you {}? oh! oh! jail for mother! jail for mother for One Thousand Years!!!!",
-            diagnostic.code(),
-            diagnostic.snippets().map(|snippets| {
-                snippets.map(|snippet| snippet.message.map(|x| x.to_owned()))
-                .collect::<Option<Vec<String>>>()
-            }).flatten().map(|x| x.join(", ")).unwrap_or_else(||"try and cause miette to panic".into())
-        )?;
-
-        Ok(())
-    }
-}
diff --git a/src/utils.rs b/src/utils.rs
index 3f3b396..65adab6 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -2,7 +2,10 @@ use std::fmt;
 
 use thiserror::Error;
 
-use crate::Diagnostic;
+use crate::{Diagnostic, DiagnosticReport};
+
+/// Convenience alias. This is intended to be used as the return type for `main()`
+pub type DiagnosticResult<T> = Result<T, DiagnosticReport>;
 
 /// Convenience [Diagnostic] that can be used as an "anonymous" wrapper for
 /// Errors. This is intended to be paired with [IntoDiagnostic].


Test patch:
diff --git a/tests/derive.rs b/tests/derive.rs
index af3b2f3..5d700a3 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -171,7 +171,7 @@ fn fmt_help() {
         Y { len: usize },
 
         #[diagnostic(code(foo::x), help("{} x {self:?} x {:?}", 1, "2"))]
-        Z
+        Z,
     }
 
     assert_eq!(
diff --git a/tests/printer.rs b/tests/printer.rs
new file mode 100644
index 0000000..049f09c
--- /dev/null
+++ b/tests/printer.rs
@@ -0,0 +1,289 @@
+use miette::{
+    DefaultReportPrinter, Diagnostic, DiagnosticReport, MietteError, MietteTheme, SourceSpan,
+};
+use thiserror::Error;
+
+fn fmt_report(diag: DiagnosticReport) -> String {
+    // Mostly for dev purposes.
+    let theme = if std::env::var("STYLE").is_ok() {
+        MietteTheme::unicode()
+    } else if std::env::var("BASIC").is_ok() {
+        MietteTheme::none()
+    } else {
+        MietteTheme::unicode_nocolor()
+    };
+    let printer = DefaultReportPrinter::new_themed(theme);
+    let mut out = String::new();
+    printer.render_report(&mut out, diag.inner()).unwrap();
+    out
+}
+
+#[test]
+fn single_line_highlight() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight: ("this bit here", 9, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │   text\n   ·   ──┬─\n   ·     ╰── this bit here\n 3 │     here\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn single_line_highlight_no_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight: (9, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │   text\n   ·   ────\n 3 │     here\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn multiple_same_line_highlights() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = "source\n  text text text text text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("this bit here", 9, 4).into(),
+        highlight2: ("also this bit", 14, 4).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │   text text text text text\n   ·   ──┬─ ──┬─\n   ·     ╰── this bit here\n   ·          ╰── also this bit\n 3 │     here\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn multiline_highlight_adjacent() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight: ("these two lines", 9, 11).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │     source\n 2 │ ╭─▶   text\n 3 │ ├─▶     here\n   · ╰──── these two lines\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn multiline_highlight_flyby() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = r#"line1
+line2
+line3
+line4
+line5
+"#
+    .to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("block 1", 0, len).into(),
+        highlight2: ("block 2", 10, 9).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││   line3\n 4 │ │├─▶ line4\n   · │╰──── block 2\n 6 │ ├──▶ line5\n   · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn multiline_highlight_no_label() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = r#"line1
+line2
+line3
+line4
+line5
+"#
+    .to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("block 1", 0, len).into(),
+        highlight2: (10, 9).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭──▶ line1\n 2 │ │╭─▶ line2\n 3 │ ││   line3\n 4 │ │╰─▶ line4\n 6 │ ├──▶ line5\n   · ╰───── block 1\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here\nmore here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("this bit here", 0, 10).into(),
+        highlight2: ("also this bit", 20, 6).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ ╭─▶ source\n 2 │ ├─▶   text\n   · ╰──── this bit here\n 3 │ ╭─▶     here\n 4 │ ├─▶ more here\n   · ╰──── also this bit\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+// TODO: This breaks because those highlights aren't "truly" overlapping (in absolute byte offset), but they ARE overlapping in lines. Need to detect the latter case better
+#[ignore]
+/// Lines are overlapping, but the offsets themselves aren't, so they _look_
+/// disjunct if you only look at offsets.
+fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("this bit here", 0, 8).into(),
+        highlight2: ("also this bit", 9, 10).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │   text\n   ·   ──┬─\n   ·     ╰── this bit here\n 3 │     here\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
+
+#[test]
+/// Offsets themselves are overlapping, regardless of lines.
+#[ignore]
+fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError> {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, "This is the part that broke")]
+        ctx: SourceSpan,
+        #[highlight(ctx)]
+        highlight1: SourceSpan,
+        #[highlight(ctx)]
+        highlight2: SourceSpan,
+    }
+
+    let src = "source\n  text\n    here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: ("bad_file.rs", 0, len).into(),
+        highlight1: ("this bit here", 0, 8).into(),
+        highlight2: ("also this bit", 10, 10).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    assert_eq!("Error [oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n 1 │ source\n 2 │   text\n   ·   ──┬─\n   ·     ╰── this bit here\n 3 │     here\n\n﹦ try doing it better next time?\n".to_string(), out);
+    Ok(())
+}
diff --git a/tests/reporter.rs b/tests/reporter.rs
deleted file mode 100644
index 7130d8e..0000000
--- a/tests/reporter.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-use miette::{Diagnostic, DiagnosticReport, DiagnosticSnippet, MietteError, SourceSpan};
-use thiserror::Error;
-
-#[derive(Debug, Error)]
-#[error("oops!")]
-struct MyBad {
-    message: String,
-    src: String,
-    ctx: SourceSpan,
-    highlight: SourceSpan,
-}
-
-impl Diagnostic for MyBad {
-    fn code(&self) -> Box<dyn std::fmt::Display> {
-        Box::new(&"oops::my::bad")
-    }
-
-    fn help(&self) -> Option<Box<dyn std::fmt::Display>> {
-        Some(Box::new(&"try doing it better next time?"))
-    }
-
-    fn snippets(&self) -> Option<Box<dyn Iterator<Item = DiagnosticSnippet> + '_>> {
-        Some(Box::new(
-            vec![DiagnosticSnippet {
-                message: Some(self.message.as_ref()),
-                source: &self.src,
-                context: self.ctx.clone(),
-                highlights: Some(vec![self.highlight.clone()]),
-            }]
-            .into_iter(),
-        ))
-    }
-}
-
-#[test]
-fn fancy() -> Result<(), MietteError> {
-    let src = "source\n  text\n    here".to_string();
-    let len = src.len();
-    let err = MyBad {
-        message: "This is the part that broke".into(),
-        src,
-        ctx: ("bad_file.rs", 0, len).into(),
-        highlight: ("this bit here", 9, 4).into(),
-    };
-    let rep: DiagnosticReport = err.into();
-    let out = format!("{:?}", rep);
-    // println!("{}", out);
-    assert_eq!("Error[oops::my::bad]: oops!\n\n[bad_file.rs] This is the part that broke:\n\n    1  | source\n    2  |   text\n    ⫶  |   ^^^^ this bit here\n    3  |     here\n\n﹦try doing it better next time?\n".to_string(), out);
-    Ok(())
-}