From 01b60a7df8d0373c205ef7be564e28d210e809b3 Mon Sep 17 00:00:00 2001 From: Khang Nguyen Duy <40488299+iceghost@users.noreply.github.com> Date: Sun, 25 Jun 2023 12:15:28 +0700 Subject: [PATCH 01/67] docs: fix broken links (#275) the links point to structs, whereas it should be traits --- README.md | 4 ++-- README.tpl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ff25b3a..61fcf50 100644 --- a/README.md +++ b/README.md @@ -648,7 +648,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 From 89806755a4d89e2dc15bfd1d94f6b758df205571 Mon Sep 17 00:00:00 2001 From: 0x009922 Date: Thu, 29 Jun 2023 08:42:12 +0700 Subject: [PATCH 02/67] docs: a little cleaning in README (#274) --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 61fcf50..8375876 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type. `thiserror` is a great way to define them, and plays nicely with `miette`! */ -use miette::{Diagnostic, SourceSpan}; +use miette::{Diagnostic, NamedSource, Result, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,12 +123,10 @@ Use this `Result` type (or its expanded version) as the return type throughout your app (but NOT your libraries! Those should always return concrete types!). */ -use miette::{NamedSource, Result}; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. let src = "source\n text\n here".to_string(); - let len = src.len(); Err(MyBad { src: NamedSource::new("bad_file.rs", src), From 2e3e5c9d15e234495369e9b47d032644dd5664ad Mon Sep 17 00:00:00 2001 From: Bennett Hardwick Date: Thu, 29 Jun 2023 11:58:35 +1000 Subject: [PATCH 03/67] feat(protocol): add StdError impl for Box (#273) --- src/protocol.rs | 24 ++++++++--- tests/test_derive_source_chain.rs | 69 +++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 tests/test_derive_source_chain.rs diff --git a/src/protocol.rs b/src/protocol.rs index 7531db1..f516984 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -69,16 +69,28 @@ 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_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_impls! { + Box, + Box, + Box +} + impl From for Box { 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()); +} From c3d4db2618c06743c6db615a372a347e0bd4ed1a Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Sat, 8 Jul 2023 07:16:53 +0700 Subject: [PATCH 04/67] docs: Fix broken links from macro docs. (#278) --- src/eyreish/macros.rs | 5 +++++ 1 file changed, 5 insertions(+) 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)*) => {{ From bfacb6329f0063b5dc5a8c0b4344743cf3fa183a Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sun, 16 Jul 2023 04:46:54 +0300 Subject: [PATCH 05/67] doc: document `#[diagnostic(transparent)]` (#266) --- README.md | 75 ++++++++++++++++++++++++++++++++---------------------- src/lib.rs | 14 +++++++++- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 8375876..a60ef11 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, @@ -32,7 +32,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) @@ -51,7 +51,7 @@ libraries and such might not want. - [Acknowledgements](#acknowledgements) - [License](#license) -## Features +### Features - Generic [`Diagnostic`] protocol, compatible (and dependent on) [`std::error::Error`]. @@ -76,7 +76,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 +88,7 @@ If you want to use the fancy printer in all these screenshots: $ cargo add miette --features fancy ``` -## Example +### Example ```rust /* @@ -168,9 +168,9 @@ diagnostic help: Change int or string to be the right types and try again. diagnostic code: nu::parser::unsupported_operation For more details, see https://docs.rs/nu-parser/0.1.0/nu-parser/enum.ParseError.html#variant.UnsupportedOperation"> -## 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 @@ -185,7 +185,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)] @@ -197,6 +197,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 } ``` @@ -204,7 +216,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 @@ -246,8 +258,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}; @@ -260,8 +271,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 @@ -291,7 +303,7 @@ enabled: miette = { version = "X.Y.Z", features = ["fancy"] } ``` -### ... diagnostic code URLs +#### ... 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: @@ -344,7 +356,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 @@ -392,7 +404,7 @@ pub struct MyErrorType { } ``` -#### ... 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 @@ -428,7 +440,7 @@ let err = Foo { }; ``` -### ... multiple related errors +#### ... multiple related errors `miette` supports collecting multiple errors into a single diagnostic, and printing them all together nicely. @@ -448,7 +460,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) @@ -531,7 +543,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 @@ -564,7 +576,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 @@ -583,12 +595,13 @@ miette::set_hook(Box::new(|_| { .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 @@ -597,6 +610,7 @@ 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[ @@ -608,26 +622,25 @@ let report = miette!( println!("{:?}", report) ``` -## Acknowledgements +### 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). diff --git a/src/lib.rs b/src/lib.rs index cc113ce..20589ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -186,7 +186,7 @@ //! //! ```rust //! // lib/error.rs -//! use miette::Diagnostic; +//! use miette::{Diagnostic, SourceSpan}; //! use thiserror::Error; //! //! #[derive(Error, Diagnostic, Debug)] @@ -198,6 +198,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 //! } //! ``` //! From 10c3b7f809ec7f8885c4f63fc9e36d3e419c339e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:48:00 -0700 Subject: [PATCH 06/67] docs: update readme again --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a60ef11..7aabe99 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type. `thiserror` is a great way to define them, and plays nicely with `miette`! */ -use miette::{Diagnostic, NamedSource, Result, SourceSpan}; +use miette::{Diagnostic, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,10 +123,12 @@ Use this `Result` type (or its expanded version) as the return type throughout your app (but NOT your libraries! Those should always return concrete types!). */ +use miette::{NamedSource, Result}; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. let src = "source\n text\n here".to_string(); + let len = src.len(); Err(MyBad { src: NamedSource::new("bad_file.rs", src), From f8d13825015317828dc268071b8f9bd50579e4ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:48:59 -0700 Subject: [PATCH 07/67] docs: update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0b842de..955c27d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # `miette` Release Changelog + +## 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) From f4d056e1ffeb9a0bf36e2a6501365bd7e00db22d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 15 Jul 2023 18:49:14 -0700 Subject: [PATCH 08/67] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 160556f..dd6e584 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "5.9.0" +version = "5.10.0" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.9.0" } +miette-derive = { path = "miette-derive", version = "=5.10.0" } once_cell = "1.8.0" unicode-width = "0.1.9" diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 461a328..becdc17 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "5.9.0" +version = "5.10.0" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From a9c2bae9dc095e530c5ff4cccb6a08b05084afc8 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Mon, 18 Sep 2023 23:12:56 +0700 Subject: [PATCH 09/67] docs: Improve linking. (#289) --- src/eyreish/error.rs | 8 ++++---- src/handlers/theme.rs | 11 ++++++----- src/protocol.rs | 2 +- 3 files changed, 11 insertions(+), 10 deletions(-) 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/handlers/theme.rs b/src/handlers/theme.rs index 1f5236a..aa6649e 100644 --- a/src/handlers/theme.rs +++ b/src/handlers/theme.rs @@ -55,9 +55,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 +79,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 +160,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/protocol.rs b/src/protocol.rs index f516984..36c3539 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -232,7 +232,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, From cc81382a6070dd226a20e4a39518d88e957ac0e1 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Thu, 21 Sep 2023 00:37:40 +0700 Subject: [PATCH 10/67] fix(misc): Improve ci and fix clippy (#290) * ci: Update actions, replace actions-rs. * `actions/checkout` is updated from `v1` to the current `v4`. * `actions-rs/toolchain` is replaced by `dtolnay/rust-toolchain` as the `actions-rs` actions haven't been maintained in a long time. * clippy: Remove unnecessary call to `into_iter`. The parameter takes `IntoIterator`, so we don't have to call `into_iter` at the call site. * clippy: Remove explicit lifetime that can be elided. * clippy: tests: Fix useless conversion warnings. * clippy: tests: Remove call to `format!`. * Fix minimal-versions build. Due to changes in the nightly compiler, using a recent nightly requires proc-macro2 1.0.60 or later: https://github.com/dtolnay/proc-macro2/issues/356 * ci: Use is-terminal 0.4.7 for MSRV builds. is-terminal 0.4.8 updated its MSRV to 1.63, so we can't use it with our MSRV of 1.56. Force usage of the older version which has an older MSRV. --- .github/workflows/ci.yml | 27 ++++++++----------- miette-derive/Cargo.toml | 2 +- src/handlers/graphical.rs | 4 +-- src/miette_diagnostic.rs | 2 +- tests/derive.rs | 37 ++++++--------------------- tests/test_diagnostic_source_macro.rs | 2 +- tests/test_json.rs | 16 ------------ 7 files changed, 24 insertions(+), 66 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4fafac3..912bfcc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,14 +10,12 @@ 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: rustfmt run: cargo fmt --all -- --check - name: docs @@ -32,14 +30,15 @@ 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: ${{ matrix.rust }} components: clippy - override: true + - name: Force older version of is-terminal for MSRV builds + if: matrix.rust == '1.56.0' + run: cargo update -p is-terminal --precise 0.4.7 - name: Clippy run: cargo clippy --all -- -D warnings - name: Run tests @@ -54,14 +53,12 @@ jobs: 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 +72,11 @@ 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 diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index becdc17..b5a7032 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette" proc-macro = true [dependencies] -proc-macro2 = "1.0" +proc-macro2 = "1.0.60" quote = "1.0" syn = "2.0.11" diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index b5dd754..e61f595 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -382,10 +382,10 @@ impl GraphicalReportHandler { 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 { diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index 67b75d0..dc0468e 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 } diff --git a/tests/derive.rs b/tests/derive.rs index 7faae42..dbaf7cb 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -189,7 +189,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 +201,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 +219,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 +245,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 +261,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 +273,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] diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 536aedf..df30b2e 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -79,7 +79,7 @@ fn test_diagnostic_source() { 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(); diff --git a/tests/test_json.rs b/tests/test_json.rs index 5bd14cb..ae482b8 100644 --- a/tests/test_json.rs +++ b/tests/test_json.rs @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -644,7 +633,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -700,7 +688,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -728,7 +715,6 @@ mod json_report_handler { "related": [] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -822,7 +808,6 @@ mod json_report_handler { }] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); @@ -920,7 +905,6 @@ mod json_report_handler { }] }"# .lines() - .into_iter() .map(|s| s.trim_matches(|c| c == ' ' || c == '\n')) .collect(); assert_eq!(expected, out); From db0b7e403a5ae52ae360991b6508490d8c579886 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 20 Sep 2023 15:36:33 -0700 Subject: [PATCH 11/67] feat(labels): Add support for primary label in specifying line/col information (#291) --- miette-derive/src/label.rs | 79 +++++++++++++++++++++------ src/handlers/graphical.rs | 27 +++++++-- src/miette_diagnostic.rs | 12 ++-- src/protocol.rs | 32 +++++++++-- tests/graphical.rs | 73 ++++++++++++++++++------- tests/test_diagnostic_source_macro.rs | 2 +- 6 files changed, 174 insertions(+), 51 deletions(-) diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index e0bc70a..dd5ec69 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -20,10 +20,12 @@ struct Label { label: Option, ty: syn::Type, span: syn::Member, + primary: bool, } struct LabelAttr { label: Option, + primary: bool, } impl Parse for LabelAttr { @@ -40,10 +42,22 @@ impl Parse for LabelAttr { } }); let la = input.lookahead1(); - let label = if la.peek(syn::token::Paren) { - // #[label("{}", x)] + let (primary, label) = if la.peek(syn::token::Paren) { + // #[label(primary?, "{}", x)] let content; parenthesized!(content in input); + + let primary = if content.peek(syn::Ident) { + let ident: syn::Ident = content.parse()?; + if ident != "primary" { + return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); + } + let _ = content.parse::(); + true + } else { + false + }; + if content.peek(syn::LitStr) { let fmt = content.parse()?; let args = if content.is_empty() { @@ -56,22 +70,27 @@ impl Parse for LabelAttr { args, has_bonus_display: false, }; - Some(display) + (primary, Some(display)) + } else if !primary { + return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The argument must be a literal string or the keyword `primary`.")); } else { - return Err(syn::Error::new(input.span(), "Invalid argument to label() attribute. The first argument must be a literal string.")); + (primary, None) } } else if la.peek(Token![=]) { // #[label = "blabla"] input.parse::()?; - Some(Display { - fmt: input.parse()?, - args: TokenStream::new(), - has_bonus_display: false, - }) + ( + false, + Some(Display { + fmt: input.parse()?, + args: TokenStream::new(), + has_bonus_display: false, + }), + ) } else { - None + (false, None) }; - Ok(LabelAttr { label }) + Ok(LabelAttr { label, primary }) } } @@ -100,12 +119,21 @@ impl Labels { }) }; use quote::ToTokens; - let LabelAttr { label } = + let LabelAttr { label, primary } = syn::parse2::(attr.meta.to_token_stream())?; + + if primary && labels.iter().any(|l: &Label| l.primary) { + return Err(syn::Error::new( + field.span(), + "Cannot have more than one primary label.", + )); + } + labels.push(Label { label, span, ty: field.ty.clone(), + primary, }); } } @@ -120,13 +148,23 @@ 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 Label { + span, + label, + ty, + primary, + } = highlight; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; 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( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -134,7 +172,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(&self.#span) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) @@ -161,7 +199,7 @@ impl 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 Label { span, label, ty, primary } = label; let field = match &span { syn::Member::Named(ident) => ident.clone(), syn::Member::Unnamed(syn::Index { index, .. }) => { @@ -169,11 +207,16 @@ impl Labels { } }; let var = quote! { __miette_internal_var }; + let ctor = if *primary { + quote! { miette::LabeledSpan::new_primary_with_span } + } else { + quote! { miette::LabeledSpan::new_with_span } + }; 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( + .map(|#var| #ctor( std::option::Option::Some(format!(#fmt #args)), #var.clone(), )) @@ -181,7 +224,7 @@ impl Labels { } else { quote! { miette::macro_helpers::OptionalWrapper::<#ty>::new().to_option(#field) - .map(|#var| miette::LabeledSpan::new_with_span( + .map(|#var| #ctor( std::option::Option::None, #var.clone(), )) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index e61f595..a7399fd 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -391,6 +391,11 @@ impl GraphicalReportHandler { ) -> fmt::Result { let (contents, lines) = self.get_lines(source, context.inner())?; + let primary_label = labels + .iter() + .find(|label| label.primary()) + .or_else(|| labels.first()); + // sorting is your friend let labels = labels .iter() @@ -431,19 +436,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! diff --git a/src/miette_diagnostic.rs b/src/miette_diagnostic.rs index dc0468e..9863e88 100644 --- a/src/miette_diagnostic.rs +++ b/src/miette_diagnostic.rs @@ -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/protocol.rs b/src/protocol.rs index 36c3539..be313b1 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -249,6 +249,7 @@ pub struct LabeledSpan { #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] label: Option, span: SourceSpan, + primary: bool, } impl LabeledSpan { @@ -257,6 +258,7 @@ impl LabeledSpan { Self { label, span: SourceSpan::new(SourceOffset(offset), SourceOffset(len)), + primary: false, } } @@ -265,6 +267,16 @@ 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, } } @@ -340,6 +352,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")] @@ -350,7 +367,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, }) ); @@ -358,7 +376,8 @@ 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, }) ) } @@ -370,20 +389,23 @@ 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)) diff --git a/tests/graphical.rs b/tests/graphical.rs index 0c69470..887e454 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -86,7 +86,7 @@ fn single_line_highlight_span_full_line() { println!("Error: {}", out); let expected = r#" × oops! - ╭─[issue:1:1] + ╭─[issue:2:1] 1 │ source 2 │ text · ──┬─ @@ -120,7 +120,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 · ───┬── @@ -159,7 +159,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 · ──┬─ @@ -198,7 +198,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 · ──┬─ @@ -235,7 +235,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 +270,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 · ──┬─ @@ -343,7 +343,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 @@ -379,7 +379,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 · ──┬── @@ -416,7 +416,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 · ──┬── @@ -453,7 +453,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 · ▲ @@ -490,7 +490,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 · ──── @@ -526,7 +526,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 · ──┬─ @@ -569,7 +569,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 · ──┬─ ──┬─ ──┬─ @@ -616,7 +616,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 · ──┬─ ──┬─ ──┬─ @@ -655,7 +655,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 @@ -969,7 +969,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 · ──┬─ @@ -1031,7 +1031,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 · ──┬─ @@ -1136,7 +1136,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 · ──┬─ @@ -1201,7 +1201,7 @@ fn zero_length_eol_span() { println!("Error: {}", out); let expected = r#" × oops! - ╭─[issue:1:1] + ╭─[issue:2:1] 1 │ this is the first line 2 │ this is the second line · ▲ @@ -1212,3 +1212,38 @@ 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, + #[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); +} diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index df30b2e..0ca396a 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -91,7 +91,7 @@ fn test_diagnostic_source_pass_extra_info() { println!("Error: {}", out); let expected = r#" × TestError ╰─▶ × A complex error happened - ╭─[1:1] + ╭─[1:2] 1 │ Hello · ──┬─ · ╰── here From 1f448e47751d0f914134b0e9138fdb1a5a95d55c Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 26 Sep 2023 00:31:44 +0700 Subject: [PATCH 12/67] fix(tests): Fix `cargo test` with default features. (#294) A couple of tests require `fancy-no-backtrace`, so mark them accordingly. --- tests/test_diagnostic_source_macro.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 0ca396a..334ca34 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -75,6 +75,7 @@ fn test_diagnostic_source() { 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 { @@ -106,6 +107,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 { @@ -147,6 +149,7 @@ struct NestedError { the_other_err: Box, } +#[cfg(feature = "fancy-no-backtrace")] #[test] fn test_nested_diagnostic_source_is_output() { let inner_error = TestStructError { From 06b348230aaf153b8b050322f05e5d185351d2d1 Mon Sep 17 00:00:00 2001 From: Bruce Mitchener Date: Tue, 26 Sep 2023 00:33:07 +0700 Subject: [PATCH 13/67] fix(clippy): Add missing semicolons where nothing is returned. (#293) --- src/handlers/graphical.rs | 4 ++-- src/handlers/json.rs | 2 +- src/protocol.rs | 12 ++++++------ tests/derive.rs | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index a7399fd..770944d 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -678,10 +678,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')?; 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/protocol.rs b/src/protocol.rs index be313b1..4a6012c 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -379,7 +379,7 @@ fn test_serialize_labeled_span() { "span": { "offset": 0, "length": 0, }, "primary": false, }) - ) + ); } #[cfg(feature = "serde")] @@ -408,7 +408,7 @@ fn test_deserialize_labeled_span() { "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)); } /** @@ -597,7 +597,7 @@ fn test_serialize_source_span() { assert_eq!( json!(SourceSpan::from(0)), json!({ "offset": 0, "length": 0}) - ) + ); } #[cfg(feature = "serde")] @@ -606,7 +606,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)); } /** @@ -708,12 +708,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/tests/derive.rs b/tests/derive.rs index dbaf7cb..f4739a7 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -568,7 +568,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] @@ -582,5 +582,5 @@ fn test_unit_enum_display() { assert_eq!( Enum::UnitVariant.help().unwrap().to_string(), "hello from unit help" - ) + ); } From ba313282a8e6950fa7c19b9b66d45deb83aaa1bd Mon Sep 17 00:00:00 2001 From: Thomas Versteeg Date: Fri, 29 Sep 2023 13:56:09 +0000 Subject: [PATCH 14/67] docs: typo in README.md (#295) Dynamic diagnostics example contains a macro invocation without a '!' symbol. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7aabe99..7d73ba3 100644 --- a/README.md +++ b/README.md @@ -615,7 +615,7 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or 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 '+'", From 88d00e0e20bf95e03b8f81dcd5adf38c917e190e Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sat, 21 Oct 2023 11:50:15 +0800 Subject: [PATCH 15/67] feat(derive): Allow optional sources in derive (#301) Fixes: https://github.com/zkat/miette/issues/217 --- miette-derive/src/source_code.rs | 42 +++++++++++++++++++++++++++++--- tests/derive.rs | 42 ++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) 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/tests/derive.rs b/tests/derive.rs index f4739a7..d1da1bb 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -584,3 +584,45 @@ fn test_unit_enum_display() { "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()); +} From d37ada876a5831d3f47622274e334c9a24aa5d2b Mon Sep 17 00:00:00 2001 From: Andre Meyering Date: Mon, 23 Oct 2023 19:09:00 +0200 Subject: [PATCH 16/67] docs(README): Move import of `NamedResult` to where it is used (#309) An example in README.md makes use of `miette::NamedResult` . It is imported in the second "example-step", but already used in the first one. Beginners, that copy the example step-by-step to understand it, will run into compile errors, as `NamedResult` was not imported. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7d73ba3..a29b428 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type. `thiserror` is a great way to define them, and plays nicely with `miette`! */ -use miette::{Diagnostic, SourceSpan}; +use miette::{Diagnostic, NamedSource, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,7 +123,7 @@ Use this `Result` type (or its expanded version) as the return type throughout your app (but NOT your libraries! Those should always return concrete types!). */ -use miette::{NamedSource, Result}; +use miette::Result; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. From a8b4ae012aa0cf03b53a18f013c2b3f76c5040e7 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 2 Nov 2023 12:33:55 -0400 Subject: [PATCH 17/67] fix(graphical): Extend error text span to whole code points (#312) Fixes: https://github.com/zkat/miette/issues/223 This fixes a panic when an error starts inside a Unicode code point. The range is extended to start (or end) at the beginning (or end) of the character inside which the byte offset is located. --- src/handlers/graphical.rs | 19 +++++++--- tests/graphical.rs | 74 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 770944d..44d92b7 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -651,11 +651,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() { @@ -706,8 +717,8 @@ 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 = self.visual_offset(line, byte_end, false).max(start + 1); let vbar_offset = (start + end) / 2; let num_left = vbar_offset - start; diff --git a/tests/graphical.rs b/tests/graphical.rs index 887e454..31db4dc 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1247,3 +1247,77 @@ fn primary_label() { 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(()) +} From 3d6f903df0e7c9d0eb9a1fdbbf0028bab5496429 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Thu, 9 Nov 2023 13:21:32 -0800 Subject: [PATCH 18/67] fix(formatting): Fix formatting bug when an empty span is not aligned to a char boundary (#314) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous output looked like this: ---- single_line_with_wide_char_unaligned_span_empty stdout ---- Error: 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? Note that the .max(start + 1) term is still necessary in the nonempty branch, since it's possible to have a nonempty span covering zero-width text. * remove uncessary if statement start > end in all cases. --- src/handlers/graphical.rs | 44 ++++++++++++++++++++------------------- tests/graphical.rs | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 44d92b7..8cec88b 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -718,31 +718,33 @@ impl GraphicalReportHandler { let byte_start = hl.offset(); let byte_end = hl.offset() + hl.len(); let start = self.visual_offset(line, byte_start, true).max(highest); - let end = self.visual_offset(line, byte_end, false).max(start + 1); + 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) diff --git a/tests/graphical.rs b/tests/graphical.rs index 31db4dc..d24879a 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1321,3 +1321,40 @@ fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> { 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(()) +} From c7ba5b7e52e05991cecd3ca925c710bbe49850b9 Mon Sep 17 00:00:00 2001 From: ManicMarrc <122530518+ManicMarrc@users.noreply.github.com> Date: Fri, 10 Nov 2023 04:22:47 +0700 Subject: [PATCH 19/67] feat(derive): Make `miette-derive` be able to be turned off (#304) --- Cargo.toml | 5 +++-- src/error.rs | 42 +++++++++++++++++++++++++++++++++--------- src/lib.rs | 1 + 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dd6e584..f8c2cb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.10.0" } +miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } once_cell = "1.8.0" unicode-width = "0.1.9" @@ -44,7 +44,8 @@ lazy_static = "1.4" serde_json = "1.0.64" [features] -default = [] +default = ["derive"] +derive = ["miette-derive"] no-format-args-capture = [] fancy-no-backtrace = [ "owo-colors", 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/lib.rs b/src/lib.rs index 20589ef..3cb021b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -652,6 +652,7 @@ //! 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::*; From fd77257cee0f5d03aa7dccb4ba8cbaa40c1a88c6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 15 Nov 2023 12:34:24 -0600 Subject: [PATCH 20/67] feat(graphical): Expose additional `textwrap` options (#321) --- src/handler.rs | 34 +++++++ src/handlers/graphical.rs | 74 +++++++++++++-- src/lib.rs | 1 + tests/graphical.rs | 184 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 285 insertions(+), 8 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e983a55..e32f3ef 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -55,6 +55,9 @@ 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) word_separator: Option, + pub(crate) word_splitter: Option, } impl MietteHandlerOpts { @@ -86,6 +89,27 @@ impl MietteHandlerOpts { 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); @@ -233,6 +257,16 @@ 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(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), } diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 8cec88b..35e9c79 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -30,6 +30,9 @@ pub struct GraphicalReportHandler { pub(crate) context_lines: usize, pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, + pub(crate) break_words: bool, + pub(crate) word_separator: Option, + pub(crate) word_splitter: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -51,6 +54,9 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + break_words: true, + word_separator: None, + word_splitter: None, } } @@ -64,6 +70,9 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + break_words: true, + word_separator: None, + word_splitter: None, } } @@ -122,6 +131,24 @@ impl GraphicalReportHandler { 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); @@ -159,9 +186,17 @@ impl GraphicalReportHandler { if let Some(footer) = &self.footer { writeln!(f)?; let width = self.termwidth.saturating_sub(4); - let opts = textwrap::Options::new(width) + let mut opts = textwrap::Options::new(width) .initial_indent(" ") - .subsequent_indent(" "); + .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, "{}", textwrap::fill(footer, opts))?; } Ok(()) @@ -212,9 +247,16 @@ 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))?; @@ -251,9 +293,17 @@ 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(); @@ -280,9 +330,17 @@ impl GraphicalReportHandler { if let Some(help) = diagnostic.help() { let width = self.termwidth.saturating_sub(4); 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(" "); + .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, "{}", textwrap::fill(&help.to_string(), opts))?; } Ok(()) diff --git a/src/lib.rs b/src/lib.rs index 3cb021b..08de732 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -593,6 +593,7 @@ //! .unicode(false) //! .context_lines(3) //! .tab_width(4) +//! .break_words(true) //! .build(), //! ) //! })) diff --git a/tests/graphical.rs b/tests/graphical.rs index d24879a..536efcd 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -34,6 +34,190 @@ 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 = " × 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 = " × 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 empty_source() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] From 251d6d59292397458328ef57fb7957faedafd019 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Wed, 15 Nov 2023 19:35:46 +0100 Subject: [PATCH 21/67] fix(docs): add example to README and docs fixing #96 (#319) --- README.md | 17 +++++++++++++++++ src/lib.rs | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/README.md b/README.md index a29b428..5f79c52 100644 --- a/README.md +++ b/README.md @@ -305,6 +305,23 @@ enabled: 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 diff --git a/src/lib.rs b/src/lib.rs index 08de732..d80efa7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -304,6 +304,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 From 865d67c8dda119ddd03ac43be22f4fa272a9f433 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Wed, 15 Nov 2023 19:39:29 +0100 Subject: [PATCH 22/67] feat(graphical): support rendering labels that contain newlines (#318) Fixes: https://github.com/zkat/miette/issues/85 --- src/handlers/graphical.rs | 313 ++++++++++++++++++++++++++++++++------ tests/graphical.rs | 171 +++++++++++++++++++++ 2 files changed, 435 insertions(+), 49 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 35e9c79..baadfb3 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler: See [`set_hook()`](crate::set_hook) for more details on customizing your global printer. -*/ + */ #[derive(Debug, Clone)] pub struct GraphicalReportHandler { pub(crate) links: LinkStyle, @@ -545,7 +545,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, @@ -557,11 +563,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)?; } } } @@ -575,6 +577,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, @@ -643,6 +730,7 @@ impl GraphicalReportHandler { max_gutter: usize, line: &Line, highlights: &[FancySpan], + render_mode: LabelRenderMode, ) -> fmt::Result { if max_gutter == 0 { return Ok(()); @@ -652,15 +740,33 @@ impl GraphicalReportHandler { let applicable = highlights.iter().filter(|hl| line.span_applies(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 + for _ in 0..max_gutter.saturating_sub(i) + 2 { + gutter.push(' '); + } + } else { + gutter.push_str(&chars.lbot.style(hl.style).to_string()); + + gutter.push_str( + &chars + .hbar + .to_string() + .repeat( + max_gutter.saturating_sub(i) + // if we are rendering a multiline label, then leave a bit of space for the + // rcross character + + if render_mode == LabelRenderMode::MultiLineFirst { + 1 + } else { + 2 + }, + ) + .style(hl.style) + .to_string(), + ); + } break; } else { gutter.push_str(&chars.vbar.style(hl.style).to_string()); @@ -811,27 +917,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; } } } @@ -839,13 +958,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(()) } @@ -924,6 +1110,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, @@ -941,10 +1137,10 @@ impl Line { 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) } // A 'flyby' is a multi-line span that technically covers this line, but @@ -974,7 +1170,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, } @@ -985,9 +1184,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 { @@ -997,7 +1204,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/tests/graphical.rs b/tests/graphical.rs index 536efcd..f59f849 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -251,6 +251,52 @@ 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, + #[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)] @@ -725,6 +771,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)] @@ -853,6 +987,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)] From 7ff4f874d693a665af4df40f4e94505013e3e262 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonathan=20D=C3=B6nszelmann?= Date: Wed, 15 Nov 2023 20:40:16 +0100 Subject: [PATCH 23/67] fix(graphical): rendering bug on small spans in large spans (#316) Fixes: https://github.com/zkat/miette/issues/317 It turned out there were two really. One related to how many characters were added for the arrowheads in the gutter, and one where the gutter was extended to a number of characters, including ansi escape codes. However, because ansi escape codes are rather big, there would never be any extension since the system thought the string was already long enough, even though you don't actually see the width of those codes. --- src/handlers/graphical.rs | 69 ++++++++++++++++++++++++++++++++++----- tests/graphical.rs | 6 ++-- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index baadfb3..3193472 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -20,7 +20,7 @@ This printer can be customized by using [`new_themed()`](GraphicalReportHandler: See [`set_hook()`](crate::set_hook) for more details on customizing your global printer. - */ +*/ #[derive(Debug, Clone)] pub struct GraphicalReportHandler { pub(crate) links: LinkStyle, @@ -468,7 +468,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; } } @@ -674,7 +674,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) { @@ -735,18 +735,35 @@ impl GraphicalReportHandler { 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) { 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 - for _ in 0..max_gutter.saturating_sub(i) + 2 { + 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( @@ -754,25 +771,42 @@ impl GraphicalReportHandler { .hbar .to_string() .repeat( - max_gutter.saturating_sub(i) + num_repeat // if we are rendering a multiline label, then leave a bit of space for the // rcross character - + if render_mode == LabelRenderMode::MultiLineFirst { + - if render_mode == LabelRenderMode::MultiLineFirst { 1 } else { - 2 + 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(()) } @@ -1133,9 +1167,12 @@ 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 @@ -1143,6 +1180,20 @@ impl 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 // does not begin or end within the line itself. This method is used to // calculate gutters. diff --git a/tests/graphical.rs b/tests/graphical.rs index f59f849..aabf167 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -283,9 +283,9 @@ if true { let expected = r#" × oops! ╭─[issue:1:1] 1 │ ╭─▶ if true { - 2 │ │╭▶ a - · ││ ┬ - · ││ ╰── small + 2 │ │ a + · │ ┬ + · │ ╰── small 3 │ │ } else { 4 │ │ b 5 │ ├─▶ } From b0744462adbbfbb6d845f382db36be883c7f3c45 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Thu, 4 Jan 2024 12:44:52 -0600 Subject: [PATCH 24/67] feat(graphical): Add `wrap_lines: bool` option allowing wrapping be disabled entirely (#328) --- src/handler.rs | 15 ++++++++++- src/handlers/graphical.rs | 54 +++++++++++++++++++++++++++++++++++---- tests/graphical.rs | 44 +++++++++++++++++++++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index e32f3ef..38c9fc8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -56,6 +56,7 @@ pub struct MietteHandlerOpts { 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, } @@ -89,6 +90,16 @@ impl MietteHandlerOpts { 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. @@ -98,7 +109,6 @@ impl MietteHandlerOpts { 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); @@ -260,6 +270,9 @@ impl MietteHandlerOpts { 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) } diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 3193472..951303a 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -30,6 +30,7 @@ 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, @@ -54,6 +55,7 @@ impl GraphicalReportHandler { context_lines: 1, tab_width: 4, with_cause_chain: true, + wrap_lines: true, break_words: true, word_separator: None, word_splitter: None, @@ -69,6 +71,7 @@ impl GraphicalReportHandler { footer: None, context_lines: 1, tab_width: 4, + wrap_lines: true, with_cause_chain: true, break_words: true, word_separator: None, @@ -131,6 +134,12 @@ 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; @@ -197,7 +206,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(footer, opts))?; + writeln!(f, "{}", self.wrap(footer, opts))?; } Ok(()) } @@ -258,7 +267,7 @@ impl GraphicalReportHandler { 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(()); @@ -314,10 +323,10 @@ impl GraphicalReportHandler { inner_renderer.with_cause_chain = false; inner_renderer.render_report(&mut inner, diag)?; - writeln!(f, "{}", textwrap::fill(&inner, opts))?; + 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))?; } } } @@ -341,7 +350,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; + writeln!(f, "{}", self.wrap(&help.to_string(), opts))?; } Ok(()) } @@ -810,6 +819,41 @@ impl GraphicalReportHandler { 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, diff --git a/tests/graphical.rs b/tests/graphical.rs index aabf167..21185f5 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -218,6 +218,50 @@ fn word_wrap_options() -> Result<(), MietteError> { 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 = + " × 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 empty_source() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] From 19c22143cb544616046784e35c5e78cc5b881289 Mon Sep 17 00:00:00 2001 From: Hytak Date: Thu, 11 Jan 2024 19:42:32 +0100 Subject: [PATCH 25/67] feat(graphical): render disjoint snippets separately for cleaner output (#324) --- src/handlers/graphical.rs | 120 +++++++++++++++++++------------------- tests/graphical.rs | 92 +++++++++++++++++++++++++++++ 2 files changed, 151 insertions(+), 61 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 951303a..021ba34 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -6,7 +6,7 @@ use unicode_width::UnicodeWidthChar; use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; use crate::handlers::theme::*; 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 @@ -386,66 +386,58 @@ 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 = source + .read_span(right.inner(), self.context_lines, self.context_lines) + .map_err(|_| fmt::Error)?; + + 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(()) } @@ -458,10 +450,16 @@ impl GraphicalReportHandler { ) -> fmt::Result { let (contents, lines) = self.get_lines(source, context.inner())?; - let primary_label = labels - .iter() + // 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(|| labels.first()); + .or_else(|| ctx_labels.clone().next()); // sorting is your friend let labels = labels diff --git a/tests/graphical.rs b/tests/graphical.rs index 21185f5..4e8b737 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1757,3 +1757,95 @@ fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> assert_eq!(expected, out); Ok(()) } + +#[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(()) +} From 55bfc4201638e412dd2732d394389e22a7398822 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Thu, 11 Jan 2024 14:13:00 -0800 Subject: [PATCH 26/67] tests: improve robustness of color_format tests (#329) Previously, these tests would have spurious failures when NO_COLOR or FORCE_COLOR was set in the user's environment, since we weren't clearing one variable before testing a value for the other one. The previous version of the code also did not restore environment variable values on panic, which could cause spurious failures in other tests after one test fails. --- tests/color_format.rs | 73 +++++++++++++++++++++++++++++-------------- 1 file changed, 49 insertions(+), 24 deletions(-) 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] From cb2ae2e18b446a5e90885faf8a30b5672c307df8 Mon Sep 17 00:00:00 2001 From: Brooks Rady Date: Wed, 31 Jan 2024 00:03:01 +0000 Subject: [PATCH 27/67] fix(graphical): render cause chains for inner errors (#330) The default `GraphicalReportHandler` disables the printing of cause chains for any inner errors (errors `related()` to a source diagnostic) when it disables nested footer printing. This results in lost cause chain information when printing with the default report handler. --- src/handlers/graphical.rs | 16 ++++-- tests/test_diagnostic_source_macro.rs | 82 +++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 6 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 021ba34..7812925 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -317,9 +317,10 @@ impl GraphicalReportHandler { 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; inner_renderer.render_report(&mut inner, diag)?; @@ -362,6 +363,9 @@ impl GraphicalReportHandler { parent_src: Option<&dyn SourceCode>, ) -> fmt::Result { if let Some(related) = diagnostic.related() { + let mut inner_renderer = self.clone(); + // Re-enable the printing of nested cause chains for related errors + inner_renderer.with_cause_chain = true; writeln!(f)?; for rel in related { match rel.severity() { @@ -369,12 +373,12 @@ impl GraphicalReportHandler { 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(()) diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index 334ca34..e5305ac 100644 --- a/tests/test_diagnostic_source_macro.rs +++ b/tests/test_diagnostic_source_macro.rs @@ -194,3 +194,85 @@ 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); +} From f1dc89c07640445d224b61ef96c6b25fcdf62dee Mon Sep 17 00:00:00 2001 From: Boshen Date: Sun, 4 Feb 2024 11:12:49 +0800 Subject: [PATCH 28/67] fix(handler): remove the two extra `is_terminal` sys call from `MietteHandlerOpts::build` (#325) `GraphicalReportHandler::new()` calls `GraphicalTheme::default()` https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/graphical.rs#L52 which calls `std::io::stdout().is_terminal()` and `std::io::stderr().is_terminal()` https://github.com/zkat/miette/blob/7ff4f874d693a665af4df40f4e94505013e3e262/src/handlers/theme.rs#L72 but this default theme is overridden by `with_theme(theme)`. --- src/handler.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 38c9fc8..3c09373 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -247,10 +247,9 @@ impl MietteHandlerOpts { ThemeStyles::none() }; 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); if let Some(with_cause_chain) = self.with_cause_chain { if with_cause_chain { handler = handler.with_cause_chain(); From c0a298e5a8d699acf9fcd61b5d5fa4f6279a47ab Mon Sep 17 00:00:00 2001 From: John Nunley Date: Sat, 3 Feb 2024 19:31:41 -0800 Subject: [PATCH 29/67] feat(deps): Bump terminal-size to v0.3.0 (#308) BREAKING CHANGE: This requires an MSRV bump to 1.70.0. Signed-off-by: John Nunley --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index f8c2cb5..a730148 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ 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 } +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 } From 0d5c2ce7536b0ea205346595d8a00d00bfb6cbd2 Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sun, 4 Feb 2024 06:33:59 +0300 Subject: [PATCH 30/67] feat(source-code): Don't override provided source code (#300) BREAKING CHANGE: Source code is no longer overridden if it was provided by the diagnostic's own `source_code()` impl. --- src/eyreish/wrapper.rs | 89 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 2 deletions(-) diff --git a/src/eyreish/wrapper.rs b/src/eyreish/wrapper.rs index 91a5ef3..6e65eb7 100644 --- a/src/eyreish/wrapper.rs +++ b/src/eyreish/wrapper.rs @@ -163,7 +163,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 +197,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 +232,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)); + } +} From fad0e76ad2e19d5cac13cf8324338aca0d623d93 Mon Sep 17 00:00:00 2001 From: Gavrilikhin Daniil Date: Sun, 4 Feb 2024 06:38:00 +0300 Subject: [PATCH 31/67] feat(source): use `usize` for length (#265) BREAKING CHANGE: This changes `SourceSpan`'s length type to `usize`. --- src/protocol.rs | 8 ++++---- tests/derive.rs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/protocol.rs b/src/protocol.rs index 4a6012c..b403494 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -526,10 +526,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, } } @@ -559,8 +559,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) } } diff --git a/tests/derive.rs b/tests/derive.rs index d1da1bb..ac29eee 100644 --- a/tests/derive.rs +++ b/tests/derive.rs @@ -406,7 +406,7 @@ impl ForwardsTo { fn new() -> Self { ForwardsTo { src: SNIPPET_TEXT.into(), - label: SourceSpan::new(11.into(), 6.into()), + label: SourceSpan::new(11.into(), 6), } } } From 1df3b1a537f2e54cd40ec45f5cd851337a22e95a Mon Sep 17 00:00:00 2001 From: Adam Curtis Date: Sat, 3 Feb 2024 22:40:17 -0500 Subject: [PATCH 32/67] feat(source): Allow inner source type of a NamedSource to be borrowed (#254) BREAKING CHANGE: This makes the `NamedSource` type generic over its `Source` type, instead of boxing it. --- README.md | 2 +- src/lib.rs | 2 +- src/named_source.rs | 21 ++++++++------- tests/graphical.rs | 56 +++++++++++++++++++-------------------- tests/narrated.rs | 30 ++++++++++----------- tests/test_derive_attr.rs | 12 ++++----- tests/test_json.rs | 30 ++++++++++----------- 7 files changed, 78 insertions(+), 75 deletions(-) diff --git a/README.md b/README.md index 5f79c52..8af2845 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,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, diff --git a/src/lib.rs b/src/lib.rs index d80efa7..862e242 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,7 +109,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, diff --git a/src/named_source.rs b/src/named_source.rs index 31ad1d1..8b0b75a 100644 --- a/src/named_source.rs +++ b/src/named_source.rs @@ -3,12 +3,12 @@ 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, +pub struct NamedSource { + source: S, name: String, } -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) @@ -17,12 +17,15 @@ impl std::fmt::Debug for NamedSource { } } -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(), } } @@ -34,12 +37,12 @@ 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 } } -impl SourceCode for NamedSource { +impl SourceCode for NamedSource { fn read_span<'a>( &'a self, span: &crate::SourceSpan, diff --git a/tests/graphical.rs b/tests/graphical.rs index 4e8b737..ffb701c 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -269,7 +269,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, } @@ -348,7 +348,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, } @@ -379,7 +379,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, } @@ -416,7 +416,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, } @@ -455,7 +455,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, } @@ -494,7 +494,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, } @@ -566,7 +566,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, } @@ -602,7 +602,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, } @@ -638,7 +638,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, } @@ -675,7 +675,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, } @@ -712,7 +712,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, } @@ -749,7 +749,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, } @@ -785,7 +785,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, } @@ -910,7 +910,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"] @@ -955,7 +955,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"] @@ -1002,7 +1002,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, } @@ -1075,7 +1075,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"] @@ -1126,7 +1126,7 @@ fn multiline_highlight_no_label() -> Result<(), MietteError> { #[source] source: Inner, #[source_code] - src: NamedSource, + src: NamedSource, #[label = "block 1"] highlight1: SourceSpan, #[label] @@ -1190,7 +1190,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"] @@ -1236,7 +1236,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"] @@ -1264,7 +1264,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"] @@ -1346,7 +1346,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] @@ -1402,7 +1402,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] @@ -1462,7 +1462,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] @@ -1479,7 +1479,7 @@ fn related_severity() -> Result<(), MietteError> { )] Error { #[source_code] - src: NamedSource, + src: NamedSource, #[label("this bit here")] highlight: SourceSpan, }, @@ -1492,7 +1492,7 @@ fn related_severity() -> Result<(), MietteError> { )] Warning { #[source_code] - src: NamedSource, + src: NamedSource, #[label("this bit here")] highlight: SourceSpan, }, @@ -1505,7 +1505,7 @@ fn related_severity() -> Result<(), MietteError> { )] Advice { #[source_code] - src: NamedSource, + src: NamedSource, #[label("this bit here")] highlight: SourceSpan, }, @@ -1588,7 +1588,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, } 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_json.rs b/tests/test_json.rs index ae482b8..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, } @@ -65,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, } @@ -110,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, } @@ -155,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, } @@ -200,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, } @@ -244,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, } @@ -289,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"] @@ -354,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, } @@ -399,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"] @@ -463,7 +463,7 @@ mod json_report_handler { #[source] source: Inner, #[source_code] - src: NamedSource, + src: NamedSource, #[label = "block 1"] highlight1: SourceSpan, #[label] @@ -536,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"] @@ -591,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, #[label = "this bit here"] highlight1: SourceSpan, #[label = "also this bit"] @@ -646,7 +646,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"] @@ -728,7 +728,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, #[related] @@ -821,7 +821,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, #[related] From e65d0a78cc639653f061a45d8ce35b1a3551ade7 Mon Sep 17 00:00:00 2001 From: Adam Curtis Date: Sat, 3 Feb 2024 22:47:46 -0500 Subject: [PATCH 33/67] feat(highlighting): add syntax highlighting support with syntect crate (#313) --- .github/workflows/ci.yml | 11 ++- Cargo.toml | 5 +- src/handler.rs | 66 +++++++++++++- src/handlers/graphical.rs | 50 +++++++++-- src/highlighters/blank.rs | 36 ++++++++ src/highlighters/mod.rs | 116 ++++++++++++++++++++++++ src/highlighters/syntect.rs | 170 ++++++++++++++++++++++++++++++++++++ src/lib.rs | 31 +++++++ src/named_source.rs | 35 +++++--- src/protocol.rs | 22 +++++ tests/graphical.rs | 98 +++++++++++++++++++++ 11 files changed, 617 insertions(+), 23 deletions(-) create mode 100644 src/highlighters/blank.rs create mode 100644 src/highlighters/mod.rs create mode 100644 src/highlighters/syntect.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 912bfcc..32125cc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,8 +26,12 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: + features: [fancy, syntect-highlighter] rust: [1.56.0, stable] os: [ubuntu-latest, macOS-latest, windows-latest] + exclude: + - features: syntect-highlighter + rust: 1.56.0 steps: - uses: actions/checkout@v4 @@ -43,10 +47,10 @@ jobs: 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 + run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture miri: name: Miri @@ -78,5 +82,4 @@ jobs: with: toolchain: nightly - name: Run minimal version build - run: cargo build -Z minimal-versions --all-features - + run: cargo build -Z minimal-versions --features fancy,no-format-args-capture diff --git a/Cargo.toml b/Cargo.toml index a730148..0123cc0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } once_cell = "1.8.0" unicode-width = "0.1.9" -owo-colors = { version = "3.0.0", optional = true } +owo-colors = { version = "3.4.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 } @@ -28,6 +28,7 @@ backtrace = { version = "0.3.61", 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 } +syntect = { version = "5.1.0", optional = true } [dev-dependencies] semver = "1.0.4" @@ -42,6 +43,7 @@ regex = "1.5" lazy_static = "1.4" serde_json = "1.0.64" +strip-ansi-escapes = "0.2.0" [features] default = ["derive"] @@ -57,6 +59,7 @@ fancy-no-backtrace = [ "supports-unicode", ] fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"] +syntect-highlighter = ["fancy-no-backtrace", "syntect"] [workspace] members = ["miette-derive"] diff --git a/src/handler.rs b/src/handler.rs index 3c09373..a564f44 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; @@ -59,6 +61,7 @@ pub struct MietteHandlerOpts { pub(crate) wrap_lines: Option, pub(crate) word_separator: Option, pub(crate) word_splitter: Option, + pub(crate) highlighter: Option, } impl MietteHandlerOpts { @@ -84,6 +87,43 @@ 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); @@ -246,10 +286,34 @@ 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) + || supports_color::on(supports_color::Stream::Stderr).is_some() + { + 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_themed(theme) .with_width(width) - .with_links(linkify); + .with_links(linkify) + .with_theme(theme); + handler.highlighter = highlighter; if let Some(with_cause_chain) = self.with_cause_chain { if with_cause_chain { handler = handler.with_cause_chain(); diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 7812925..d7e1883 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -1,10 +1,11 @@ use std::fmt::{self, Write}; -use owo_colors::{OwoColorize, Style}; +use owo_colors::{OwoColorize, Style, StyledList}; use unicode_width::UnicodeWidthChar; use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; use crate::handlers::theme::*; +use crate::highlighters::{Highlighter, MietteHighlighter}; use crate::protocol::{Diagnostic, Severity}; use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents}; @@ -34,6 +35,7 @@ pub struct GraphicalReportHandler { pub(crate) break_words: bool, pub(crate) word_separator: Option, pub(crate) word_splitter: Option, + pub(crate) highlighter: MietteHighlighter, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -59,6 +61,7 @@ impl GraphicalReportHandler { break_words: true, word_separator: None, word_splitter: None, + highlighter: MietteHighlighter::default(), } } @@ -76,6 +79,7 @@ impl GraphicalReportHandler { break_words: true, word_separator: None, word_splitter: None, + highlighter: MietteHighlighter::default(), } } @@ -169,6 +173,23 @@ 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 + } } impl Default for GraphicalReportHandler { @@ -472,6 +493,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. @@ -545,7 +568,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 @@ -881,13 +906,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 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..47e7f7c --- /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 is_terminal::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 862e242..f8d92f4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,6 +47,7 @@ //! - [... delayed source code](#-delayed-source-code) //! - [... handler options](#-handler-options) //! - [... dynamic diagnostics](#-dynamic-diagnostics) +//! - [... syntax highlighting](#-syntax-highlighting) //! - [Acknowledgements](#acknowledgements) //! - [License](#license) //! @@ -643,6 +644,34 @@ //! 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. +//! //! ## Acknowledgements //! //! `miette` was not developed in a void. It owes enormous credit to various @@ -691,6 +720,8 @@ mod eyreish; #[cfg(feature = "fancy-no-backtrace")] mod handler; mod handlers; +#[cfg(feature = "fancy-no-backtrace")] +pub mod highlighters; #[doc(hidden)] pub mod macro_helpers; mod miette_diagnostic; diff --git a/src/named_source.rs b/src/named_source.rs index 8b0b75a..99e74a8 100644 --- a/src/named_source.rs +++ b/src/named_source.rs @@ -6,13 +6,15 @@ use crate::{MietteError, MietteSpanContents, SourceCode, SpanContents}; pub struct NamedSource { source: S, name: String, + language: Option, } 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(()) } } @@ -27,6 +29,7 @@ impl NamedSource { Self { source, name: name.as_ref().to_string(), + language: None, } } @@ -40,6 +43,12 @@ impl NamedSource { 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 { @@ -49,16 +58,20 @@ impl SourceCode for NamedSource { 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 b403494..042526e 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -433,6 +433,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 + } } /** @@ -452,6 +461,8 @@ pub struct MietteSpanContents<'a> { line_count: usize, // Optional filename name: Option, + // Optional language + language: Option, } impl<'a> MietteSpanContents<'a> { @@ -470,6 +481,7 @@ impl<'a> MietteSpanContents<'a> { column, line_count, name: None, + language: None, } } @@ -489,8 +501,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> { @@ -512,6 +531,9 @@ 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`] diff --git a/tests/graphical.rs b/tests/graphical.rs index ffb701c..ac9ec10 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(); @@ -1758,6 +1760,102 @@ fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError> 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)] From e5c7ae469e40a8bc102e1fca3b8fd4b2ec137696 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 19:24:07 -0800 Subject: [PATCH 34/67] feat(deps): remove is-terminal dep in favor of `std::io::IsTerminal` --- .github/workflows/ci.yml | 8 ++++---- Cargo.toml | 8 +++----- src/handlers/theme.rs | 3 ++- src/protocol.rs | 2 +- tests/graphical.rs | 17 +++++++++-------- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32125cc..e6871c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,11 +27,11 @@ jobs: strategy: matrix: features: [fancy, syntect-highlighter] - rust: [1.56.0, stable] + rust: [1.70.0, stable] os: [ubuntu-latest, macOS-latest, windows-latest] exclude: - features: syntect-highlighter - rust: 1.56.0 + rust: 1.70.0 steps: - uses: actions/checkout@v4 @@ -41,7 +41,7 @@ jobs: toolchain: ${{ matrix.rust }} components: clippy - name: Force older version of is-terminal for MSRV builds - if: matrix.rust == '1.56.0' + if: matrix.rust == '1.70.0' run: cargo update -p is-terminal --precise 0.4.7 - name: Clippy run: cargo clippy --all -- -D warnings @@ -49,7 +49,7 @@ jobs: if: matrix.rust == 'stable' run: cargo test --all --verbose --features ${{matrix.features}} - name: Run tests - if: matrix.rust == '1.56.0' + if: matrix.rust == '1.70.0' run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture miri: diff --git a/Cargo.toml b/Cargo.toml index 0123cc0..7567d95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,11 +19,10 @@ once_cell = "1.8.0" unicode-width = "0.1.9" owo-colors = { version = "3.4.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 } +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.61", optional = true } terminal_size = { version = "0.3.0", optional = true } backtrace-ext = { version = "0.2.1", optional = true } @@ -51,7 +50,6 @@ derive = ["miette-derive"] no-format-args-capture = [] fancy-no-backtrace = [ "owo-colors", - "is-terminal", "textwrap", "terminal_size", "supports-hyperlinks", diff --git a/src/handlers/theme.rs b/src/handlers/theme.rs index aa6649e..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; /** diff --git a/src/protocol.rs b/src/protocol.rs index 042526e..8ca719d 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -257,7 +257,7 @@ 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, } } diff --git a/tests/graphical.rs b/tests/graphical.rs index ac9ec10..86d79e1 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1842,11 +1842,11 @@ fn syntax_highlighter_on_real_file() { let expected = format!( r#" × This is an error ╭─[{filename}:{l2}:{CO}] - {l1} │ + {l1} │ {l2} │ let (filename, line) = (file!(), line!() as usize); · ─────────────┬───────────── · ╰── this is a label - {l3} │ + {l3} │ ╰──── "#, l1 = line - 1, @@ -1855,6 +1855,7 @@ fn syntax_highlighter_on_real_file() { ); 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> { @@ -1888,13 +1889,13 @@ fn triple_adjacent_highlight() -> Result<(), MietteError> { 1 │ source · ───┬── · ╰── this bit here - 2 │ - 3 │ + 2 │ + 3 │ 4 │ text · ──┬─ · ╰── also this bit - 5 │ - 6 │ + 5 │ + 6 │ 7 │ here · ──┬─ · ╰── finally we got @@ -1934,10 +1935,10 @@ fn non_adjacent_highlight() -> Result<(), MietteError> { 1 │ source · ───┬── · ╰── this bit here - 2 │ + 2 │ ╰──── ╭─[bad_file.rs:5:3] - 4 │ + 4 │ 5 │ text here · ──┬─ · ╰── also this bit From 4c48584f304414c6924bede3308b455cfef60749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 19:29:28 -0800 Subject: [PATCH 35/67] feat(deps): remove once_cell dep in favor of `std::sync::OnceLock` BREAKING CHANGE: This requires an MSRV bump to 1.70.0. --- Cargo.toml | 1 - src/eyreish/mod.rs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7567d95..4833e59 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,6 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } -once_cell = "1.8.0" unicode-width = "0.1.9" owo-colors = { version = "3.4.0", optional = true } diff --git a/src/eyreish/mod.rs b/src/eyreish/mod.rs index 0efceed..0dfd878 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::*; @@ -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`]. From 29d000f201b259a056867a2876384f97653a6e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 19:35:52 -0800 Subject: [PATCH 36/67] feat(deps): bump some semver-breaking deps to newer versions --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4833e59..cbf357a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } unicode-width = "0.1.9" owo-colors = { version = "3.4.0", optional = true } -textwrap = { version = "0.15.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 } From ab59a7bc9bceace5761a862ee2ebff3e5943b12f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 19:44:46 -0800 Subject: [PATCH 37/67] feat(MSRV): Actually bump the MSRV to 1.70.0 --- Cargo.toml | 2 +- README.md | 42 ++++++++++++++++++++++++++++++++++++++---- clippy.toml | 2 +- src/lib.rs | 4 ++++ src/protocol.rs | 8 +++----- 5 files changed, 47 insertions(+), 11 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index cbf357a..15c83e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,7 @@ 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] diff --git a/README.md b/README.md index 8af2845..0e936b3 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ libraries and such might not want. - [... delayed source code](#-delayed-source-code) - [... handler options](#-handler-options) - [... dynamic diagnostics](#-dynamic-diagnostics) + - [... syntax highlighting](#-syntax-highlighting) - [Acknowledgements](#acknowledgements) - [License](#license) @@ -96,7 +97,7 @@ You can derive a `Diagnostic` from any `std::error::Error` type. `thiserror` is a great way to define them, and plays nicely with `miette`! */ -use miette::{Diagnostic, NamedSource, SourceSpan}; +use miette::{Diagnostic, SourceSpan}; use thiserror::Error; #[derive(Error, Debug, Diagnostic)] @@ -123,7 +124,7 @@ Use this `Result` type (or its expanded version) as the return type throughout your app (but NOT your libraries! Those should always return concrete types!). */ -use miette::Result; +use miette::{NamedSource, Result}; fn this_fails() -> Result<()> { // You can use plain strings as a `Source`, or anything that implements // the one-method `Source` trait. @@ -305,7 +306,7 @@ enabled: miette = { version = "X.Y.Z", features = ["fancy"] } ``` -Another way to display a diagnostic is by printing them using the debug formatter. +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: @@ -611,6 +612,7 @@ miette::set_hook(Box::new(|_| { .unicode(false) .context_lines(3) .tab_width(4) + .break_words(true) .build(), ) })) @@ -632,7 +634,7 @@ then you may want to use [`miette!`], [`diagnostic!`] macros or 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 '+'", @@ -641,6 +643,38 @@ let report = miette!( 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. + +### MSRV + +This crate requires rustc 1.70.0 or later. + ### Acknowledgements `miette` was not developed in a void. It owes enormous credit to various 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/src/lib.rs b/src/lib.rs index f8d92f4..7674f2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -672,6 +672,10 @@ //! [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting) //! method. See the [`highlighters`] module docs for more details. //! +//! ## MSRV +//! +//! This crate requires rustc 1.70.0 or later. +//! //! ## Acknowledgements //! //! `miette` was not developed in a void. It owes enormous credit to various diff --git a/src/protocol.rs b/src/protocol.rs index 8ca719d..4c0101f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -179,6 +179,7 @@ impl From> for Box Self { - Severity::Error - } -} + #[cfg(feature = "serde")] #[test] From 52b32403861ddb5d1aad916ee4e1a8b061c972c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:01:25 -0800 Subject: [PATCH 38/67] docs: update changelog --- CHANGELOG.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 955c27d..f0bfcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # `miette` Release Changelog + +## 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) From 5d4b262f7b752396077e12650ebbda07acbef54b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:01:50 -0800 Subject: [PATCH 39/67] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15c83e7..eb08466 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "5.10.0" +version = "6.0.0" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true } +miette-derive = { path = "miette-derive", version = "=6.0.0", optional = true } unicode-width = "0.1.9" owo-colors = { version = "3.4.0", optional = true } diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index b5a7032..40906de 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "5.10.0" +version = "6.0.0" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From 95964055546384ebcb93de0ce1381f57dcbbaa90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:10:38 -0800 Subject: [PATCH 40/67] ci: fix is-terminal nonsense --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6871c4..d5f437e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,9 +40,6 @@ jobs: with: toolchain: ${{ matrix.rust }} components: clippy - - name: Force older version of is-terminal for MSRV builds - if: matrix.rust == '1.70.0' - run: cargo update -p is-terminal --precise 0.4.7 - name: Clippy run: cargo clippy --all -- -D warnings - name: Run tests From 8b46679c3647e1455d91b4c68743c619fb3f3eb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:12:46 -0800 Subject: [PATCH 41/67] fix(graphical): oops. Fix theme issue --- src/handler.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index a564f44..dcf8b13 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -311,8 +311,7 @@ impl MietteHandlerOpts { let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles }); 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 { From 1fa7f5241fb91d2e5bad9b0e26bcc7cd5f9011f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:14:01 -0800 Subject: [PATCH 42/67] fix(fmt): remove nightly-only fmt flags --- rustfmt.toml | 2 -- src/protocol.rs | 2 -- tests/graphical.rs | 2 +- 3 files changed, 1 insertion(+), 5 deletions(-) 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/protocol.rs b/src/protocol.rs index 4c0101f..6b98a97 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -191,8 +191,6 @@ pub enum Severity { Error, } - - #[cfg(feature = "serde")] #[test] fn test_serialize_severity() { diff --git a/tests/graphical.rs b/tests/graphical.rs index 86d79e1..456231a 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1855,7 +1855,7 @@ fn syntax_highlighter_on_real_file() { ); 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> { From 3747fccf8d7751191869337eed75c4a90656b144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:22:36 -0800 Subject: [PATCH 43/67] test: fix some fancy tests after NamedSource change --- tests/graphical.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/graphical.rs b/tests/graphical.rs index 456231a..32e3441 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -304,7 +304,7 @@ fn multiple_spans_multiline() { #[diagnostic(severity(Error))] struct MyBad { #[source_code] - src: NamedSource, + src: NamedSource<&'static str>, #[label("big")] big: SourceSpan, #[label("small")] @@ -824,7 +824,7 @@ fn multiline_label() -> 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\nand\nthis\ntoo")] highlight: SourceSpan, } @@ -864,7 +864,7 @@ fn multiple_multi_line_labels() -> Result<(), MietteError> { #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))] struct MyBad { #[source_code] - src: NamedSource, + src: NamedSource, #[label = "x\ny"] highlight1: SourceSpan, #[label = "z\nw"] @@ -1040,7 +1040,7 @@ fn multiline_highlight_multiline_label() -> 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\nare the problem"] highlight: SourceSpan, } @@ -1620,7 +1620,7 @@ fn primary_label() { #[error("oops!")] struct MyBad { #[source_code] - src: NamedSource, + src: NamedSource<&'static str>, #[label] first_label: SourceSpan, #[label(primary, "nope")] @@ -1656,7 +1656,7 @@ fn single_line_with_wide_char_unaligned_span_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, } @@ -1693,7 +1693,7 @@ fn single_line_with_wide_char_unaligned_span_end() -> 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, } @@ -1730,7 +1730,7 @@ fn single_line_with_wide_char_unaligned_span_empty() -> 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, } @@ -1769,7 +1769,7 @@ fn syntax_highlighter() { #[diagnostic()] pub struct Test { #[source_code] - src: NamedSource, + src: NamedSource, #[label("this is a label")] src_span: SourceSpan, } @@ -1814,7 +1814,7 @@ fn syntax_highlighter_on_real_file() { #[diagnostic()] pub struct Test { #[source_code] - src: NamedSource, + src: NamedSource, #[label("this is a label")] src_span: SourceSpan, } @@ -1864,7 +1864,7 @@ fn triple_adjacent_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"] highlight1: SourceSpan, #[label = "also this bit"] @@ -1913,7 +1913,7 @@ fn non_adjacent_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"] highlight1: SourceSpan, #[label = "also this bit"] From ab7c066e7675d8c7ecb956000d278fc31f3bc6a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:25:23 -0800 Subject: [PATCH 44/67] fix(highlighter): ugh, missed another spot --- src/highlighters/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/highlighters/mod.rs b/src/highlighters/mod.rs index 47e7f7c..d605c1c 100644 --- a/src/highlighters/mod.rs +++ b/src/highlighters/mod.rs @@ -80,7 +80,7 @@ impl MietteHighlighter { impl Default for MietteHighlighter { #[cfg(feature = "syntect-highlighter")] fn default() -> Self { - use is_terminal::IsTerminal; + 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 From e515a3c0ec7a51c8ac9de79e17fd9294bcaf87f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:27:39 -0800 Subject: [PATCH 45/67] docs: update changelog --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bfcad..4de742a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # `miette` Release Changelog + +## 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) From cf2d8c0b2c2b81567a6472185545733f2ca878f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sat, 3 Feb 2024 20:27:47 -0800 Subject: [PATCH 46/67] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index eb08466..450cf2d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "6.0.0" +version = "6.0.1" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.40" -miette-derive = { path = "miette-derive", version = "=6.0.0", optional = true } +miette-derive = { path = "miette-derive", version = "=6.0.1", optional = true } unicode-width = "0.1.9" owo-colors = { version = "3.4.0", optional = true } diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 40906de..7945edf 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "6.0.0" +version = "6.0.1" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From c7144ee513bf8f06c5f7d89c45436802994a51fc Mon Sep 17 00:00:00 2001 From: David Calavera Date: Sun, 4 Feb 2024 16:54:18 -0800 Subject: [PATCH 47/67] feat(fancy): Add option to change the link display text (#335) This option allows to globally change the default `(link)` display text with any other text provider by users. --- src/handlers/graphical.rs | 13 ++++++++++++- tests/graphical.rs | 22 ++++++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index d7e1883..3f74227 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -36,6 +36,7 @@ pub struct GraphicalReportHandler { 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)] @@ -62,6 +63,7 @@ impl GraphicalReportHandler { word_separator: None, word_splitter: None, highlighter: MietteHighlighter::default(), + link_display_text: None, } } @@ -80,6 +82,7 @@ impl GraphicalReportHandler { word_separator: None, word_splitter: None, highlighter: MietteHighlighter::default(), + link_display_text: None, } } @@ -190,6 +193,13 @@ impl GraphicalReportHandler { 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 { @@ -246,11 +256,12 @@ 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)?; diff --git a/tests/graphical.rs b/tests/graphical.rs index 32e3441..7e5de78 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1341,6 +1341,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)] From a4011d174c40acbba5b0176db7cb71ec5ca0cb49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sun, 4 Feb 2024 17:25:35 -0800 Subject: [PATCH 48/67] feat(deps): bump dependencies Fixes: https://github.com/zkat/miette/issues/336 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 --- Cargo.toml | 20 ++++++++++---------- miette-derive/Cargo.toml | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 450cf2d..f3b809e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,34 +13,34 @@ rust-version = "1.70.0" exclude = ["images/", "tests/", "miette-derive/"] [dependencies] -thiserror = "1.0.40" +thiserror = "1.0.56" miette-derive = { path = "miette-derive", version = "=6.0.1", optional = true } -unicode-width = "0.1.9" +unicode-width = "0.1.11" -owo-colors = { version = "3.4.0", 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.61", 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"] } +trybuild = { version = "1.0.89", features = ["diff"] } syn = { version = "2.0", features = ["full"] } -regex = "1.5" +regex = "1.10" lazy_static = "1.4" -serde_json = "1.0.64" +serde_json = "1.0.113" strip-ansi-escapes = "0.2.0" [features] diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 7945edf..d1f2fdd 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -11,6 +11,6 @@ repository = "https://github.com/zkat/miette" proc-macro = true [dependencies] -proc-macro2 = "1.0.60" +proc-macro2 = "1.0.78" quote = "1.0" -syn = "2.0.11" +syn = "2.0.48" From ea12e3f7812d037219bead5cd5b8f4ee838509d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sun, 4 Feb 2024 17:59:06 -0800 Subject: [PATCH 49/67] docs: update changelog --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de742a..893921a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # `miette` Release Changelog + +## 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) From ecb01022f09a290fda90d0657d7df653c3499e09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Sun, 4 Feb 2024 17:59:20 -0800 Subject: [PATCH 50/67] chore: Release --- Cargo.toml | 4 ++-- miette-derive/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f3b809e..4ffe9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette" -version = "6.0.1" +version = "7.0.0" authors = ["Kat Marchán "] description = "Fancy diagnostic reporting library and protocol for us mere mortals who aren't compiler hackers." categories = ["rust-patterns"] @@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"] [dependencies] thiserror = "1.0.56" -miette-derive = { path = "miette-derive", version = "=6.0.1", optional = true } +miette-derive = { path = "miette-derive", version = "=7.0.0", optional = true } unicode-width = "0.1.11" owo-colors = { version = "4.0.0", optional = true } diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index d1f2fdd..822f754 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "miette-derive" -version = "6.0.1" +version = "7.0.0" authors = ["Kat Marchán "] edition = "2018" license = "Apache-2.0" From 6e829f8c0ce2fc7bb2fc4041e6a6072f12db1f71 Mon Sep 17 00:00:00 2001 From: Brooks Rady Date: Wed, 7 Feb 2024 18:17:42 +0000 Subject: [PATCH 51/67] fix(tests): revert test-breaking changes of e5c7ae4 (#339) The commit e5c7ae4 seemed to (potentially erronously?) remove a number of spaces following the left `|` bar of some graphical report handler tests. That change had no effect on the `syntax_highlighter_on_real_file()` test, presumably because the trailing whitespace is removed by `strip_ansi_escapes::strip_str()`, but it did break the `triple_adjacent_highlight()` and `non_adjacent_hightlight()` tests. Restoring the spaces removed in e5c7ae4 fixes the failing tests on main. * fix(ci): move from minimal-versions to direct-minimal-versions --- .github/workflows/ci.yml | 2 +- Cargo.toml | 2 +- miette-derive/Cargo.toml | 2 +- tests/graphical.rs | 16 ++++++++-------- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5f437e..b1eef7d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,4 +79,4 @@ jobs: with: toolchain: nightly - name: Run minimal version build - run: cargo build -Z minimal-versions --features fancy,no-format-args-capture + run: cargo build -Z direct-minimal-versions --features fancy,no-format-args-capture diff --git a/Cargo.toml b/Cargo.toml index 4ffe9b8..0fa1685 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ futures = { version = "0.3", default-features = false } indenter = "0.3.3" rustversion = "1.0" trybuild = { version = "1.0.89", features = ["diff"] } -syn = { version = "2.0", features = ["full"] } +syn = { version = "2.0.48", features = ["full"] } regex = "1.10" lazy_static = "1.4" diff --git a/miette-derive/Cargo.toml b/miette-derive/Cargo.toml index 822f754..c1e4eb2 100644 --- a/miette-derive/Cargo.toml +++ b/miette-derive/Cargo.toml @@ -12,5 +12,5 @@ proc-macro = true [dependencies] proc-macro2 = "1.0.78" -quote = "1.0" +quote = "1.0.35" syn = "2.0.48" diff --git a/tests/graphical.rs b/tests/graphical.rs index 7e5de78..b5fa4d8 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -1864,11 +1864,11 @@ fn syntax_highlighter_on_real_file() { let expected = format!( r#" × This is an error ╭─[{filename}:{l2}:{CO}] - {l1} │ + {l1} │ {l2} │ let (filename, line) = (file!(), line!() as usize); · ─────────────┬───────────── · ╰── this is a label - {l3} │ + {l3} │ ╰──── "#, l1 = line - 1, @@ -1911,13 +1911,13 @@ fn triple_adjacent_highlight() -> Result<(), MietteError> { 1 │ source · ───┬── · ╰── this bit here - 2 │ - 3 │ + 2 │ + 3 │ 4 │ text · ──┬─ · ╰── also this bit - 5 │ - 6 │ + 5 │ + 6 │ 7 │ here · ──┬─ · ╰── finally we got @@ -1957,10 +1957,10 @@ fn non_adjacent_highlight() -> Result<(), MietteError> { 1 │ source · ───┬── · ╰── this bit here - 2 │ + 2 │ ╰──── ╭─[bad_file.rs:5:3] - 4 │ + 4 │ 5 │ text here · ──┬─ · ╰── also this bit From c2f06f6cca15cbdd083dbff3d46b7729056ac6a4 Mon Sep 17 00:00:00 2001 From: Brooks Rady Date: Wed, 7 Feb 2024 18:20:18 +0000 Subject: [PATCH 52/67] feat(derive): enable more boxed types to be #[diagnostic_source] (#338) --- src/protocol.rs | 21 +++++++++++++++++++-- tests/test_diagnostic_source_macro.rs | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/protocol.rs b/src/protocol.rs index 6b98a97..310fc86 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -69,7 +69,7 @@ pub trait Diagnostic: std::error::Error { } } -macro_rules! box_impls { +macro_rules! box_error_impls { ($($box_type:ty),*) => { $( impl std::error::Error for $box_type { @@ -85,12 +85,29 @@ macro_rules! box_impls { } } -box_impls! { +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 { diff --git a/tests/test_diagnostic_source_macro.rs b/tests/test_diagnostic_source_macro.rs index e5305ac..939812c 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,6 +79,12 @@ 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()); } From 6f09250cca14561f07fba899a8e6d3c0df14230e Mon Sep 17 00:00:00 2001 From: Brooks Rady Date: Thu, 8 Feb 2024 22:16:49 +0000 Subject: [PATCH 53/67] feat(source): derive common traits for NamedSource, SourceSpan, and SourceOffset (#340) --- src/named_source.rs | 1 + src/protocol.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/named_source.rs b/src/named_source.rs index 99e74a8..ea11cd2 100644 --- a/src/named_source.rs +++ b/src/named_source.rs @@ -3,6 +3,7 @@ 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`. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NamedSource { source: S, name: String, diff --git a/src/protocol.rs b/src/protocol.rs index 310fc86..9f312db 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -550,7 +550,7 @@ impl<'a> SpanContents<'a> for MietteSpanContents<'a> { } /// 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. @@ -652,7 +652,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); From 03060245d816a53a33209e6b7e1c3c42948e9962 Mon Sep 17 00:00:00 2001 From: Nahor Date: Thu, 15 Feb 2024 18:14:04 -0800 Subject: [PATCH 54/67] feat(collection): add support for collection of labels (#341) Fixes: https://github.com/zkat/miette/issues/315 Allow errors to have a number of labels determined at runtime. An example of this is when the rust compiler labels all the arms of a `match` expression when one of them has an incompatible type To allow customization of the text for each label in a collection, add support for using LabeledSpan in collections instead of just regular spans --- README.md | 52 ++++++ miette-derive/src/label.rs | 217 +++++++++++++++------- src/macro_helpers.rs | 23 +++ src/protocol.rs | 5 + tests/test_derive_collection.rs | 306 ++++++++++++++++++++++++++++++++ 5 files changed, 537 insertions(+), 66 deletions(-) create mode 100644 tests/test_derive_collection.rs diff --git a/README.md b/README.md index 0e936b3..302e0b5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ libraries and such might not want. - [... handler options](#-handler-options) - [... dynamic diagnostics](#-dynamic-diagnostics) - [... syntax highlighting](#-syntax-highlighting) + - [... collection of labels](#-collection-of-labels) - [Acknowledgements](#acknowledgements) - [License](#license) @@ -671,6 +672,57 @@ 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. diff --git a/miette-derive/src/label.rs b/miette-derive/src/label.rs index dd5ec69..cd6994a 100644 --- a/miette-derive/src/label.rs +++ b/miette-derive/src/label.rs @@ -16,16 +16,23 @@ use crate::{ pub struct Labels(Vec