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)
91KB
1.5K
SLoC
sourceannot
A library to render snippets of source code with annotations.
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT)
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 originalu16sequence. - [
Snippet::with_chars()] usescharindices 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], currentlyPlainOutputfor writing rendered annotations to anystd::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