From fbf6664ef5582c9a15bba881a6ee1ca058102d7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kat=20March=C3=A1n?= Date: Wed, 22 Sep 2021 13:49:54 -0700 Subject: [PATCH] feat(narrated): updated narrated handler --- Cargo.toml | 3 +- src/handlers/narratable.rs | 149 +++++++++++++++++++++++++++++++++---- 2 files changed, 134 insertions(+), 18 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index af5be46..2cb6ac4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ exclude = ["images/", "tests/", "miette-derive/"] thiserror = "1.0.26" miette-derive = { path = "miette-derive", version = "=3.0.0-beta.0"} once_cell = "1.8.0" +itertools = "0.10.1" owo-colors = { version = "2.0.0", optional = true } atty = { version = "0.2.14", optional = true } @@ -23,7 +24,6 @@ term_size = { version = "0.3.2", optional = true } supports-hyperlinks = { version = "1.1.0", optional = true } supports-color = { version = "1.0.4", optional = true } supports-unicode = { version = "1.0.0", optional = true } -itertools = { version = "0.10.1", optional = true } backtrace = { version = "0.3.61", optional = true } [dev-dependencies] @@ -46,7 +46,6 @@ fancy = [ "supports-hyperlinks", "supports-color", "supports-unicode", - "itertools", "backtrace" ] diff --git a/src/handlers/narratable.rs b/src/handlers/narratable.rs index 31ec81c..ba40078 100644 --- a/src/handlers/narratable.rs +++ b/src/handlers/narratable.rs @@ -1,8 +1,10 @@ use std::fmt; +use itertools::Itertools; + use crate::chain::Chain; use crate::protocol::{Diagnostic, Severity}; -use crate::{ReportHandler, SourceCode, SourceSpan, SpanContents}; +use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents}; /** [ReportHandler] that renders plain text and avoids extraneous graphics. @@ -11,6 +13,7 @@ non-graphical environments, such as non-TTY output. */ #[derive(Debug, Clone)] pub struct NarratableReportHandler { + context_lines: usize, footer: Option, } @@ -18,7 +21,10 @@ impl NarratableReportHandler { /// Create a new [NarratableReportHandler]. There are no customization /// options. pub fn new() -> Self { - Self { footer: None } + Self { + footer: None, + context_lines: 1, + } } /// Set the footer to be displayed at the end of the report. @@ -26,6 +32,12 @@ impl NarratableReportHandler { self.footer = Some(footer); self } + + /// Sets the number of lines of context to show around each error. + pub fn with_context_lines(mut self, lines: usize) -> Self { + self.context_lines = lines; + self + } } impl Default for NarratableReportHandler { @@ -46,13 +58,7 @@ impl NarratableReportHandler { ) -> fmt::Result { self.render_header(f, diagnostic)?; self.render_causes(f, diagnostic)?; - - // if let Some(labels) = diagnostic.labels() { - // for label in labels { - // self.render_label(f, &label)?; - // } - // } - + self.render_snippets(f, diagnostic)?; self.render_footer(f, diagnostic)?; Ok(()) } @@ -91,22 +97,131 @@ impl NarratableReportHandler { Ok(()) } - /* + fn render_snippets( + &self, + f: &mut impl fmt::Write, + diagnostic: &(dyn Diagnostic), + ) -> fmt::Result { + if let Some(source) = diagnostic.source_code() { + 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 contexts = labels.iter().cloned().zip(contents.iter()).coalesce( + |(left, left_conts), (right, right_conts)| { + 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() + { + Ok(( + new_span, // We'll throw this away later + left_conts, + )) + } else { + Err(((left, left_conts), (right, right_conts))) + } + } else { + Err(((left, left_conts), (right, right_conts))) + } + }, + ); + for (ctx, _) in contexts { + self.render_context(f, source, &ctx, &labels[..])?; + } + } + } + } + Ok(()) + } + + fn render_context<'a>( + &self, + f: &mut impl fmt::Write, + source: &'a dyn SourceCode, + context: &LabeledSpan, + labels: &[LabeledSpan], + ) -> fmt::Result { + let (contents, lines) = self.get_lines(source, context.inner())?; + write!(f, "Begin snippet")?; + if let Some(filename) = contents.name() { + write!(f, " for {}", filename,)?; + } + writeln!( + f, + " starting at line {}, column {}", + contents.line() + 1, + contents.column() + 1 + )?; + writeln!(f)?; + for line in &lines { + writeln!(f, "snippet line {}: {}", line.line_number, line.text)?; + let relevant = labels.iter().filter(|l| line.span_starts(l.inner())); + for label in relevant { + let contents = source + .read_span(label.inner(), self.context_lines, self.context_lines) + .map_err(|_| fmt::Error)?; + if contents.line() + 1 == line.line_number { + write!( + f, + " label starting at line {}, column {}", + contents.line() + 1, + contents.column() + 1 + )?; + if let Some(label) = label.label() { + write!(f, ": {}", label)?; + } + writeln!(f)?; + } + } + } + Ok(()) + } + fn get_lines<'a>( &'a self, - source: &'a dyn Source, - ) -> Result<(Box, Vec), fmt::Error> { - let context_data = source.read_span(&snippet.context).map_err(|_| fmt::Error)?; + source: &'a dyn SourceCode, + context_span: &'a SourceSpan, + ) -> Result<(Box + 'a>, Vec), fmt::Error> { + let context_data = source + .read_span(context_span, self.context_lines, self.context_lines) + .map_err(|_| fmt::Error)?; let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); let mut line = context_data.line(); let mut column = context_data.column(); - let mut offset = snippet.context.offset(); + let mut offset = context_data.span().offset(); let mut line_offset = offset; let mut iter = context.chars().peekable(); let mut line_str = String::new(); let mut lines = Vec::new(); while let Some(char) = iter.next() { offset += char.len_utf8(); + let mut at_end_of_file = false; match char { '\r' => { if iter.next_if_eq(&'\n').is_some() { @@ -117,8 +232,10 @@ impl NarratableReportHandler { line_str.push(char); column += 1; } + at_end_of_file = iter.peek().is_none(); } '\n' => { + at_end_of_file = iter.peek().is_none(); line += 1; column = 0; } @@ -127,7 +244,8 @@ impl NarratableReportHandler { column += 1; } } - if iter.peek().is_none() { + + if iter.peek().is_none() && !at_end_of_file { line += 1; } @@ -143,7 +261,6 @@ impl NarratableReportHandler { } Ok((context_data, lines)) } - */ } impl ReportHandler for NarratableReportHandler {