Project Overview
The annotate-snippets
crate helps compiler and linter authors generate rich, readable error messages by visualizing source‐code fragments with annotations. It formats multi‐line spans, applies custom styling, supports folding long contexts, and emits to terminals (with ANSI colors) or structured targets like SVG/HTML.
Problems Solved
- Displaying precise, multi‐line error spans in context
- Collapsing irrelevant lines via fold points
- Rendering colored output consistently across terminals
- Sanitizing and normalizing untrusted source snippets
- Generating both text‐based and graphical (SVG) diagnostics
Key Features
- Multi‐line span annotations with labels and levels
- Automatic folding of unchanged code sections
- Fully customizable styling (colors, prefixes, margins)
- Dual backends: terminal (ANSI) and SVG/HTML renderers
- Zero‐cost abstractions suitable for real‐time diagnostics
- Lightweight, no heavy dependencies
Rust Ecosystem Integration
- Plug into any Rust compiler or linter front end
- Emit ANSI‐colored diagnostics in
cargo
or custom REPLs - Produce embeddable SVG/HTML snippets for IDEs and documentation
- Works alongside
codespan-reporting
or as a standalone renderer
Quick Start
// Cargo.toml
// [dependencies]
// annotate-snippets = "0.7"
use annotate_snippets::{Renderer, snippet};
// Build a simple snippet with a single error span
let snippet = snippet! {
title: ("Error", "Division by zero"),
slices: [
("src/main.rs", 1, {
source: "let x = 10 / 0;".to_string(),
annotations: [
(8, 9, "cannot divide by zero", Error)
]
})
]
};
let rendered = Renderer::default().render(&snippet);
println!("{}", rendered);
This example shows how you can construct and render a diagnostic in under ten lines. For advanced usage—custom themes, folding controls, SVG output—see the Usage section.
Getting Started
This guide shows how to add annotate-snippets to your project and render a styled code snippet in under five minutes.
1. Add the Dependency
In your project directory, run:
cargo add annotate-snippets
Or add this to Cargo.toml:
[dependencies]
annotate-snippets = "0.11"
2. Write a Simple Example
Create src/main.rs
with the following:
use annotate_snippets::{
formatter::DisplayList,
snippet::{Annotation, AnnotationType, Slice, Snippet, SourceAnnotation},
};
fn main() {
// 1. Define a code slice with a single error annotation
let slice = Slice {
source: "fn main() {\n println!(\"Hello, world!\")\n}".into(),
line_start: 1,
origin: Some("src/main.rs".into()),
fold: false,
annotations: vec![
SourceAnnotation {
id: None,
label: "missing semicolon".into(),
annotation_type: AnnotationType::Error,
range: 31..32, // position of the semicolon
}
],
};
// 2. Build the snippet: a title, the slice above, and an optional footer
let snippet = Snippet {
title: Some(Annotation {
id: None,
label: Some("Syntax error".into()),
annotation_type: AnnotationType::Error,
}),
slices: vec![slice],
footer: vec![Annotation {
id: None,
label: Some("Add a semicolon at the end of the println! call".into()),
annotation_type: AnnotationType::Help,
}],
options: Default::default(),
};
// 3. Render as plain text and print
let rendered = DisplayList::default().format(&snippet);
println!("{}", rendered);
}
3. Run and View
cargo run
You’ll see output similar to:
error: Syntax error
--> src/main.rs:2:32
|
2 | println!("Hello, world!")
| ^ missing semicolon
|
help: Add a semicolon at the end of the println! call
4. Next Steps
- Explore
examples/format.rs
for advanced styling and multiple annotations. - Enable colored output by adding the
term
feature:annotate-snippets = { version = "0.11", features = ["term"] }
- Integrate into error-reporting workflows or documentation generators.
Core Concepts
Annotate-snippets decouples diagnostic data from rendering. You build a tree of diagnostic elements—severity levels, titles, messages, code snippets with annotations and optional patches—then hand it to a Renderer
to produce formatted output. This pipeline lets you:
- Define severity and customize labels via
Level
- Represent code excerpts and error spans with
Snippet
andAnnotation
- Group related elements into a
Group
- Render one or more groups through a configurable
Renderer
1. Severity Levels (Level)
Use Level
to tag messages as errors, warnings, info, notes or help, and customize or suppress their prefixes.
use annotate_snippets::Level;
// Predefined constants
let error_level = Level::ERROR; // "error:"
let warning_level = Level::WARNING; // "warning:"
let info_level = Level::INFO; // "info:"
// Override default label
let caution = Level::WARNING.with_name("caution"); // "caution:"
// Suppress label entirely
let silent = Level::INFO.no_name(); // no "info:" prefix
// Create Title and Message
let title = error_level.title("unexpected token");
let message = error_level.message("found `;` in expression");
2. Constructing Snippets and Annotations
Build a Snippet
to display source code, then attach one or more Annotation
s to highlight spans.
use annotate_snippets::snippet::{Snippet, Annotation, AnnotationKind};
let code = r#"fn add(a: i32, b: &str) -> i32 {
a + b.len()
}
"#;
let snippet = Snippet::source(code)
.path("src/main.rs")
.line_start(1)
// Primary highlight for mismatched type
.annotation(
AnnotationKind::Primary
.span(11..16) // bytes covering "&str"
.label("expected `i32` here")
.highlight_source(true)
)
// Context note pointing at `.len()`
.annotation(
AnnotationKind::Context
.span(24..29) // bytes covering "b.len"
.label("calling `.len()` on `&str`")
);
3. Grouping Elements (Group & Element)
Combine a title, optional message, and any number of elements (Snippet
, other Snippet
instances or patch suggestions) into a Group
.
use annotate_snippets::snippet::Group;
let group = Group::with_title(title)
.with_message(message)
.element(snippet);
4. Rendering Diagnostics (Renderer)
Instantiate a Renderer
, adjust its settings, and render one or more Group
s to a formatted string.
use annotate_snippets::renderer::Renderer;
use annotate_snippets::OutputTheme;
fn main() {
// Start with ANSI‐colored Unicode output
let renderer = Renderer::styled()
.term_width(80) // wrap at 80 cols
.theme(OutputTheme::Unicode) // use Unicode box‐drawing
.anonymized_line_numbers(false) // show real line numbers
.short_message(false); // include snippets
let groups = vec![group]; // prepare your groups
let output = renderer.render(&groups); // produce diagnostics
print!("{}", output);
}
By following this pipeline—selecting a severity Level
, constructing Snippet
and Annotation
elements, grouping them, then rendering—you gain full control over both the content and appearance of your tool’s diagnostics.
Usage Cookbook
This cookbook provides task-oriented recipes for common diagnostic-rendering workflows using annotate_snippets
.
Render a Custom Error Snippet
Purpose: Manually construct and render a compiler-style error report with full control over titles, codes, spans, and theming.
use annotate_snippets::renderer::OutputTheme;
use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
fn main() {
// 1. Your source code as a &str.
let source = r#"
pub static C: u32 = 0 - 1;
"#;
// 2. Build an ERROR level with a custom name, title, and ID.
let level = Level::ERROR
.with_name(Some("error: internal compiler error"))
.title("could not evaluate static initializer")
.id("E0080");
// 3. Create a snippet pointing to the byte range of “0 - 1”.
let snippet = Snippet::source(source)
.path("src/lib.rs")
.annotation(
AnnotationKind::Primary
.span(17..22)
.label("attempt to compute `0_u32 - 1_u32`, which would overflow"),
);
// 4. Group and render with Unicode-friendly arrows.
let group = Group::with_title(level).element(snippet);
let renderer = Renderer::styled().theme(OutputTheme::Unicode);
println!("{}", renderer.render(&[group]));
}
Practical Tips:
- Switch
OutputTheme::Unicode
toLight
orNoColor
for plain-text logs. - Add secondary annotations via
AnnotationKind::Secondary
for context notes. - Render multiple groups by passing a slice to
renderer.render(&[group1, group2])
.
Highlight a Type Mismatch with Colorized Output
Purpose: Display a type-mismatch error with colorized annotations in terminals using termcolor
.
use annotate_snippets::renderer::termcolor::{ColorChoice, StandardStream};
use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
fn main() -> std::io::Result<()> {
let source = r#"
fn main() {
let x: i32 = "hello";
}
"#;
let snippet = Snippet::source(source)
.path("src/main.rs")
.line_start(1)
.annotation(
AnnotationKind::Primary
.span( twenty_two .. thirty_three ) // approximate byte indices
.label("expected `i32`, found `&str`"),
);
let group = Group::with_title(Level::ERROR.title("type mismatch")).element(snippet);
// Use termcolor to emit ANSI escapes
let mut writer = StandardStream::stderr(ColorChoice::Auto);
let renderer = Renderer::termcolor();
renderer.render_to(&[group], &mut writer)?;
Ok(())
}
Practical Tips:
- Enable
ColorChoice::Auto
to respectNO_COLOR
and terminal capabilities. - Adjust
line_start()
and spans to align annotations correctly. - Chain
AnnotationKind::Context
for supplementary notes.
Add Error Code Hyperlinks
Purpose: Tag diagnostics with an error code and clickable URL, enabling quick jumps to online docs.
use annotate_snippets::renderer::OutputTheme;
use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
fn main() {
let source = r#"
//@ compile-flags: -Zterminal-urls=yes
fn main() {
let () = 4; //~ ERROR
}
"#;
// Build an ERROR group with ID and hyperlink
let group = Group::with_title(
Level::ERROR
.title("mismatched types")
.id("E0308")
.id_url("https://doc.rust-lang.org/error_codes/E0308.html"),
)
.element(
Snippet::source(source)
.path("examples/id_hyperlink.rs")
.line_start(1)
.annotation(
AnnotationKind::Primary
.span(59..61)
.label("expected integer, found `()`"),
)
.annotation(
AnnotationKind::Context
.span(64..65)
.label("this expression has type `{integer}`"),
),
);
let renderer = Renderer::styled().theme(OutputTheme::Unicode);
println!("{}", renderer.render(&[group]));
}
Practical Tips:
- In Rust tests, add
// compile-flags: -Zterminal-urls=yes
to enable OSC 8 links. - Pair
.id()
with.id_url()
to generate clickable codes in supporting terminals. - Use
OutputTheme::Unicode
or any theme emitting OSC 8 sequences.
Display Diagnostics from Multiple Files
Purpose: Combine snippets from different source files to present a unified diagnostic spanning multiple contexts.
use annotate_snippets::renderer::OutputTheme;
use annotate_snippets::{AnnotationKind, Group, Level, Renderer, Snippet};
fn main() {
let src1 = r#"pub fn add(a: i32, b: i32) -> i32 { a + b }"#;
let src2 = r#"let sum: u32 = add(1, -1);"#;
let snippet1 = Snippet::source(src1)
.path("src/lib.rs")
.line_start(1)
.annotation(
AnnotationKind::Secondary
.span(19..24)
.label("parameter `b` is `i32`"),
);
let snippet2 = Snippet::source(src2)
.path("src/main.rs")
.line_start(1)
.annotation(
AnnotationKind::Primary
.span(13..28)
.label("mismatched types: expected `u32`, found `i32`"),
);
let group = Group::with_title(Level::ERROR.title("mismatched types"))
.element(snippet1)
.element(snippet2);
let renderer = Renderer::styled().theme(OutputTheme::Unicode);
println!("{}", renderer.render(&[group]));
}
Practical Tips:
- Use
.line_start()
to align line numbers per file. - Chain
.element()
for each file snippet in the same diagnostic. - Choose
Primary
for the main error andSecondary
orContext
for supporting notes.
API Reference Map
This section maps core public items to their rustdoc locations and outlines how autogenerated docs are organized. Use these links to jump into detailed API docs without duplicating content here.
Top-Level Exports (src/lib.rs)
crate::normalize_untrusted_str
Location: functions indexcrate::Level
Re-export fromsrc/level.rs
crate::Renderer
Re-export fromsrc/renderer/mod.rs
crate::snippet
Module root for all snippet-related types (src/snippet.rs
)
Severity Levels (src/level.rs)
Module path: crate::Level
Rustdoc: level/index.html
Public items:
- Constants:
ERROR
,WARNING
,INFO
,NOTE
,HELP
- Constructors:
Level::new(name: &str)
- Visibility control:
hide()
,show()
- Label and style:
set_label()
,set_style()
,label() -> &str
,style() -> Style
Renderer (src/renderer/mod.rs)
Module path: crate::renderer
Rustdoc: renderer/index.html
Key items:
struct Renderer
Methods:
•Renderer::default()
,with_color(true/false)
•with_config(Config)
,build(&Snippet) -> String
- Configuration:
Config
,Theme
,Style
,Color
,AnnotationType
- Helpers for plain vs. ANSI-colored output
- Source snippets with annotations, multiline spans, and suggestions
Snippet Data Structures (src/snippet.rs)
Module path: crate::snippet
Rustdoc: snippet/index.html
Public types:
struct Snippet
– top-level container (title, slices, footer, options)struct Group
– groups multiple snippets under one headerstruct Element
– atomic piece within a snippet (text or annotation)struct Annotation
– labeled span within sourceenum AnnotationType
–Error
,Warning
,Info
,Note
,Help
struct Patch
– suggested edit with replacement textstruct SourceAnnotation
– bindsAnnotation
to a file slice
Each of these items appears under the crate::snippet
module in the autogenerated docs. Navigate there for struct field details, method signatures, and usage examples.
Testing & Quality Assurance
This section shows how to run the full CI suite locally, work with snapshot tests, and add your own tests to maintain code quality.
Running the Full CI Suite Locally
Prerequisites
Install the tooling used in CI:
cargo install cargo-audit cargo-hack cargo-deny cargo-tarpaulin
rustup component add rustfmt clippy rust-src
Step-by-step Commands
Run formatting, linting, builds, tests, docs, audits and coverage in the same order CI does:
# Enforce Rust style
cargo fmt --all -- --check
# Lint & deny warnings
cargo clippy --all -- -D warnings
# Test every feature combination
cargo hack test --each-feature --workspace
# Build docs without warnings
RUSTDOCFLAGS="-D warnings" cargo doc --all-features --document-private-items --no-deps
# Enforce dependency policies
cargo deny check bans licenses sources
# Audit security advisories
cargo audit
# Generate coverage report (lcov + console summary)
cargo tarpaulin --out Lcov --ignore-tests
Tip: Wrap these in a shell script (e.g. ci-local.sh
) and run with bash ci-local.sh
to mimic CI.
Snapshot Tests
We use the insta
crate to lock expected diagnostic output. Snapshots live under snapshots/
alongside your test files.
Running & Updating Snapshots
- Run all tests (including snapshots):
cargo test
- Update snapshots after intentional changes:
INSTA_UPDATE=1 cargo test
Writing a New Snapshot Test
Create an integration test under tests/
, import insta
and the renderer:
// tests/snapshots.rs
use annotate_snippets::{AnnotationKind, Group, Level, Snippet, Renderer};
use insta::assert_snapshot;
#[test]
fn error_with_unicode_width() {
let source = "こんにちは、世界";
let snippet = Snippet::source(source)
.path("<file>")
.annotation(AnnotationKind::Primary.span(18..24).label("world"));
let group = Group::with_title(Level::ERROR.title("Unicode"));
let rendered = Renderer::plain().render(&[group.element(snippet)]);
assert_snapshot!(rendered);
}
assert_snapshot!
writes a file undersnapshots/tests__snapshots_rs/error_with_unicode_width.snap
.- Run
INSTA_UPDATE=1 cargo test
to accept changes.
Adding Unit & Integration Tests
Unit Tests in src/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snippet_roundtrip() {
let snippet = Snippet::source("foo").annotation(AnnotationKind::Primary.span(0..3));
let plain = Renderer::plain().render(&[snippet.into_group()]);
assert!(plain.contains("foo"));
}
}
New Integration Test
- Place under
tests/
with.rs
extension. - Use public APIs to drive behavior.
- Use
assert_eq!
,assert!
orinsta
for golden outputs.
Tips for CI Parity
- Match the toolchain version:
rustup override set stable
. - Export
CARGO_TERM_COLOR=always CLICOLOR=1 RUST_BACKTRACE=1
to replicate CI logs. - Use
cargo hack
to verify minimal supported Rust version:cargo hack check --rust-version --locked --ignore-private
- Keep your local lockfile in sync:
cargo update --locked
.
By following these steps, you’ll ensure local runs match GitHub Actions CI, maintain snapshot accuracy, and add reliable tests to annotate-snippets-rs
.
Contributing Guide
This guide walks you through setting up your local environment, enforcing code quality, and following commit conventions for annotate-snippets-rs.
1. Developer Setup
- Install Rust (matching or exceeding the MSRV in
Cargo.toml
):rustup toolchain install stable rustup default stable
- Install Python and
pre-commit
:pip install pre-commit
- Clone and enter the repo:
git clone https://github.com/rust-lang/annotate-snippets-rs.git cd annotate-snippets-rs
2. Rust Lint Overrides in Tests (.clippy.toml
)
Clippy enforces strict lints in production code but relaxes common patterns in tests:
# .clippy.toml
allow-print-in-tests = true # println!() in #[test]
allow-expect-in-tests = true # assert!()/assert_eq!() in #[test]
allow-unwrap-in-tests = true # .unwrap() in #[test]
allow-dbg-in-tests = true # dbg!() in #[test]
Place .clippy.toml
at the repo root. Clippy ignores these lints inside #[test]
functions only.
3. Pre-commit Hooks (.pre-commit-config.yaml
)
Automate file validation and commit‐message checks on every commit:
# .pre-commit-config.yaml
default_install_hook_types: ["pre-commit", "commit-msg"]
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-yaml
- id: check-json
- id: check-toml
- id: check-merge-conflict
- id: check-case-conflict
- id: detect-private-key
- repo: https://github.com/crate-ci/typos
rev: v1.32.0
hooks:
- id: typos
- repo: https://github.com/crate-ci/committed
rev: v1.1.7
hooks:
- id: committed # enforces commit-msg style
Install and activate hooks:
pre-commit install # enables pre-commit checks
pre-commit install --hook-type commit-msg
Run all checks manually (e.g., in CI):
pre-commit run --all-files
4. Commit Message Conventions
Local Enforcement (committed.toml
)
Use Conventional Commits, ignore bot authors, and disallow merge commits:
# committed.toml
style = "conventional"
ignore_author_re = "(dependabot|renovate)"
merge_commit = false
Keep committed.toml
in the repo root. The committed
hook (via pre-commit) will reject non-conforming messages.
CI Enforcement (.github/workflows/committed.yml
)
GitHub Actions runs committed
on each pull request:
# .github/workflows/committed.yml
name: Commit Message Lint
on: [pull_request]
jobs:
committed:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Lint commit messages
uses: crate-ci/committed@v1
with:
config: committed.toml
A failed action indicates one or more commit messages need adjustment.
5. Workflow Summary
- Create a feature branch:
git checkout -b feat/your-feature
- Make changes, run tests, and format code:
cargo fmt -- --check cargo clippy --all-targets -- -D warnings cargo test
- Stage and commit with a Conventional Commit message:
git commit -m "feat: add XYZ annotation helper"
- Push and open a PR. CI will run formatting, linting, tests, and commit-message checks.
To bypass hooks locally (discouraged):
git commit --no-verify
Thank you for contributing!
Release & Versioning
Annotate-snippets-rs follows Semantic Versioning and uses a structured changelog to communicate updates. Releases occur via the master
branch and the release.toml
configuration.
Semantic Versioning Guarantees
- MAJOR version when you make incompatible API changes.
- MINOR version when you add functionality in a backward-compatible manner.
- PATCH version when you make backward-compatible bug fixes.
Commit messages and PR titles should clearly signal the impact level. Breaking changes must appear under a “Breaking Changes” heading in the changelog.
Reading and Updating CHANGELOG.md
We adopt the Keep a Changelog format. Each release entry looks like:
## [1.3.0] - 2025-08-01
### Added
- `display::FancyFormatter`: supports colored output [#250]
### Fixed
- Off-by-one in multiline annotations [#248]
At the bottom you’ll find compare links:
[Unreleased]: https://github.com/rust-lang/annotate-snippets-rs/compare/1.3.0...HEAD
[1.3.0]: https://github.com/rust-lang/annotate-snippets-rs/compare/1.2.1...1.3.0
To add a new release:
- Copy the
## [Unreleased]
header, rename to## [X.Y.Z] – YYYY-MM-DD
. - Move your entries under the new header.
- Update the bottom links:
- Change the old
Unreleased
to your new version’s compare. - Add a fresh
Unreleased
section above.
- Change the old
Cutting a Release with cargo-release
We use cargo-release driven by release.toml
. This automates version bumps, tagging, changelog prep, and publishing.
- Ensure your PR is merged into
master
. - Run one of:
# For a new patch release cargo release patch --config release.toml # For a minor release cargo release minor --config release.toml # For a major release cargo release major --config release.toml
- cargo-release will:
- Bump
version
in Cargo.toml - Move changelog entries under the new header
- Create a Git tag (
vX.Y.Z
) - Push commits and tags
- Publish to crates.io
- Bump
Configuring release.toml
The default release.toml
enforces branch and dependency policy:
# release.toml
branch = "master"
tag-name = "v{{version}}"
sign-tags = true
# Keep internal dependencies locked on release
dependency-version = "fix"
Fields to adjust as needed:
branch
: restricts where releases occur.dependency-version
:"fix"
pins to exact versions."caret"
uses^
semver ranges.