Chat about this codebase

AI-powered code exploration

Online

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 and Annotation
  • 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 Annotations 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 Groups 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 to Light or NoColor 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 respect NO_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 and Secondary or Context 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 index
  • crate::Level
    Re-export from src/level.rs
  • crate::Renderer
    Re-export from src/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 header
  • struct Element – atomic piece within a snippet (text or annotation)
  • struct Annotation – labeled span within source
  • enum AnnotationTypeError, Warning, Info, Note, Help
  • struct Patch – suggested edit with replacement text
  • struct SourceAnnotation – binds Annotation 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 under snapshots/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! or insta 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

  1. Install Rust (matching or exceeding the MSRV in Cargo.toml):
    rustup toolchain install stable
    rustup default stable
    
  2. Install Python and pre-commit:
    pip install pre-commit
    
  3. 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

  1. Create a feature branch:
    git checkout -b feat/your-feature
  2. Make changes, run tests, and format code:
    cargo fmt -- --check
    cargo clippy --all-targets -- -D warnings
    cargo test
    
  3. Stage and commit with a Conventional Commit message:
    git commit -m "feat: add XYZ annotation helper"
  4. 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:

  1. Copy the ## [Unreleased] header, rename to ## [X.Y.Z] – YYYY-MM-DD.
  2. Move your entries under the new header.
  3. Update the bottom links:
    • Change the old Unreleased to your new version’s compare.
    • Add a fresh Unreleased section above.

Cutting a Release with cargo-release

We use cargo-release driven by release.toml. This automates version bumps, tagging, changelog prep, and publishing.

  1. Ensure your PR is merged into master.
  2. 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
    
  3. 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

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.