mirror of https://github.com/zkat/miette.git
fix(graphical): Align highlights correctly with wide unicode characters and tabs (#202)
Fixes: https://github.com/zkat/miette/issues/97 Fixes: https://github.com/zkat/miette/issues/87 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. Co-authored-by: Benjamin Lee <benjamin@computer.surgery>
This commit is contained in:
parent
5f3429b062
commit
196c09ce7a
|
|
@ -1,6 +1,7 @@
|
||||||
use std::fmt::{self, Write};
|
use std::fmt::{self, Write};
|
||||||
|
|
||||||
use owo_colors::{OwoColorize, Style};
|
use owo_colors::{OwoColorize, Style};
|
||||||
|
use unicode_width::UnicodeWidthChar;
|
||||||
|
|
||||||
use crate::diagnostic_chain::DiagnosticChain;
|
use crate::diagnostic_chain::DiagnosticChain;
|
||||||
use crate::handlers::theme::*;
|
use crate::handlers::theme::*;
|
||||||
|
|
@ -27,7 +28,7 @@ pub struct GraphicalReportHandler {
|
||||||
pub(crate) theme: GraphicalTheme,
|
pub(crate) theme: GraphicalTheme,
|
||||||
pub(crate) footer: Option<String>,
|
pub(crate) footer: Option<String>,
|
||||||
pub(crate) context_lines: usize,
|
pub(crate) context_lines: usize,
|
||||||
pub(crate) tab_width: Option<usize>,
|
pub(crate) tab_width: usize,
|
||||||
pub(crate) with_cause_chain: bool,
|
pub(crate) with_cause_chain: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,7 +49,7 @@ impl GraphicalReportHandler {
|
||||||
theme: GraphicalTheme::default(),
|
theme: GraphicalTheme::default(),
|
||||||
footer: None,
|
footer: None,
|
||||||
context_lines: 1,
|
context_lines: 1,
|
||||||
tab_width: None,
|
tab_width: 4,
|
||||||
with_cause_chain: true,
|
with_cause_chain: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,14 +62,14 @@ impl GraphicalReportHandler {
|
||||||
theme,
|
theme,
|
||||||
footer: None,
|
footer: None,
|
||||||
context_lines: 1,
|
context_lines: 1,
|
||||||
tab_width: None,
|
tab_width: 4,
|
||||||
with_cause_chain: true,
|
with_cause_chain: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the displayed tab width in spaces.
|
/// Set the displayed tab width in spaces.
|
||||||
pub fn tab_width(mut self, width: usize) -> Self {
|
pub fn tab_width(mut self, width: usize) -> Self {
|
||||||
self.tab_width = Some(width);
|
self.tab_width = width;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -441,12 +442,7 @@ impl GraphicalReportHandler {
|
||||||
self.render_line_gutter(f, max_gutter, line, &labels)?;
|
self.render_line_gutter(f, max_gutter, line, &labels)?;
|
||||||
|
|
||||||
// And _now_ we can print out the line text itself!
|
// And _now_ we can print out the line text itself!
|
||||||
if let Some(w) = self.tab_width {
|
self.render_line_text(f, &line.text)?;
|
||||||
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.
|
// Next, we write all the highlights that apply to this particular line.
|
||||||
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
|
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
|
||||||
|
|
@ -604,6 +600,46 @@ impl GraphicalReportHandler {
|
||||||
Ok(())
|
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<Item = usize> + '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];
|
||||||
|
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(
|
fn render_single_line_highlights(
|
||||||
&self,
|
&self,
|
||||||
f: &mut impl fmt::Write,
|
f: &mut impl fmt::Write,
|
||||||
|
|
@ -617,63 +653,44 @@ impl GraphicalReportHandler {
|
||||||
let mut highest = 0;
|
let mut highest = 0;
|
||||||
|
|
||||||
let chars = &self.theme.characters;
|
let chars = &self.theme.characters;
|
||||||
for hl in single_liners {
|
|
||||||
let hl_len = std::cmp::max(1, hl.len());
|
|
||||||
|
|
||||||
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;
|
|
||||||
let start = std::cmp::max(local_offset, highest);
|
|
||||||
let end = local_offset + hl_len;
|
|
||||||
if start < end {
|
|
||||||
underlines.push_str(
|
|
||||||
&format!(
|
|
||||||
"{:width$}{}{}{}",
|
|
||||||
"",
|
|
||||||
chars.underline.to_string().repeat(num_left),
|
|
||||||
if hl.len() == 0 {
|
|
||||||
chars.uarrow
|
|
||||||
} else if hl.label().is_some() {
|
|
||||||
chars.underbar
|
|
||||||
} else {
|
|
||||||
chars.underline
|
|
||||||
},
|
|
||||||
chars.underline.to_string().repeat(num_right),
|
|
||||||
width = local_offset.saturating_sub(highest),
|
|
||||||
)
|
|
||||||
.style(hl.style)
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
highest = std::cmp::max(highest, end);
|
|
||||||
}
|
|
||||||
writeln!(f, "{}", underlines)?;
|
|
||||||
|
|
||||||
let vbar_offsets: Vec<_> = single_liners
|
let vbar_offsets: Vec<_> = single_liners
|
||||||
.iter()
|
.iter()
|
||||||
.map(|hl| {
|
.map(|hl| {
|
||||||
let local_offset = if let Some(w) = self.tab_width {
|
let byte_start = hl.offset();
|
||||||
// Only count tabs that affect the position of the
|
let byte_end = hl.offset() + hl.len().max(1);
|
||||||
// highlighted line and ignore tabs past the span.
|
let start = self.visual_offset(line, byte_start).max(highest);
|
||||||
let tab_count = &line.text[..hl.offset() - line.offset].matches('\t').count();
|
let end = self.visual_offset(line, byte_end);
|
||||||
let tabs_as_spaces = tab_count * w - tab_count;
|
|
||||||
hl.offset() - line.offset + tabs_as_spaces
|
let vbar_offset = (start + end) / 2;
|
||||||
} else {
|
let num_left = vbar_offset - start;
|
||||||
hl.offset() - line.offset
|
let num_right = end - vbar_offset - 1;
|
||||||
};
|
if start < end {
|
||||||
(hl, local_offset + (std::cmp::max(1, hl.len()) / 2))
|
underlines.push_str(
|
||||||
|
&format!(
|
||||||
|
"{:width$}{}{}{}",
|
||||||
|
"",
|
||||||
|
chars.underline.to_string().repeat(num_left),
|
||||||
|
if hl.len() == 0 {
|
||||||
|
chars.uarrow
|
||||||
|
} else if hl.label().is_some() {
|
||||||
|
chars.underbar
|
||||||
|
} else {
|
||||||
|
chars.underline
|
||||||
|
},
|
||||||
|
chars.underline.to_string().repeat(num_right),
|
||||||
|
width = start.saturating_sub(highest),
|
||||||
|
)
|
||||||
|
.style(hl.style)
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
highest = std::cmp::max(highest, end);
|
||||||
|
|
||||||
|
(hl, vbar_offset)
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
writeln!(f, "{}", underlines)?;
|
||||||
|
|
||||||
for hl in single_liners.iter().rev() {
|
for hl in single_liners.iter().rev() {
|
||||||
if let Some(label) = hl.label() {
|
if let Some(label) = hl.label() {
|
||||||
self.write_no_linum(f, linum_width)?;
|
self.write_no_linum(f, linum_width)?;
|
||||||
|
|
|
||||||
|
|
@ -82,7 +82,7 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
|
||||||
let src = "source\n 👼🏼text\n here".to_string();
|
let src = "source\n 👼🏼text\n here".to_string();
|
||||||
let err = MyBad {
|
let err = MyBad {
|
||||||
src: NamedSource::new("bad_file.rs", src),
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
highlight: (9, 6).into(),
|
highlight: (13, 8).into(),
|
||||||
};
|
};
|
||||||
let out = fmt_report(err.into());
|
let out = fmt_report(err.into());
|
||||||
println!("Error: {}", out);
|
println!("Error: {}", out);
|
||||||
|
|
@ -92,8 +92,8 @@ fn single_line_with_wide_char() -> Result<(), MietteError> {
|
||||||
╭─[bad_file.rs:1:1]
|
╭─[bad_file.rs:1:1]
|
||||||
1 │ source
|
1 │ source
|
||||||
2 │ 👼🏼text
|
2 │ 👼🏼text
|
||||||
· ───┬──
|
· ───┬──
|
||||||
· ╰── this bit here
|
· ╰── this bit here
|
||||||
3 │ here
|
3 │ here
|
||||||
╰────
|
╰────
|
||||||
help: try doing it better next time?
|
help: try doing it better next time?
|
||||||
|
|
@ -157,10 +157,10 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
|
||||||
|
|
||||||
std::env::set_var("REPLACE_TABS", "4");
|
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 {
|
let err = MyBad {
|
||||||
src: NamedSource::new("bad_file.rs", src),
|
src: NamedSource::new("bad_file.rs", src),
|
||||||
highlight: (12, 4).into(),
|
highlight: (14, 4).into(),
|
||||||
};
|
};
|
||||||
let out = fmt_report(err.into());
|
let out = fmt_report(err.into());
|
||||||
println!("Error: {}", out);
|
println!("Error: {}", out);
|
||||||
|
|
@ -169,7 +169,7 @@ fn single_line_with_tab_in_middle() -> Result<(), MietteError> {
|
||||||
× oops!
|
× oops!
|
||||||
╭─[bad_file.rs:1:1]
|
╭─[bad_file.rs:1:1]
|
||||||
1 │ source
|
1 │ source
|
||||||
2 │ text text
|
2 │ text = text
|
||||||
· ──┬─
|
· ──┬─
|
||||||
· ╰── this bit here
|
· ╰── this bit here
|
||||||
3 │ here
|
3 │ here
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue