diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 4fafac3..233b6b4 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -10,14 +10,18 @@ jobs:
name: Check fmt & build docs
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v4
- name: Install Rust
- uses: actions-rs/toolchain@v1
+ uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: stable
components: rustfmt
- override: true
+ - name: Install cargo-readme
+ run: cargo install cargo-readme
+ - name: Check doc consistency
+ shell: bash
+ run: diff -q README.md <(cargo readme)
+ || { echo "::error::Update lib.rs then use cargo-readme to update README.md"; exit 1; }
- name: rustfmt
run: cargo fmt --all -- --check
- name: docs
@@ -28,40 +32,53 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- rust: [1.56.0, stable]
+ features: [fancy, syntect-highlighter]
+ rust: [1.70.0, stable]
os: [ubuntu-latest, macOS-latest, windows-latest]
+ exclude:
+ - features: syntect-highlighter
+ rust: 1.70.0
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v4
- name: Install Rust
- uses: actions-rs/toolchain@v1
+ uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: ${{ matrix.rust }}
components: clippy
- override: true
- name: Clippy
run: cargo clippy --all -- -D warnings
- name: Run tests
if: matrix.rust == 'stable'
- run: cargo test --all --verbose --features fancy
+ run: cargo test --all --verbose --features ${{matrix.features}}
- name: Run tests
- if: matrix.rust == '1.56.0'
- run: cargo test --all --verbose --features fancy no-format-args-capture
+ if: matrix.rust == '1.70.0'
+ run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture
+
+ wasm:
+ name: Check Wasm build
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install Rust
+ uses: dtolnay/rust-toolchain@master
+ with:
+ toolchain: stable
+ targets: wasm32-unknown-unknown
+ - name: Check wasm target
+ run: cargo check --target wasm32-unknown-unknown --features fancy-no-syscall
miri:
name: Miri
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v4
- name: Install Rust
- uses: actions-rs/toolchain@v1
+ uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: nightly
components: miri,rust-src
- override: true
- name: Run tests with miri
env:
MIRIFLAGS: -Zmiri-disable-isolation -Zmiri-strict-provenance
@@ -75,13 +92,10 @@ jobs:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- - uses: actions/checkout@v1
+ - uses: actions/checkout@v4
- name: Install Rust
- uses: actions-rs/toolchain@v1
+ uses: dtolnay/rust-toolchain@master
with:
- profile: minimal
toolchain: nightly
- override: true
- name: Run minimal version build
- run: cargo build -Z minimal-versions --all-features
-
+ run: cargo build -Z direct-minimal-versions --features fancy,no-format-args-capture
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0b842de..b179baa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,109 @@
# `miette` Release Changelog
+
+## 7.2.0 (2024-03-07)
+
+### Features
+
+* **wasm:** add feature "fancy-no-syscall" for wasm targets (#349) ([328bf379](https://github.com/zkat/miette/commit/328bf3792213fc0bed94e72a39acb722b65141dd))
+
+### Bug Fixes
+
+* **label-collections:** Label collection fixes and cleanup (#343) ([75fea093](https://github.com/zkat/miette/commit/75fea0935e495d0215518c80d32dd820910982e3))
+* **invalid span:** skip the snippet when read_span fails (#347) ([7d9dfc6e](https://github.com/zkat/miette/commit/7d9dfc6e8e591f9606c3da55bd8465962358b20f))
+* **redundant-import:** fix a warning and CI failure in nightly (#348) ([6ea86a22](https://github.com/zkat/miette/commit/6ea86a2248854acf88df345814b6c97d31b8b4d9))
+
+
+## 7.1.0 (2024-02-16)
+
+### Features
+
+* **derive:** enable more boxed types to be #[diagnostic_source] (#338) ([c2f06f6c](https://github.com/zkat/miette/commit/c2f06f6cca15cbdd083dbff3d46b7729056ac6a4))
+* **source:** derive common traits for NamedSource, SourceSpan, and SourceOffset (#340) ([6f09250c](https://github.com/zkat/miette/commit/6f09250cca14561f07fba899a8e6d3c0df14230e))
+* **collection:** add support for collection of labels (#341) ([03060245](https://github.com/zkat/miette/commit/03060245d816a53a33209e6b7e1c3c42948e9962))
+
+### Bug Fixes
+
+* **tests:** revert test-breaking changes of e5c7ae4 (#339) ([6e829f8c](https://github.com/zkat/miette/commit/6e829f8c0ce2fc7bb2fc4041e6a6072f12db1f71))
+
+
+## 7.0.0 (2024-02-05)
+
+This is a small breaking release on the heels of 6.0 because I neglected to
+bump owo-colors. I figured it's a good time to do it, before 6.0 gets more
+widely used.
+
+### Features
+
+* **fancy:** Add option to change the link display text (#335) ([c7144ee5](https://github.com/zkat/miette/commit/c7144ee513bf8f06c5f7d89c45436802994a51fc))
+* **deps:** bump dependencies ([a4011d17](https://github.com/zkat/miette/commit/a4011d174c40acbba5b0176db7cb71ec5ca0cb49))
+ * **BREAKING CHANGE**: This bumps owo-colors to 4.0, which is a breaking change because we expose its styles as part of the graphical renderer API
+
+
+## 6.0.1 (2024-02-04)
+
+### Bug Fixes
+
+* **graphical:** oops. Fix theme issue ([8b46679c](https://github.com/zkat/miette/commit/8b46679c3647e1455d91b4c68743c619fb3f3eb3))
+* **fmt:** remove nightly-only fmt flags ([1fa7f524](https://github.com/zkat/miette/commit/1fa7f5241fb91d2e5bad9b0e26bcc7cd5f9011f1))
+* **highlighter:** ugh, missed another spot ([ab7c066e](https://github.com/zkat/miette/commit/ab7c066e7675d8c7ecb956000d278fc31f3bc6a1))
+
+
+## 6.0.0 (2024-02-04)
+
+The long-awaited 6.0 release of `miette` is here, with TONS of goodies, not
+least of which is syntax highlighting support!
+
+It also comes with a few breaking changes so make sure to check below and
+update your code as needed!
+
+### Features
+
+* **labels:** Add support for primary label in specifying line/col information (#291) ([db0b7e40](https://github.com/zkat/miette/commit/db0b7e403a5ae52ae360991b6508490d8c579886))
+* **derive:** Allow optional sources in derive (#301) ([88d00e0e](https://github.com/zkat/miette/commit/88d00e0e20bf95e03b8f81dcd5adf38c917e190e))
+* **derive:** Make `miette-derive` be able to be turned off (#304) ([c7ba5b7e](https://github.com/zkat/miette/commit/c7ba5b7e52e05991cecd3ca925c710bbe49850b9))
+* **graphical:** Expose additional `textwrap` options (#321) ([fd77257c](https://github.com/zkat/miette/commit/fd77257cee0f5d03aa7dccb4ba8cbaa40c1a88c6))
+* **graphical:** support rendering labels that contain newlines (#318) ([865d67c8](https://github.com/zkat/miette/commit/865d67c8dda119ddd03ac43be22f4fa272a9f433))
+* **graphical:** Add `wrap_lines: bool` option allowing wrapping be disabled entirely (#328) ([b0744462](https://github.com/zkat/miette/commit/b0744462adbbfbb6d845f382db36be883c7f3c45))
+* **graphical:** render disjoint snippets separately for cleaner output (#324) ([19c22143](https://github.com/zkat/miette/commit/19c22143cb544616046784e35c5e78cc5b881289))
+* **deps:** Bump terminal-size to v0.3.0 (#308) ([c0a298e5](https://github.com/zkat/miette/commit/c0a298e5a8d699acf9fcd61b5d5fa4f6279a47ab))
+ * **BREAKING CHANGE**: This requires an MSRV bump to 1.70.0.
+* **source-code:** Don't override provided source code (#300) ([0d5c2ce7](https://github.com/zkat/miette/commit/0d5c2ce7536b0ea205346595d8a00d00bfb6cbd2))
+ * **BREAKING CHANGE**: Source code is no longer overridden if it was provided by the diagnostic's own `source_code()` impl.
+* **source:** use `usize` for length (#265) ([fad0e76a](https://github.com/zkat/miette/commit/fad0e76ad2e19d5cac13cf8324338aca0d623d93))
+ * **BREAKING CHANGE**: This changes `SourceSpan`'s length type to `usize`.
+* **source:** Allow inner source type of a NamedSource to be borrowed (#254) ([1df3b1a5](https://github.com/zkat/miette/commit/1df3b1a537f2e54cd40ec45f5cd851337a22e95a))
+ * **BREAKING CHANGE**: This makes the `NamedSource` type generic over its `Source` type, instead of boxing it.
+* **highlighting:** add syntax highlighting support with syntect crate (#313) ([e65d0a78](https://github.com/zkat/miette/commit/e65d0a78cc639653f061a45d8ce35b1a3551ade7))
+* **deps:** remove is-terminal dep in favor of `std::io::IsTerminal` ([e5c7ae46](https://github.com/zkat/miette/commit/e5c7ae469e40a8bc102e1fca3b8fd4b2ec137696))
+* **deps:** remove once_cell dep in favor of `std::sync::OnceLock` ([4c48584f](https://github.com/zkat/miette/commit/4c48584f304414c6924bede3308b455cfef60749))
+ * **BREAKING CHANGE**: This requires an MSRV bump to 1.70.0.
+* **deps:** bump some semver-breaking deps to newer versions ([29d000f2](https://github.com/zkat/miette/commit/29d000f201b259a056867a2876384f97653a6e9e))
+* **MSRV:** Actually bump the MSRV to 1.70.0 ([ab59a7bc](https://github.com/zkat/miette/commit/ab59a7bc9bceace5761a862ee2ebff3e5943b12f))
+
+### Bug Fixes
+
+* **misc:** Improve ci and fix clippy (#290) ([cc81382a](https://github.com/zkat/miette/commit/cc81382a6070dd226a20e4a39518d88e957ac0e1))
+* **tests:** Fix `cargo test` with default features. (#294) ([1f448e47](https://github.com/zkat/miette/commit/1f448e47751d0f914134b0e9138fdb1a5a95d55c))
+* **clippy:** Add missing semicolons where nothing is returned. (#293) ([06b34823](https://github.com/zkat/miette/commit/06b348230aaf153b8b050322f05e5d185351d2d1))
+* **graphical:** Extend error text span to whole code points (#312) ([a8b4ae01](https://github.com/zkat/miette/commit/a8b4ae012aa0cf03b53a18f013c2b3f76c5040e7))
+* **formatting:** Fix formatting bug when an empty span is not aligned to a char boundary (#314) ([3d6f903d](https://github.com/zkat/miette/commit/3d6f903df0e7c9d0eb9a1fdbbf0028bab5496429))
+* **docs:** add example to README and docs fixing #96 (#319) ([251d6d59](https://github.com/zkat/miette/commit/251d6d59292397458328ef57fb7957faedafd019))
+* **graphical:** rendering bug on small spans in large spans (#316) ([7ff4f874](https://github.com/zkat/miette/commit/7ff4f874d693a665af4df40f4e94505013e3e262))
+* **graphical:** render cause chains for inner errors (#330) ([cb2ae2e1](https://github.com/zkat/miette/commit/cb2ae2e18b446a5e90885faf8a30b5672c307df8))
+* **handler:** remove the two extra `is_terminal` sys call from `MietteHandlerOpts::build` (#325) ([f1dc89c0](https://github.com/zkat/miette/commit/f1dc89c07640445d224b61ef96c6b25fcdf62dee))
+
+### Documentation
+
+* **README:** Move import of `NamedResult` to where it is used (#309) ([d37ada87](https://github.com/zkat/miette/commit/d37ada876a5831d3f47622274e334c9a24aa5d2b))
+
+
+## 5.10.0 (2023-07-16)
+
+### Features
+
+* **protocol:** add StdError impl for Box (#273) ([2e3e5c9d](https://github.com/zkat/miette/commit/2e3e5c9d15e234495369e9b47d032644dd5664ad))
+
## 5.9.0 (2023-05-18)
diff --git a/Cargo.toml b/Cargo.toml
index 160556f..057bd9e 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "miette"
-version = "5.9.0"
+version = "7.2.0"
authors = ["Kat Marchán "]
description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers."
categories = ["rust-patterns"]
@@ -9,53 +9,61 @@ documentation = "https://docs.rs/miette"
license = "Apache-2.0"
readme = "README.md"
edition = "2018"
-rust-version = "1.56.0"
+rust-version = "1.70.0"
exclude = ["images/", "tests/", "miette-derive/"]
[dependencies]
-thiserror = "1.0.40"
-miette-derive = { path = "miette-derive", version = "=5.9.0" }
-once_cell = "1.8.0"
-unicode-width = "0.1.9"
+thiserror = "1.0.56"
+miette-derive = { path = "miette-derive", version = "=7.2.0", optional = true }
+unicode-width = "0.1.11"
+cfg-if = "1.0.0"
-owo-colors = { version = "3.0.0", optional = true }
-is-terminal = { version = "0.4.0", optional = true }
-textwrap = { version = "0.15.0", optional = true }
-supports-hyperlinks = { version = "2.0.0", optional = true }
-supports-color = { version = "2.0.0", optional = true }
-supports-unicode = { version = "2.0.0", optional = true }
-backtrace = { version = "0.3.61", optional = true }
-terminal_size = { version = "0.1.17", optional = true }
+owo-colors = { version = "4.0.0", optional = true }
+textwrap = { version = "0.16.0", optional = true }
+supports-hyperlinks = { version = "3.0.0", optional = true }
+supports-color = { version = "3.0.0", optional = true }
+supports-unicode = { version = "3.0.0", optional = true }
+backtrace = { version = "0.3.69", optional = true }
+terminal_size = { version = "0.3.0", optional = true }
backtrace-ext = { version = "0.2.1", optional = true }
-serde = { version = "1.0.162", features = ["derive"], optional = true }
+serde = { version = "1.0.196", features = ["derive"], optional = true }
+syntect = { version = "5.1.0", optional = true }
[dev-dependencies]
-semver = "1.0.4"
+semver = "1.0.21"
# Eyre devdeps
futures = { version = "0.3", default-features = false }
-indenter = "0.3.0"
+indenter = "0.3.3"
rustversion = "1.0"
-trybuild = { version = "1.0.19", features = ["diff"] }
-syn = { version = "2.0", features = ["full"] }
-regex = "1.5"
+trybuild = { version = "1.0.89", features = ["diff"] }
+syn = { version = "2.0.48", features = ["full"] }
+regex = "1.10"
lazy_static = "1.4"
-serde_json = "1.0.64"
+serde_json = "1.0.113"
+strip-ansi-escapes = "0.2.0"
[features]
-default = []
+default = ["derive"]
+derive = ["miette-derive"]
no-format-args-capture = []
-fancy-no-backtrace = [
+fancy-base = [
"owo-colors",
- "is-terminal",
"textwrap",
+]
+fancy-no-syscall = [
+ "fancy-base",
+]
+fancy-no-backtrace = [
+ "fancy-base",
"terminal_size",
"supports-hyperlinks",
"supports-color",
"supports-unicode",
]
fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
+syntect-highlighter = ["fancy-no-backtrace", "syntect"]
[workspace]
members = ["miette-derive"]
diff --git a/README.md b/README.md
index f49dd69..ec74050 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
You run miette? You run her code like the software? Oh. Oh! Error code for
coder! Error code for One Thousand Lines!
-## About
+### About
`miette` is a diagnostic library for Rust. It includes a series of
traits/protocols that allow you to hook into its error reporting facilities,
@@ -13,18 +13,20 @@ can print out like this (or in any format you like!):
> **NOTE: You must enable the `"fancy"` crate feature to get fancy report
@@ -32,7 +34,7 @@ output like in the screenshots above.** You should only do this in your
toplevel crate, as the fancy feature pulls in a number of dependencies that
libraries and such might not want.
-## Table of Contents
+### Table of Contents
- [About](#about)
- [Features](#features)
@@ -44,14 +46,18 @@ libraries and such might not want.
- [... in `main()`](#-in-main)
- [... diagnostic code URLs](#-diagnostic-code-urls)
- [... snippets](#-snippets)
+ - [... help text](#-help-text)
+ - [... severity level](#-severity-level)
- [... multiple related errors](#-multiple-related-errors)
- [... delayed source code](#-delayed-source-code)
- [... handler options](#-handler-options)
- [... dynamic diagnostics](#-dynamic-diagnostics)
+ - [... syntax highlighting](#-syntax-highlighting)
+ - [... collection of labels](#-collection-of-labels)
- [Acknowledgements](#acknowledgements)
- [License](#license)
-## Features
+### Features
- Generic [`Diagnostic`] protocol, compatible (and dependent on)
[`std::error::Error`].
@@ -76,7 +82,7 @@ the following features:
- Cause chain printing
- Turns diagnostic codes into links in [supported terminals](https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda).
-## Installing
+### Installing
```sh
$ cargo add miette
@@ -88,7 +94,7 @@ If you want to use the fancy printer in all these screenshots:
$ cargo add miette --features fancy
```
-## Example
+### Example
```rust
/*
@@ -110,7 +116,7 @@ struct MyBad {
// The Source that we're gonna be printing snippets out of.
// This can be a String if you don't have or care about file names.
#[source_code]
- src: NamedSource,
+ src: NamedSource,
// Snippets and highlights can be included in the diagnostic!
#[label("This bit here")]
bad_bit: SourceSpan,
@@ -174,9 +180,9 @@ snippet line 3: here
diagnostic help: try doing it better next time?
">
-## Using
+### Using
-### ... in libraries
+#### ... in libraries
`miette` is _fully compatible_ with library usage. Consumers who don't know
about, or don't want, `miette` features can safely use its error types as
@@ -191,7 +197,7 @@ the trait directly, just like with `std::error::Error`.
```rust
// lib/error.rs
-use miette::Diagnostic;
+use miette::{Diagnostic, SourceSpan};
use thiserror::Error;
#[derive(Error, Diagnostic, Debug)]
@@ -203,6 +209,18 @@ pub enum MyLibError {
#[error("Oops it blew up")]
#[diagnostic(code(my_lib::bad_code))]
BadThingHappened,
+
+ #[error(transparent)]
+ // Use `#[diagnostic(transparent)]` to wrap another [`Diagnostic`]. You won't see labels otherwise
+ #[diagnostic(transparent)]
+ AnotherError(#[from] AnotherError),
+}
+
+#[derive(Error, Diagnostic, Debug)]
+#[error("another error")]
+pub struct AnotherError {
+ #[label("here")]
+ pub at: SourceSpan
}
```
@@ -210,7 +228,7 @@ Then, return this error type from all your fallible public APIs. It's a best
practice to wrap any "external" error types in your error `enum` instead of
using something like [`Report`] in a library.
-### ... in application code
+#### ... in application code
Application code tends to work a little differently than libraries. You
don't always need or care to define dedicated error wrappers for errors
@@ -252,8 +270,7 @@ pub fn some_tool() -> Result {
}
```
-To construct your own simple adhoc error use the [`miette!`] macro:
-
+To construct your own simple adhoc error use the [miette!] macro:
```rust
// my_app/lib/my_internal_file.rs
use miette::{miette, IntoDiagnostic, Result, WrapErr};
@@ -266,8 +283,9 @@ pub fn some_tool() -> Result {
.map_err(|_| miette!("Invalid version {}", version))?)
}
```
+There are also similar [bail!] and [ensure!] macros.
-### ... in `main()`
+#### ... in `main()`
`main()` is just like any other part of your application-internal code. Use
`Result` as your return value, and it will pretty-print your diagnostics
@@ -297,7 +315,24 @@ enabled:
miette = { version = "X.Y.Z", features = ["fancy"] }
```
-### ... diagnostic code URLs
+Another way to display a diagnostic is by printing them using the debug formatter.
+This is, in fact, what returning diagnostics from main ends up doing.
+To do it yourself, you can write the following:
+
+```rust
+use miette::{IntoDiagnostic, Result};
+use semver::Version;
+
+fn just_a_random_function() {
+ let version_result: Result = "1.2.x".parse().into_diagnostic();
+ match version_result {
+ Err(e) => println!("{:?}", e),
+ Ok(version) => println!("{}", version),
+ }
+}
+```
+
+#### ... diagnostic code URLs
`miette` supports providing a URL for individual diagnostics. This URL will
be displayed as an actual link in supported terminals, like so:
@@ -350,7 +385,7 @@ use thiserror::Error;
struct MyErr;
```
-### ... snippets
+#### ... snippets
Along with its general error handling and reporting features, `miette` also
includes facilities for adding error spans/annotations/labels to your
@@ -434,7 +469,20 @@ let err = Foo {
};
```
-### ... multiple related errors
+#### ... severity level
+`miette` provides a way to set the severity level of a diagnostic.
+
+```rust
+use miette::Diagnostic;
+use thiserror::Error;
+
+#[derive(Debug, Diagnostic, Error)]
+#[error("welp")]
+#[diagnostic(severity(Warning))]
+struct Foo;
+```
+
+#### ... multiple related errors
`miette` supports collecting multiple errors into a single diagnostic, and
printing them all together nicely.
@@ -454,7 +502,7 @@ struct MyError {
}
```
-### ... delayed source code
+#### ... delayed source code
Sometimes it makes sense to add source code to the error message later.
One option is to use [`with_source_code()`](Report::with_source_code)
@@ -537,7 +585,7 @@ fn main() -> miette::Result<()> {
}
```
-### ... Diagnostic-based error sources.
+#### ... Diagnostic-based error sources.
When one uses the `#[source]` attribute on a field, that usually comes
from `thiserror`, and implements a method for
@@ -570,7 +618,7 @@ struct MyError {
struct OtherError;
```
-### ... handler options
+#### ... handler options
[`MietteHandler`] is the default handler, and is very customizable. In
most cases, you can simply use [`MietteHandlerOpts`] to tweak its behavior
@@ -586,15 +634,17 @@ miette::set_hook(Box::new(|_| {
.unicode(false)
.context_lines(3)
.tab_width(4)
+ .break_words(true)
.build(),
)
}))
+
```
See the docs for [`MietteHandlerOpts`] for more details on what you can
customize!
-### ... dynamic diagnostics
+#### ... dynamic diagnostics
If you...
- ...don't know all the possible errors upfront
@@ -603,9 +653,10 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or
[`MietteDiagnostic`] directly to create diagnostic on the fly.
```rust
+
let source = "2 + 2 * 2 = 8".to_string();
let report = miette!(
- labels = vec[
+ labels = vec![
LabeledSpan::at(12..13, "this should be 6"),
],
help = "'*' has greater precedence than '+'",
@@ -614,26 +665,108 @@ let report = miette!(
println!("{:?}", report)
```
-## Acknowledgements
+#### ... syntax highlighting
+
+`miette` can be configured to highlight syntax in source code snippets.
+
+
+
+To use the built-in highlighting functionality, you must enable the
+`syntect-highlighter` crate feature. When this feature is enabled, `miette` will
+automatically use the [`syntect`] crate to highlight the `#[source_code]`
+field of your [`Diagnostic`].
+
+Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
+* [language()](SpanContents::language) - Provides the name of the language
+ as a string. For example `"Rust"` will indicate Rust syntax highlighting.
+ You can set the language of the [`SpanContents`] produced by a
+ [`NamedSource`] via the [`with_language`](NamedSource::with_language)
+ method.
+* [name()](SpanContents::name) - In the absence of an explicitly set
+ language, the name is assumed to contain a file name or file path.
+ The highlighter will check for a file extension at the end of the name and
+ try to guess the syntax from that.
+
+If you want to use a custom highlighter, you can provide a custom
+implementation of the [`Highlighter`](highlighters::Highlighter)
+trait to [`MietteHandlerOpts`] by calling the
+[`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
+method. See the [`highlighters`] module docs for more details.
+
+#### ... collection of labels
+
+When the number of labels is unknown, you can use a collection of `SourceSpan`
+(or any type convertible into `SourceSpan`). For this, add the `collection`
+parameter to `label` and use any type than can be iterated over for the field.
+
+```rust
+#[derive(Debug, Diagnostic, Error)]
+#[error("oops!")]
+struct MyError {
+ #[label("main issue")]
+ primary_span: SourceSpan,
+
+ #[label(collection, "related to this")]
+ other_spans: Vec>,
+}
+
+let report: miette::Report = MyError {
+ primary_span: (6, 9).into(),
+ other_spans: vec![19..26, 30..41],
+}.into();
+
+println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
+```
+
+A collection can also be of `LabeledSpan` if you want to have different text
+for different labels. Labels with no text will use the one from the `label`
+attribute
+
+```rust
+#[derive(Debug, Diagnostic, Error)]
+#[error("oops!")]
+struct MyError {
+ #[label("main issue")]
+ primary_span: SourceSpan,
+
+ #[label(collection, "related to this")]
+ other_spans: Vec, // LabeledSpan
+}
+
+let report: miette::Report = MyError {
+ primary_span: (6, 9).into(),
+ other_spans: vec![
+ LabeledSpan::new(None, 19, 7), // Use default text `related to this`
+ LabeledSpan::new(Some("and also this".to_string()), 30, 11), // Use specific text
+ ],
+}.into();
+
+println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
+```
+
+### MSRV
+
+This crate requires rustc 1.70.0 or later.
+
+### Acknowledgements
`miette` was not developed in a void. It owes enormous credit to various
other projects and their authors:
-- [`anyhow`](http://crates.io/crates/anyhow) and
- [`color-eyre`](https://crates.io/crates/color-eyre): these two
- enormously influential error handling libraries have pushed forward the
- experience of application-level error handling and error reporting.
- `miette`'s `Report` type is an attempt at a very very rough version of
- their `Report` types.
-- [`thiserror`](https://crates.io/crates/thiserror) for setting the
- standard for library-level error definitions, and for being the
- inspiration behind `miette`'s derive macro.
+- [`anyhow`](http://crates.io/crates/anyhow) and [`color-eyre`](https://crates.io/crates/color-eyre):
+ these two enormously influential error handling libraries have pushed
+ forward the experience of application-level error handling and error
+ reporting. `miette`'s `Report` type is an attempt at a very very rough
+ version of their `Report` types.
+- [`thiserror`](https://crates.io/crates/thiserror) for setting the standard
+ for library-level error definitions, and for being the inspiration behind
+ `miette`'s derive macro.
- `rustc` and [@estebank](https://github.com/estebank) for their
state-of-the-art work in compiler diagnostics.
- [`ariadne`](https://crates.io/crates/ariadne) for pushing forward how
_pretty_ these diagnostics can really look!
-## License
+### License
`miette` is released to the Rust community under the [Apache license
2.0](./LICENSE).
@@ -652,7 +785,7 @@ under the Apache License. Some code is taken from
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
-[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
+[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
-[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
+[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html
diff --git a/README.tpl b/README.tpl
index d598eb8..f1126f1 100644
--- a/README.tpl
+++ b/README.tpl
@@ -12,7 +12,7 @@
[`MietteHandler`]: https://docs.rs/miette/latest/miette/struct.MietteHandler.html
[`MietteDiagnostic`]: https://docs.rs/miette/latest/miette/struct.MietteDiagnostic.html
[`Report`]: https://docs.rs/miette/latest/miette/struct.Report.html
-[`ReportHandler`]: https://docs.rs/miette/latest/miette/struct.ReportHandler.html
+[`ReportHandler`]: https://docs.rs/miette/latest/miette/trait.ReportHandler.html
[`Result`]: https://docs.rs/miette/latest/miette/type.Result.html
-[`SourceCode`]: https://docs.rs/miette/latest/miette/struct.SourceCode.html
+[`SourceCode`]: https://docs.rs/miette/latest/miette/trait.SourceCode.html
[`SourceSpan`]: https://docs.rs/miette/latest/miette/struct.SourceSpan.html
diff --git a/clippy.toml b/clippy.toml
index 0d369b5..1645c19 100644
--- a/clippy.toml
+++ b/clippy.toml
@@ -1 +1 @@
-msrv = "1.56.0"
+msrv = "1.70.0"
diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml
index 461a328..e8082a0 100644
--- a/miette-derive/Cargo.toml
+++ b/miette-derive/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "miette-derive"
-version = "5.9.0"
+version = "7.2.0"
authors = ["Kat Marchán "]
edition = "2018"
license = "Apache-2.0"
@@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette"
proc-macro = true
[dependencies]
-proc-macro2 = "1.0"
-quote = "1.0"
-syn = "2.0.11"
+proc-macro2 = "1.0.78"
+quote = "1.0.35"
+syn = "2.0.48"
diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs
index e0bc70a..ab2ceac 100644
--- a/miette-derive/src/label.rs
+++ b/miette-derive/src/label.rs
@@ -16,14 +16,23 @@ use crate::{
pub struct Labels(Vec);
+#[derive(PartialEq, Eq)]
+enum LabelType {
+ Default,
+ Primary,
+ Collection,
+}
+
struct Label {
label: Option,
ty: syn::Type,
span: syn::Member,
+ lbl_ty: LabelType,
}
struct LabelAttr {
label: Option,
+ lbl_ty: LabelType,
}
impl Parse for LabelAttr {
@@ -40,10 +49,26 @@ impl Parse for LabelAttr {
}
});
let la = input.lookahead1();
- let label = if la.peek(syn::token::Paren) {
- // #[label("{}", x)]
+ let (lbl_ty, label) = if la.peek(syn::token::Paren) {
+ // #[label(primary?, "{}", x)]
let content;
parenthesized!(content in input);
+
+ let attr = match content.parse::>()? {
+ Some(ident) if ident == "primary" => {
+ let _ = content.parse::();
+ LabelType::Primary
+ }
+ Some(ident) if ident == "collection" => {
+ let _ = content.parse::();
+ LabelType::Collection
+ }
+ Some(_) => {
+ return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
+ }
+ _ => LabelType::Default,
+ };
+
if content.peek(syn::LitStr) {
let fmt = content.parse()?;
let args = if content.is_empty() {
@@ -56,22 +81,27 @@ impl Parse for LabelAttr {
args,
has_bonus_display: false,
};
- Some(display)
+ (attr, Some(display))
+ } else if !content.is_empty() {
+ return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or either the keyword `primary` or `collection`."));
} else {
- return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string."));
+ (attr, None)
}
} else if la.peek(Token![=]) {
// #[label = "blabla"]
input.parse::()?;
- Some(Display {
- fmt: input.parse()?,
- args: TokenStream::new(),
- has_bonus_display: false,
- })
+ (
+ LabelType::Default,
+ Some(Display {
+ fmt: input.parse()?,
+ args: TokenStream::new(),
+ has_bonus_display: false,
+ }),
+ )
} else {
- None
+ (LabelType::Default, None)
};
- Ok(LabelAttr { label })
+ Ok(LabelAttr { label, lbl_ty })
}
}
@@ -100,12 +130,25 @@ impl Labels {
})
};
use quote::ToTokens;
- let LabelAttr { label } =
+ let LabelAttr { label, lbl_ty } =
syn::parse2::(attr.meta.to_token_stream())?;
+
+ if lbl_ty == LabelType::Primary
+ && labels
+ .iter()
+ .any(|l: &Label| l.lbl_ty == LabelType::Primary)
+ {
+ return Err(syn::Error::new(
+ field.span(),
+ "Cannot have more than one primary label.",
+ ));
+ }
+
labels.push(Label {
label,
span,
ty: field.ty.clone(),
+ lbl_ty,
});
}
}
@@ -119,36 +162,81 @@ impl Labels {
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option {
let (display_pat, display_members) = display_pat_members(fields);
- let labels = self.0.iter().map(|highlight| {
- let Label { span, label, ty } = highlight;
- let var = quote! { __miette_internal_var };
- if let Some(display) = label {
- let (fmt, args) = display.expand_shorthand_cloned(&display_members);
- quote! {
- miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
- .map(|#var| miette::LabeledSpan::new_with_span(
- std::option::Option::Some(format!(#fmt #args)),
- #var.clone(),
- ))
- }
- } else {
- quote! {
- miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
- .map(|#var| miette::LabeledSpan::new_with_span(
- std::option::Option::None,
- #var.clone(),
- ))
- }
+ let labels = self.0.iter().filter_map(|highlight| {
+ let Label {
+ span,
+ label,
+ ty,
+ lbl_ty,
+ } = highlight;
+ if *lbl_ty == LabelType::Collection {
+ return None;
}
+ let var = quote! { __miette_internal_var };
+ let display = if let Some(display) = label {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ quote! { std::option::Option::Some(format!(#fmt #args)) }
+ } else {
+ quote! { std::option::Option::None }
+ };
+ let ctor = if *lbl_ty == LabelType::Primary {
+ quote! { miette::LabeledSpan::new_primary_with_span }
+ } else {
+ quote! { miette::LabeledSpan::new_with_span }
+ };
+
+ Some(quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span)
+ .map(|#var| #ctor(
+ #display,
+ #var.clone(),
+ ))
+ })
});
+ let collections_chain = self.0.iter().filter_map(|label| {
+ let Label {
+ span,
+ label,
+ ty: _,
+ lbl_ty,
+ } = label;
+ if *lbl_ty != LabelType::Collection {
+ return None;
+ }
+ let display = if let Some(display) = label {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ quote! { std::option::Option::Some(format!(#fmt #args)) }
+ } else {
+ quote! { std::option::Option::None }
+ };
+ Some(quote! {
+ .chain({
+ let display = #display;
+ self.#span.iter().map(move |span| {
+ use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
+ let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
+ if display.is_some() && labeled_span.label().is_none() {
+ labeled_span.set_label(display.clone())
+ }
+ Some(labeled_span)
+ })
+ })
+ })
+ });
+
Some(quote! {
#[allow(unused_variables)]
fn labels(&self) -> std::option::Option + '_>> {
use miette::macro_helpers::ToOption;
let Self #display_pat = self;
- std::option::Option::Some(Box::new(vec![
+
+ let labels_iter = vec![
#(#labels),*
- ].into_iter().filter(Option::is_some).map(Option::unwrap)))
+ ]
+ .into_iter()
+ #(#collections_chain)*;
+
+ std::option::Option::Some(Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
}
})
}
@@ -160,8 +248,11 @@ impl Labels {
|ident, fields, DiagnosticConcreteArgs { labels, .. }| {
let (display_pat, display_members) = display_pat_members(fields);
labels.as_ref().and_then(|labels| {
- let variant_labels = labels.0.iter().map(|label| {
- let Label { span, label, ty } = label;
+ let variant_labels = labels.0.iter().filter_map(|label| {
+ let Label { span, label, ty, lbl_ty } = label;
+ if *lbl_ty == LabelType::Collection {
+ return None;
+ }
let field = match &span {
syn::Member::Named(ident) => ident.clone(),
syn::Member::Unnamed(syn::Index { index, .. }) => {
@@ -169,24 +260,56 @@ impl Labels {
}
};
let var = quote! { __miette_internal_var };
- if let Some(display) = label {
+ let display = if let Some(display) = label {
let (fmt, args) = display.expand_shorthand_cloned(&display_members);
- quote! {
- miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
- .map(|#var| miette::LabeledSpan::new_with_span(
- std::option::Option::Some(format!(#fmt #args)),
- #var.clone(),
- ))
- }
+ quote! { std::option::Option::Some(format!(#fmt #args)) }
} else {
- quote! {
- miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
- .map(|#var| miette::LabeledSpan::new_with_span(
- std::option::Option::None,
- #var.clone(),
- ))
- }
+ quote! { std::option::Option::None }
+ };
+ let ctor = if *lbl_ty == LabelType::Primary {
+ quote! { miette::LabeledSpan::new_primary_with_span }
+ } else {
+ quote! { miette::LabeledSpan::new_with_span }
+ };
+
+ Some(quote! {
+ miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field)
+ .map(|#var| #ctor(
+ #display,
+ #var.clone(),
+ ))
+ })
+ });
+ let collections_chain = labels.0.iter().filter_map(|label| {
+ let Label { span, label, ty: _, lbl_ty } = label;
+ if *lbl_ty != LabelType::Collection {
+ return None;
}
+ let field = match &span {
+ syn::Member::Named(ident) => ident.clone(),
+ syn::Member::Unnamed(syn::Index { index, .. }) => {
+ format_ident!("_{}", index)
+ }
+ };
+ let display = if let Some(display) = label {
+ let (fmt, args) = display.expand_shorthand_cloned(&display_members);
+ quote! { std::option::Option::Some(format!(#fmt #args)) }
+ } else {
+ quote! { std::option::Option::None }
+ };
+ Some(quote! {
+ .chain({
+ let display = #display;
+ #field.iter().map(move |span| {
+ use miette::macro_helpers::{ToLabelSpanWrapper,ToLabeledSpan};
+ let mut labeled_span = ToLabelSpanWrapper::to_labeled_span(span.clone());
+ if display.is_some() && labeled_span.label().is_none() {
+ labeled_span.set_label(display.clone());
+ }
+ Some(labeled_span)
+ })
+ })
+ })
});
let variant_name = ident.clone();
match &fields {
@@ -194,9 +317,12 @@ impl Labels {
_ => Some(quote! {
Self::#variant_name #display_pat => {
use miette::macro_helpers::ToOption;
- std::option::Option::Some(std::boxed::Box::new(vec![
+ let labels_iter = vec![
#(#variant_labels),*
- ].into_iter().filter(Option::is_some).map(Option::unwrap)))
+ ]
+ .into_iter()
+ #(#collections_chain)*;
+ std::option::Option::Some(std::boxed::Box::new(labels_iter.filter(Option::is_some).map(Option::unwrap)))
}
}),
}
diff --git a/miette-derive/src/source_code.rs b/miette-derive/src/source_code.rs
index 62f28e7..e1b85ab 100644
--- a/miette-derive/src/source_code.rs
+++ b/miette-derive/src/source_code.rs
@@ -10,6 +10,7 @@ use crate::{
pub struct SourceCode {
source_code: syn::Member,
+ is_option: bool,
}
impl SourceCode {
@@ -27,6 +28,19 @@ impl SourceCode {
for (i, field) in fields.iter().enumerate() {
for attr in &field.attrs {
if attr.path().is_ident("source_code") {
+ let is_option = if let syn::Type::Path(syn::TypePath {
+ path: syn::Path { segments, .. },
+ ..
+ }) = &field.ty
+ {
+ segments
+ .last()
+ .map(|seg| seg.ident == "Option")
+ .unwrap_or(false)
+ } else {
+ false
+ };
+
let source_code = if let Some(ident) = field.ident.clone() {
syn::Member::Named(ident)
} else {
@@ -35,7 +49,10 @@ impl SourceCode {
span: field.span(),
})
};
- return Ok(Some(SourceCode { source_code }));
+ return Ok(Some(SourceCode {
+ source_code,
+ is_option,
+ }));
}
}
}
@@ -45,11 +62,21 @@ impl SourceCode {
pub(crate) fn gen_struct(&self, fields: &syn::Fields) -> Option {
let (display_pat, _display_members) = display_pat_members(fields);
let src = &self.source_code;
+ let ret = if self.is_option {
+ quote! {
+ self.#src.as_ref().map(|s| s as _)
+ }
+ } else {
+ quote! {
+ Some(&self.#src)
+ }
+ };
+
Some(quote! {
#[allow(unused_variables)]
fn source_code(&self) -> std::option::Option<&dyn miette::SourceCode> {
let Self #display_pat = self;
- Some(&self.#src)
+ #ret
}
})
}
@@ -68,10 +95,19 @@ impl SourceCode {
}
};
let variant_name = ident.clone();
+ let ret = if source_code.is_option {
+ quote! {
+ #field.as_ref().map(|s| s as _)
+ }
+ } else {
+ quote! {
+ std::option::Option::Some(#field)
+ }
+ };
match &fields {
syn::Fields::Unit => None,
_ => Some(quote! {
- Self::#variant_name #display_pat => std::option::Option::Some(#field),
+ Self::#variant_name #display_pat => #ret,
}),
}
})
diff --git a/rustfmt.toml b/rustfmt.toml
index 8f9ebdd..3a26366 100644
--- a/rustfmt.toml
+++ b/rustfmt.toml
@@ -1,3 +1 @@
edition = "2021"
-wrap_comments = true
-format_code_in_doc_comments = true
diff --git a/src/error.rs b/src/error.rs
index 56041ca..4e57a78 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,27 +1,51 @@
-use std::io;
+use std::{fmt, io};
use thiserror::Error;
-use crate::{self as miette, Diagnostic};
+use crate::Diagnostic;
/**
Error enum for miette. Used by certain operations in the protocol.
*/
-#[derive(Debug, Diagnostic, Error)]
+#[derive(Debug, Error)]
pub enum MietteError {
/// Wrapper around [`std::io::Error`]. This is returned when something went
/// wrong while reading a [`SourceCode`](crate::SourceCode).
#[error(transparent)]
- #[diagnostic(code(miette::io_error), url(docsrs))]
IoError(#[from] io::Error),
/// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
/// bounds of a given [`SourceCode`](crate::SourceCode).
#[error("The given offset is outside the bounds of its Source")]
- #[diagnostic(
- code(miette::span_out_of_bounds),
- help("Double-check your spans. Do you have an off-by-one error?"),
- url(docsrs)
- )]
OutOfBounds,
}
+
+impl Diagnostic for MietteError {
+ fn code<'a>(&'a self) -> Option> {
+ match self {
+ MietteError::IoError(_) => Some(Box::new("miette::io_error")),
+ MietteError::OutOfBounds => Some(Box::new("miette::span_out_of_bounds")),
+ }
+ }
+
+ fn help<'a>(&'a self) -> Option> {
+ match self {
+ MietteError::IoError(_) => None,
+ MietteError::OutOfBounds => Some(Box::new(
+ "Double-check your spans. Do you have an off-by-one error?",
+ )),
+ }
+ }
+
+ fn url<'a>(&'a self) -> Option> {
+ let crate_version = env!("CARGO_PKG_VERSION");
+ let variant = match self {
+ MietteError::IoError(_) => "#variant.IoError",
+ MietteError::OutOfBounds => "#variant.OutOfBounds",
+ };
+ Some(Box::new(format!(
+ "https://docs.rs/miette/{}/miette/enum.MietteError.html{}",
+ crate_version, variant,
+ )))
+ }
+}
diff --git a/src/eyreish/error.rs b/src/eyreish/error.rs
index 6b0dc34..677f368 100644
--- a/src/eyreish/error.rs
+++ b/src/eyreish/error.rs
@@ -30,9 +30,9 @@ impl Report {
/// Create a new error object from a printable error message.
///
- /// If the argument implements std::error::Error, prefer `Report::new`
+ /// If the argument implements [`std::error::Error`], prefer `Report::new`
/// instead which preserves the underlying error's cause chain and
- /// backtrace. If the argument may or may not implement std::error::Error
+ /// backtrace. If the argument may or may not implement [`std::error::Error`]
/// now or in the future, use `miette!(err)` which handles either way
/// correctly.
///
@@ -206,7 +206,7 @@ impl Report {
/// Create a new error from an error message to wrap the existing error.
///
/// For attaching a higher level error message to a `Result` as it is
- /// propagated, the [crate::WrapErr] extension trait may be more
+ /// propagated, the [`WrapErr`](crate::WrapErr) extension trait may be more
/// convenient than this function.
///
/// The primary reason to use `error.wrap_err(...)` instead of
@@ -233,7 +233,7 @@ impl Report {
unsafe { Report::construct(error, vtable, handler) }
}
- /// Compatibility re-export of wrap_err for interop with `anyhow`
+ /// Compatibility re-export of `wrap_err` for interop with `anyhow`
pub fn context(self, msg: D) -> Self
where
D: Display + Send + Sync + 'static,
diff --git a/src/eyreish/macros.rs b/src/eyreish/macros.rs
index 938ac32..e13309f 100644
--- a/src/eyreish/macros.rs
+++ b/src/eyreish/macros.rs
@@ -222,6 +222,9 @@ macro_rules! ensure {
/// ## `anyhow`/`eyre` Users
///
/// You can just replace `use`s of the `anyhow!`/`eyre!` macros with `miette!`.
+///
+/// [`diagnostic!`]: crate::diagnostic!
+/// [`Report`]: crate::Report
#[macro_export]
macro_rules! miette {
($($key:ident = $value:expr,)* $fmt:literal $($arg:tt)*) => {
@@ -282,6 +285,8 @@ macro_rules! miette {
)]
/// assert_eq!(diag.message, "1 + 2 = 3");
/// ```
+///
+/// [`MietteDiagnostic`]: crate::MietteDiagnostic
#[macro_export]
macro_rules! diagnostic {
($fmt:literal $($arg:tt)*) => {{
diff --git a/src/eyreish/mod.rs b/src/eyreish/mod.rs
index 0efceed..544c240 100644
--- a/src/eyreish/mod.rs
+++ b/src/eyreish/mod.rs
@@ -7,8 +7,7 @@
use core::fmt::Display;
use std::error::Error as StdError;
-
-use once_cell::sync::OnceCell;
+use std::sync::OnceLock;
#[allow(unreachable_pub)]
pub use into_diagnostic::*;
@@ -25,10 +24,10 @@ pub use ReportHandler as EyreContext;
#[allow(unreachable_pub)]
pub use WrapErr as Context;
-#[cfg(not(feature = "fancy-no-backtrace"))]
+#[cfg(not(feature = "fancy-base"))]
use crate::DebugReportHandler;
use crate::Diagnostic;
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
use crate::MietteHandler;
use error::ErrorImpl;
@@ -62,7 +61,7 @@ unsafe impl Send for Report {}
pub type ErrorHook =
Box Box + Sync + Send + 'static>;
-static HOOK: OnceCell = OnceCell::new();
+static HOOK: OnceLock = OnceLock::new();
/// Error indicating that [`set_hook()`] was unable to install the provided
/// [`ErrorHook`].
@@ -103,9 +102,9 @@ fn capture_handler(error: &(dyn Diagnostic + 'static)) -> Box
}
fn get_default_printer(_err: &(dyn Diagnostic + 'static)) -> Box {
- #[cfg(feature = "fancy-no-backtrace")]
+ #[cfg(feature = "fancy-base")]
return Box::new(MietteHandler::new());
- #[cfg(not(feature = "fancy-no-backtrace"))]
+ #[cfg(not(feature = "fancy-base"))]
return Box::new(DebugReportHandler::new());
}
diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs
index 91a5ef3..d940fed 100644
--- a/src/eyreish/wrapper.rs
+++ b/src/eyreish/wrapper.rs
@@ -6,35 +6,9 @@ use crate::{Diagnostic, LabeledSpan, Report, SourceCode};
use crate as miette;
-#[repr(transparent)]
-pub(crate) struct DisplayError(pub(crate) M);
-
#[repr(transparent)]
pub(crate) struct MessageError(pub(crate) M);
-pub(crate) struct NoneError;
-
-impl Debug for DisplayError
-where
- M: Display,
-{
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Display::fmt(&self.0, f)
- }
-}
-
-impl Display for DisplayError
-where
- M: Display,
-{
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Display::fmt(&self.0, f)
- }
-}
-
-impl StdError for DisplayError where M: Display + 'static {}
-impl Diagnostic for DisplayError where M: Display + 'static {}
-
impl Debug for MessageError
where
M: Display + Debug,
@@ -56,21 +30,6 @@ where
impl StdError for MessageError where M: Display + Debug + 'static {}
impl Diagnostic for MessageError where M: Display + Debug + 'static {}
-impl Debug for NoneError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Debug::fmt("Option was None", f)
- }
-}
-
-impl Display for NoneError {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- Display::fmt("Option was None", f)
- }
-}
-
-impl StdError for NoneError {}
-impl Diagnostic for NoneError {}
-
#[repr(transparent)]
pub(crate) struct BoxedError(pub(crate) Box);
@@ -163,7 +122,7 @@ impl Diagnostic for WithSourceCode {
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
- Some(&self.source_code)
+ self.error.source_code().or(Some(&self.source_code))
}
fn related<'a>(&'a self) -> Option + 'a>> {
@@ -197,7 +156,7 @@ impl Diagnostic for WithSourceCode {
}
fn source_code(&self) -> Option<&dyn miette::SourceCode> {
- Some(&self.source_code)
+ self.error.source_code().or(Some(&self.source_code))
}
fn related<'a>(&'a self) -> Option + 'a>> {
@@ -232,3 +191,88 @@ impl StdError for WithSourceCode {
self.error.source()
}
}
+
+#[cfg(test)]
+mod tests {
+ use thiserror::Error;
+
+ use crate::{Diagnostic, LabeledSpan, Report, SourceCode, SourceSpan};
+
+ #[derive(Error, Debug)]
+ #[error("inner")]
+ struct Inner {
+ pub(crate) at: SourceSpan,
+ pub(crate) source_code: Option,
+ }
+
+ impl Diagnostic for Inner {
+ fn labels(&self) -> Option + '_>> {
+ Some(Box::new(std::iter::once(LabeledSpan::underline(self.at))))
+ }
+
+ fn source_code(&self) -> Option<&dyn SourceCode> {
+ self.source_code.as_ref().map(|s| s as _)
+ }
+ }
+
+ #[derive(Error, Debug)]
+ #[error("outer")]
+ struct Outer {
+ pub(crate) errors: Vec,
+ }
+
+ impl Diagnostic for Outer {
+ fn related<'a>(&'a self) -> Option + 'a>> {
+ Some(Box::new(self.errors.iter().map(|e| e as _)))
+ }
+ }
+
+ #[test]
+ fn no_override() {
+ let inner_source = "hello world";
+ let outer_source = "abc";
+
+ let report = Report::from(Inner {
+ at: (0..5).into(),
+ source_code: Some(inner_source.to_string()),
+ })
+ .with_source_code(outer_source.to_string());
+
+ let underlined = String::from_utf8(
+ report
+ .source_code()
+ .unwrap()
+ .read_span(&(0..5).into(), 0, 0)
+ .unwrap()
+ .data()
+ .to_vec(),
+ )
+ .unwrap();
+ assert_eq!(underlined, "hello");
+ }
+
+ #[test]
+ #[cfg(feature = "fancy")]
+ fn two_source_codes() {
+ let inner_source = "hello world";
+ let outer_source = "abc";
+
+ let report = Report::from(Outer {
+ errors: vec![
+ Inner {
+ at: (0..5).into(),
+ source_code: Some(inner_source.to_string()),
+ },
+ Inner {
+ at: (1..2).into(),
+ source_code: None,
+ },
+ ],
+ })
+ .with_source_code(outer_source.to_string());
+
+ let message = format!("{:?}", report);
+ assert!(message.contains(inner_source));
+ assert!(message.contains(outer_source));
+ }
+}
diff --git a/src/handler.rs b/src/handler.rs
index e983a55..95382c0 100644
--- a/src/handler.rs
+++ b/src/handler.rs
@@ -1,5 +1,7 @@
use std::fmt;
+use crate::highlighters::Highlighter;
+use crate::highlighters::MietteHighlighter;
use crate::protocol::Diagnostic;
use crate::GraphicalReportHandler;
use crate::GraphicalTheme;
@@ -55,6 +57,11 @@ pub struct MietteHandlerOpts {
pub(crate) context_lines: Option,
pub(crate) tab_width: Option,
pub(crate) with_cause_chain: Option,
+ pub(crate) break_words: Option,
+ pub(crate) wrap_lines: Option,
+ pub(crate) word_separator: Option,
+ pub(crate) word_splitter: Option,
+ pub(crate) highlighter: Option,
}
impl MietteHandlerOpts {
@@ -80,12 +87,79 @@ impl MietteHandlerOpts {
self
}
+ /// Set a syntax highlighter when rendering in graphical mode.
+ /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
+ /// force graphical mode.
+ ///
+ /// Syntax highlighting is disabled by default unless the
+ /// `syntect-highlighter` feature is enabled. Call this method
+ /// to override the default and use a custom highlighter
+ /// implmentation instead.
+ ///
+ /// Use
+ /// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
+ /// To disable highlighting completely.
+ ///
+ /// Setting this option will not force color output. In all cases, the
+ /// current color configuration via
+ /// [`color()`](MietteHandlerOpts::color()) takes precedence over
+ /// highlighter configuration.
+ pub fn with_syntax_highlighting(
+ mut self,
+ highlighter: impl Highlighter + Send + Sync + 'static,
+ ) -> Self {
+ self.highlighter = Some(MietteHighlighter::from(highlighter));
+ self
+ }
+
+ /// Disables syntax highlighting when rendering in graphical mode.
+ /// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
+ /// force graphical mode.
+ ///
+ /// Syntax highlighting is disabled by default unless the
+ /// `syntect-highlighter` feature is enabled. Call this method if you want
+ /// to disable highlighting when building with this feature.
+ pub fn without_syntax_highlighting(mut self) -> Self {
+ self.highlighter = Some(MietteHighlighter::nocolor());
+ self
+ }
+
/// Sets the width to wrap the report at. Defaults to 80.
pub fn width(mut self, width: usize) -> Self {
self.width = Some(width);
self
}
+ /// If true, long lines can be wrapped.
+ ///
+ /// If false, long lines will not be broken when they exceed the width.
+ ///
+ /// Defaults to true.
+ pub fn wrap_lines(mut self, wrap_lines: bool) -> Self {
+ self.wrap_lines = Some(wrap_lines);
+ self
+ }
+
+ /// If true, long words can be broken when wrapping.
+ ///
+ /// If false, long words will not be broken when they exceed the width.
+ ///
+ /// Defaults to true.
+ pub fn break_words(mut self, break_words: bool) -> Self {
+ self.break_words = Some(break_words);
+ self
+ }
+ /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
+ pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+ self.word_separator = Some(word_separator);
+ self
+ }
+
+ /// Sets the `textwrap::WordSplitter` to use when determining wrap points.
+ pub fn word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+ self.word_splitter = Some(word_splitter);
+ self
+ }
/// Include the cause chain of the top-level error in the report.
pub fn with_cause_chain(mut self) -> Self {
self.with_cause_chain = Some(true);
@@ -191,17 +265,15 @@ impl MietteHandlerOpts {
let characters = match self.unicode {
Some(true) => ThemeCharacters::unicode(),
Some(false) => ThemeCharacters::ascii(),
- None if supports_unicode::on(supports_unicode::Stream::Stderr) => {
- ThemeCharacters::unicode()
- }
+ None if syscall::supports_unicode() => ThemeCharacters::unicode(),
None => ThemeCharacters::ascii(),
};
let styles = if self.color == Some(false) {
ThemeStyles::none()
- } else if let Some(color) = supports_color::on(supports_color::Stream::Stderr) {
+ } else if let Some(color_has_16m) = syscall::supports_color_has_16m() {
match self.rgb_colors {
RgbColors::Always => ThemeStyles::rgb(),
- RgbColors::Preferred if color.has_16m => ThemeStyles::rgb(),
+ RgbColors::Preferred if color_has_16m => ThemeStyles::rgb(),
_ => ThemeStyles::ansi(),
}
} else if self.color == Some(true) {
@@ -212,11 +284,31 @@ impl MietteHandlerOpts {
} else {
ThemeStyles::none()
};
+ #[cfg(not(feature = "syntect-highlighter"))]
+ let highlighter = self.highlighter.unwrap_or_else(MietteHighlighter::nocolor);
+ #[cfg(feature = "syntect-highlighter")]
+ let highlighter = if self.color == Some(false) {
+ MietteHighlighter::nocolor()
+ } else if self.color == Some(true) || syscall::supports_color() {
+ match self.highlighter {
+ Some(highlighter) => highlighter,
+ None => match self.rgb_colors {
+ // Because the syntect highlighter currently only supports 24-bit truecolor,
+ // respect RgbColor::Never by disabling the highlighter.
+ // TODO: In the future, find a way to convert the RGB syntect theme
+ // into an ANSI color theme.
+ RgbColors::Never => MietteHighlighter::nocolor(),
+ _ => MietteHighlighter::syntect_truecolor(),
+ },
+ }
+ } else {
+ MietteHighlighter::nocolor()
+ };
let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
- let mut handler = GraphicalReportHandler::new()
+ let mut handler = GraphicalReportHandler::new_themed(theme)
.with_width(width)
- .with_links(linkify)
- .with_theme(theme);
+ .with_links(linkify);
+ handler.highlighter = highlighter;
if let Some(with_cause_chain) = self.with_cause_chain {
if with_cause_chain {
handler = handler.with_cause_chain();
@@ -233,6 +325,19 @@ impl MietteHandlerOpts {
if let Some(w) = self.tab_width {
handler = handler.tab_width(w);
}
+ if let Some(b) = self.break_words {
+ handler = handler.with_break_words(b)
+ }
+ if let Some(b) = self.wrap_lines {
+ handler = handler.with_wrap_lines(b)
+ }
+ if let Some(s) = self.word_separator {
+ handler = handler.with_word_separator(s)
+ }
+ if let Some(s) = self.word_splitter {
+ handler = handler.with_word_splitter(s)
+ }
+
MietteHandler {
inner: Box::new(handler),
}
@@ -257,26 +362,13 @@ impl MietteHandlerOpts {
if let Some(linkify) = self.linkify {
linkify
} else {
- supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr)
+ syscall::supports_hyperlinks()
}
}
- #[cfg(not(miri))]
pub(crate) fn get_width(&self) -> usize {
- self.width.unwrap_or_else(|| {
- terminal_size::terminal_size()
- .unwrap_or((terminal_size::Width(80), terminal_size::Height(0)))
- .0
- .0 as usize
- })
- }
-
- #[cfg(miri)]
- // miri doesn't support a syscall (specifically ioctl)
- // performed by terminal_size, which causes test execution to fail
- // so when miri is running we'll just fallback to a constant
- pub(crate) fn get_width(&self) -> usize {
- self.width.unwrap_or(80)
+ self.width
+ .unwrap_or_else(|| syscall::terminal_width().unwrap_or(80))
}
}
@@ -321,3 +413,63 @@ impl ReportHandler for MietteHandler {
self.inner.debug(diagnostic, f)
}
}
+
+mod syscall {
+ use cfg_if::cfg_if;
+
+ #[inline]
+ pub(super) fn terminal_width() -> Option {
+ cfg_if! {
+ if #[cfg(any(feature = "fancy-no-syscall", miri))] {
+ None
+ } else {
+ terminal_size::terminal_size().map(|size| size.0 .0 as usize)
+ }
+ }
+ }
+
+ #[inline]
+ pub(super) fn supports_hyperlinks() -> bool {
+ cfg_if! {
+ if #[cfg(feature = "fancy-no-syscall")] {
+ false
+ } else {
+ supports_hyperlinks::on(supports_hyperlinks::Stream::Stderr)
+ }
+ }
+ }
+
+ #[cfg(feature = "syntect-highlighter")]
+ #[inline]
+ pub(super) fn supports_color() -> bool {
+ cfg_if! {
+ if #[cfg(feature = "fancy-no-syscall")] {
+ false
+ } else {
+ supports_color::on(supports_color::Stream::Stderr).is_some()
+ }
+ }
+ }
+
+ #[inline]
+ pub(super) fn supports_color_has_16m() -> Option {
+ cfg_if! {
+ if #[cfg(feature = "fancy-no-syscall")] {
+ None
+ } else {
+ supports_color::on(supports_color::Stream::Stderr).map(|color| color.has_16m)
+ }
+ }
+ }
+
+ #[inline]
+ pub(super) fn supports_unicode() -> bool {
+ cfg_if! {
+ if #[cfg(feature = "fancy-no-syscall")] {
+ false
+ } else {
+ supports_unicode::on(supports_unicode::Stream::Stderr)
+ }
+ }
+ }
+}
diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs
index b5dd754..9c1d7ab 100644
--- a/src/handlers/graphical.rs
+++ b/src/handlers/graphical.rs
@@ -1,12 +1,13 @@
use std::fmt::{self, Write};
-use owo_colors::{OwoColorize, Style};
-use unicode_width::UnicodeWidthChar;
+use owo_colors::{OwoColorize, Style, StyledList};
+use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
use crate::handlers::theme::*;
+use crate::highlighters::{Highlighter, MietteHighlighter};
use crate::protocol::{Diagnostic, Severity};
-use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
+use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
/**
A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
@@ -30,6 +31,12 @@ pub struct GraphicalReportHandler {
pub(crate) context_lines: usize,
pub(crate) tab_width: usize,
pub(crate) with_cause_chain: bool,
+ pub(crate) wrap_lines: bool,
+ pub(crate) break_words: bool,
+ pub(crate) word_separator: Option,
+ pub(crate) word_splitter: Option,
+ pub(crate) highlighter: MietteHighlighter,
+ pub(crate) link_display_text: Option,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -51,6 +58,12 @@ impl GraphicalReportHandler {
context_lines: 1,
tab_width: 4,
with_cause_chain: true,
+ wrap_lines: true,
+ break_words: true,
+ word_separator: None,
+ word_splitter: None,
+ highlighter: MietteHighlighter::default(),
+ link_display_text: None,
}
}
@@ -63,7 +76,13 @@ impl GraphicalReportHandler {
footer: None,
context_lines: 1,
tab_width: 4,
+ wrap_lines: true,
with_cause_chain: true,
+ break_words: true,
+ word_separator: None,
+ word_splitter: None,
+ highlighter: MietteHighlighter::default(),
+ link_display_text: None,
}
}
@@ -122,6 +141,30 @@ impl GraphicalReportHandler {
self
}
+ /// Enables or disables wrapping of lines to fit the width.
+ pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
+ self.wrap_lines = wrap_lines;
+ self
+ }
+
+ /// Enables or disables breaking of words during wrapping.
+ pub fn with_break_words(mut self, break_words: bool) -> Self {
+ self.break_words = break_words;
+ self
+ }
+
+ /// Sets the word separator to use when wrapping.
+ pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
+ self.word_separator = Some(word_separator);
+ self
+ }
+
+ /// Sets the word splitter to usewhen wrapping.
+ pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
+ self.word_splitter = Some(word_splitter);
+ self
+ }
+
/// Sets the 'global' footer for this handler.
pub fn with_footer(mut self, footer: String) -> Self {
self.footer = Some(footer);
@@ -133,6 +176,30 @@ impl GraphicalReportHandler {
self.context_lines = lines;
self
}
+
+ /// Enable syntax highlighting for source code snippets, using the given
+ /// [`Highlighter`]. See the [crate::highlighters] crate for more details.
+ pub fn with_syntax_highlighting(
+ mut self,
+ highlighter: impl Highlighter + Send + Sync + 'static,
+ ) -> Self {
+ self.highlighter = MietteHighlighter::from(highlighter);
+ self
+ }
+
+ /// Disable syntax highlighting. This uses the
+ /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter.
+ pub fn without_syntax_highlighting(mut self) -> Self {
+ self.highlighter = MietteHighlighter::nocolor();
+ self
+ }
+
+ /// Sets the display text for links.
+ /// Miette displays `(link)` if this option is not set.
+ pub fn with_link_display_text(mut self, text: impl Into) -> Self {
+ self.link_display_text = Some(text.into());
+ self
+ }
}
impl Default for GraphicalReportHandler {
@@ -158,11 +225,19 @@ impl GraphicalReportHandler {
self.render_related(f, diagnostic, src)?;
if let Some(footer) = &self.footer {
writeln!(f)?;
- let width = self.termwidth.saturating_sub(4);
- let opts = textwrap::Options::new(width)
+ let width = self.termwidth.saturating_sub(2);
+ let mut opts = textwrap::Options::new(width)
.initial_indent(" ")
- .subsequent_indent(" ");
- writeln!(f, "{}", textwrap::fill(footer, opts))?;
+ .subsequent_indent(" ")
+ .break_words(self.break_words);
+ if let Some(word_separator) = self.word_separator {
+ opts = opts.word_separator(word_separator);
+ }
+ if let Some(word_splitter) = self.word_splitter.clone() {
+ opts = opts.word_splitter(word_splitter);
+ }
+
+ writeln!(f, "{}", self.wrap(footer, opts))?;
}
Ok(())
}
@@ -181,15 +256,15 @@ impl GraphicalReportHandler {
} else {
"".to_string()
};
+ let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
let link = format!(
"\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
url,
code.style(severity_style),
- "(link)".style(self.theme.styles.link)
+ display_text.style(self.theme.styles.link)
);
write!(header, "{}", link)?;
writeln!(f, "{}", header)?;
- writeln!(f)?;
} else if let Some(code) = diagnostic.code() {
write!(header, "{}", code.style(severity_style),)?;
if self.links == LinkStyle::Text && diagnostic.url().is_some() {
@@ -197,8 +272,8 @@ impl GraphicalReportHandler {
write!(header, " ({})", url.style(self.theme.styles.link))?;
}
writeln!(f, "{}", header)?;
- writeln!(f)?;
}
+ writeln!(f)?;
Ok(())
}
@@ -212,11 +287,18 @@ impl GraphicalReportHandler {
let initial_indent = format!(" {} ", severity_icon.style(severity_style));
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
let width = self.termwidth.saturating_sub(2);
- let opts = textwrap::Options::new(width)
+ let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
- .subsequent_indent(&rest_indent);
+ .subsequent_indent(&rest_indent)
+ .break_words(self.break_words);
+ if let Some(word_separator) = self.word_separator {
+ opts = opts.word_separator(word_separator);
+ }
+ if let Some(word_splitter) = self.word_splitter.clone() {
+ opts = opts.word_splitter(word_splitter);
+ }
- writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
+ writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
if !self.with_cause_chain {
return Ok(());
@@ -251,23 +333,36 @@ impl GraphicalReportHandler {
)
.style(severity_style)
.to_string();
- let opts = textwrap::Options::new(width)
+ let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
- .subsequent_indent(&rest_indent);
+ .subsequent_indent(&rest_indent)
+ .break_words(self.break_words);
+ if let Some(word_separator) = self.word_separator {
+ opts = opts.word_separator(word_separator);
+ }
+ if let Some(word_splitter) = self.word_splitter.clone() {
+ opts = opts.word_splitter(word_splitter);
+ }
+
match error {
ErrorKind::Diagnostic(diag) => {
let mut inner = String::new();
- // Don't print footer for inner errors
let mut inner_renderer = self.clone();
+ // Don't print footer for inner errors
inner_renderer.footer = None;
+ // Cause chains are already flattened, so don't double-print the nested error
inner_renderer.with_cause_chain = false;
+ // Since everything from here on is indented, shrink the virtual terminal
+ inner_renderer.termwidth -= rest_indent.width();
inner_renderer.render_report(&mut inner, diag)?;
- writeln!(f, "{}", textwrap::fill(&inner, opts))?;
+ // If there was no header, remove the leading newline
+ let inner = inner.trim_start_matches('\n');
+ writeln!(f, "{}", self.wrap(&inner, opts))?;
}
ErrorKind::StdError(err) => {
- writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
+ writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
}
}
}
@@ -278,12 +373,20 @@ impl GraphicalReportHandler {
fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
if let Some(help) = diagnostic.help() {
- let width = self.termwidth.saturating_sub(4);
+ let width = self.termwidth.saturating_sub(2);
let initial_indent = " help: ".style(self.theme.styles.help).to_string();
- let opts = textwrap::Options::new(width)
+ let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent)
- .subsequent_indent(" ");
- writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
+ .subsequent_indent(" ")
+ .break_words(self.break_words);
+ if let Some(word_separator) = self.word_separator {
+ opts = opts.word_separator(word_separator);
+ }
+ if let Some(word_splitter) = self.word_splitter.clone() {
+ opts = opts.word_splitter(word_splitter);
+ }
+
+ writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
}
Ok(())
}
@@ -295,19 +398,22 @@ impl GraphicalReportHandler {
parent_src: Option<&dyn SourceCode>,
) -> fmt::Result {
if let Some(related) = diagnostic.related() {
- writeln!(f)?;
+ let mut inner_renderer = self.clone();
+ // Re-enable the printing of nested cause chains for related errors
+ inner_renderer.with_cause_chain = true;
for rel in related {
+ writeln!(f)?;
match rel.severity() {
Some(Severity::Error) | None => write!(f, "Error: ")?,
Some(Severity::Warning) => write!(f, "Warning: ")?,
Some(Severity::Advice) => write!(f, "Advice: ")?,
};
- self.render_header(f, rel)?;
- self.render_causes(f, rel)?;
+ inner_renderer.render_header(f, rel)?;
+ inner_renderer.render_causes(f, rel)?;
let src = rel.source_code().or(parent_src);
- self.render_snippets(f, rel, src)?;
- self.render_footer(f, rel)?;
- self.render_related(f, rel, src)?;
+ inner_renderer.render_snippets(f, rel, src)?;
+ inner_renderer.render_footer(f, rel)?;
+ inner_renderer.render_related(f, rel, src)?;
}
}
Ok(())
@@ -319,78 +425,97 @@ impl GraphicalReportHandler {
diagnostic: &(dyn Diagnostic),
opt_source: Option<&dyn SourceCode>,
) -> fmt::Result {
- if let Some(source) = opt_source {
- if let Some(labels) = diagnostic.labels() {
- let mut labels = labels.collect::>();
- labels.sort_unstable_by_key(|l| l.inner().offset());
- if !labels.is_empty() {
- let contents = labels
- .iter()
- .map(|label| {
- source.read_span(label.inner(), self.context_lines, self.context_lines)
- })
- .collect::>>, MietteError>>()
- .map_err(|_| fmt::Error)?;
- let mut contexts = Vec::with_capacity(contents.len());
- for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
- if contexts.is_empty() {
- contexts.push((right, right_conts));
- } else {
- let (left, left_conts) = contexts.last().unwrap().clone();
- let left_end = left.offset() + left.len();
- let right_end = right.offset() + right.len();
- if left_conts.line() + left_conts.line_count() >= right_conts.line() {
- // The snippets will overlap, so we create one Big Chunky Boi
- let new_span = LabeledSpan::new(
- left.label().map(String::from),
- left.offset(),
- if right_end >= left_end {
- // Right end goes past left end
- right_end - left.offset()
- } else {
- // right is contained inside left
- left.len()
- },
- );
- if source
- .read_span(
- new_span.inner(),
- self.context_lines,
- self.context_lines,
- )
- .is_ok()
- {
- contexts.pop();
- contexts.push((
- // We'll throw this away later
- new_span, left_conts,
- ));
- } else {
- contexts.push((right, right_conts));
- }
- } else {
- contexts.push((right, right_conts));
- }
- }
- }
- for (ctx, _) in contexts {
- self.render_context(f, source, &ctx, &labels[..])?;
+ let source = match opt_source {
+ Some(source) => source,
+ None => return Ok(()),
+ };
+ let labels = match diagnostic.labels() {
+ Some(labels) => labels,
+ None => return Ok(()),
+ };
+
+ let mut labels = labels.collect::>();
+ labels.sort_unstable_by_key(|l| l.inner().offset());
+
+ let mut contexts = Vec::with_capacity(labels.len());
+ for right in labels.iter().cloned() {
+ let right_conts =
+ match source.read_span(right.inner(), self.context_lines, self.context_lines) {
+ Ok(cont) => cont,
+ Err(err) => {
+ writeln!(
+ f,
+ " [{} `{}` (offset: {}, length: {}): {:?}]",
+ "Failed to read contents for label".style(self.theme.styles.error),
+ right
+ .label()
+ .unwrap_or("")
+ .style(self.theme.styles.link),
+ right.offset().style(self.theme.styles.link),
+ right.len().style(self.theme.styles.link),
+ err.style(self.theme.styles.warning)
+ )?;
+ return Ok(());
}
+ };
+
+ if contexts.is_empty() {
+ contexts.push((right, right_conts));
+ continue;
+ }
+
+ let (left, left_conts) = contexts.last().unwrap();
+ if left_conts.line() + left_conts.line_count() >= right_conts.line() {
+ // The snippets will overlap, so we create one Big Chunky Boi
+ let left_end = left.offset() + left.len();
+ let right_end = right.offset() + right.len();
+ let new_end = std::cmp::max(left_end, right_end);
+
+ let new_span = LabeledSpan::new(
+ left.label().map(String::from),
+ left.offset(),
+ new_end - left.offset(),
+ );
+ // Check that the two contexts can be combined
+ if let Ok(new_conts) =
+ source.read_span(new_span.inner(), self.context_lines, self.context_lines)
+ {
+ contexts.pop();
+ // We'll throw the contents away later
+ contexts.push((new_span, new_conts));
+ continue;
}
}
+
+ contexts.push((right, right_conts));
}
+ for (ctx, _) in contexts {
+ self.render_context(f, source, &ctx, &labels[..])?;
+ }
+
Ok(())
}
- fn render_context<'a>(
+ fn render_context(
&self,
f: &mut impl fmt::Write,
- source: &'a dyn SourceCode,
+ source: &dyn SourceCode,
context: &LabeledSpan,
labels: &[LabeledSpan],
) -> fmt::Result {
let (contents, lines) = self.get_lines(source, context.inner())?;
+ // only consider labels from the context as primary label
+ let ctx_labels = labels.iter().filter(|l| {
+ context.inner().offset() <= l.inner().offset()
+ && l.inner().offset() + l.inner().len()
+ <= context.inner().offset() + context.inner().len()
+ });
+ let primary_label = ctx_labels
+ .clone()
+ .find(|label| label.primary())
+ .or_else(|| ctx_labels.clone().next());
+
// sorting is your friend
let labels = labels
.iter()
@@ -398,6 +523,8 @@ impl GraphicalReportHandler {
.map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
.collect::>();
+ let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
+
// 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.
@@ -405,7 +532,7 @@ impl GraphicalReportHandler {
for line in &lines {
let mut num_highlights = 0;
for hl in &labels {
- if !line.span_line_only(hl) && line.span_applies(hl) {
+ if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
num_highlights += 1;
}
}
@@ -431,19 +558,33 @@ impl GraphicalReportHandler {
self.theme.characters.hbar,
)?;
- if let Some(source_name) = contents.name() {
+ // If there is a primary label, then use its span
+ // as the reference point for line/column information.
+ let primary_contents = match primary_label {
+ Some(label) => source
+ .read_span(label.inner(), 0, 0)
+ .map_err(|_| fmt::Error)?,
+ None => contents,
+ };
+
+ if let Some(source_name) = primary_contents.name() {
let source_name = source_name.style(self.theme.styles.link);
writeln!(
f,
"[{}:{}:{}]",
source_name,
- contents.line() + 1,
- contents.column() + 1
+ primary_contents.line() + 1,
+ primary_contents.column() + 1
)?;
} else if lines.len() <= 1 {
writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
} else {
- writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
+ writeln!(
+ f,
+ "[{}:{}]",
+ primary_contents.line() + 1,
+ primary_contents.column() + 1
+ )?;
}
// Now it's time for the fun part--actually rendering everything!
@@ -457,7 +598,9 @@ impl GraphicalReportHandler {
self.render_line_gutter(f, max_gutter, line, &labels)?;
// And _now_ we can print out the line text itself!
- self.render_line_text(f, &line.text)?;
+ let styled_text =
+ StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
+ self.render_line_text(f, &styled_text)?;
// Next, we write all the highlights that apply to this particular line.
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
@@ -468,7 +611,13 @@ impl GraphicalReportHandler {
// no line number!
self.write_no_linum(f, linum_width)?;
// gutter _again_
- self.render_highlight_gutter(f, max_gutter, line, &labels)?;
+ self.render_highlight_gutter(
+ f,
+ max_gutter,
+ line,
+ &labels,
+ LabelRenderMode::SingleLine,
+ )?;
self.render_single_line_highlights(
f,
line,
@@ -480,11 +629,7 @@ impl GraphicalReportHandler {
}
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, &labels)?;
- self.render_multi_line_end(f, hl)?;
+ self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
}
}
}
@@ -498,6 +643,91 @@ impl GraphicalReportHandler {
Ok(())
}
+ fn render_multi_line_end(
+ &self,
+ f: &mut impl fmt::Write,
+ labels: &[FancySpan],
+ max_gutter: usize,
+ linum_width: usize,
+ line: &Line,
+ label: &FancySpan,
+ ) -> fmt::Result {
+ // no line number!
+ self.write_no_linum(f, linum_width)?;
+
+ if let Some(label_parts) = label.label_parts() {
+ // if it has a label, how long is it?
+ let (first, rest) = label_parts
+ .split_first()
+ .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
+
+ if rest.is_empty() {
+ // gutter _again_
+ self.render_highlight_gutter(
+ f,
+ max_gutter,
+ line,
+ &labels,
+ LabelRenderMode::SingleLine,
+ )?;
+
+ self.render_multi_line_end_single(
+ f,
+ first,
+ label.style,
+ LabelRenderMode::SingleLine,
+ )?;
+ } else {
+ // gutter _again_
+ self.render_highlight_gutter(
+ f,
+ max_gutter,
+ line,
+ &labels,
+ LabelRenderMode::MultiLineFirst,
+ )?;
+
+ self.render_multi_line_end_single(
+ f,
+ first,
+ label.style,
+ LabelRenderMode::MultiLineFirst,
+ )?;
+ for label_line in rest {
+ // no line number!
+ self.write_no_linum(f, linum_width)?;
+ // gutter _again_
+ self.render_highlight_gutter(
+ f,
+ max_gutter,
+ line,
+ &labels,
+ LabelRenderMode::MultiLineRest,
+ )?;
+ self.render_multi_line_end_single(
+ f,
+ label_line,
+ label.style,
+ LabelRenderMode::MultiLineRest,
+ )?;
+ }
+ }
+ } else {
+ // gutter _again_
+ self.render_highlight_gutter(
+ f,
+ max_gutter,
+ line,
+ &labels,
+ LabelRenderMode::SingleLine,
+ )?;
+ // has no label
+ writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
+ }
+
+ Ok(())
+ }
+
fn render_line_gutter(
&self,
f: &mut impl fmt::Write,
@@ -510,7 +740,7 @@ impl GraphicalReportHandler {
}
let chars = &self.theme.characters;
let mut gutter = String::new();
- let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+ let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
let mut arrow = false;
for (i, hl) in applicable.enumerate() {
if line.span_starts(hl) {
@@ -566,33 +796,121 @@ impl GraphicalReportHandler {
max_gutter: usize,
line: &Line,
highlights: &[FancySpan],
+ render_mode: LabelRenderMode,
) -> fmt::Result {
if max_gutter == 0 {
return Ok(());
}
+
+ // keeps track of how many colums wide the gutter is
+ // important for ansi since simply measuring the size of the final string
+ // gives the wrong result when the string contains ansi codes.
+ let mut gutter_cols = 0;
+
let chars = &self.theme.characters;
let mut gutter = String::new();
- let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
+ let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) {
- gutter.push_str(&chars.lbot.style(hl.style).to_string());
- gutter.push_str(
- &chars
- .hbar
- .to_string()
- .repeat(max_gutter.saturating_sub(i) + 2)
- .style(hl.style)
- .to_string(),
- );
+ if render_mode == LabelRenderMode::MultiLineRest {
+ // this is to make multiline labels work. We want to make the right amount
+ // of horizontal space for them, but not actually draw the lines
+ let horizontal_space = max_gutter.saturating_sub(i) + 2;
+ for _ in 0..horizontal_space {
+ gutter.push(' ');
+ }
+ // account for one more horizontal space, since in multiline mode
+ // we also add in the vertical line before the label like this:
+ // 2 │ ╭─▶ text
+ // 3 │ ├─▶ here
+ // · ╰──┤ these two lines
+ // · │ are the problem
+ // ^this
+ gutter_cols += horizontal_space + 1;
+ } else {
+ let num_repeat = max_gutter.saturating_sub(i) + 2;
+
+ gutter.push_str(&chars.lbot.style(hl.style).to_string());
+
+ gutter.push_str(
+ &chars
+ .hbar
+ .to_string()
+ .repeat(
+ num_repeat
+ // if we are rendering a multiline label, then leave a bit of space for the
+ // rcross character
+ - if render_mode == LabelRenderMode::MultiLineFirst {
+ 1
+ } else {
+ 0
+ },
+ )
+ .style(hl.style)
+ .to_string(),
+ );
+
+ // we count 1 for the lbot char, and then a few more, the same number
+ // as we just repeated for. For each repeat we only add 1, even though
+ // due to ansi escape codes the number of bytes in the string could grow
+ // a lot each time.
+ gutter_cols += num_repeat + 1;
+ }
break;
} else {
gutter.push_str(&chars.vbar.style(hl.style).to_string());
+
+ // we may push many bytes for the ansi escape codes style adds,
+ // but we still only add a single character-width to the string in a terminal
+ gutter_cols += 1;
}
}
- write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
+
+ // now calculate how many spaces to add based on how many columns we just created.
+ // it's the max width of the gutter, minus how many character-widths we just generated
+ // capped at 0 (though this should never go below in reality), and then we add 3 to
+ // account for arrowheads when a gutter line ends
+ let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
+ // we then write the gutter and as many spaces as we need
+ write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
Ok(())
}
+ fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
+ if self.wrap_lines {
+ textwrap::fill(text, opts)
+ } else {
+ // Format without wrapping, but retain the indentation options
+ // Implementation based on `textwrap::indent`
+ let mut result = String::with_capacity(2 * text.len());
+ let trimmed_indent = opts.subsequent_indent.trim_end();
+ for (idx, line) in text.split_terminator('\n').enumerate() {
+ if idx > 0 {
+ result.push('\n');
+ }
+ if idx == 0 {
+ if line.trim().is_empty() {
+ result.push_str(opts.initial_indent.trim_end());
+ } else {
+ result.push_str(opts.initial_indent);
+ }
+ } else {
+ if line.trim().is_empty() {
+ result.push_str(trimmed_indent);
+ } else {
+ result.push_str(opts.subsequent_indent);
+ }
+ }
+ result.push_str(line);
+ }
+ if text.ends_with('\n') {
+ // split_terminator will have eaten the final '\n'.
+ result.push('\n');
+ }
+ result
+ }
+ }
+
fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
write!(
f,
@@ -618,13 +936,26 @@ impl GraphicalReportHandler {
/// Returns an iterator over the visual width of each character in a line.
fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator- + 'a {
let mut column = 0;
+ let mut escaped = false;
let tab_width = self.tab_width;
text.chars().map(move |c| {
- let width = if c == '\t' {
+ let width = match (escaped, c) {
// Round up to the next multiple of tab_width
- tab_width - column % tab_width
- } else {
- c.width().unwrap_or(0)
+ (false, '\t') => tab_width - column % tab_width,
+ // start of ANSI escape
+ (false, '\x1b') => {
+ escaped = true;
+ 0
+ }
+ // use Unicode width for all other characters
+ (false, c) => c.width().unwrap_or(0),
+ // end of ANSI escape
+ (true, 'm') => {
+ escaped = false;
+ 0
+ }
+ // characters are zero width within escape sequence
+ (true, _) => 0,
};
column += width;
width
@@ -632,11 +963,22 @@ impl GraphicalReportHandler {
}
/// Returns the visual column position of a byte offset on a specific line.
- fn visual_offset(&self, line: &Line, offset: usize) -> usize {
+ ///
+ /// If the offset occurs in the middle of a character, the returned column
+ /// corresponds to that character's first column in `start` is true, or its
+ /// last column if `start` is false.
+ fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
let line_range = line.offset..=(line.offset + line.length);
assert!(line_range.contains(&offset));
- let text_index = offset - line.offset;
+ let mut text_index = offset - line.offset;
+ while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
+ if start {
+ text_index -= 1;
+ } else {
+ text_index += 1;
+ }
+ }
let text = &line.text[..text_index.min(line.text.len())];
let text_width = self.line_visual_char_width(text).sum();
if text_index > line.text.len() {
@@ -659,10 +1001,10 @@ impl GraphicalReportHandler {
for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
if c == '\t' {
for _ in 0..width {
- f.write_char(' ')?
+ f.write_char(' ')?;
}
} else {
- f.write_char(c)?
+ f.write_char(c)?;
}
}
f.write_char('\n')?;
@@ -687,32 +1029,34 @@ impl GraphicalReportHandler {
.map(|hl| {
let byte_start = hl.offset();
let byte_end = hl.offset() + hl.len();
- let start = self.visual_offset(line, byte_start).max(highest);
- let end = self.visual_offset(line, byte_end).max(start + 1);
+ let start = self.visual_offset(line, byte_start, true).max(highest);
+ let end = if hl.len() == 0 {
+ start + 1
+ } else {
+ self.visual_offset(line, byte_end, false).max(start + 1)
+ };
let vbar_offset = (start + end) / 2;
let num_left = vbar_offset - start;
let num_right = end - vbar_offset - 1;
- if start < end {
- underlines.push_str(
- &format!(
- "{:width$}{}{}{}",
- "",
- chars.underline.to_string().repeat(num_left),
- if hl.len() == 0 {
- chars.uarrow
- } else if hl.label().is_some() {
- chars.underbar
- } else {
- chars.underline
- },
- chars.underline.to_string().repeat(num_right),
- width = start.saturating_sub(highest),
- )
- .style(hl.style)
- .to_string(),
- );
- }
+ underlines.push_str(
+ &format!(
+ "{:width$}{}{}{}",
+ "",
+ chars.underline.to_string().repeat(num_left),
+ if hl.len() == 0 {
+ chars.uarrow
+ } else if hl.label().is_some() {
+ chars.underbar
+ } else {
+ chars.underline
+ },
+ chars.underline.to_string().repeat(num_right),
+ width = start.saturating_sub(highest),
+ )
+ .style(hl.style)
+ .to_string(),
+ );
highest = std::cmp::max(highest, end);
(hl, vbar_offset)
@@ -721,27 +1065,40 @@ impl GraphicalReportHandler {
writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() {
- if let Some(label) = hl.label() {
- self.write_no_linum(f, linum_width)?;
- self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
- let mut curr_offset = 1usize;
- for (offset_hl, vbar_offset) in &vbar_offsets {
- while curr_offset < *vbar_offset + 1 {
- write!(f, " ")?;
- curr_offset += 1;
- }
- if *offset_hl != hl {
- write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
- curr_offset += 1;
- } else {
- let lines = format!(
- "{}{} {}",
- chars.lbot,
- chars.hbar.to_string().repeat(2),
- label,
- );
- writeln!(f, "{}", lines.style(hl.style))?;
- break;
+ if let Some(label) = hl.label_parts() {
+ if label.len() == 1 {
+ self.write_label_text(
+ f,
+ line,
+ linum_width,
+ max_gutter,
+ all_highlights,
+ chars,
+ &vbar_offsets,
+ hl,
+ &label[0],
+ LabelRenderMode::SingleLine,
+ )?;
+ } else {
+ let mut first = true;
+ for label_line in &label {
+ self.write_label_text(
+ f,
+ line,
+ linum_width,
+ max_gutter,
+ all_highlights,
+ chars,
+ &vbar_offsets,
+ hl,
+ label_line,
+ if first {
+ LabelRenderMode::MultiLineFirst
+ } else {
+ LabelRenderMode::MultiLineRest
+ },
+ )?;
+ first = false;
}
}
}
@@ -749,13 +1106,80 @@ impl GraphicalReportHandler {
Ok(())
}
- fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
- writeln!(
+ // I know it's not good practice, but making this a function makes a lot of sense
+ // and making a struct for this does not...
+ #[allow(clippy::too_many_arguments)]
+ fn write_label_text(
+ &self,
+ f: &mut impl fmt::Write,
+ line: &Line,
+ linum_width: usize,
+ max_gutter: usize,
+ all_highlights: &[FancySpan],
+ chars: &ThemeCharacters,
+ vbar_offsets: &[(&&FancySpan, usize)],
+ hl: &&FancySpan,
+ label: &str,
+ render_mode: LabelRenderMode,
+ ) -> fmt::Result {
+ self.write_no_linum(f, linum_width)?;
+ self.render_highlight_gutter(
f,
- "{} {}",
- self.theme.characters.hbar.style(hl.style),
- hl.label().unwrap_or_else(|| "".into()),
+ max_gutter,
+ line,
+ all_highlights,
+ LabelRenderMode::SingleLine,
)?;
+ let mut curr_offset = 1usize;
+ for (offset_hl, vbar_offset) in vbar_offsets {
+ while curr_offset < *vbar_offset + 1 {
+ write!(f, " ")?;
+ curr_offset += 1;
+ }
+ if *offset_hl != hl {
+ write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
+ curr_offset += 1;
+ } else {
+ let lines = match render_mode {
+ LabelRenderMode::SingleLine => format!(
+ "{}{} {}",
+ chars.lbot,
+ chars.hbar.to_string().repeat(2),
+ label,
+ ),
+ LabelRenderMode::MultiLineFirst => {
+ format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
+ }
+ LabelRenderMode::MultiLineRest => {
+ format!(" {} {}", chars.vbar, label,)
+ }
+ };
+ writeln!(f, "{}", lines.style(hl.style))?;
+ break;
+ }
+ }
+ Ok(())
+ }
+
+ fn render_multi_line_end_single(
+ &self,
+ f: &mut impl fmt::Write,
+ label: &str,
+ style: Style,
+ render_mode: LabelRenderMode,
+ ) -> fmt::Result {
+ match render_mode {
+ LabelRenderMode::SingleLine => {
+ writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
+ }
+ LabelRenderMode::MultiLineFirst => {
+ writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
+ }
+ LabelRenderMode::MultiLineRest => {
+ writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
+ }
+ }
+
Ok(())
}
@@ -834,6 +1258,16 @@ impl ReportHandler for GraphicalReportHandler {
Support types
*/
+#[derive(PartialEq, Debug)]
+enum LabelRenderMode {
+ /// we're rendering a single line label (or not rendering in any special way)
+ SingleLine,
+ /// we're rendering a multiline label
+ MultiLineFirst,
+ /// we're rendering the rest of a multiline label
+ MultiLineRest,
+}
+
#[derive(Debug)]
struct Line {
line_number: usize,
@@ -847,14 +1281,31 @@ impl Line {
span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
}
+ /// Returns whether `span` should be visible on this line, either in the gutter or under the
+ /// text on this line
fn span_applies(&self, span: &FancySpan) -> bool {
let spanlen = if span.len() == 0 { 1 } else { span.len() };
// 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() + spanlen > self.offset + self.length) //todo
- // Span ends on this line
- || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
+ // Span passes through this line
+ || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
+ // Span ends on this line
+ || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
+ }
+
+ /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
+ /// that are only visible on this line and do not span multiple lines)
+ fn span_applies_gutter(&self, span: &FancySpan) -> bool {
+ let spanlen = if span.len() == 0 { 1 } else { span.len() };
+ // Span starts in this line
+ self.span_applies(span)
+ && !(
+ // as long as it doesn't start *and* end on this line
+ (span.offset() >= self.offset && span.offset() < self.offset + self.length)
+ && (span.offset() + spanlen > self.offset
+ && span.offset() + spanlen <= self.offset + self.length)
+ )
}
// A 'flyby' is a multi-line span that technically covers this line, but
@@ -884,7 +1335,10 @@ impl Line {
#[derive(Debug, Clone)]
struct FancySpan {
- label: Option
,
+ /// this is deliberately an option of a vec because I wanted to be very explicit
+ /// that there can also be *no* label. If there is a label, it can have multiple
+ /// lines which is what the vec is for.
+ label: Option>,
span: SourceSpan,
style: Style,
}
@@ -895,9 +1349,17 @@ impl PartialEq for FancySpan {
}
}
+fn split_label(v: String) -> Vec {
+ v.split('\n').map(|i| i.to_string()).collect()
+}
+
impl FancySpan {
fn new(label: Option, span: SourceSpan, style: Style) -> Self {
- FancySpan { label, span, style }
+ FancySpan {
+ label: label.map(split_label),
+ span,
+ style,
+ }
}
fn style(&self) -> Style {
@@ -907,7 +1369,15 @@ impl FancySpan {
fn label(&self) -> Option {
self.label
.as_ref()
- .map(|l| l.style(self.style()).to_string())
+ .map(|l| l.join("\n").style(self.style()).to_string())
+ }
+
+ fn label_parts(&self) -> Option> {
+ self.label.as_ref().map(|l| {
+ l.iter()
+ .map(|i| i.style(self.style()).to_string())
+ .collect()
+ })
}
fn offset(&self) -> usize {
diff --git a/src/handlers/json.rs b/src/handlers/json.rs
index 29e21a0..0b4a405 100644
--- a/src/handlers/json.rs
+++ b/src/handlers/json.rs
@@ -96,7 +96,7 @@ impl JSONReportHandler {
}
write!(f, r#""{}""#, escape(&error.to_string()))?;
}
- write!(f, "],")?
+ write!(f, "],")?;
} else {
write!(f, r#""causes": [],"#)?;
}
diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs
index fde2dc9..ecc7b15 100644
--- a/src/handlers/mod.rs
+++ b/src/handlers/mod.rs
@@ -5,20 +5,20 @@ Reporters included with `miette`.
#[allow(unreachable_pub)]
pub use debug::*;
#[allow(unreachable_pub)]
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
pub use graphical::*;
#[allow(unreachable_pub)]
pub use json::*;
#[allow(unreachable_pub)]
pub use narratable::*;
#[allow(unreachable_pub)]
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
pub use theme::*;
mod debug;
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
mod graphical;
mod json;
mod narratable;
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
mod theme;
diff --git a/src/handlers/theme.rs b/src/handlers/theme.rs
index 1f5236a..892ffc2 100644
--- a/src/handlers/theme.rs
+++ b/src/handlers/theme.rs
@@ -1,4 +1,5 @@
-use is_terminal::IsTerminal;
+use std::io::IsTerminal;
+
use owo_colors::Style;
/**
@@ -55,9 +56,9 @@ impl GraphicalTheme {
/// A "basic" graphical theme that skips colors and unicode characters and
/// just does monochrome ascii art. If you want a completely non-graphical
- /// rendering of your `Diagnostic`s, check out
- /// [crate::NarratableReportHandler], or write your own
- /// [crate::ReportHandler]!
+ /// rendering of your [`Diagnostic`](crate::Diagnostic)s, check out
+ /// [`NarratableReportHandler`](crate::NarratableReportHandler), or write
+ /// your own [`ReportHandler`](crate::ReportHandler)
pub fn none() -> Self {
Self {
characters: ThemeCharacters::ascii(),
@@ -79,7 +80,8 @@ impl Default for GraphicalTheme {
}
/**
-Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler].
+Styles for various parts of graphical rendering for the
+[`GraphicalReportHandler`](crate::GraphicalReportHandler).
*/
#[derive(Debug, Clone)]
pub struct ThemeStyles {
@@ -159,7 +161,7 @@ impl ThemeStyles {
// https://github.com/zesterer/ariadne/blob/e3cb394cb56ecda116a0a1caecd385a49e7f6662/src/draw.rs
/// Characters to be used when drawing when using
-/// [crate::GraphicalReportHandler].
+/// [`GraphicalReportHandler`](crate::GraphicalReportHandler).
#[allow(missing_docs)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ThemeCharacters {
diff --git a/src/highlighters/blank.rs b/src/highlighters/blank.rs
new file mode 100644
index 0000000..50a9c65
--- /dev/null
+++ b/src/highlighters/blank.rs
@@ -0,0 +1,36 @@
+use owo_colors::Style;
+
+use crate::SpanContents;
+
+use super::{Highlighter, HighlighterState};
+
+/// The default syntax highlighter. It applies `Style::default()` to input text.
+/// This is used by default when no syntax highlighting features are enabled.
+#[derive(Debug, Clone)]
+pub struct BlankHighlighter;
+
+impl Highlighter for BlankHighlighter {
+ fn start_highlighter_state<'h>(
+ &'h self,
+ _source: &dyn SpanContents<'_>,
+ ) -> Box {
+ Box::new(BlankHighlighterState)
+ }
+}
+
+impl Default for BlankHighlighter {
+ fn default() -> Self {
+ BlankHighlighter
+ }
+}
+
+/// The default highlighter state. It applies `Style::default()` to input text.
+/// This is used by default when no syntax highlighting features are enabled.
+#[derive(Debug, Clone)]
+pub struct BlankHighlighterState;
+
+impl HighlighterState for BlankHighlighterState {
+ fn highlight_line<'s>(&mut self, line: &'s str) -> Vec> {
+ vec![Style::default().style(line)]
+ }
+}
diff --git a/src/highlighters/mod.rs b/src/highlighters/mod.rs
new file mode 100644
index 0000000..d605c1c
--- /dev/null
+++ b/src/highlighters/mod.rs
@@ -0,0 +1,116 @@
+//! This module provides a trait for creating custom syntax highlighters that
+//! highlight [`Diagnostic`](crate::Diagnostic) source code with ANSI escape
+//! sequences when rendering with the [`GraphicalReportHighlighter`](crate::GraphicalReportHandler).
+//!
+//! It also provides built-in highlighter implementations that you can use out of the box.
+//! By default, there are no syntax highlighters exported by miette
+//! (except for the no-op [`BlankHighlighter`]).
+//! To enable support for specific highlighters, you should enable their associated feature flag.
+//!
+//! Currently supported syntax highlighters and their feature flags:
+//! * `syntect-highlighter` - Enables [`syntect`](https://docs.rs/syntect/latest/syntect/) syntax highlighting support via the [`SyntectHighlighter`]
+//!
+
+use std::{ops::Deref, sync::Arc};
+
+use crate::SpanContents;
+use owo_colors::Styled;
+
+#[cfg(feature = "syntect-highlighter")]
+pub use self::syntect::*;
+pub use blank::*;
+
+mod blank;
+#[cfg(feature = "syntect-highlighter")]
+mod syntect;
+
+/// A syntax highlighter for highlighting miette [`SourceCode`](crate::SourceCode) snippets.
+pub trait Highlighter {
+ /// Creates a new [HighlighterState] to begin parsing and highlighting
+ /// a [SpanContents].
+ ///
+ /// The [GraphicalReportHandler](crate::GraphicalReportHandler) will call
+ /// this method at the start of rendering a [SpanContents].
+ ///
+ /// The [SpanContents] is provided as input only so that the [Highlighter]
+ /// can detect language syntax and make other initialization decisions prior
+ /// to highlighting, but it is not intended that the Highlighter begin
+ /// highlighting at this point. The returned [HighlighterState] is
+ /// responsible for the actual rendering.
+ fn start_highlighter_state<'h>(
+ &'h self,
+ source: &dyn SpanContents<'_>,
+ ) -> Box;
+}
+
+/// A stateful highlighter that incrementally highlights lines of a particular
+/// source code.
+///
+/// The [GraphicalReportHandler](crate::GraphicalReportHandler)
+/// will create a highlighter state by calling
+/// [start_highlighter_state](Highlighter::start_highlighter_state) at the
+/// start of rendering, then it will iteratively call
+/// [highlight_line](HighlighterState::highlight_line) to render individual
+/// highlighted lines. This allows [Highlighter] implementations to maintain
+/// mutable parsing and highlighting state.
+pub trait HighlighterState {
+ /// Highlight an individual line from the source code by returning a vector of [Styled]
+ /// regions.
+ fn highlight_line<'s>(&mut self, line: &'s str) -> Vec>;
+}
+
+/// Arcified trait object for Highlighter. Used internally by [GraphicalReportHandler]
+///
+/// Wrapping the trait object in this way allows us to implement Debug and Clone.
+#[derive(Clone)]
+#[repr(transparent)]
+pub(crate) struct MietteHighlighter(Arc);
+
+impl MietteHighlighter {
+ pub(crate) fn nocolor() -> Self {
+ Self::from(BlankHighlighter)
+ }
+
+ #[cfg(feature = "syntect-highlighter")]
+ pub(crate) fn syntect_truecolor() -> Self {
+ Self::from(SyntectHighlighter::default())
+ }
+}
+
+impl Default for MietteHighlighter {
+ #[cfg(feature = "syntect-highlighter")]
+ fn default() -> Self {
+ use std::io::IsTerminal;
+ match std::env::var("NO_COLOR") {
+ _ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => {
+ //TODO: should use ANSI styling instead of 24-bit truecolor here
+ Self(Arc::new(SyntectHighlighter::default()))
+ }
+ Ok(string) if string != "0" => MietteHighlighter::nocolor(),
+ _ => Self(Arc::new(SyntectHighlighter::default())),
+ }
+ }
+ #[cfg(not(feature = "syntect-highlighter"))]
+ fn default() -> Self {
+ return MietteHighlighter::nocolor();
+ }
+}
+
+impl From for MietteHighlighter {
+ fn from(value: T) -> Self {
+ Self(Arc::new(value))
+ }
+}
+
+impl std::fmt::Debug for MietteHighlighter {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "MietteHighlighter(...)")
+ }
+}
+
+impl Deref for MietteHighlighter {
+ type Target = dyn Highlighter + Send + Sync;
+ fn deref(&self) -> &Self::Target {
+ &*self.0
+ }
+}
diff --git a/src/highlighters/syntect.rs b/src/highlighters/syntect.rs
new file mode 100644
index 0000000..57ebadf
--- /dev/null
+++ b/src/highlighters/syntect.rs
@@ -0,0 +1,170 @@
+use std::path::Path;
+
+// all syntect imports are explicitly qualified, but their paths are shortened for convenience
+mod syntect {
+ pub(super) use syntect::{
+ highlighting::{
+ Color, HighlightIterator, HighlightState, Highlighter, Style, Theme, ThemeSet,
+ },
+ parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
+ };
+}
+
+use owo_colors::{Rgb, Style, Styled};
+
+use crate::{
+ highlighters::{Highlighter, HighlighterState},
+ SpanContents,
+};
+
+use super::BlankHighlighterState;
+
+/// Highlights miette [SourceCode] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
+///
+/// Currently only 24-bit truecolor output is supported due to syntect themes
+/// representing color as RGBA.
+#[derive(Debug, Clone)]
+pub struct SyntectHighlighter {
+ theme: syntect::Theme,
+ syntax_set: syntect::SyntaxSet,
+ use_bg_color: bool,
+}
+
+impl Default for SyntectHighlighter {
+ fn default() -> Self {
+ let theme_set = syntect::ThemeSet::load_defaults();
+ let theme = theme_set.themes["base16-ocean.dark"].clone();
+ Self::new_themed(theme, false)
+ }
+}
+
+impl Highlighter for SyntectHighlighter {
+ fn start_highlighter_state<'h>(
+ &'h self,
+ source: &dyn SpanContents<'_>,
+ ) -> Box {
+ if let Some(syntax) = self.detect_syntax(source) {
+ let highlighter = syntect::Highlighter::new(&self.theme);
+ let parse_state = syntect::ParseState::new(syntax);
+ let highlight_state =
+ syntect::HighlightState::new(&highlighter, syntect::ScopeStack::new());
+ Box::new(SyntectHighlighterState {
+ syntax_set: &self.syntax_set,
+ highlighter,
+ parse_state,
+ highlight_state,
+ use_bg_color: self.use_bg_color,
+ })
+ } else {
+ Box::new(BlankHighlighterState)
+ }
+ }
+}
+
+impl SyntectHighlighter {
+ /// Create a syntect highlighter with the given theme and syntax set.
+ pub fn new(syntax_set: syntect::SyntaxSet, theme: syntect::Theme, use_bg_color: bool) -> Self {
+ Self {
+ theme,
+ syntax_set,
+ use_bg_color,
+ }
+ }
+
+ /// Create a syntect highlighter with the given theme and the default syntax set.
+ pub fn new_themed(theme: syntect::Theme, use_bg_color: bool) -> Self {
+ Self::new(
+ syntect::SyntaxSet::load_defaults_nonewlines(),
+ theme,
+ use_bg_color,
+ )
+ }
+
+ /// Determine syntect SyntaxReference to use for given SourceCode
+ fn detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference> {
+ // use language if given
+ if let Some(language) = contents.language() {
+ return self.syntax_set.find_syntax_by_name(language);
+ }
+ // otherwise try to use any file extension provided in the name
+ if let Some(name) = contents.name() {
+ if let Some(ext) = Path::new(name).extension() {
+ return self
+ .syntax_set
+ .find_syntax_by_extension(ext.to_string_lossy().as_ref());
+ }
+ }
+ // finally, attempt to guess syntax based on first line
+ return self.syntax_set.find_syntax_by_first_line(
+ &std::str::from_utf8(contents.data())
+ .ok()?
+ .split('\n')
+ .next()?,
+ );
+ }
+}
+
+/// Stateful highlighting iterator for [SyntectHighlighter]
+#[derive(Debug)]
+pub(crate) struct SyntectHighlighterState<'h> {
+ syntax_set: &'h syntect::SyntaxSet,
+ highlighter: syntect::Highlighter<'h>,
+ parse_state: syntect::ParseState,
+ highlight_state: syntect::HighlightState,
+ use_bg_color: bool,
+}
+
+impl<'h> HighlighterState for SyntectHighlighterState<'h> {
+ fn highlight_line<'s>(&mut self, line: &'s str) -> Vec> {
+ if let Ok(ops) = self.parse_state.parse_line(line, &self.syntax_set) {
+ let use_bg_color = self.use_bg_color;
+ syntect::HighlightIterator::new(
+ &mut self.highlight_state,
+ &ops,
+ line,
+ &mut self.highlighter,
+ )
+ .map(|(style, str)| (convert_style(style, use_bg_color).style(str)))
+ .collect()
+ } else {
+ vec![Style::default().style(line)]
+ }
+ }
+}
+
+/// Convert syntect [syntect::Style] into owo_colors [Style] */
+#[inline]
+fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
+ if use_bg_color {
+ let fg = blend_fg_color(syntect_style);
+ let bg = convert_color(syntect_style.background);
+ Style::new().color(fg).on_color(bg)
+ } else {
+ let fg = convert_color(syntect_style.foreground);
+ Style::new().color(fg)
+ }
+}
+
+/// Blend foreground RGB into background RGB according to alpha channel
+#[inline]
+fn blend_fg_color(syntect_style: syntect::Style) -> Rgb {
+ let fg = syntect_style.foreground;
+ if fg.a == 0xff {
+ return convert_color(fg);
+ }
+ let bg = syntect_style.background;
+ let ratio = fg.a as u32;
+ let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
+ let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
+ let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
+ Rgb(r as u8, g as u8, b as u8)
+}
+
+/// Convert syntect color into owo color.
+///
+/// Note: ignores alpha channel. use [`blend_fg_color`] if you need that
+///
+#[inline]
+fn convert_color(color: syntect::Color) -> Rgb {
+ Rgb(color.r, color.g, color.b)
+}
diff --git a/src/lib.rs b/src/lib.rs
index cc113ce..eee1b9a 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -43,10 +43,14 @@
//! - [... in `main()`](#-in-main)
//! - [... diagnostic code URLs](#-diagnostic-code-urls)
//! - [... snippets](#-snippets)
+//! - [... help text](#-help-text)
+//! - [... severity level](#-severity-level)
//! - [... multiple related errors](#-multiple-related-errors)
//! - [... delayed source code](#-delayed-source-code)
//! - [... handler options](#-handler-options)
//! - [... dynamic diagnostics](#-dynamic-diagnostics)
+//! - [... syntax highlighting](#-syntax-highlighting)
+//! - [... collection of labels](#-collection-of-labels)
//! - [Acknowledgements](#acknowledgements)
//! - [License](#license)
//!
@@ -109,7 +113,7 @@
//! // The Source that we're gonna be printing snippets out of.
//! // This can be a String if you don't have or care about file names.
//! #[source_code]
-//! src: NamedSource,
+//! src: NamedSource,
//! // Snippets and highlights can be included in the diagnostic!
//! #[label("This bit here")]
//! bad_bit: SourceSpan,
@@ -186,7 +190,7 @@
//!
//! ```rust
//! // lib/error.rs
-//! use miette::Diagnostic;
+//! use miette::{Diagnostic, SourceSpan};
//! use thiserror::Error;
//!
//! #[derive(Error, Diagnostic, Debug)]
@@ -198,6 +202,18 @@
//! #[error("Oops it blew up")]
//! #[diagnostic(code(my_lib::bad_code))]
//! BadThingHappened,
+//!
+//! #[error(transparent)]
+//! // Use `#[diagnostic(transparent)]` to wrap another [`Diagnostic`]. You won't see labels otherwise
+//! #[diagnostic(transparent)]
+//! AnotherError(#[from] AnotherError),
+//! }
+//!
+//! #[derive(Error, Diagnostic, Debug)]
+//! #[error("another error")]
+//! pub struct AnotherError {
+//! #[label("here")]
+//! pub at: SourceSpan
//! }
//! ```
//!
@@ -292,6 +308,23 @@
//! miette = { version = "X.Y.Z", features = ["fancy"] }
//! ```
//!
+//! Another way to display a diagnostic is by printing them using the debug formatter.
+//! This is, in fact, what returning diagnostics from main ends up doing.
+//! To do it yourself, you can write the following:
+//!
+//! ```rust
+//! use miette::{IntoDiagnostic, Result};
+//! use semver::Version;
+//!
+//! fn just_a_random_function() {
+//! let version_result: Result = "1.2.x".parse().into_diagnostic();
+//! match version_result {
+//! Err(e) => println!("{:?}", e),
+//! Ok(version) => println!("{}", version),
+//! }
+//! }
+//! ```
+//!
//! ### ... diagnostic code URLs
//!
//! `miette` supports providing a URL for individual diagnostics. This URL will
@@ -393,7 +426,7 @@
//! }
//! ```
//!
-//! #### ... help text
+//! ### ... help text
//! `miette` provides two facilities for supplying help text for your errors:
//!
//! The first is the `#[help()]` format attribute that applies to structs or
@@ -429,6 +462,19 @@
//! };
//! ```
//!
+//! ### ... severity level
+//! `miette` provides a way to set the severity level of a diagnostic.
+//!
+//! ```rust
+//! use miette::Diagnostic;
+//! use thiserror::Error;
+//!
+//! #[derive(Debug, Diagnostic, Error)]
+//! #[error("welp")]
+//! #[diagnostic(severity(Warning))]
+//! struct Foo;
+//! ```
+//!
//! ### ... multiple related errors
//!
//! `miette` supports collecting multiple errors into a single diagnostic, and
@@ -581,6 +627,7 @@
//! .unicode(false)
//! .context_lines(3)
//! .tab_width(4)
+//! .break_words(true)
//! .build(),
//! )
//! }))
@@ -604,7 +651,7 @@
//!
//! let source = "2 + 2 * 2 = 8".to_string();
//! let report = miette!(
-//! labels = vec[
+//! labels = vec![
//! LabeledSpan::at(12..13, "this should be 6"),
//! ],
//! help = "'*' has greater precedence than '+'",
@@ -613,6 +660,89 @@
//! println!("{:?}", report)
//! ```
//!
+//! ### ... syntax highlighting
+//!
+//! `miette` can be configured to highlight syntax in source code snippets.
+//!
+//!
+//!
+//! To use the built-in highlighting functionality, you must enable the
+//! `syntect-highlighter` crate feature. When this feature is enabled, `miette` will
+//! automatically use the [`syntect`] crate to highlight the `#[source_code]`
+//! field of your [`Diagnostic`].
+//!
+//! Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order:
+//! * [language()](SpanContents::language) - Provides the name of the language
+//! as a string. For example `"Rust"` will indicate Rust syntax highlighting.
+//! You can set the language of the [`SpanContents`] produced by a
+//! [`NamedSource`] via the [`with_language`](NamedSource::with_language)
+//! method.
+//! * [name()](SpanContents::name) - In the absence of an explicitly set
+//! language, the name is assumed to contain a file name or file path.
+//! The highlighter will check for a file extension at the end of the name and
+//! try to guess the syntax from that.
+//!
+//! If you want to use a custom highlighter, you can provide a custom
+//! implementation of the [`Highlighter`](highlighters::Highlighter)
+//! trait to [`MietteHandlerOpts`] by calling the
+//! [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
+//! method. See the [`highlighters`] module docs for more details.
+//!
+//! ### ... collection of labels
+//!
+//! When the number of labels is unknown, you can use a collection of `SourceSpan`
+//! (or any type convertible into `SourceSpan`). For this, add the `collection`
+//! parameter to `label` and use any type than can be iterated over for the field.
+//!
+//! ```rust,ignore
+//! #[derive(Debug, Diagnostic, Error)]
+//! #[error("oops!")]
+//! struct MyError {
+//! #[label("main issue")]
+//! primary_span: SourceSpan,
+//!
+//! #[label(collection, "related to this")]
+//! other_spans: Vec>,
+//! }
+//!
+//! let report: miette::Report = MyError {
+//! primary_span: (6, 9).into(),
+//! other_spans: vec![19..26, 30..41],
+//! }.into();
+//!
+//! println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
+//! ```
+//!
+//! A collection can also be of `LabeledSpan` if you want to have different text
+//! for different labels. Labels with no text will use the one from the `label`
+//! attribute
+//!
+//! ```rust,ignore
+//! #[derive(Debug, Diagnostic, Error)]
+//! #[error("oops!")]
+//! struct MyError {
+//! #[label("main issue")]
+//! primary_span: SourceSpan,
+//!
+//! #[label(collection, "related to this")]
+//! other_spans: Vec, // LabeledSpan
+//! }
+//!
+//! let report: miette::Report = MyError {
+//! primary_span: (6, 9).into(),
+//! other_spans: vec![
+//! LabeledSpan::new(None, 19, 7), // Use default text `related to this`
+//! LabeledSpan::new(Some("and also this".to_string()), 30, 11), // Use specific text
+//! ],
+//! }.into();
+//!
+//! println!("{:?}", report.with_source_code("About something or another or yet another ...".to_string()));
+//! ```
+//!
+//! ## MSRV
+//!
+//! This crate requires rustc 1.70.0 or later.
+//!
//! ## Acknowledgements
//!
//! `miette` was not developed in a void. It owes enormous credit to various
@@ -640,11 +770,12 @@
//! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
//! under the Apache License. Some code is taken from
//! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
+#[cfg(feature = "derive")]
pub use miette_derive::*;
pub use error::*;
pub use eyreish::*;
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
pub use handler::*;
pub use handlers::*;
pub use miette_diagnostic::*;
@@ -657,9 +788,11 @@ mod chain;
mod diagnostic_chain;
mod error;
mod eyreish;
-#[cfg(feature = "fancy-no-backtrace")]
+#[cfg(feature = "fancy-base")]
mod handler;
mod handlers;
+#[cfg(feature = "fancy-base")]
+pub mod highlighters;
#[doc(hidden)]
pub mod macro_helpers;
mod miette_diagnostic;
diff --git a/src/macro_helpers.rs b/src/macro_helpers.rs
index 5520899..157f2b3 100644
--- a/src/macro_helpers.rs
+++ b/src/macro_helpers.rs
@@ -1,3 +1,5 @@
+use crate::protocol::{LabeledSpan, SourceSpan};
+
// Huge thanks to @jam1gamer for this hack:
// https://twitter.com/jam1garner/status/1515887996444323840
@@ -36,3 +38,24 @@ impl ToOption for &OptionalWrapper {
Some(value)
}
}
+
+#[doc(hidden)]
+#[derive(Debug)]
+pub struct ToLabelSpanWrapper {}
+pub trait ToLabeledSpan {
+ #[doc(hidden)]
+ fn to_labeled_span(span: T) -> LabeledSpan;
+}
+impl ToLabeledSpan for ToLabelSpanWrapper {
+ fn to_labeled_span(span: LabeledSpan) -> LabeledSpan {
+ span
+ }
+}
+impl ToLabeledSpan for ToLabelSpanWrapper
+where
+ T: Into,
+{
+ fn to_labeled_span(span: T) -> LabeledSpan {
+ LabeledSpan::new_with_span(None, span.into())
+ }
+}
diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs
index 67b75d0..9863e88 100644
--- a/src/miette_diagnostic.rs
+++ b/src/miette_diagnostic.rs
@@ -252,7 +252,7 @@ impl MietteDiagnostic {
/// ```
pub fn and_labels(mut self, labels: impl IntoIterator- ) -> Self {
let mut all_labels = self.labels.unwrap_or_default();
- all_labels.extend(labels.into_iter());
+ all_labels.extend(labels);
self.labels = Some(all_labels);
self
}
@@ -292,14 +292,16 @@ fn test_serialize_miette_diagnostic() {
"offset": 0,
"length": 0
},
- "label": "label1"
+ "label": "label1",
+ "primary": false
},
{
"span": {
"offset": 1,
"length": 2
},
- "label": "label2"
+ "label": "label2",
+ "primary": false
}
]
});
@@ -350,14 +352,16 @@ fn test_deserialize_miette_diagnostic() {
"offset": 0,
"length": 0
},
- "label": "label1"
+ "label": "label1",
+ "primary": false
},
{
"span": {
"offset": 1,
"length": 2
},
- "label": "label2"
+ "label": "label2",
+ "primary": false
}
]
});
diff --git a/src/named_source.rs b/src/named_source.rs
index 31ad1d1..ea11cd2 100644
--- a/src/named_source.rs
+++ b/src/named_source.rs
@@ -3,27 +3,34 @@ use crate::{MietteError, MietteSpanContents, SourceCode, SpanContents};
/// Utility struct for when you have a regular [`SourceCode`] type that doesn't
/// implement `name`. For example [`String`]. Or if you want to override the
/// `name` returned by the `SourceCode`.
-pub struct NamedSource {
- source: Box
,
+#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
+pub struct NamedSource {
+ source: S,
name: String,
+ language: Option,
}
-impl std::fmt::Debug for NamedSource {
+impl std::fmt::Debug for NamedSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("NamedSource")
.field("name", &self.name)
- .field("source", &"");
+ .field("source", &"")
+ .field("language", &self.language);
Ok(())
}
}
-impl NamedSource {
+impl NamedSource {
/// Create a new `NamedSource` using a regular [`SourceCode`] and giving
/// its returned [`SpanContents`] a name.
- pub fn new(name: impl AsRef, source: impl SourceCode + Send + Sync + 'static) -> Self {
+ pub fn new(name: impl AsRef, source: S) -> Self
+ where
+ S: Send + Sync,
+ {
Self {
- source: Box::new(source),
+ source,
name: name.as_ref().to_string(),
+ language: None,
}
}
@@ -34,28 +41,38 @@ impl NamedSource {
/// Returns a reference the inner [`SourceCode`] type for this
/// `NamedSource`.
- pub fn inner(&self) -> &(dyn SourceCode + 'static) {
- &*self.source
+ pub fn inner(&self) -> &S {
+ &self.source
+ }
+
+ /// Sets the [`language`](SpanContents::language) for this source code.
+ pub fn with_language(mut self, language: impl Into) -> Self {
+ self.language = Some(language.into());
+ self
}
}
-impl SourceCode for NamedSource {
+impl SourceCode for NamedSource {
fn read_span<'a>(
&'a self,
span: &crate::SourceSpan,
context_lines_before: usize,
context_lines_after: usize,
) -> Result + 'a>, MietteError> {
- let contents = self
- .inner()
- .read_span(span, context_lines_before, context_lines_after)?;
- Ok(Box::new(MietteSpanContents::new_named(
+ let inner_contents =
+ self.inner()
+ .read_span(span, context_lines_before, context_lines_after)?;
+ let mut contents = MietteSpanContents::new_named(
self.name.clone(),
- contents.data(),
- *contents.span(),
- contents.line(),
- contents.column(),
- contents.line_count(),
- )))
+ inner_contents.data(),
+ *inner_contents.span(),
+ inner_contents.line(),
+ inner_contents.column(),
+ inner_contents.line_count(),
+ );
+ if let Some(language) = &self.language {
+ contents = contents.with_language(language);
+ }
+ Ok(Box::new(contents))
}
}
diff --git a/src/protocol.rs b/src/protocol.rs
index 7531db1..589cd0b 100644
--- a/src/protocol.rs
+++ b/src/protocol.rs
@@ -69,16 +69,45 @@ pub trait Diagnostic: std::error::Error {
}
}
-impl std::error::Error for Box {
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- (**self).source()
- }
+macro_rules! box_error_impls {
+ ($($box_type:ty),*) => {
+ $(
+ impl std::error::Error for $box_type {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ (**self).source()
+ }
- fn cause(&self) -> Option<&dyn std::error::Error> {
- self.source()
+ fn cause(&self) -> Option<&dyn std::error::Error> {
+ self.source()
+ }
+ }
+ )*
}
}
+box_error_impls! {
+ Box,
+ Box,
+ Box
+}
+
+macro_rules! box_borrow_impls {
+ ($($box_type:ty),*) => {
+ $(
+ impl std::borrow::Borrow for $box_type {
+ fn borrow(&self) -> &(dyn Diagnostic + 'static) {
+ self.as_ref()
+ }
+ }
+ )*
+ }
+}
+
+box_borrow_impls! {
+ Box,
+ Box
+}
+
impl From
for Box
{
@@ -167,6 +196,7 @@ impl From> for Box Self {
- Severity::Error
- }
-}
-
#[cfg(feature = "serde")]
#[test]
fn test_serialize_severity() {
@@ -220,7 +245,7 @@ whole thing--meaning you should be able to support `SourceCode`s which are
gigabytes or larger in size.
*/
pub trait SourceCode: Send + Sync {
- /// Read the bytes for a specific span from this SourceCode, keeping a
+ /// Read the bytes for a specific span from this `SourceCode`, keeping a
/// certain number of lines before and after the span as context.
fn read_span<'a>(
&'a self,
@@ -237,6 +262,7 @@ pub struct LabeledSpan {
#[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))]
label: Option,
span: SourceSpan,
+ primary: bool,
}
impl LabeledSpan {
@@ -244,7 +270,8 @@ impl LabeledSpan {
pub const fn new(label: Option, offset: ByteOffset, len: usize) -> Self {
Self {
label,
- span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)),
+ span: SourceSpan::new(SourceOffset(offset), len),
+ primary: false,
}
}
@@ -253,9 +280,24 @@ impl LabeledSpan {
Self {
label,
span: span.into(),
+ primary: false,
}
}
+ /// Makes a new labeled primary span using an existing span.
+ pub fn new_primary_with_span(label: Option, span: impl Into) -> Self {
+ Self {
+ label,
+ span: span.into(),
+ primary: true,
+ }
+ }
+
+ /// Change the text of the label
+ pub fn set_label(&mut self, label: Option) {
+ self.label = label;
+ }
+
/// Makes a new label at specified span
///
/// # Examples
@@ -328,6 +370,11 @@ impl LabeledSpan {
pub const fn is_empty(&self) -> bool {
self.span.is_empty()
}
+
+ /// True if this `LabeledSpan` is a primary span.
+ pub const fn primary(&self) -> bool {
+ self.primary
+ }
}
#[cfg(feature = "serde")]
@@ -338,7 +385,8 @@ fn test_serialize_labeled_span() {
assert_eq!(
json!(LabeledSpan::new(None, 0, 0)),
json!({
- "span": { "offset": 0, "length": 0 }
+ "span": { "offset": 0, "length": 0, },
+ "primary": false,
})
);
@@ -346,9 +394,10 @@ fn test_serialize_labeled_span() {
json!(LabeledSpan::new(Some("label".to_string()), 0, 0)),
json!({
"label": "label",
- "span": { "offset": 0, "length": 0 }
+ "span": { "offset": 0, "length": 0, },
+ "primary": false,
})
- )
+ );
}
#[cfg(feature = "serde")]
@@ -358,23 +407,26 @@ fn test_deserialize_labeled_span() {
let span: LabeledSpan = serde_json::from_value(json!({
"label": null,
- "span": { "offset": 0, "length": 0 }
+ "span": { "offset": 0, "length": 0, },
+ "primary": false,
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
- "span": { "offset": 0, "length": 0 }
+ "span": { "offset": 0, "length": 0, },
+ "primary": false
}))
.unwrap();
assert_eq!(span, LabeledSpan::new(None, 0, 0));
let span: LabeledSpan = serde_json::from_value(json!({
"label": "label",
- "span": { "offset": 0, "length": 0 }
+ "span": { "offset": 0, "length": 0, },
+ "primary": false
}))
.unwrap();
- assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0))
+ assert_eq!(span, LabeledSpan::new(Some("label".to_string()), 0, 0));
}
/**
@@ -399,6 +451,15 @@ pub trait SpanContents<'a> {
fn column(&self) -> usize;
/// Total number of lines covered by this `SpanContents`.
fn line_count(&self) -> usize;
+
+ /// Optional method. The language name for this source code, if any.
+ /// This is used to drive syntax highlighting.
+ ///
+ /// Examples: Rust, TOML, C
+ ///
+ fn language(&self) -> Option<&str> {
+ None
+ }
}
/**
@@ -418,6 +479,8 @@ pub struct MietteSpanContents<'a> {
line_count: usize,
// Optional filename
name: Option,
+ // Optional language
+ language: Option,
}
impl<'a> MietteSpanContents<'a> {
@@ -436,6 +499,7 @@ impl<'a> MietteSpanContents<'a> {
column,
line_count,
name: None,
+ language: None,
}
}
@@ -455,8 +519,15 @@ impl<'a> MietteSpanContents<'a> {
column,
line_count,
name: Some(name),
+ language: None,
}
}
+
+ /// Sets the [`language`](SourceCode::language) for syntax highlighting.
+ pub fn with_language(mut self, language: impl Into) -> Self {
+ self.language = Some(language.into());
+ self
+ }
}
impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
@@ -478,10 +549,13 @@ impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
fn name(&self) -> Option<&str> {
self.name.as_deref()
}
+ fn language(&self) -> Option<&str> {
+ self.language.as_deref()
+ }
}
/// Span within a [`SourceCode`]
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SourceSpan {
/// The start of the span.
@@ -492,10 +566,10 @@ pub struct SourceSpan {
impl SourceSpan {
/// Create a new [`SourceSpan`].
- pub const fn new(start: SourceOffset, length: SourceOffset) -> Self {
+ pub const fn new(start: SourceOffset, length: usize) -> Self {
Self {
offset: start,
- length: length.offset(),
+ length,
}
}
@@ -525,8 +599,8 @@ impl From<(ByteOffset, usize)> for SourceSpan {
}
}
-impl From<(SourceOffset, SourceOffset)> for SourceSpan {
- fn from((start, len): (SourceOffset, SourceOffset)) -> Self {
+impl From<(SourceOffset, usize)> for SourceSpan {
+ fn from((start, len): (SourceOffset, usize)) -> Self {
Self::new(start, len)
}
}
@@ -563,7 +637,7 @@ fn test_serialize_source_span() {
assert_eq!(
json!(SourceSpan::from(0)),
json!({ "offset": 0, "length": 0})
- )
+ );
}
#[cfg(feature = "serde")]
@@ -572,7 +646,7 @@ fn test_deserialize_source_span() {
use serde_json::json;
let span: SourceSpan = serde_json::from_value(json!({ "offset": 0, "length": 0})).unwrap();
- assert_eq!(span, SourceSpan::from(0))
+ assert_eq!(span, SourceSpan::from(0));
}
/**
@@ -583,7 +657,7 @@ pub type ByteOffset = usize;
/**
Newtype that represents the [`ByteOffset`] from the beginning of a [`SourceCode`]
*/
-#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
+#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SourceOffset(ByteOffset);
@@ -674,12 +748,12 @@ fn test_source_offset_from_location() {
fn test_serialize_source_offset() {
use serde_json::json;
- assert_eq!(json!(SourceOffset::from(0)), 0)
+ assert_eq!(json!(SourceOffset::from(0)), 0);
}
#[cfg(feature = "serde")]
#[test]
fn test_deserialize_source_offset() {
let offset: SourceOffset = serde_json::from_str("0").unwrap();
- assert_eq!(offset, SourceOffset::from(0))
+ assert_eq!(offset, SourceOffset::from(0));
}
diff --git a/src/source_impls.rs b/src/source_impls.rs
index e43a665..e362b4a 100644
--- a/src/source_impls.rs
+++ b/src/source_impls.rs
@@ -1,12 +1,7 @@
/*!
Default trait implementations for [`SourceCode`].
*/
-use std::{
- borrow::{Cow, ToOwned},
- collections::VecDeque,
- fmt::Debug,
- sync::Arc,
-};
+use std::{borrow::Cow, collections::VecDeque, fmt::Debug, sync::Arc};
use crate::{MietteError, MietteSpanContents, SourceCode, SourceSpan, SpanContents};
diff --git a/tests/color_format.rs b/tests/color_format.rs
index bb90708..95d40e0 100644
--- a/tests/color_format.rs
+++ b/tests/color_format.rs
@@ -3,6 +3,7 @@
use lazy_static::lazy_static;
use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors};
use regex::Regex;
+use std::ffi::OsString;
use std::fmt::{self, Debug};
use std::sync::Mutex;
use thiserror::Error;
@@ -42,16 +43,29 @@ fn color_format(handler: MietteHandler) -> ColorFormat {
}
}
-/// Runs a function with an environment variable set to a specific value, then
-/// sets it back to it's original value once completed.
-fn with_env_var(var: &str, value: &str, body: F) {
- let old_value = std::env::var_os(var);
- std::env::set_var(var, value);
- body();
- if let Some(old_value) = old_value {
- std::env::set_var(var, old_value);
- } else {
- std::env::remove_var(var);
+/// Store the current value of an environment variable on construction, and then
+/// restore that value when the guard is dropped.
+struct EnvVarGuard<'a> {
+ var: &'a str,
+ old_value: Option,
+}
+
+impl EnvVarGuard<'_> {
+ fn new(var: &str) -> EnvVarGuard<'_> {
+ EnvVarGuard {
+ var,
+ old_value: std::env::var_os(var),
+ }
+ }
+}
+
+impl Drop for EnvVarGuard<'_> {
+ fn drop(&mut self) {
+ if let Some(old_value) = &self.old_value {
+ std::env::set_var(self.var, old_value);
+ } else {
+ std::env::remove_var(self.var);
+ }
}
}
@@ -72,22 +86,33 @@ fn check_colors MietteHandlerOpts>(
//
// Since environment variables are shared for the entire process, we need
// to ensure that only one test that modifies these env vars runs at a time.
- let guard = COLOR_ENV_VARS.lock().unwrap();
+ let lock = COLOR_ENV_VARS.lock().unwrap();
- with_env_var("NO_COLOR", "1", || {
- let handler = make_handler(MietteHandlerOpts::new()).build();
- assert_eq!(color_format(handler), no_support);
- });
- with_env_var("FORCE_COLOR", "1", || {
- let handler = make_handler(MietteHandlerOpts::new()).build();
- assert_eq!(color_format(handler), ansi_support);
- });
- with_env_var("FORCE_COLOR", "3", || {
- let handler = make_handler(MietteHandlerOpts::new()).build();
- assert_eq!(color_format(handler), rgb_support);
- });
+ let guards = (
+ EnvVarGuard::new("NO_COLOR"),
+ EnvVarGuard::new("FORCE_COLOR"),
+ );
+ // Clear color environment variables that may be set outside of 'cargo test'
+ std::env::remove_var("NO_COLOR");
+ std::env::remove_var("FORCE_COLOR");
- drop(guard);
+ std::env::set_var("NO_COLOR", "1");
+ let handler = make_handler(MietteHandlerOpts::new()).build();
+ assert_eq!(color_format(handler), no_support);
+ std::env::remove_var("NO_COLOR");
+
+ std::env::set_var("FORCE_COLOR", "1");
+ let handler = make_handler(MietteHandlerOpts::new()).build();
+ assert_eq!(color_format(handler), ansi_support);
+ std::env::remove_var("FORCE_COLOR");
+
+ std::env::set_var("FORCE_COLOR", "3");
+ let handler = make_handler(MietteHandlerOpts::new()).build();
+ assert_eq!(color_format(handler), rgb_support);
+ std::env::remove_var("FORCE_COLOR");
+
+ drop(guards);
+ drop(lock);
}
#[test]
diff --git a/tests/derive.rs b/tests/derive.rs
index 7faae42..aa631dc 100644
--- a/tests/derive.rs
+++ b/tests/derive.rs
@@ -6,12 +6,14 @@ fn related() {
#[derive(Error, Debug, Diagnostic)]
#[error("welp")]
#[diagnostic(code(foo::bar::baz))]
+ #[allow(dead_code)]
struct Foo {
#[related]
related: Vec,
}
#[derive(Error, Debug, Diagnostic)]
+ #[allow(dead_code)]
enum Bar {
#[error("variant1")]
#[diagnostic(code(foo::bar::baz))]
@@ -29,6 +31,7 @@ fn related() {
#[derive(Error, Debug, Diagnostic)]
#[error("welp2")]
+ #[allow(dead_code)]
struct Baz;
}
@@ -37,6 +40,7 @@ fn related_report() {
#[derive(Error, Debug, Diagnostic)]
#[error("welp")]
#[diagnostic(code(foo::bar::baz))]
+ #[allow(dead_code)]
struct Foo {
#[related]
related: Vec,
@@ -189,7 +193,7 @@ fn fmt_help() {
assert_eq!(
"1 x hello x \"2\"".to_string(),
- FooStruct("hello".into()).help().unwrap().to_string()
+ FooStruct("hello").help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@@ -201,12 +205,7 @@ fn fmt_help() {
assert_eq!(
"1 x hello x \"2\"".to_string(),
- BarStruct {
- my_field: "hello".into()
- }
- .help()
- .unwrap()
- .to_string()
+ BarStruct { my_field: "hello" }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@@ -224,7 +223,7 @@ fn fmt_help() {
assert_eq!(
"1 x bar x \"2\"".to_string(),
- FooEnum::X("bar".into()).help().unwrap().to_string()
+ FooEnum::X("bar").help().unwrap().to_string()
);
assert_eq!(
@@ -250,12 +249,7 @@ fn help_field() {
assert_eq!(
"x".to_string(),
- Foo {
- do_this: Some("x".into())
- }
- .help()
- .unwrap()
- .to_string()
+ Foo { do_this: Some("x") }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@@ -271,16 +265,11 @@ fn help_field() {
assert_eq!(
"x".to_string(),
- Bar::A(Some("x".into())).help().unwrap().to_string()
+ Bar::A(Some("x")).help().unwrap().to_string()
);
assert_eq!(
"x".to_string(),
- Bar::B {
- do_this: Some("x".into())
- }
- .help()
- .unwrap()
- .to_string()
+ Bar::B { do_this: Some("x") }.help().unwrap().to_string()
);
#[derive(Debug, Diagnostic, Error)]
@@ -288,20 +277,14 @@ fn help_field() {
#[diagnostic()]
struct Baz<'a>(#[help] Option<&'a str>);
- assert_eq!(
- "x".to_string(),
- Baz(Some("x".into())).help().unwrap().to_string()
- );
+ assert_eq!("x".to_string(), Baz(Some("x")).help().unwrap().to_string());
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic()]
struct Quux<'a>(#[help] &'a str);
- assert_eq!(
- "x".to_string(),
- Quux("x".into()).help().unwrap().to_string()
- );
+ assert_eq!("x".to_string(), Quux("x").help().unwrap().to_string());
}
#[test]
@@ -309,6 +292,7 @@ fn test_snippet_named_struct() {
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(code(foo::bar::baz))]
+ #[allow(dead_code)]
struct Foo<'a> {
#[source_code]
src: &'a str,
@@ -331,6 +315,7 @@ fn test_snippet_unnamed_struct() {
#[derive(Debug, Diagnostic, Error)]
#[error("welp")]
#[diagnostic(code(foo::bar::baz))]
+ #[allow(dead_code)]
struct Foo<'a>(
#[source_code] &'a str,
#[label("{0}")] SourceSpan,
@@ -427,7 +412,7 @@ impl ForwardsTo {
fn new() -> Self {
ForwardsTo {
src: SNIPPET_TEXT.into(),
- label: SourceSpan::new(11.into(), 6.into()),
+ label: SourceSpan::new(11.into(), 6),
}
}
}
@@ -589,7 +574,7 @@ fn test_unit_struct_display() {
#[error("unit only")]
#[diagnostic(code(foo::bar::overridden), help("hello from unit help"))]
struct UnitOnly;
- assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help")
+ assert_eq!(UnitOnly.help().unwrap().to_string(), "hello from unit help");
}
#[test]
@@ -603,5 +588,47 @@ fn test_unit_enum_display() {
assert_eq!(
Enum::UnitVariant.help().unwrap().to_string(),
"hello from unit help"
- )
+ );
+}
+
+#[test]
+fn test_optional_source_code() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("struct with optional source")]
+ struct Struct {
+ #[source_code]
+ src: Option,
+ }
+ assert!(Struct { src: None }.source_code().is_none());
+ assert!(Struct {
+ src: Some("".to_string())
+ }
+ .source_code()
+ .is_some());
+
+ #[derive(Debug, Diagnostic, Error)]
+ enum Enum {
+ #[error("variant1 with optional source")]
+ Variant1 {
+ #[source_code]
+ src: Option,
+ },
+ #[error("variant2 with optional source")]
+ Variant2 {
+ #[source_code]
+ src: Option,
+ },
+ }
+ assert!(Enum::Variant1 { src: None }.source_code().is_none());
+ assert!(Enum::Variant1 {
+ src: Some("".to_string())
+ }
+ .source_code()
+ .is_some());
+ assert!(Enum::Variant2 { src: None }.source_code().is_none());
+ assert!(Enum::Variant2 {
+ src: Some("".to_string())
+ }
+ .source_code()
+ .is_some());
}
diff --git a/tests/graphical.rs b/tests/graphical.rs
index 0c69470..c6b5e1a 100644
--- a/tests/graphical.rs
+++ b/tests/graphical.rs
@@ -21,12 +21,14 @@ fn fmt_report(diag: Report) -> String {
.unwrap();
} else if let Ok(w) = std::env::var("REPLACE_TABS") {
GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
+ .without_syntax_highlighting()
.with_width(80)
.tab_width(w.parse().expect("Invalid tab width."))
.render_report(&mut out, diag.as_ref())
.unwrap();
} else {
GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
+ .without_syntax_highlighting()
.with_width(80)
.render_report(&mut out, diag.as_ref())
.unwrap();
@@ -34,6 +36,380 @@ fn fmt_report(diag: Report) -> String {
out
}
+fn fmt_report_with_settings(
+ diag: Report,
+ with_settings: fn(GraphicalReportHandler) -> GraphicalReportHandler,
+) -> String {
+ let mut out = String::new();
+
+ let handler = with_settings(GraphicalReportHandler::new_themed(
+ GraphicalTheme::unicode_nocolor(),
+ ));
+
+ handler.render_report(&mut out, diag.as_ref()).unwrap();
+
+ println!("Error:\n```\n{}\n```", out);
+
+ out
+}
+
+#[test]
+fn word_wrap_options() -> Result<(), MietteError> {
+ // By default, a long word should not break
+ let out =
+ fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| handler);
+
+ let expected = "\n × abcdefghijklmnopqrstuvwxyz\n".to_string();
+ assert_eq!(expected, out);
+
+ // A long word can break with a smaller width
+ let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
+ handler.with_width(10)
+ });
+ let expected = r#"
+ × abcd
+ │ efgh
+ │ ijkl
+ │ mnop
+ │ qrst
+ │ uvwx
+ │ yz
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Unless, word breaking is disabled
+ let out = fmt_report_with_settings(Report::msg("abcdefghijklmnopqrstuvwxyz"), |handler| {
+ handler.with_width(10).with_break_words(false)
+ });
+ let expected = "\n × abcdefghijklmnopqrstuvwxyz\n".to_string();
+ assert_eq!(expected, out);
+
+ // Breaks should start at the boundary of each word if possible
+ let out = fmt_report_with_settings(
+ Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
+ |handler| handler.with_width(10),
+ );
+ let expected = r#"
+ × 12
+ │ 123
+ │ 1234
+ │ 1234
+ │ 5
+ │ 1234
+ │ 56
+ │ 1234
+ │ 567
+ │ 1234
+ │ 5678
+ │ 90
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // But long words should not break if word breaking is disabled
+ let out = fmt_report_with_settings(
+ Report::msg("12 123 1234 12345 123456 1234567 1234567890"),
+ |handler| handler.with_width(10).with_break_words(false),
+ );
+ let expected = r#"
+ × 12
+ │ 123
+ │ 1234
+ │ 12345
+ │ 123456
+ │ 1234567
+ │ 1234567890
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Unless, of course, there are hyphens
+ let out = fmt_report_with_settings(
+ Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
+ |handler| handler.with_width(10).with_break_words(false),
+ );
+ let expected = r#"
+ × a-b
+ │ a-b-
+ │ c a-
+ │ b-c-
+ │ d a-
+ │ b-c-
+ │ d-e
+ │ a-b-
+ │ c-d-
+ │ e-f
+ │ a-b-
+ │ c-d-
+ │ e-f-
+ │ g a-
+ │ b-c-
+ │ d-e-
+ │ f-g-
+ │ h
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Which requires an additional opt-out
+ let out = fmt_report_with_settings(
+ Report::msg("a-b a-b-c a-b-c-d a-b-c-d-e a-b-c-d-e-f a-b-c-d-e-f-g a-b-c-d-e-f-g-h"),
+ |handler| {
+ handler
+ .with_width(10)
+ .with_break_words(false)
+ .with_word_splitter(textwrap::WordSplitter::NoHyphenation)
+ },
+ );
+ let expected = r#"
+ × a-b
+ │ a-b-c
+ │ a-b-c-d
+ │ a-b-c-d-e
+ │ a-b-c-d-e-f
+ │ a-b-c-d-e-f-g
+ │ a-b-c-d-e-f-g-h
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Or if there are _other_ unicode word boundaries
+ let out = fmt_report_with_settings(
+ Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
+ |handler| handler.with_width(10).with_break_words(false),
+ );
+ let expected = r#"
+ × a/b
+ │ a/b/
+ │ c a/
+ │ b/c/
+ │ d a/
+ │ b/c/
+ │ d/e
+ │ a/b/
+ │ c/d/
+ │ e/f
+ │ a/b/
+ │ c/d/
+ │ e/f/
+ │ g a/
+ │ b/c/
+ │ d/e/
+ │ f/g/
+ │ h
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Such things require you to opt-in to only breaking on ASCII whitespace
+ let out = fmt_report_with_settings(
+ Report::msg("a/b a/b/c a/b/c/d a/b/c/d/e a/b/c/d/e/f a/b/c/d/e/f/g a/b/c/d/e/f/g/h"),
+ |handler| {
+ handler
+ .with_width(10)
+ .with_break_words(false)
+ .with_word_separator(textwrap::WordSeparator::AsciiSpace)
+ },
+ );
+ let expected = r#"
+ × a/b
+ │ a/b/c
+ │ a/b/c/d
+ │ a/b/c/d/e
+ │ a/b/c/d/e/f
+ │ a/b/c/d/e/f/g
+ │ a/b/c/d/e/f/g/h
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ Ok(())
+}
+
+#[test]
+fn wrap_option() -> Result<(), MietteError> {
+ // A line should break on the width
+ let out = fmt_report_with_settings(
+ Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+ |handler| handler.with_width(15),
+ );
+ let expected = r#"
+ × abc def
+ │ ghi jkl
+ │ mno pqr
+ │ stu vwx
+ │ yz abc
+ │ def ghi
+ │ jkl mno
+ │ pqr stu
+ │ vwx yz
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ // Unless, wrapping is disabled
+ let out = fmt_report_with_settings(
+ Report::msg("abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz"),
+ |handler| handler.with_width(15).with_wrap_lines(false),
+ );
+ let expected =
+ "\n × abc def ghi jkl mno pqr stu vwx yz abc def ghi jkl mno pqr stu vwx yz\n".to_string();
+ assert_eq!(expected, out);
+
+ // Then, user-defined new lines should be preserved wrapping is disabled
+ let out = fmt_report_with_settings(
+ Report::msg("abc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz\nabc def ghi jkl mno pqr stu vwx yz"),
+ |handler| handler.with_width(15).with_wrap_lines(false),
+ );
+ let expected = r#"
+ × abc def ghi jkl mno pqr stu vwx yz
+ │ abc def ghi jkl mno pqr stu vwx yz
+ │ abc def ghi jkl mno pqr stu vwx yz
+"#
+ .to_string();
+ assert_eq!(expected, out);
+
+ Ok(())
+}
+
+#[test]
+fn wrapping_nested_errors() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("This is the parent error, the error withhhhh the children, kiddos, pups, as it were, and so on...")]
+ #[diagnostic(
+ code(mama::error),
+ help(
+ "try doing it better next time? I mean, you could have also done better thisssss time, but no?"
+ )
+ )]
+ struct MamaError {
+ #[diagnostic_source]
+ baby: BabyError,
+ }
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Wah wah: I may be small, but I'll cause a proper bout of trouble — justt try wrapping this mess of a line, buddo!")]
+ #[diagnostic(
+ code(baby::error),
+ help(
+ "it cannot be helped... woulddddddd you really want to get rid of an error that's so cute?"
+ )
+ )]
+ struct BabyError;
+
+ let err = MamaError { baby: BabyError };
+ let out = fmt_report_with_settings(err.into(), |handler| handler.with_width(50));
+ let expected = r#"mama::error
+
+ × This is the parent error, the error withhhhh
+ │ the children, kiddos, pups, as it were, and
+ │ so on...
+ ╰─▶ baby::error
+
+ × Wah wah: I may be small, but I'll
+ │ cause a proper bout of trouble — justt
+ │ try wrapping this mess of a line,
+ │ buddo!
+ help: it cannot be helped... woulddddddd
+ you really want to get rid of an
+ error that's so cute?
+
+ help: try doing it better next time? I mean,
+ you could have also done better thisssss
+ time, but no?
+"#;
+ assert_eq!(expected, out);
+ Ok(())
+}
+
+#[test]
+fn wrapping_related_errors() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("This is the parent error, the error withhhhh the children, kiddos, pups, as it were, and so on...")]
+ #[diagnostic(
+ code(mama::error),
+ help(
+ "try doing it better next time? I mean, you could have also done better thisssss time, but no?"
+ )
+ )]
+ struct MamaError {
+ #[diagnostic_source]
+ baby: BrotherError,
+ }
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Welcome to the brother-error brotherhood — where all of the wee baby errors join into a formidable force")]
+ #[diagnostic(code(brother::error))]
+ struct BrotherError {
+ #[related]
+ brethren: Vec>,
+ }
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Wah wah: I may be small, but I'll cause a proper bout of trouble — justt try wrapping this mess of a line, buddo!")]
+ #[diagnostic(help(
+ "it cannot be helped... woulddddddd you really want to get rid of an error that's so cute?"
+ ))]
+ struct BabyError;
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Wah wah: I may be small, but I'll cause a proper bout of trouble — justt try wrapping this mess of a line, buddo!")]
+ #[diagnostic(severity(Warning))]
+ struct BabyWarning;
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Wah wah: I may be small, but I'll cause a proper bout of trouble — justt try wrapping this mess of a line, buddo!")]
+ #[diagnostic(severity(Advice))]
+ struct BabyAdvice;
+
+ let err = MamaError {
+ baby: BrotherError {
+ brethren: vec![BabyError.into(), BabyWarning.into(), BabyAdvice.into()],
+ },
+ };
+ let out = fmt_report_with_settings(err.into(), |handler| handler.with_width(50));
+ let expected = r#"mama::error
+
+ × This is the parent error, the error withhhhh
+ │ the children, kiddos, pups, as it were, and
+ │ so on...
+ ╰─▶ brother::error
+
+ × Welcome to the brother-error
+ │ brotherhood — where all of the wee
+ │ baby errors join into a formidable
+ │ force
+
+ Error:
+ × Wah wah: I may be small, but I'll
+ │ cause a proper bout of trouble — justt
+ │ try wrapping this mess of a line,
+ │ buddo!
+ help: it cannot be helped... woulddddddd
+ you really want to get rid of an
+ error that's so cute?
+
+ Warning:
+ ⚠ Wah wah: I may be small, but I'll
+ │ cause a proper bout of trouble — justt
+ │ try wrapping this mess of a line,
+ │ buddo!
+
+ Advice:
+ ☞ Wah wah: I may be small, but I'll
+ │ cause a proper bout of trouble — justt
+ │ try wrapping this mess of a line,
+ │ buddo!
+
+ help: try doing it better next time? I mean,
+ you could have also done better thisssss
+ time, but no?
+"#;
+ assert_eq!(expected, out);
+ Ok(())
+}
+
#[test]
fn empty_source() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@@ -41,7 +417,7 @@ fn empty_source() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -67,6 +443,53 @@ fn empty_source() -> Result<(), MietteError> {
Ok(())
}
+#[test]
+fn multiple_spans_multiline() {
+ #[derive(Error, Debug, Diagnostic)]
+ #[error("oops!")]
+ #[diagnostic(severity(Error))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource<&'static str>,
+ #[label("big")]
+ big: SourceSpan,
+ #[label("small")]
+ small: SourceSpan,
+ }
+ let err = MyBad {
+ src: NamedSource::new(
+ "issue",
+ "\
+if true {
+ a
+} else {
+ b
+}",
+ ),
+ big: (0, 32).into(),
+ small: (14, 1).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+
+ let expected = r#"
+ × oops!
+ ╭─[issue:1:1]
+ 1 │ ╭─▶ if true {
+ 2 │ │ a
+ · │ ┬
+ · │ ╰── small
+ 3 │ │ } else {
+ 4 │ │ b
+ 5 │ ├─▶ }
+ · ╰──── big
+ ╰────
+"#
+ .to_string();
+
+ assert_eq!(expected, out);
+}
+
#[test]
fn single_line_highlight_span_full_line() {
#[derive(Error, Debug, Diagnostic)]
@@ -74,7 +497,7 @@ fn single_line_highlight_span_full_line() {
#[diagnostic(severity(Error))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource<&'static str>,
#[label("This bit here")]
bad_bit: SourceSpan,
}
@@ -85,8 +508,9 @@ fn single_line_highlight_span_full_line() {
let out = fmt_report(err.into());
println!("Error: {}", out);
- let expected = r#" × oops!
- ╭─[issue:1:1]
+ let expected = r#"
+ × oops!
+ ╭─[issue:2:1]
1 │ source
2 │ text
· ──┬─
@@ -105,7 +529,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -120,7 +544,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:7]
1 │ source
2 │ 👼🏼text
· ───┬──
@@ -142,7 +566,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -159,7 +583,7 @@ fn single_line_with_two_tabs() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -181,7 +605,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -198,7 +622,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:8]
1 │ source
2 │ text = text
· ──┬─
@@ -220,7 +644,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -235,7 +659,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -270,7 +694,7 @@ fn external_source() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -292,7 +716,7 @@ fn single_line_highlight_offset_zero() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -328,7 +752,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -343,7 +767,7 @@ fn single_line_highlight_offset_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:1:7]
1 │ source
· ▲
· ╰── this bit here
@@ -364,7 +788,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -379,7 +803,7 @@ fn single_line_highlight_include_end_of_line() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬──
@@ -401,7 +825,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -416,7 +840,7 @@ fn single_line_highlight_include_end_of_line_crlf() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬──
@@ -438,7 +862,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -453,7 +877,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ▲
@@ -475,7 +899,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label]
highlight: SourceSpan,
}
@@ -490,7 +914,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ────
@@ -511,7 +935,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -526,7 +950,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:1]
1 │ source
2 │ text
· ──┬─
@@ -541,6 +965,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
Ok(())
}
+#[test]
+fn multiline_label() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here\nand\nthis\ntoo")]
+ highlight: SourceSpan,
+ }
+
+ let src = "source\ntext\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (7, 4).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:1]
+ 1 │ source
+ 2 │ text
+ · ──┬─
+ · ╰─┤ this bit here
+ · │ and
+ · │ this
+ · │ too
+ 3 │ here
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
+#[test]
+fn multiple_multi_line_labels() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "x\ny"]
+ highlight1: SourceSpan,
+ #[label = "z\nw"]
+ highlight2: SourceSpan,
+ #[label = "a\nb"]
+ highlight3: SourceSpan,
+ }
+
+ let src = "source\n text text text text text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (9, 4).into(),
+ highlight2: (14, 4).into(),
+ highlight3: (24, 4).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:3]
+ 1 │ source
+ 2 │ text text text text text
+ · ──┬─ ──┬─ ──┬─
+ · │ │ ╰─┤ a
+ · │ │ │ b
+ · │ ╰─┤ z
+ · │ │ w
+ · ╰─┤ x
+ · │ y
+ 3 │ here
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
#[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@@ -548,7 +1060,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "x"]
highlight1: SourceSpan,
#[label = "y"]
@@ -569,7 +1081,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text text text text text
· ──┬─ ──┬─ ──┬─
@@ -593,7 +1105,7 @@ fn multiple_same_line_highlights_with_tabs_in_middle() -> Result<(), MietteError
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "x"]
highlight1: SourceSpan,
#[label = "y"]
@@ -616,7 +1128,7 @@ fn multiple_same_line_highlights_with_tabs_in_middle() -> Result<(), MietteError
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text text text text text
· ──┬─ ──┬─ ──┬─
@@ -640,7 +1152,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "these two lines"]
highlight: SourceSpan,
}
@@ -655,7 +1167,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ ╭─▶ text
3 │ ├─▶ here
@@ -669,6 +1181,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
Ok(())
}
+#[test]
+fn multiline_highlight_multiline_label() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "these two lines\nare the problem"]
+ highlight: SourceSpan,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 11).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:3]
+ 1 │ source
+ 2 │ ╭─▶ text
+ 3 │ ├─▶ here
+ · ╰──┤ these two lines
+ · │ are the problem
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
#[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@@ -676,7 +1225,7 @@ fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label = "block 2"]
@@ -727,7 +1276,7 @@ fn multiline_highlight_no_label() -> Result<(), MietteError> {
#[source]
source: Inner,
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label]
@@ -791,7 +1340,7 @@ fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -837,7 +1386,7 @@ fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError>
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -865,7 +1414,7 @@ fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -940,6 +1489,28 @@ fn disable_url_links() -> Result<(), MietteError> {
Ok(())
}
+#[test]
+fn url_links_with_display_text() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(
+ code(oops::my::bad),
+ help("try doing it better next time?"),
+ url("https://example.com")
+ )]
+ struct MyBad;
+ let err = MyBad;
+ let out = fmt_report_with_settings(err.into(), |handler| {
+ handler.with_link_display_text("Read the documentation")
+ });
+
+ println!("Error: {}", out);
+ assert!(out.contains("https://example.com"));
+ assert!(out.contains("Read the documentation"));
+ assert!(out.contains("oops::my::bad"));
+ Ok(())
+}
+
#[test]
fn related() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)]
@@ -947,7 +1518,7 @@ fn related() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
@@ -969,7 +1540,7 @@ fn related() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -988,7 +1559,6 @@ Error: oops::my::bad
2 │ text
╰────
help: try doing it better next time?
-
"#
.trim_start()
.to_string();
@@ -1003,7 +1573,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
@@ -1031,7 +1601,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -1063,7 +1633,7 @@ fn related_severity() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
@@ -1080,7 +1650,7 @@ fn related_severity() -> Result<(), MietteError> {
)]
Error {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -1093,7 +1663,7 @@ fn related_severity() -> Result<(), MietteError> {
)]
Warning {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -1106,7 +1676,7 @@ fn related_severity() -> Result<(), MietteError> {
)]
Advice {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -1136,7 +1706,7 @@ fn related_severity() -> Result<(), MietteError> {
let expected = r#"oops::my::bad
× oops!
- ╭─[bad_file.rs:1:1]
+ ╭─[bad_file.rs:2:3]
1 │ source
2 │ text
· ──┬─
@@ -1155,6 +1725,7 @@ Error: oops::my::related::error
2 │ text
╰────
help: try doing it better next time?
+
Warning: oops::my::related::warning
⚠ oops!
@@ -1165,6 +1736,7 @@ Warning: oops::my::related::warning
2 │ text
╰────
help: try doing it better next time?
+
Advice: oops::my::related::advice
☞ oops!
@@ -1189,7 +1761,7 @@ fn zero_length_eol_span() {
#[diagnostic(severity(Error))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource<&'static str>,
#[label("This bit here")]
bad_bit: SourceSpan,
}
@@ -1200,8 +1772,9 @@ fn zero_length_eol_span() {
let out = fmt_report(err.into());
println!("Error: {}", out);
- let expected = r#" × oops!
- ╭─[issue:1:1]
+ let expected = r#"
+ × oops!
+ ╭─[issue:2:1]
1 │ this is the first line
2 │ this is the second line
· ▲
@@ -1212,3 +1785,567 @@ fn zero_length_eol_span() {
assert_eq!(expected, out);
}
+
+#[test]
+fn primary_label() {
+ #[derive(Error, Debug, Diagnostic)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource<&'static str>,
+ #[label]
+ first_label: SourceSpan,
+ #[label(primary, "nope")]
+ second_label: SourceSpan,
+ }
+ let err = MyBad {
+ src: NamedSource::new("issue", "this is the first line\nthis is the second line"),
+ first_label: (2, 4).into(),
+ second_label: (24, 4).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+
+ // line 2 should be the primary, not line 1
+ let expected = r#"
+ × oops!
+ ╭─[issue:2:2]
+ 1 │ this is the first line
+ · ────
+ 2 │ this is the second line
+ · ──┬─
+ · ╰── nope
+ ╰────
+"#
+ .to_string();
+
+ assert_eq!(expected, out);
+}
+
+#[test]
+fn single_line_with_wide_char_unaligned_span_start() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ }
+
+ let src = "source\n 👼🏼text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (10, 5).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:4]
+ 1 │ source
+ 2 │ 👼🏼text
+ · ──┬─
+ · ╰── this bit here
+ 3 │ here
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
+#[test]
+fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ }
+
+ let src = "source\n text 👼🏼\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 6).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:3]
+ 1 │ source
+ 2 │ text 👼🏼
+ · ───┬───
+ · ╰── this bit here
+ 3 │ here
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
+#[test]
+fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ }
+
+ let src = "source\n 👼🏼text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (10, 0).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = r#"oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:2:4]
+ 1 │ source
+ 2 │ 👼🏼text
+ · ▲
+ · ╰── this bit here
+ 3 │ here
+ ╰────
+ help: try doing it better next time?
+"#
+ .trim_start()
+ .to_string();
+ assert_eq!(expected, out);
+ Ok(())
+}
+
+#[test]
+#[cfg(feature = "syntect-highlighter")]
+fn syntax_highlighter() {
+ std::env::set_var("REPLACE_TABS", "4");
+ #[derive(Debug, Error, Diagnostic)]
+ #[error("This is an error")]
+ #[diagnostic()]
+ pub struct Test {
+ #[source_code]
+ src: NamedSource,
+ #[label("this is a label")]
+ src_span: SourceSpan,
+ }
+ let src = NamedSource::new(
+ "hello_world", //NOTE: intentionally missing file extension
+ "fn main() {\n println!(\"Hello, World!\");\n}\n".to_string(),
+ )
+ .with_language("Rust");
+ let err = Test {
+ src,
+ src_span: (16, 26).into(),
+ };
+ let mut out = String::new();
+ GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
+ .render_report(&mut out, &err)
+ .unwrap();
+ let expected = r#"
+ × This is an error
+ ╭─[hello_world:2:5]
+ 1 │ fn main() {
+ 2 │ println!("Hello, World!");
+ · ─────────────┬────────────
+ · ╰── this is a label
+ 3 │ }
+ ╰────
+"#;
+ assert!(out.contains("\u{1b}[38;2;180;142;173m"));
+ assert_eq!(expected, strip_ansi_escapes::strip_str(out))
+}
+
+// This test reads a line from the current source file and renders it with Rust
+// syntax highlighting. The goal is to test syntax highlighting on a non-trivial
+// source code example. However, if tests are running in an environment where
+// source files are missing, this will cause problems. In that case, it would
+// be better to use include_str!() on a sufficiently complex example file.
+#[test]
+#[cfg(feature = "syntect-highlighter")]
+fn syntax_highlighter_on_real_file() {
+ std::env::set_var("REPLACE_TABS", "4");
+
+ #[derive(Debug, Error, Diagnostic)]
+ #[error("This is an error")]
+ #[diagnostic()]
+ pub struct Test {
+ #[source_code]
+ src: NamedSource,
+ #[label("this is a label")]
+ src_span: SourceSpan,
+ }
+ // BEGIN SOURCE SNIPPET
+
+ let (filename, line) = (file!(), line!() as usize);
+
+ // END SOURCE SNIPPET
+ // SourceSpan constants for column and length
+ const CO: usize = 28;
+ const LEN: usize = 27;
+ let file_src = std::fs::read_to_string(&filename).unwrap();
+ let offset = miette::SourceOffset::from_location(&file_src, line, CO);
+ let err = Test {
+ src: NamedSource::new(&filename, file_src.clone()),
+ src_span: SourceSpan::new(offset, LEN.into()),
+ };
+
+ let mut out = String::new();
+ GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
+ .with_context_lines(1)
+ .render_report(&mut out, &err)
+ .unwrap();
+
+ let expected = format!(
+ r#"
+ × This is an error
+ ╭─[{filename}:{l2}:{CO}]
+ {l1} │
+ {l2} │ let (filename, line) = (file!(), line!() as usize);
+ · ─────────────┬─────────────
+ · ╰── this is a label
+ {l3} │
+ ╰────
+"#,
+ l1 = line - 1,
+ l2 = line,
+ l3 = line + 1
+ );
+ assert!(out.contains("\u{1b}[38;2;180;142;173m"));
+ assert_eq!(expected, strip_ansi_escapes::strip_str(out));
+}
+
+#[test]
+fn triple_adjacent_highlight() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "this bit here"]
+ highlight1: SourceSpan,
+ #[label = "also this bit"]
+ highlight2: SourceSpan,
+ #[label = "finally we got"]
+ highlight3: SourceSpan,
+ }
+
+ let src = "source\n\n\n text\n\n\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (0, 6).into(),
+ highlight2: (11, 4).into(),
+ highlight3: (22, 4).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:1:1]
+ 1 │ source
+ · ───┬──
+ · ╰── this bit here
+ 2 │
+ 3 │
+ 4 │ text
+ · ──┬─
+ · ╰── also this bit
+ 5 │
+ 6 │
+ 7 │ here
+ · ──┬─
+ · ╰── finally we got
+ ╰────
+ help: try doing it better next time?
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn non_adjacent_highlight() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "this bit here"]
+ highlight1: SourceSpan,
+ #[label = "also this bit"]
+ highlight2: SourceSpan,
+ }
+
+ let src = "source\n\n\n\n text here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (0, 6).into(),
+ highlight2: (12, 4).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ ╭─[bad_file.rs:1:1]
+ 1 │ source
+ · ───┬──
+ · ╰── this bit here
+ 2 │
+ ╰────
+ ╭─[bad_file.rs:5:3]
+ 4 │
+ 5 │ text here
+ · ──┬─
+ · ╰── also this bit
+ ╰────
+ help: try doing it better next time?
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_bad_offset() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "1st"]
+ highlight1: SourceSpan,
+ }
+
+ let src = "blabla blibli".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (50, 6).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ [Failed to read contents for label `1st` (offset: 50, length: 6): OutOfBounds]
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_bad_length() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label = "1st"]
+ highlight1: SourceSpan,
+ }
+
+ let src = "blabla blibli".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (0, 50).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ [Failed to read contents for label `1st` (offset: 0, length: 50): OutOfBounds]
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_no_label() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label]
+ highlight1: SourceSpan,
+ }
+
+ let src = "blabla blibli".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (50, 6).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ [Failed to read contents for label `` (offset: 50, length: 6): OutOfBounds]
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_2nd_label() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ #[diagnostic(code(oops::my::bad), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("1st")]
+ highlight1: SourceSpan,
+ #[label("2nd")]
+ highlight2: SourceSpan,
+ }
+
+ let src = "blabla blibli".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight1: (0, 6).into(),
+ highlight2: (50, 6).into(),
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::bad
+
+ × oops!
+ [Failed to read contents for label `2nd` (offset: 50, length: 6): OutOfBounds]
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_inner() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops inside!")]
+ #[diagnostic(code(oops::my::inner), help("help info"))]
+ struct MyInner {
+ #[source_code]
+ src: NamedSource,
+ #[label("inner label")]
+ inner_label: SourceSpan,
+ }
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops outside!")]
+ #[diagnostic(code(oops::my::outer), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("outer label")]
+ outer_label: SourceSpan,
+ #[source]
+ inner: MyInner,
+ }
+
+ let src_outer = "outer source".to_string();
+ let src_inner = "inner source".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src_outer),
+ outer_label: (0, 6).into(),
+ inner: MyInner {
+ src: NamedSource::new("bad_file2.rs", src_inner),
+ inner_label: (60, 6).into(),
+ },
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::outer
+
+ × oops outside!
+ ╰─▶ oops inside!
+ ╭─[bad_file.rs:1:1]
+ 1 │ outer source
+ · ───┬──
+ · ╰── outer label
+ ╰────
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
+
+#[test]
+fn invalid_span_related() -> Result<(), MietteError> {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops inside!")]
+ #[diagnostic(code(oops::my::inner), help("help info"))]
+ struct MyRelated {
+ #[source_code]
+ src: NamedSource,
+ #[label("inner label")]
+ inner_label: SourceSpan,
+ }
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops outside!")]
+ #[diagnostic(code(oops::my::outer), help("help info"))]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("outer label")]
+ outer_label: SourceSpan,
+ #[related]
+ inner: Vec,
+ }
+
+ let src_outer = "outer source".to_string();
+ let src_inner = "related source".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src_outer),
+ outer_label: (0, 6).into(),
+ inner: vec![MyRelated {
+ src: NamedSource::new("bad_file2.rs", src_inner),
+ inner_label: (60, 6).into(),
+ }],
+ };
+ let out = fmt_report(err.into());
+ println!("Error: {}", out);
+ let expected = "oops::my::outer
+
+ × oops outside!
+ ╭─[bad_file.rs:1:1]
+ 1 │ outer source
+ · ───┬──
+ · ╰── outer label
+ ╰────
+ help: help info
+
+Error: oops::my::inner
+
+ × oops inside!
+ [Failed to read contents for label `inner label` (offset: 60, length: 6): OutOfBounds]
+ help: help info
+";
+ assert_eq!(expected, &out);
+ Ok(())
+}
diff --git a/tests/narrated.rs b/tests/narrated.rs
index 0bdb41d..52acd13 100644
--- a/tests/narrated.rs
+++ b/tests/narrated.rs
@@ -29,7 +29,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -65,7 +65,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -101,7 +101,7 @@ fn single_line_highlight_offset_zero() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -136,7 +136,7 @@ fn single_line_highlight_with_empty_span() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -172,7 +172,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label]
highlight: SourceSpan,
}
@@ -208,7 +208,7 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -244,7 +244,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "x"]
highlight1: SourceSpan,
#[label = "y"]
@@ -288,7 +288,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "these two lines"]
highlight: SourceSpan,
}
@@ -325,7 +325,7 @@ fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label = "block 2"]
@@ -378,7 +378,7 @@ fn multiline_highlight_no_label() -> Result<(), MietteError> {
#[source]
source: Inner,
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label]
@@ -444,7 +444,7 @@ fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -492,7 +492,7 @@ fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError>
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -520,7 +520,7 @@ fn multiple_multiline_highlights_overlapping_offsets() -> Result<(), MietteError
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -559,7 +559,7 @@ fn related() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
@@ -614,7 +614,7 @@ fn related_source_code_propagation() -> Result<(), MietteError> {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
#[related]
diff --git a/tests/test_derive_attr.rs b/tests/test_derive_attr.rs
index 5ee950a..f1b0f3d 100644
--- a/tests/test_derive_attr.rs
+++ b/tests/test_derive_attr.rs
@@ -10,7 +10,7 @@ fn enum_uses_base_attr() {
enum MyBad {
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -32,7 +32,7 @@ fn enum_uses_variant_attr() {
#[diagnostic(code(error::on::variant))]
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -55,7 +55,7 @@ fn multiple_attrs_allowed_on_item() {
enum MyBad {
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -79,7 +79,7 @@ fn multiple_attrs_allowed_on_variant() {
#[diagnostic(help("try doing it correctly"))]
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -104,7 +104,7 @@ fn attrs_can_be_split_between_item_and_variants() {
#[diagnostic(url("https://example.com/foo/bar"))]
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
@@ -130,7 +130,7 @@ fn attr_not_required() {
enum MyBad {
Only {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
},
diff --git a/tests/test_derive_collection.rs b/tests/test_derive_collection.rs
new file mode 100644
index 0000000..952b505
--- /dev/null
+++ b/tests/test_derive_collection.rs
@@ -0,0 +1,306 @@
+use std::{
+ collections::{LinkedList, VecDeque},
+ ops::Range,
+};
+
+// Testing of the `diagnostic` attr used by derive(Diagnostic)
+use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
+use thiserror::Error;
+
+#[test]
+fn attr_collection_in_enum() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ enum MyBad {
+ Only {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: Vec,
+ },
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad::Only {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![(1, 2).into(), (3, 4).into()],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_in_struct() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: Vec,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![(1, 2).into(), (3, 4).into()],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_as_deque() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: VecDeque,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: VecDeque::from([(1, 2).into(), (3, 4).into()]),
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_as_linked_list() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: LinkedList,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: LinkedList::from([(1, 2).into(), (3, 4).into()]),
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_of_tuple() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: Vec<(usize, usize)>,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![(1, 2), (3, 4)],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_of_range() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: Vec>,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![1..3, 3..7],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_of_labeled_span_in_struct() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "then there")]
+ highlight2: Vec,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![
+ LabeledSpan::new_with_span(Some("continuing here".to_string()), (1, 2)),
+ LabeledSpan::new_with_span(None, (3, 4)),
+ ],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("continuing here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("then there".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_of_labeled_span_in_enum() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ enum MyBad {
+ Only {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "then there")]
+ highlight2: Vec,
+ },
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad::Only {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![
+ LabeledSpan::new_with_span(Some("continuing here".to_string()), (1, 2)),
+ LabeledSpan::new_with_span(None, (3, 4)),
+ ],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("continuing here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("then there".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+}
+
+#[test]
+fn attr_collection_multi() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("oops!")]
+ struct MyBad {
+ #[source_code]
+ src: NamedSource,
+ #[label("this bit here")]
+ highlight: SourceSpan,
+ #[label(collection, "and here")]
+ highlight2: Vec,
+ #[label(collection, "and there")]
+ highlight3: Vec,
+ }
+
+ let src = "source\n text\n here".to_string();
+ let err = MyBad {
+ src: NamedSource::new("bad_file.rs", src),
+ highlight: (9, 4).into(),
+ highlight2: vec![(1, 2).into(), (3, 4).into()],
+ highlight3: vec![(5, 6).into(), (7, 8).into()],
+ };
+ let mut label_iter = err.labels().unwrap();
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("this bit here".into()), 9usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 1usize, 2usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and here".into()), 3usize, 4usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and there".into()), 5usize, 6usize);
+ assert_eq!(err_span, expectation);
+ let err_span = label_iter.next().unwrap();
+ let expectation = LabeledSpan::new(Some("and there".into()), 7usize, 8usize);
+ assert_eq!(err_span, expectation);
+}
diff --git a/tests/test_derive_source_chain.rs b/tests/test_derive_source_chain.rs
new file mode 100644
index 0000000..fb7ddb2
--- /dev/null
+++ b/tests/test_derive_source_chain.rs
@@ -0,0 +1,69 @@
+use std::sync::Arc;
+
+use miette::{miette, Diagnostic};
+use thiserror::Error;
+
+#[test]
+fn test_source() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Bar")]
+ struct Bar;
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Foo")]
+ struct Foo {
+ #[source]
+ bar: Bar,
+ }
+
+ let e = miette!(Foo { bar: Bar });
+ let mut chain = e.chain();
+
+ assert_eq!("Foo", chain.next().unwrap().to_string());
+ assert_eq!("Bar", chain.next().unwrap().to_string());
+ assert!(chain.next().is_none());
+}
+
+#[test]
+fn test_source_boxed() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Bar")]
+ struct Bar;
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Foo")]
+ struct Foo {
+ #[source]
+ bar: Box,
+ }
+
+ let error = miette!(Foo { bar: Box::new(Bar) });
+
+ let mut chain = error.chain();
+
+ assert_eq!("Foo", chain.next().unwrap().to_string());
+ assert_eq!("Bar", chain.next().unwrap().to_string());
+ assert!(chain.next().is_none());
+}
+
+#[test]
+fn test_source_arc() {
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Bar")]
+ struct Bar;
+
+ #[derive(Debug, Diagnostic, Error)]
+ #[error("Foo")]
+ struct Foo {
+ #[source]
+ bar: Arc,
+ }
+
+ let error = miette!(Foo { bar: Arc::new(Bar) });
+
+ let mut chain = error.chain();
+
+ assert_eq!("Foo", chain.next().unwrap().to_string());
+ assert_eq!("Bar", chain.next().unwrap().to_string());
+ assert!(chain.next().is_none());
+}
diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs
index 536aedf..4f15ea0 100644
--- a/tests/test_diagnostic_source_macro.rs
+++ b/tests/test_diagnostic_source_macro.rs
@@ -41,6 +41,14 @@ struct TestTupleError(#[diagnostic_source] AnErr);
#[error("TestError")]
struct TestBoxedError(#[diagnostic_source] Box);
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("TestError")]
+struct TestBoxedSendError(#[diagnostic_source] Box);
+
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("TestError")]
+struct TestBoxedSendSyncError(#[diagnostic_source] Box);
+
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("TestError")]
struct TestArcedError(#[diagnostic_source] std::sync::Arc);
@@ -71,15 +79,22 @@ fn test_diagnostic_source() {
let error = TestBoxedError(Box::new(AnErr));
assert!(error.diagnostic_source().is_some());
+ let error = TestBoxedSendError(Box::new(AnErr));
+ assert!(error.diagnostic_source().is_some());
+
+ let error = TestBoxedSendSyncError(Box::new(AnErr));
+ assert!(error.diagnostic_source().is_some());
+
let error = TestArcedError(std::sync::Arc::new(AnErr));
assert!(error.diagnostic_source().is_some());
}
+#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_diagnostic_source_pass_extra_info() {
let diag = TestBoxedError(Box::new(SourceError {
code: String::from("Hello\nWorld!"),
- help: format!("Have you tried turning it on and off again?"),
+ help: String::from("Have you tried turning it on and off again?"),
label: (1, 4),
}));
let mut out = String::new();
@@ -89,9 +104,10 @@ fn test_diagnostic_source_pass_extra_info() {
.render_report(&mut out, &diag)
.unwrap();
println!("Error: {}", out);
- let expected = r#" × TestError
+ let expected = r#"
+ × TestError
╰─▶ × A complex error happened
- ╭─[1:1]
+ ╭─[1:2]
1 │ Hello
· ──┬─
· ╰── here
@@ -106,6 +122,7 @@ fn test_diagnostic_source_pass_extra_info() {
assert_eq!(expected, out);
}
+#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_diagnostic_source_is_output() {
let diag = TestStructError {
@@ -122,7 +139,8 @@ fn test_diagnostic_source_is_output() {
.unwrap();
println!("{}", out);
- let expected = r#" × TestError
+ let expected = r#"
+ × TestError
╰─▶ × A complex error happened
╭────
1 │ right here
@@ -147,6 +165,7 @@ struct NestedError {
the_other_err: Box,
}
+#[cfg(feature = "fancy-no-backtrace")]
#[test]
fn test_nested_diagnostic_source_is_output() {
let inner_error = TestStructError {
@@ -169,7 +188,8 @@ fn test_nested_diagnostic_source_is_output() {
.unwrap();
println!("{}", out);
- let expected = r#" × A nested error happened
+ let expected = r#"
+ × A nested error happened
├─▶ × TestError
│
╰─▶ × A complex error happened
@@ -191,3 +211,89 @@ fn test_nested_diagnostic_source_is_output() {
assert_eq!(expected, out);
}
+
+#[derive(Debug, miette::Diagnostic, thiserror::Error)]
+#[error("A multi-error happened")]
+struct MultiError {
+ #[related]
+ related_errs: Vec>,
+}
+
+#[cfg(feature = "fancy-no-backtrace")]
+#[test]
+fn test_nested_cause_chains_for_related_errors_are_output() {
+ let inner_error = TestStructError {
+ asdf_inner_foo: SourceError {
+ code: String::from("This is another error"),
+ help: String::from("You should fix this"),
+ label: (3, 4),
+ },
+ };
+ let first_error = NestedError {
+ code: String::from("right here"),
+ label: (6, 4),
+ the_other_err: Box::new(inner_error),
+ };
+ let second_error = SourceError {
+ code: String::from("You're actually a mess"),
+ help: String::from("Get a grip..."),
+ label: (3, 4),
+ };
+ let multi_error = MultiError {
+ related_errs: vec![Box::new(first_error), Box::new(second_error)],
+ };
+ let diag = NestedError {
+ code: String::from("the outside world"),
+ label: (6, 4),
+ the_other_err: Box::new(multi_error),
+ };
+ let mut out = String::new();
+ miette::GraphicalReportHandler::new_themed(miette::GraphicalTheme::unicode_nocolor())
+ .with_width(80)
+ .with_footer("Yooo, a footer".to_string())
+ .render_report(&mut out, &diag)
+ .unwrap();
+ println!("{}", out);
+
+ let expected = r#"
+ × A nested error happened
+ ╰─▶ × A multi-error happened
+
+ Error:
+ × A nested error happened
+ ├─▶ × TestError
+ │
+ ╰─▶ × A complex error happened
+ ╭────
+ 1 │ This is another error
+ · ──┬─
+ · ╰── here
+ ╰────
+ help: You should fix this
+
+ ╭────
+ 1 │ right here
+ · ──┬─
+ · ╰── here
+ ╰────
+
+ Error:
+ × A complex error happened
+ ╭────
+ 1 │ You're actually a mess
+ · ──┬─
+ · ╰── here
+ ╰────
+ help: Get a grip...
+
+ ╭────
+ 1 │ the outside world
+ · ──┬─
+ · ╰── here
+ ╰────
+
+ Yooo, a footer
+"#;
+
+ assert_eq!(expected, out);
+}
diff --git a/tests/test_json.rs b/tests/test_json.rs
index 5bd14cb..664318a 100644
--- a/tests/test_json.rs
+++ b/tests/test_json.rs
@@ -20,7 +20,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -52,7 +52,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -66,7 +65,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -98,7 +97,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -112,7 +110,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -144,7 +142,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -158,7 +155,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -190,7 +187,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -204,7 +200,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label]
highlight: SourceSpan,
}
@@ -235,7 +231,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -249,7 +244,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label("this bit here")]
highlight: SourceSpan,
}
@@ -281,7 +276,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -295,7 +289,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "x"]
highlight1: SourceSpan,
#[label = "y"]
@@ -347,7 +341,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -361,7 +354,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "these two lines"]
highlight: SourceSpan,
}
@@ -393,7 +386,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -407,7 +399,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label = "block 2"]
@@ -456,7 +448,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -472,7 +463,7 @@ mod json_report_handler {
#[source]
source: Inner,
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "block 1"]
highlight1: SourceSpan,
#[label]
@@ -532,7 +523,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -546,7 +536,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource,
#[label = "this bit here"]
highlight1: SourceSpan,
#[label = "also this bit"]
@@ -588,7 +578,6 @@ mod json_report_handler {
"related": []
}"#
.lines()
- .into_iter()
.map(|s| s.trim_matches(|c| c == ' ' || c == '\n'))
.collect();
assert_eq!(expected, out);
@@ -602,7 +591,7 @@ mod json_report_handler {
#[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
struct MyBad {
#[source_code]
- src: NamedSource,
+ src: NamedSource