diff --git a/src/handler.rs b/src/handler.rs index 8f1385c..a564f44 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -58,6 +58,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, pub(crate) highlighter: Option, @@ -129,6 +130,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. @@ -138,7 +149,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); @@ -299,7 +309,7 @@ impl MietteHandlerOpts { MietteHighlighter::nocolor() }; let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles }); - let mut handler = GraphicalReportHandler::new() + let mut handler = GraphicalReportHandler::new_themed(theme) .with_width(width) .with_links(linkify) .with_theme(theme); @@ -323,6 +333,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 767675a..d7e1883 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -7,7 +7,7 @@ use crate::diagnostic_chain::{DiagnosticChain, ErrorKind}; use crate::handlers::theme::*; use crate::highlighters::{Highlighter, MietteHighlighter}; use crate::protocol::{Diagnostic, Severity}; -use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; +use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents}; /** A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a @@ -31,6 +31,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, @@ -56,6 +57,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, @@ -72,6 +74,7 @@ impl GraphicalReportHandler { footer: None, context_lines: 1, tab_width: 4, + wrap_lines: true, with_cause_chain: true, break_words: true, word_separator: None, @@ -135,6 +138,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; @@ -218,7 +227,7 @@ impl GraphicalReportHandler { opts = opts.word_splitter(word_splitter); } - writeln!(f, "{}", textwrap::fill(footer, opts))?; + writeln!(f, "{}", self.wrap(footer, opts))?; } Ok(()) } @@ -279,7 +288,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(()); @@ -329,16 +338,17 @@ 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)?; - 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))?; } } } @@ -362,7 +372,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(()) } @@ -374,6 +384,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() { @@ -381,12 +394,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(()) @@ -398,66 +411,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(()) } @@ -470,10 +475,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 @@ -835,6 +846,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/color_format.rs b/tests/color_format.rs index bb90708..95d40e0 100644 --- a/tests/color_format.rs +++ b/tests/color_format.rs @@ -3,6 +3,7 @@ use lazy_static::lazy_static; use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors}; use regex::Regex; +use std::ffi::OsString; use std::fmt::{self, Debug}; use std::sync::Mutex; use thiserror::Error; @@ -42,16 +43,29 @@ fn color_format(handler: MietteHandler) -> ColorFormat { } } -/// Runs a function with an environment variable set to a specific value, then -/// sets it back to it's original value once completed. -fn with_env_var(var: &str, value: &str, body: F) { - let old_value = std::env::var_os(var); - std::env::set_var(var, value); - body(); - if let Some(old_value) = old_value { - std::env::set_var(var, old_value); - } else { - std::env::remove_var(var); +/// Store the current value of an environment variable on construction, and then +/// restore that value when the guard is dropped. +struct EnvVarGuard<'a> { + var: &'a str, + old_value: Option, +} + +impl EnvVarGuard<'_> { + fn new(var: &str) -> EnvVarGuard<'_> { + EnvVarGuard { + var, + old_value: std::env::var_os(var), + } + } +} + +impl Drop for EnvVarGuard<'_> { + fn drop(&mut self) { + if let Some(old_value) = &self.old_value { + std::env::set_var(self.var, old_value); + } else { + std::env::remove_var(self.var); + } } } @@ -72,22 +86,33 @@ fn check_colors MietteHandlerOpts>( // // Since environment variables are shared for the entire process, we need // to ensure that only one test that modifies these env vars runs at a time. - let guard = COLOR_ENV_VARS.lock().unwrap(); + let lock = COLOR_ENV_VARS.lock().unwrap(); - with_env_var("NO_COLOR", "1", || { - let handler = make_handler(MietteHandlerOpts::new()).build(); - assert_eq!(color_format(handler), no_support); - }); - with_env_var("FORCE_COLOR", "1", || { - let handler = make_handler(MietteHandlerOpts::new()).build(); - assert_eq!(color_format(handler), ansi_support); - }); - with_env_var("FORCE_COLOR", "3", || { - let handler = make_handler(MietteHandlerOpts::new()).build(); - assert_eq!(color_format(handler), rgb_support); - }); + let guards = ( + EnvVarGuard::new("NO_COLOR"), + EnvVarGuard::new("FORCE_COLOR"), + ); + // Clear color environment variables that may be set outside of 'cargo test' + std::env::remove_var("NO_COLOR"); + std::env::remove_var("FORCE_COLOR"); - drop(guard); + std::env::set_var("NO_COLOR", "1"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), no_support); + std::env::remove_var("NO_COLOR"); + + std::env::set_var("FORCE_COLOR", "1"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), ansi_support); + std::env::remove_var("FORCE_COLOR"); + + std::env::set_var("FORCE_COLOR", "3"); + let handler = make_handler(MietteHandlerOpts::new()).build(); + assert_eq!(color_format(handler), rgb_support); + std::env::remove_var("FORCE_COLOR"); + + drop(guards); + drop(lock); } #[test] diff --git a/tests/graphical.rs b/tests/graphical.rs index e2f2f8c..c4588fc 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -220,6 +220,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)] @@ -1811,4 +1855,95 @@ 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> { + #[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(()) } 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); +}