use std::fmt;
use owo_colors::OwoColorize;
use super::handler::{GraphicalReportHandler, LinkStyle};
use crate::{Diagnostic, Severity};
impl GraphicalReportHandler {
pub fn render_report(
&self,
f: &mut impl fmt::Write,
diagnostic: &dyn Diagnostic,
) -> fmt::Result {
writeln!(f)?;
self.render_title(f, diagnostic)?;
let src = diagnostic.source_code();
self.render_snippets(f, diagnostic, src)?;
self.render_footer(f, diagnostic)?;
Ok(())
}
fn render_title(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
let (severity_style, severity_icon) = match diagnostic.severity() {
Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
};
let width = self.termwidth.saturating_sub(2);
let title = match (self.links, diagnostic.url(), diagnostic.code()) {
(LinkStyle::Link, Some(url), Some(code)) => {
const CTL: &str = "\u{1b}]8;;";
const END: &str = "\u{1b}]8;;\u{1b}\\";
let code = code.style(severity_style);
let title = diagnostic.style(severity_style);
format!("{CTL}{url}\u{1b}\\{code}{END}: {title}")
}
(_, _, Some(code)) if severity_style.is_plain() => format!("{code}: {diagnostic}"),
(_, _, Some(code)) => {
format!("{}", format_args!("{code}: {diagnostic}").style(severity_style))
}
_ if severity_style.is_plain() => diagnostic.to_string(),
_ => format!("{}", diagnostic.style(severity_style)),
};
if !title.contains('\n')
&& severity_icon.len().saturating_add(title.len()).saturating_add(3) <= width
{
f.write_str(" ")?;
if severity_style.is_plain() {
f.write_str(severity_icon)?;
} else {
write!(f, "{}", severity_icon.style(severity_style))?;
}
f.write_char(' ')?;
f.write_str(title.trim_end_matches(' '))?;
} else {
let (initial_indent, rest_indent) = if severity_style.is_plain() {
(format!(" {severity_icon} "), format!(" {} ", self.theme.characters.vbar))
} else {
(
format!(" {} ", severity_icon.style(severity_style)),
format!(" {} ", self.theme.characters.vbar.style(severity_style)),
)
};
let opts = Self::wrap_options(width, &initial_indent, &rest_indent);
Self::write_fill(f, &title, opts)?;
}
f.write_char('\n')?;
Ok(())
}
fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
if let Some(help) = diagnostic.help() {
const PREFIX: &str = " help: ";
let width = self.termwidth.saturating_sub(4);
if memchr::memchr(b'\n', help.as_bytes()).is_none()
&& PREFIX.len().saturating_add(help.len()) <= width
{
if self.theme.styles.help.is_plain() {
f.write_str(PREFIX)?;
} else {
write!(f, "{}", PREFIX.style(self.theme.styles.help))?;
}
f.write_str(help.trim_end_matches(' '))?;
} else {
let initial_indent = PREFIX.style(self.theme.styles.help).to_string();
let opts = Self::wrap_options(width, &initial_indent, " ");
Self::write_fill(f, &help, opts)?;
}
f.write_char('\n')?;
}
if let Some(note) = diagnostic.note() {
let width = self.termwidth.saturating_sub(4);
let initial_indent = " note: ".style(self.theme.styles.note).to_string();
let opts = Self::wrap_options(width, &initial_indent, " ");
Self::write_fill(f, ¬e, opts)?;
f.write_char('\n')?;
}
Ok(())
}
fn wrap_options<'a>(
width: usize,
initial_indent: &'a str,
subsequent_indent: &'a str,
) -> textwrap::Options<'a> {
textwrap::Options::new(width)
.initial_indent(initial_indent)
.subsequent_indent(subsequent_indent)
}
fn write_fill(f: &mut impl fmt::Write, text: &str, opts: textwrap::Options<'_>) -> fmt::Result {
if Self::fits_on_line(text, &opts) {
f.write_str(opts.initial_indent)?;
f.write_str(text.trim_end_matches(' '))
} else {
f.write_str(&textwrap::fill(text, opts))
}
}
#[cfg(test)]
fn fill(text: &str, opts: textwrap::Options<'_>) -> String {
if Self::fits_on_line(text, &opts) {
let text = text.trim_end_matches(' ');
let mut result = String::with_capacity(opts.initial_indent.len() + text.len());
result.push_str(opts.initial_indent);
result.push_str(text);
return result;
}
textwrap::fill(text, opts)
}
fn fits_on_line(text: &str, opts: &textwrap::Options<'_>) -> bool {
if memchr::memchr(b'\n', text.as_bytes()).is_some() {
return false;
}
opts.initial_indent.len().saturating_add(text.len()) <= opts.width || {
let available = opts.width.saturating_sub(Self::display_width(opts.initial_indent));
Self::display_width(text) <= available
}
}
fn display_width(text: &str) -> usize {
if !text.is_ascii() {
return textwrap::core::display_width(text);
}
let bytes = text.as_bytes();
let mut width = 0;
let mut i = 0;
while i < bytes.len() {
if bytes[i] != b'\x1b' {
width += usize::from((b' '..=b'~').contains(&bytes[i]));
i += 1;
continue;
}
i += 1;
let Some(&kind) = bytes.get(i) else { break };
i += 1;
match kind {
b'[' => {
while i < bytes.len() {
let byte = bytes[i];
i += 1;
if (b'@'..=b'~').contains(&byte) {
break;
}
}
}
b']' => {
while i < bytes.len() {
if bytes[i] == b'\x07' {
i += 1;
break;
}
if bytes[i] == b'\x1b' && bytes.get(i + 1) == Some(&b'\\') {
i += 2;
break;
}
i += 1;
}
}
_ => {}
}
}
width
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(
miri,
ignore = "exhaustive equivalence check over safe text wrapping code; interpreting every \
textwrap case under Miri takes more than 16 minutes"
)]
fn fill_fast_path_matches_textwrap() {
let texts = [
"",
"short diagnostic",
"trailing spaces ",
" leading spaces",
"two inner spaces",
"Café 火",
"combining e\u{301}",
"emoji 🐂",
"\u{1b}[31mstyled text\u{1b}[0m",
"\u{1b}]8;;https://example.com\u{1b}\\linked\u{1b}]8;;\u{1b}\\",
"\u{1b}]0;title\u{7}visible",
"control\tcharacters\u{7}",
"incomplete \u{1b}[31",
"first\nsecond",
];
for width in 0..32 {
for initial_indent in ["", " ", " help: ", "\u{1b}[31m × \u{1b}[0m"] {
for text in texts {
let opts = textwrap::Options::new(width)
.initial_indent(initial_indent)
.subsequent_indent(" ");
assert_eq!(
GraphicalReportHandler::fill(text, opts.clone()),
textwrap::fill(text, opts.clone()),
"width={width}, indent={initial_indent:?}, text={text:?}"
);
let mut output = String::new();
GraphicalReportHandler::write_fill(&mut output, text, opts.clone()).unwrap();
assert_eq!(
output,
textwrap::fill(text, opts),
"streaming: width={width}, indent={initial_indent:?}, text={text:?}"
);
}
}
}
}
}