Merge branch 'main' of github.com:zkat/miette into rendering-bug

This commit is contained in:
jdonszelmann 2023-11-15 19:56:48 +01:00
commit 2c704bb168
No known key found for this signature in database
GPG Key ID: E0C1EA36407B2FF2
7 changed files with 815 additions and 90 deletions

View File

@ -14,7 +14,7 @@ exclude = ["images/", "tests/", "miette-derive/"]
[dependencies] [dependencies]
thiserror = "1.0.40" 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" once_cell = "1.8.0"
unicode-width = "0.1.9" unicode-width = "0.1.9"
@ -44,7 +44,8 @@ lazy_static = "1.4"
serde_json = "1.0.64" serde_json = "1.0.64"
[features] [features]
default = [] default = ["derive"]
derive = ["miette-derive"]
no-format-args-capture = [] no-format-args-capture = []
fancy-no-backtrace = [ fancy-no-backtrace = [
"owo-colors", "owo-colors",

View File

@ -305,6 +305,23 @@ enabled:
miette = { version = "X.Y.Z", features = ["fancy"] } 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<Version> = "1.2.x".parse().into_diagnostic();
match version_result {
Err(e) => println!("{:?}", e),
Ok(version) => println!("{}", version),
}
}
```
#### ... diagnostic code URLs #### ... diagnostic code URLs
`miette` supports providing a URL for individual diagnostics. This URL will `miette` supports providing a URL for individual diagnostics. This URL will

View File

@ -1,27 +1,51 @@
use std::io; use std::{fmt, io};
use thiserror::Error; use thiserror::Error;
use crate::{self as miette, Diagnostic}; use crate::Diagnostic;
/** /**
Error enum for miette. Used by certain operations in the protocol. Error enum for miette. Used by certain operations in the protocol.
*/ */
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Error)]
pub enum MietteError { pub enum MietteError {
/// Wrapper around [`std::io::Error`]. This is returned when something went /// Wrapper around [`std::io::Error`]. This is returned when something went
/// wrong while reading a [`SourceCode`](crate::SourceCode). /// wrong while reading a [`SourceCode`](crate::SourceCode).
#[error(transparent)] #[error(transparent)]
#[diagnostic(code(miette::io_error), url(docsrs))]
IoError(#[from] io::Error), IoError(#[from] io::Error),
/// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the /// Returned when a [`SourceSpan`](crate::SourceSpan) extends beyond the
/// bounds of a given [`SourceCode`](crate::SourceCode). /// bounds of a given [`SourceCode`](crate::SourceCode).
#[error("The given offset is outside the bounds of its Source")] #[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, OutOfBounds,
} }
impl Diagnostic for MietteError {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
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<Box<dyn fmt::Display + 'a>> {
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<Box<dyn fmt::Display + 'a>> {
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,
)))
}
}

View File

@ -55,6 +55,9 @@ pub struct MietteHandlerOpts {
pub(crate) context_lines: Option<usize>, pub(crate) context_lines: Option<usize>,
pub(crate) tab_width: Option<usize>, pub(crate) tab_width: Option<usize>,
pub(crate) with_cause_chain: Option<bool>, pub(crate) with_cause_chain: Option<bool>,
pub(crate) break_words: Option<bool>,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
} }
impl MietteHandlerOpts { impl MietteHandlerOpts {
@ -86,6 +89,27 @@ impl MietteHandlerOpts {
self 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. /// Include the cause chain of the top-level error in the report.
pub fn with_cause_chain(mut self) -> Self { pub fn with_cause_chain(mut self) -> Self {
self.with_cause_chain = Some(true); self.with_cause_chain = Some(true);
@ -233,6 +257,16 @@ impl MietteHandlerOpts {
if let Some(w) = self.tab_width { if let Some(w) = self.tab_width {
handler = handler.tab_width(w); 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 { MietteHandler {
inner: Box::new(handler), inner: Box::new(handler),
} }

View File

@ -30,6 +30,9 @@ pub struct GraphicalReportHandler {
pub(crate) context_lines: usize, pub(crate) context_lines: usize,
pub(crate) tab_width: usize, pub(crate) tab_width: usize,
pub(crate) with_cause_chain: bool, pub(crate) with_cause_chain: bool,
pub(crate) break_words: bool,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -51,6 +54,9 @@ impl GraphicalReportHandler {
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
with_cause_chain: true, with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
} }
} }
@ -64,6 +70,9 @@ impl GraphicalReportHandler {
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
with_cause_chain: true, with_cause_chain: true,
break_words: true,
word_separator: None,
word_splitter: None,
} }
} }
@ -122,6 +131,24 @@ impl GraphicalReportHandler {
self 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. /// Sets the 'global' footer for this handler.
pub fn with_footer(mut self, footer: String) -> Self { pub fn with_footer(mut self, footer: String) -> Self {
self.footer = Some(footer); self.footer = Some(footer);
@ -159,9 +186,17 @@ impl GraphicalReportHandler {
if let Some(footer) = &self.footer { if let Some(footer) = &self.footer {
writeln!(f)?; writeln!(f)?;
let width = self.termwidth.saturating_sub(4); let width = self.termwidth.saturating_sub(4);
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(footer, opts))?; writeln!(f, "{}", textwrap::fill(footer, opts))?;
} }
Ok(()) Ok(())
@ -212,9 +247,16 @@ impl GraphicalReportHandler {
let initial_indent = format!(" {} ", severity_icon.style(severity_style)); let initial_indent = format!(" {} ", severity_icon.style(severity_style));
let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
let width = self.termwidth.saturating_sub(2); let width = self.termwidth.saturating_sub(2);
let opts = textwrap::Options::new(width) let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent) .initial_indent(&initial_indent)
.subsequent_indent(&rest_indent); .subsequent_indent(&rest_indent)
.break_words(self.break_words);
if let Some(word_separator) = self.word_separator {
opts = opts.word_separator(word_separator);
}
if let Some(word_splitter) = self.word_splitter.clone() {
opts = opts.word_splitter(word_splitter);
}
writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?; writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
@ -251,9 +293,17 @@ impl GraphicalReportHandler {
) )
.style(severity_style) .style(severity_style)
.to_string(); .to_string();
let opts = textwrap::Options::new(width) let mut opts = textwrap::Options::new(width)
.initial_indent(&initial_indent) .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 { match error {
ErrorKind::Diagnostic(diag) => { ErrorKind::Diagnostic(diag) => {
let mut inner = String::new(); let mut inner = String::new();
@ -280,9 +330,17 @@ impl GraphicalReportHandler {
if let Some(help) = diagnostic.help() { if let Some(help) = diagnostic.help() {
let width = self.termwidth.saturating_sub(4); let width = self.termwidth.saturating_sub(4);
let initial_indent = " help: ".style(self.theme.styles.help).to_string(); 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) .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))?; writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
} }
Ok(()) Ok(())
@ -487,7 +545,13 @@ impl GraphicalReportHandler {
// no line number! // no line number!
self.write_no_linum(f, linum_width)?; self.write_no_linum(f, linum_width)?;
// gutter _again_ // 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( self.render_single_line_highlights(
f, f,
line, line,
@ -499,11 +563,7 @@ impl GraphicalReportHandler {
} }
for hl in multi_line { for hl in multi_line {
if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
// no line number! self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
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)?;
} }
} }
} }
@ -517,6 +577,91 @@ impl GraphicalReportHandler {
Ok(()) 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( fn render_line_gutter(
&self, &self,
f: &mut impl fmt::Write, f: &mut impl fmt::Write,
@ -585,6 +730,7 @@ impl GraphicalReportHandler {
max_gutter: usize, max_gutter: usize,
line: &Line, line: &Line,
highlights: &[FancySpan], highlights: &[FancySpan],
render_mode: LabelRenderMode,
) -> fmt::Result { ) -> fmt::Result {
if max_gutter == 0 { if max_gutter == 0 {
return Ok(()); return Ok(());
@ -600,23 +746,50 @@ impl GraphicalReportHandler {
let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
for (i, hl) in applicable.enumerate() { for (i, hl) in applicable.enumerate() {
if !line.span_line_only(hl) && line.span_ends(hl) { if !line.span_line_only(hl) && line.span_ends(hl) {
let num_repeat = max_gutter.saturating_sub(i) + 2; if render_mode == LabelRenderMode::MultiLineRest {
// this is to make multiline labels work. We want to make the right amount
// of horizontal space for them, but not actually draw the lines
let horizontal_space = max_gutter.saturating_sub(i) + 2;
for _ in 0..horizontal_space {
gutter.push(' ');
}
// account for one more horizontal space, since in multiline mode
// we also add in the vertical line before the label like this:
// 2 │ ╭─▶ text
// 3 │ ├─▶ here
// · ╰──┤ these two lines
// · │ are the problem
// ^this
gutter_cols += horizontal_space + 1;
} else {
let num_repeat = max_gutter.saturating_sub(i) + 2;
gutter.push_str(&chars.lbot.style(hl.style).to_string()); gutter.push_str(&chars.lbot.style(hl.style).to_string());
gutter.push_str(
&chars
.hbar
.to_string()
.repeat(num_repeat)
.style(hl.style)
.to_string(),
);
// we count 1 for the lbot char, and then a few more, the same number gutter.push_str(
// as we just repeated for. For each repeat we only add 1, even though &chars
// due to ansi escape codes the number of bytes in the string could grow .hbar
// a lot each time. .to_string()
gutter_cols += num_repeat + 1; .repeat(
num_repeat
// if we are rendering a multiline label, then leave a bit of space for the
// rcross character
- if render_mode == LabelRenderMode::MultiLineFirst {
1
} else {
0
},
)
.style(hl.style)
.to_string(),
);
// we count 1 for the lbot char, and then a few more, the same number
// as we just repeated for. For each repeat we only add 1, even though
// due to ansi escape codes the number of bytes in the string could grow
// a lot each time.
gutter_cols += num_repeat + 1;
}
break; break;
} else { } else {
gutter.push_str(&chars.vbar.style(hl.style).to_string()); gutter.push_str(&chars.vbar.style(hl.style).to_string());
@ -743,31 +916,33 @@ impl GraphicalReportHandler {
let byte_start = hl.offset(); let byte_start = hl.offset();
let byte_end = hl.offset() + hl.len(); let byte_end = hl.offset() + hl.len();
let start = self.visual_offset(line, byte_start, true).max(highest); 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 vbar_offset = (start + end) / 2;
let num_left = vbar_offset - start; let num_left = vbar_offset - start;
let num_right = end - vbar_offset - 1; let num_right = end - vbar_offset - 1;
if start < end { underlines.push_str(
underlines.push_str( &format!(
&format!( "{:width$}{}{}{}",
"{:width$}{}{}{}", "",
"", chars.underline.to_string().repeat(num_left),
chars.underline.to_string().repeat(num_left), if hl.len() == 0 {
if hl.len() == 0 { chars.uarrow
chars.uarrow } else if hl.label().is_some() {
} else if hl.label().is_some() { chars.underbar
chars.underbar } else {
} else { chars.underline
chars.underline },
}, chars.underline.to_string().repeat(num_right),
chars.underline.to_string().repeat(num_right), width = start.saturating_sub(highest),
width = start.saturating_sub(highest), )
) .style(hl.style)
.style(hl.style) .to_string(),
.to_string(), );
);
}
highest = std::cmp::max(highest, end); highest = std::cmp::max(highest, end);
(hl, vbar_offset) (hl, vbar_offset)
@ -776,27 +951,40 @@ impl GraphicalReportHandler {
writeln!(f, "{}", underlines)?; writeln!(f, "{}", underlines)?;
for hl in single_liners.iter().rev() { for hl in single_liners.iter().rev() {
if let Some(label) = hl.label() { if let Some(label) = hl.label_parts() {
self.write_no_linum(f, linum_width)?; if label.len() == 1 {
self.render_highlight_gutter(f, max_gutter, line, all_highlights)?; self.write_label_text(
let mut curr_offset = 1usize; f,
for (offset_hl, vbar_offset) in &vbar_offsets { line,
while curr_offset < *vbar_offset + 1 { linum_width,
write!(f, " ")?; max_gutter,
curr_offset += 1; all_highlights,
} chars,
if *offset_hl != hl { &vbar_offsets,
write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; hl,
curr_offset += 1; &label[0],
} else { LabelRenderMode::SingleLine,
let lines = format!( )?;
"{}{} {}", } else {
chars.lbot, let mut first = true;
chars.hbar.to_string().repeat(2), for label_line in &label {
label, self.write_label_text(
); f,
writeln!(f, "{}", lines.style(hl.style))?; line,
break; linum_width,
max_gutter,
all_highlights,
chars,
&vbar_offsets,
hl,
label_line,
if first {
LabelRenderMode::MultiLineFirst
} else {
LabelRenderMode::MultiLineRest
},
)?;
first = false;
} }
} }
} }
@ -804,13 +992,80 @@ impl GraphicalReportHandler {
Ok(()) Ok(())
} }
fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result { // I know it's not good practice, but making this a function makes a lot of sense
writeln!( // 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, f,
"{} {}", max_gutter,
self.theme.characters.hbar.style(hl.style), line,
hl.label().unwrap_or_else(|| "".into()), 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(()) Ok(())
} }
@ -889,6 +1144,16 @@ impl ReportHandler for GraphicalReportHandler {
Support types 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)] #[derive(Debug)]
struct Line { struct Line {
line_number: usize, line_number: usize,
@ -956,7 +1221,10 @@ impl Line {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
struct FancySpan { struct FancySpan {
label: Option<String>, /// 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<Vec<String>>,
span: SourceSpan, span: SourceSpan,
style: Style, style: Style,
} }
@ -967,9 +1235,17 @@ impl PartialEq for FancySpan {
} }
} }
fn split_label(v: String) -> Vec<String> {
v.split('\n').map(|i| i.to_string()).collect()
}
impl FancySpan { impl FancySpan {
fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self { fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
FancySpan { label, span, style } FancySpan {
label: label.map(split_label),
span,
style,
}
} }
fn style(&self) -> Style { fn style(&self) -> Style {
@ -979,7 +1255,15 @@ impl FancySpan {
fn label(&self) -> Option<String> { fn label(&self) -> Option<String> {
self.label self.label
.as_ref() .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<Vec<String>> {
self.label.as_ref().map(|l| {
l.iter()
.map(|i| i.style(self.style()).to_string())
.collect()
})
} }
fn offset(&self) -> usize { fn offset(&self) -> usize {

View File

@ -304,6 +304,23 @@
//! miette = { version = "X.Y.Z", features = ["fancy"] } //! 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<Version> = "1.2.x".parse().into_diagnostic();
//! match version_result {
//! Err(e) => println!("{:?}", e),
//! Ok(version) => println!("{}", version),
//! }
//! }
//! ```
//!
//! ### ... diagnostic code URLs //! ### ... diagnostic code URLs
//! //!
//! `miette` supports providing a URL for individual diagnostics. This URL will //! `miette` supports providing a URL for individual diagnostics. This URL will
@ -593,6 +610,7 @@
//! .unicode(false) //! .unicode(false)
//! .context_lines(3) //! .context_lines(3)
//! .tab_width(4) //! .tab_width(4)
//! .break_words(true)
//! .build(), //! .build(),
//! ) //! )
//! })) //! }))
@ -652,6 +670,7 @@
//! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also //! and some from [`thiserror`](https://github.com/dtolnay/thiserror), also
//! under the Apache License. Some code is taken from //! under the Apache License. Some code is taken from
//! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed. //! [`ariadne`](https://github.com/zesterer/ariadne), which is MIT licensed.
#[cfg(feature = "derive")]
pub use miette_derive::*; pub use miette_derive::*;
pub use error::*; pub use error::*;

View File

@ -34,6 +34,190 @@ fn fmt_report(diag: Report) -> String {
out 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] #[test]
fn empty_source() -> Result<(), MietteError> { fn empty_source() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -587,6 +771,94 @@ fn single_line_highlight_at_line_start() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn multiple_same_line_highlights() -> Result<(), MietteError> { fn multiple_same_line_highlights() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -715,6 +987,43 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn multiline_highlight_flyby() -> Result<(), MietteError> { fn multiline_highlight_flyby() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[derive(Debug, Diagnostic, Error)]
@ -1367,3 +1676,40 @@ fn single_line_with_wide_char_unaligned_span_end() -> Result<(), MietteError> {
assert_eq!(expected, out); assert_eq!(expected, out);
Ok(()) 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(())
}