From 9e5872bab0a14374484a95fba29b5c123e8688a7 Mon Sep 17 00:00:00 2001 From: Benjamin Lee Date: Fri, 9 Sep 2022 20:56:19 -0700 Subject: [PATCH] fix(graphical) Align highlights correctly with tab characters (#87) Fixes: https://github.com/zkat/miette/issues/87 BREAKING CHANGE: Tabs are always expanded to spaces by the graphical handler, and `tab_width` now defaults to 4. Instead of replacing every tab with a fixed number of spaces, spaces are used to align to the next tabstop. `tab_width` controls the space between tabstops rather than the fixed width of each tab character. --- src/handlers/graphical.rs | 52 ++++++++++++++++++++++++++++----------- tests/graphical.rs | 6 ++--- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 927101f..795aa1a 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -1,7 +1,7 @@ use std::fmt::{self, Write}; use owo_colors::{OwoColorize, Style}; -use unicode_width::UnicodeWidthStr; +use unicode_width::UnicodeWidthChar; use crate::diagnostic_chain::DiagnosticChain; use crate::handlers::theme::*; @@ -28,7 +28,7 @@ pub struct GraphicalReportHandler { pub(crate) theme: GraphicalTheme, pub(crate) footer: Option, pub(crate) context_lines: usize, - pub(crate) tab_width: Option, + pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, } @@ -49,7 +49,7 @@ impl GraphicalReportHandler { theme: GraphicalTheme::default(), footer: None, context_lines: 1, - tab_width: None, + tab_width: 4, with_cause_chain: true, } } @@ -62,14 +62,14 @@ impl GraphicalReportHandler { theme, footer: None, context_lines: 1, - tab_width: None, + tab_width: 4, with_cause_chain: true, } } /// Set the displayed tab width in spaces. pub fn tab_width(mut self, width: usize) -> Self { - self.tab_width = Some(width); + self.tab_width = width; self } @@ -442,12 +442,7 @@ impl GraphicalReportHandler { self.render_line_gutter(f, max_gutter, line, &labels)?; // And _now_ we can print out the line text itself! - 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)?; - }; + self.render_line_text(f, &line.text)?; // Next, we write all the highlights that apply to this particular line. let (single_line, multi_line): (Vec<_>, Vec<_>) = labels @@ -605,15 +600,44 @@ impl GraphicalReportHandler { Ok(()) } + /// Returns an iterator over the visual width of each character in a line. + fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator + 'a { + let mut column = 0; + let tab_width = self.tab_width; + text.chars().map(move |c| { + let width = if c == '\t' { + // Round up to the next multiple of tab_width + tab_width - column % tab_width + } else { + c.width().unwrap_or(0) + }; + column += width; + width + }) + } + /// Returns the visual column position of a byte offset on a specific line. fn visual_offset(&self, line: &Line, offset: usize) -> usize { let line_range = line.offset..(line.offset + line.length); assert!(line_range.contains(&offset)); let text = &line.text[..offset - line.offset]; - let tab_count = text.matches('\t').count(); - let tab_width = self.tab_width.unwrap_or(1); - text.width() + tab_count * tab_width + self.line_visual_char_width(text).sum() + } + + /// Renders a line to the output formatter, replacing tabs with spaces. + fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result { + for (c, width) in text.chars().zip(self.line_visual_char_width(text)) { + if c == '\t' { + for _ in 0..width { + f.write_char(' ')? + } + } else { + f.write_char(c)? + } + } + f.write_char('\n')?; + Ok(()) } fn render_single_line_highlights( diff --git a/tests/graphical.rs b/tests/graphical.rs index e79acbd..420c9ae 100644 --- a/tests/graphical.rs +++ b/tests/graphical.rs @@ -157,10 +157,10 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> { std::env::set_var("REPLACE_TABS", "4"); - let src = "source\ntext\ttext\n here".to_string(); + let src = "source\ntext =\ttext\n here".to_string(); let err = MyBad { src: NamedSource::new("bad_file.rs", src), - highlight: (12, 4).into(), + highlight: (14, 4).into(), }; let out = fmt_report(err.into()); println!("Error: {}", out); @@ -169,7 +169,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> { × oops! ╭─[bad_file.rs:1:1] 1 │ source - 2 │ text text + 2 │ text = text · ──┬─ · ╰── this bit here 3 │ here