diff --git a/README.md b/README.md index 4d19c5c..9dbe265 100644 --- a/README.md +++ b/README.md @@ -389,6 +389,7 @@ miette::set_hook(Box::new(|_| { .terminal_links(true) .unicode(false) .context_lines(3) + .tab_width(4) .build()) })) diff --git a/src/handler.rs b/src/handler.rs index 10e7bf6..200342f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -39,6 +39,7 @@ pub struct MietteHandlerOpts { pub(crate) unicode: Option, pub(crate) footer: Option, pub(crate) context_lines: Option, + pub(crate) tab_width: Option, } impl MietteHandlerOpts { @@ -119,6 +120,12 @@ impl MietteHandlerOpts { self } + /// Set the displayed tab width in spaces. + pub fn tab_width(mut self, width: usize) -> Self { + self.tab_width = Some(width); + self + } + /// Builds a [MietteHandler] from this builder. pub fn build(self) -> MietteHandler { let graphical = self.is_graphical(); @@ -167,6 +174,9 @@ impl MietteHandlerOpts { if let Some(context_lines) = self.context_lines { handler = handler.with_context_lines(context_lines); } + if let Some(w) = self.tab_width { + handler = handler.tab_width(w); + } MietteHandler { inner: Box::new(handler), } diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 06bf26d..98d5e54 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -25,6 +25,7 @@ pub struct GraphicalReportHandler { pub(crate) theme: GraphicalTheme, pub(crate) footer: Option, pub(crate) context_lines: usize, + pub(crate) tab_width: Option, } impl GraphicalReportHandler { @@ -37,6 +38,7 @@ impl GraphicalReportHandler { theme: GraphicalTheme::default(), footer: None, context_lines: 1, + tab_width: None, } } @@ -48,9 +50,16 @@ impl GraphicalReportHandler { theme, footer: None, context_lines: 1, + tab_width: None, } } + /// Set the displayed tab width in spaces. + pub fn tab_width(mut self, width: usize) -> Self { + self.tab_width = Some(width); + self + } + /// Whether to enable error code linkification using [Diagnostic::url]. pub fn with_links(mut self, links: bool) -> Self { self.linkify_code = links; @@ -376,7 +385,12 @@ impl GraphicalReportHandler { self.render_line_gutter(f, max_gutter, line, &labels)?; // And _now_ we can print out the line text itself! - writeln!(f, "{}", line.text)?; + if let Some(w) = self.tab_width { + let text = line.text.replace("\t", " ".repeat(w).as_str()); + writeln!(f, "{}", text)?; + } else { + writeln!(f, "{}", line.text)?; + }; // Next, we write all the highlights that apply to this particular line. let (single_line, multi_line): (Vec<_>, Vec<_>) = labels @@ -545,10 +559,20 @@ impl GraphicalReportHandler { ) -> fmt::Result { let mut underlines = String::new(); let mut highest = 0; + let chars = &self.theme.characters; for hl in single_liners { let hl_len = std::cmp::max(1, hl.len()); - let local_offset = hl.offset() - line.offset; + + let local_offset = if let Some(w) = self.tab_width { + // Only count tabs that affect the position of the highlighted line and ignore tabs past the span. + let tab_count = &line.text[..hl.offset() - line.offset].matches('\t').count(); + let tabs_as_spaces = tab_count * w - tab_count; + hl.offset() - line.offset + tabs_as_spaces + } else { + hl.offset() - line.offset + }; + let vbar_offset = local_offset + (hl_len / 2); let num_left = vbar_offset - local_offset; let num_right = local_offset + hl_len - vbar_offset - 1; @@ -581,10 +605,15 @@ impl GraphicalReportHandler { let vbar_offsets: Vec<_> = single_liners .iter() .map(|hl| { - ( - hl, - (hl.offset() - line.offset) + (std::cmp::max(1, hl.len()) / 2), - ) + let local_offset = if let Some(w) = self.tab_width { + // Only count tabs that affect the position of the highlighted line and ignore tabs past the span. + let tab_count = &line.text[..hl.offset() - line.offset].matches('\t').count(); + let tabs_as_spaces = tab_count * w - tab_count; + hl.offset() - line.offset + tabs_as_spaces + } else { + hl.offset() - line.offset + }; + (hl, local_offset + (std::cmp::max(1, hl.len()) / 2)) }) .collect(); for hl in single_liners.iter().rev() { diff --git a/tests/graphical.rs b/tests/graphical.rs index 3609922..4f81ef8 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -19,6 +19,12 @@ fn fmt_report(diag: Report) -> String { NarratableReportHandler::new() .render_report(&mut out, diag.as_ref()) .unwrap(); + } else if let Ok(w) = std::env::var("REPLACE_TABS") { + GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor()) + .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()) .with_width(80) @@ -65,6 +71,84 @@ fn single_line_with_wide_char() -> Result<(), MietteError> { Ok(()) } +#[test] +fn single_line_with_two_tabs() -> 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, + } + + std::env::set_var("REPLACE_TABS", "4"); + + let src = "source\n\t\ttext\n here".to_string(); + let err = MyBad { + src: NamedSource::new("bad_file.rs", src), + highlight: (9, 4).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + let expected = r#"oops::my::bad + + × oops! + ╭─[bad_file.rs:1:1] + 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_tab_in_middle() -> 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, + } + + std::env::set_var("REPLACE_TABS", "4"); + + let src = "source\ntext\ttext\n here".to_string(); + let err = MyBad { + src: NamedSource::new("bad_file.rs", src), + highlight: (12, 4).into(), + }; + let out = fmt_report(err.into()); + println!("Error: {}", out); + let expected = r#"oops::my::bad + + × oops! + ╭─[bad_file.rs:1:1] + 1 │ source + 2 │ text 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_highlight() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] @@ -293,6 +377,53 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> { Ok(()) } +#[test] +fn multiple_same_line_highlights_with_tabs_in_middle() -> 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"] + highlight1: SourceSpan, + #[label = "y"] + highlight2: SourceSpan, + #[label = "z"] + highlight3: SourceSpan, + } + + std::env::set_var("REPLACE_TABS", "4"); + + let src = "source\n text text text\ttext 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:1:1] + 1 │ source + 2 │ text text text text text + · ──┬─ ──┬─ ──┬─ + · │ │ ╰── z + · │ ╰── y + · ╰── x + 3 │ here + ╰──── + help: try doing it better next time? +"# + .trim_start() + .to_string(); + assert_eq!(expected, out); + Ok(()) +} + #[test] fn multiline_highlight_adjacent() -> Result<(), MietteError> { #[derive(Debug, Diagnostic, Error)] diff --git a/tests/test_location.rs b/tests/test_location.rs index 9f86e12..45d99f1 100644 --- a/tests/test_location.rs +++ b/tests/test_location.rs @@ -30,7 +30,6 @@ impl miette::ReportHandler for LocationHandler { } fn track_caller(&mut self, location: &'static Location<'static>) { - dbg!(location); self.actual = Some(location.file()); } }