Merge branch 'main' into feature/syntect

This commit is contained in:
Kat Marchán 2024-02-03 19:15:52 -08:00 committed by GitHub
commit 8a7c0ddf99
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 399 additions and 98 deletions

View File

@ -58,6 +58,7 @@ pub struct MietteHandlerOpts {
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) break_words: Option<bool>,
pub(crate) wrap_lines: Option<bool>,
pub(crate) word_separator: Option<textwrap::WordSeparator>, pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>, pub(crate) word_splitter: Option<textwrap::WordSplitter>,
pub(crate) highlighter: Option<MietteHighlighter>, pub(crate) highlighter: Option<MietteHighlighter>,
@ -129,6 +130,16 @@ impl MietteHandlerOpts {
self 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 true, long words can be broken when wrapping.
/// ///
/// If false, long words will not be broken when they exceed the width. /// 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.break_words = Some(break_words);
self self
} }
/// Sets the `textwrap::WordSeparator` to use when determining wrap points. /// Sets the `textwrap::WordSeparator` to use when determining wrap points.
pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { pub fn word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
self.word_separator = Some(word_separator); self.word_separator = Some(word_separator);
@ -299,7 +309,7 @@ impl MietteHandlerOpts {
MietteHighlighter::nocolor() MietteHighlighter::nocolor()
}; };
let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles }); 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_width(width)
.with_links(linkify) .with_links(linkify)
.with_theme(theme); .with_theme(theme);
@ -323,6 +333,9 @@ impl MietteHandlerOpts {
if let Some(b) = self.break_words { if let Some(b) = self.break_words {
handler = handler.with_break_words(b) 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 { if let Some(s) = self.word_separator {
handler = handler.with_word_separator(s) handler = handler.with_word_separator(s)
} }

View File

@ -7,7 +7,7 @@ use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
use crate::handlers::theme::*; use crate::handlers::theme::*;
use crate::highlighters::{Highlighter, MietteHighlighter}; use crate::highlighters::{Highlighter, MietteHighlighter};
use crate::protocol::{Diagnostic, Severity}; 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 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) 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) wrap_lines: bool,
pub(crate) break_words: bool, pub(crate) break_words: bool,
pub(crate) word_separator: Option<textwrap::WordSeparator>, pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>, pub(crate) word_splitter: Option<textwrap::WordSplitter>,
@ -56,6 +57,7 @@ impl GraphicalReportHandler {
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
with_cause_chain: true, with_cause_chain: true,
wrap_lines: true,
break_words: true, break_words: true,
word_separator: None, word_separator: None,
word_splitter: None, word_splitter: None,
@ -72,6 +74,7 @@ impl GraphicalReportHandler {
footer: None, footer: None,
context_lines: 1, context_lines: 1,
tab_width: 4, tab_width: 4,
wrap_lines: true,
with_cause_chain: true, with_cause_chain: true,
break_words: true, break_words: true,
word_separator: None, word_separator: None,
@ -135,6 +138,12 @@ impl GraphicalReportHandler {
self 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. /// Enables or disables breaking of words during wrapping.
pub fn with_break_words(mut self, break_words: bool) -> Self { pub fn with_break_words(mut self, break_words: bool) -> Self {
self.break_words = break_words; self.break_words = break_words;
@ -218,7 +227,7 @@ impl GraphicalReportHandler {
opts = opts.word_splitter(word_splitter); opts = opts.word_splitter(word_splitter);
} }
writeln!(f, "{}", textwrap::fill(footer, opts))?; writeln!(f, "{}", self.wrap(footer, opts))?;
} }
Ok(()) Ok(())
} }
@ -279,7 +288,7 @@ impl GraphicalReportHandler {
opts = opts.word_splitter(word_splitter); 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 { if !self.with_cause_chain {
return Ok(()); return Ok(());
@ -329,16 +338,17 @@ impl GraphicalReportHandler {
ErrorKind::Diagnostic(diag) => { ErrorKind::Diagnostic(diag) => {
let mut inner = String::new(); let mut inner = String::new();
// Don't print footer for inner errors
let mut inner_renderer = self.clone(); let mut inner_renderer = self.clone();
// Don't print footer for inner errors
inner_renderer.footer = None; 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.with_cause_chain = false;
inner_renderer.render_report(&mut inner, diag)?; inner_renderer.render_report(&mut inner, diag)?;
writeln!(f, "{}", textwrap::fill(&inner, opts))?; writeln!(f, "{}", self.wrap(&inner, opts))?;
} }
ErrorKind::StdError(err) => { 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); opts = opts.word_splitter(word_splitter);
} }
writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
} }
Ok(()) Ok(())
} }
@ -374,6 +384,9 @@ impl GraphicalReportHandler {
parent_src: Option<&dyn SourceCode>, parent_src: Option<&dyn SourceCode>,
) -> fmt::Result { ) -> fmt::Result {
if let Some(related) = diagnostic.related() { 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)?; writeln!(f)?;
for rel in related { for rel in related {
match rel.severity() { match rel.severity() {
@ -381,12 +394,12 @@ impl GraphicalReportHandler {
Some(Severity::Warning) => write!(f, "Warning: ")?, Some(Severity::Warning) => write!(f, "Warning: ")?,
Some(Severity::Advice) => write!(f, "Advice: ")?, Some(Severity::Advice) => write!(f, "Advice: ")?,
}; };
self.render_header(f, rel)?; inner_renderer.render_header(f, rel)?;
self.render_causes(f, rel)?; inner_renderer.render_causes(f, rel)?;
let src = rel.source_code().or(parent_src); let src = rel.source_code().or(parent_src);
self.render_snippets(f, rel, src)?; inner_renderer.render_snippets(f, rel, src)?;
self.render_footer(f, rel)?; inner_renderer.render_footer(f, rel)?;
self.render_related(f, rel, src)?; inner_renderer.render_related(f, rel, src)?;
} }
} }
Ok(()) Ok(())
@ -398,66 +411,58 @@ impl GraphicalReportHandler {
diagnostic: &(dyn Diagnostic), diagnostic: &(dyn Diagnostic),
opt_source: Option<&dyn SourceCode>, opt_source: Option<&dyn SourceCode>,
) -> fmt::Result { ) -> fmt::Result {
if let Some(source) = opt_source { let source = match opt_source {
if let Some(labels) = diagnostic.labels() { Some(source) => source,
let mut labels = labels.collect::<Vec<_>>(); None => return Ok(()),
labels.sort_unstable_by_key(|l| l.inner().offset()); };
if !labels.is_empty() { let labels = match diagnostic.labels() {
let contents = labels Some(labels) => labels,
.iter() None => return Ok(()),
.map(|label| { };
source.read_span(label.inner(), self.context_lines, self.context_lines)
}) let mut labels = labels.collect::<Vec<_>>();
.collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>() labels.sort_unstable_by_key(|l| l.inner().offset());
.map_err(|_| fmt::Error)?;
let mut contexts = Vec::with_capacity(contents.len()); let mut contexts = Vec::with_capacity(labels.len());
for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) { for right in labels.iter().cloned() {
if contexts.is_empty() { let right_conts = source
contexts.push((right, right_conts)); .read_span(right.inner(), self.context_lines, self.context_lines)
} else { .map_err(|_| fmt::Error)?;
let (left, left_conts) = contexts.last().unwrap().clone();
let left_end = left.offset() + left.len(); if contexts.is_empty() {
let right_end = right.offset() + right.len(); contexts.push((right, right_conts));
if left_conts.line() + left_conts.line_count() >= right_conts.line() { continue;
// The snippets will overlap, so we create one Big Chunky Boi }
let new_span = LabeledSpan::new(
left.label().map(String::from), let (left, left_conts) = contexts.last().unwrap();
left.offset(), if left_conts.line() + left_conts.line_count() >= right_conts.line() {
if right_end >= left_end { // The snippets will overlap, so we create one Big Chunky Boi
// Right end goes past left end let left_end = left.offset() + left.len();
right_end - left.offset() let right_end = right.offset() + right.len();
} else { let new_end = std::cmp::max(left_end, right_end);
// right is contained inside left
left.len() let new_span = LabeledSpan::new(
}, left.label().map(String::from),
); left.offset(),
if source new_end - left.offset(),
.read_span( );
new_span.inner(), // Check that the two contexts can be combined
self.context_lines, if let Ok(new_conts) =
self.context_lines, source.read_span(new_span.inner(), self.context_lines, self.context_lines)
) {
.is_ok() contexts.pop();
{ // We'll throw the contents away later
contexts.pop(); contexts.push((new_span, new_conts));
contexts.push(( continue;
// 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[..])?;
}
} }
} }
contexts.push((right, right_conts));
} }
for (ctx, _) in contexts {
self.render_context(f, source, &ctx, &labels[..])?;
}
Ok(()) Ok(())
} }
@ -470,10 +475,16 @@ impl GraphicalReportHandler {
) -> fmt::Result { ) -> fmt::Result {
let (contents, lines) = self.get_lines(source, context.inner())?; let (contents, lines) = self.get_lines(source, context.inner())?;
let primary_label = labels // only consider labels from the context as primary label
.iter() 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()) .find(|label| label.primary())
.or_else(|| labels.first()); .or_else(|| ctx_labels.clone().next());
// sorting is your friend // sorting is your friend
let labels = labels let labels = labels
@ -835,6 +846,41 @@ impl GraphicalReportHandler {
Ok(()) 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 { fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
write!( write!(
f, f,

View File

@ -3,6 +3,7 @@
use lazy_static::lazy_static; use lazy_static::lazy_static;
use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors}; use miette::{Diagnostic, MietteHandler, MietteHandlerOpts, ReportHandler, RgbColors};
use regex::Regex; use regex::Regex;
use std::ffi::OsString;
use std::fmt::{self, Debug}; use std::fmt::{self, Debug};
use std::sync::Mutex; use std::sync::Mutex;
use thiserror::Error; 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 /// Store the current value of an environment variable on construction, and then
/// sets it back to it's original value once completed. /// restore that value when the guard is dropped.
fn with_env_var<F: FnOnce()>(var: &str, value: &str, body: F) { struct EnvVarGuard<'a> {
let old_value = std::env::var_os(var); var: &'a str,
std::env::set_var(var, value); old_value: Option<OsString>,
body(); }
if let Some(old_value) = old_value {
std::env::set_var(var, old_value); impl EnvVarGuard<'_> {
} else { fn new(var: &str) -> EnvVarGuard<'_> {
std::env::remove_var(var); 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<F: Fn(MietteHandlerOpts) -> MietteHandlerOpts>(
// //
// Since environment variables are shared for the entire process, we need // 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. // 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 guards = (
let handler = make_handler(MietteHandlerOpts::new()).build(); EnvVarGuard::new("NO_COLOR"),
assert_eq!(color_format(handler), no_support); EnvVarGuard::new("FORCE_COLOR"),
}); );
with_env_var("FORCE_COLOR", "1", || { // Clear color environment variables that may be set outside of 'cargo test'
let handler = make_handler(MietteHandlerOpts::new()).build(); std::env::remove_var("NO_COLOR");
assert_eq!(color_format(handler), ansi_support); std::env::remove_var("FORCE_COLOR");
});
with_env_var("FORCE_COLOR", "3", || {
let handler = make_handler(MietteHandlerOpts::new()).build();
assert_eq!(color_format(handler), rgb_support);
});
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] #[test]

View File

@ -220,6 +220,50 @@ fn word_wrap_options() -> Result<(), MietteError> {
Ok(()) 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] #[test]
fn empty_source() -> Result<(), MietteError> { fn empty_source() -> Result<(), MietteError> {
#[derive(Debug, Diagnostic, Error)] #[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!(out.contains("\u{1b}[38;2;180;142;173m"));
assert_eq!(expected, strip_ansi_escapes::strip_str(out)); 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(())
} }

View File

@ -194,3 +194,85 @@ fn test_nested_diagnostic_source_is_output() {
assert_eq!(expected, out); assert_eq!(expected, out);
} }
#[derive(Debug, miette::Diagnostic, thiserror::Error)]
#[error("A multi-error happened")]
struct MultiError {
#[related]
related_errs: Vec<Box<dyn Diagnostic>>,
}
#[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);
}