#annotations #report #code #error-report

no-std sourceannot

A library to render snippets of source code with annotations

6 releases

Uses new Rust 2024

new 0.3.1 Feb 19, 2026
0.3.0 Feb 19, 2026
0.2.1 Aug 13, 2024
0.2.0 Apr 21, 2024
0.1.1 Mar 30, 2024

#703 in Rust patterns


Used in 2 crates (via rsjsonnet-front)

MIT/Apache

91KB
1.5K SLoC

sourceannot

GitHub Actions Status crates.io Documentation MSRV License

A library to render snippets of source code with annotations.

License

Licensed under either of

at your option.


lib.rs:

A library to render snippets of source code with annotations.

This crate is meant to be used as a building block for compiler diagnostics (error reporting, warnings, lints, etc.).

This crate is #![no_std], but it depends on alloc.

Spans and positions

Annotation spans are Range<usize> indices into the snippet's source unit sequence (see Snippet). The exact unit depends on how the snippet was built:

  • [Snippet::with_utf8()] uses byte offsets into the original &str.
  • [Snippet::with_utf8_bytes()] uses byte offsets into the original &[u8].
  • [Snippet::with_latin1()] uses byte offsets into the original &[u8].
  • [Snippet::with_utf16_words()] uses 16-bit word offsets into the original u16 sequence.
  • [Snippet::with_chars()] uses char indices into the original character sequence.

These indices are not indices into the rendered output: some characters will be replaced with some representation (for example, tabs are replaced with spaces, some control characters are replaced, and invalid UTF-8 can be represented as or <XX>). The library keeps the mapping so that spans still line up with what is shown.

Output flexibility

Rendering is backend-agnostic: the library emits a stream of UTF-8 fragments tagged with metadata, and an Output implementation decides what to do with them.

This lets you render to plain text (e.g. a String or PlainOutput), or integrate with your own styling system (terminal colors, HTML, etc.).

Cargo features

  • std (enabled by default): enables features that depend on [std], currently PlainOutput for writing rendered annotations to any std::io::Write.

When the std feature is disabled, this crate is #![no_std] but still depends on alloc.

Example

// Some source code
let source = indoc::indoc! {r#"
    fn main() {
        println!("Hello, world!");
    }
"#};

// Create the snippet
let snippet = sourceannot::Snippet::with_utf8(
    1,
    source,
    4,
    sourceannot::ControlCharStyle::Codepoint,
    true,
);

// Styles are generic over the type of the metadata that accompanies each
// chunk of rendered text. In this example, we will use the following enum:
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Color {
    Default,
    Red,
    Green,
    Blue,
}
// If do not you need this per-chunk metadata, you can use `()` instead.

// Define the styles
// Use Unicode box drawing characters
let main_style = sourceannot::MainStyle {
    margin: Some(sourceannot::MarginStyle {
        line_char: '',
        discontinuity_chars: [' ', ' ', '·'],
        meta: Color::Blue,
    }),
    horizontal_char: '',
    vertical_char: '',
    top_vertical_char: '',
    top_corner_char: '',
    bottom_corner_char: '',
    spaces_meta: Color::Default,
    text_normal_meta: Color::Default,
    text_alt_meta: Color::Default,
};

// You can use a different style for each annotation, but in
// this example we will use the same style for all of them.
let annot_style = sourceannot::AnnotStyle {
    caret: '^',
    text_normal_meta: Color::Red,
    text_alt_meta: Color::Red,
    line_meta: Color::Red,
};

// Create the annotations
let mut annotations = sourceannot::Annotations::new(&snippet, &main_style);

annotations.add_annotation(
    0..44,
    &annot_style,
    vec![("this is the `main` function".into(), Color::Red)],
);
annotations.add_annotation(
    16..24,
    &annot_style,
    vec![("this is a macro invocation".into(), Color::Red)],
);

// Render the snippet with annotations. `PlainOutput` can write to any
// `std::io::Write` ignoring colors. But you could use your favorite terminal
// coloring library with a wrapper that implements the `Output` trait.
let max_line_no_width = annotations.max_line_no_width();
annotations
    .render(
        max_line_no_width,
        0,
        0,
        sourceannot::PlainOutput(std::io::stderr().lock()),
    )
    .expect("failed to write to stderr");

// You can also render to a string, which also ignores colors.
let mut rendered = String::new();
annotations.render(max_line_no_width, 0, 0, &mut rendered);

The output will look like this:

1 │ ╭ fn main() {
2 │ │     println!("Hello, world!");
  │ │     ^^^^^^^^ this is a macro invocation
3 │ │ }
  │ ╰─^ this is the `main` function

With an invalid UTF-8 source:

// Some source code
let source = indoc::indoc! {b"
    fn main() {
        println!(\"Hello, \xFFworld!\");
    }
"};

// Create the snippet
let snippet = sourceannot::Snippet::with_utf8_bytes(
    1,
    source,
    4,
    sourceannot::ControlCharStyle::Codepoint,
    true,
    sourceannot::InvalidSeqStyle::Hexadecimal,
    true,
);

// Assume styles from the previous example...

let mut annotations = sourceannot::Annotations::new(&snippet, &main_style);
annotations.add_annotation(
    0..45,
    &annot_style,
    vec![("this is the `main` function".into(), Color::Red)],
);

// Add a span that points to the invalid UTF-8 byte.
annotations.add_annotation(
    33..34,
    &annot_style,
    vec![("this an invalid UTF-8 sequence".into(), Color::Red)],
);

let max_line_no_width = annotations.max_line_no_width();
annotations
    .render(
        max_line_no_width,
        0,
        0,
        sourceannot::PlainOutput(std::io::stderr().lock()),
    )
    .expect("failed to write to stderr");

The output will look like this:

1 │ ╭ fn main() {
2 │ │     println!("Hello, <FF>world!");
  │ │                      ^^^^ this an invalid UTF-8 sequence
3 │ │ }
  │ ╰─^ this is the `main` function

Dependencies

~1.5MB
~22K SLoC