diff --git a/Cargo.lock b/Cargo.lock index cc9fc84e2..153ab2d1f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -136,7 +136,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edc932b40b31d9bea0196f54c5b1b721b9aff3882fc08d9fe4082970c7e94d3d" dependencies = [ "bpaf_derive", - "owo-colors 4.0.0", + "owo-colors", "supports-color", ] @@ -214,9 +214,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.89" +version = "1.0.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0ba8f7aaa012f30d5b2861462f6708eccd49c3c39863fe083a308035f63d723" +checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" [[package]] name = "cfg-if" @@ -253,18 +253,18 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da" +checksum = "b230ab84b0ffdf890d5a10abdbc8b83ae1c4918275daea1ab8801f71536b2651" dependencies = [ "clap_builder", ] [[package]] name = "clap_builder" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ "anstyle", "clap_lex", @@ -276,6 +276,12 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clean-path" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaa6b4b263a5d737e9bf6b7c09b72c41a5480aec4d7219af827f6564e950b6a5" + [[package]] name = "codspeed" version = "2.4.0" @@ -539,9 +545,9 @@ dependencies = [ [[package]] name = "env_logger" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c012a26a7f605efc424dd53697843a72be7dc86ad2d01f7814337794a12231d" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" dependencies = [ "env_filter", "humantime", @@ -970,9 +976,9 @@ checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libloading" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2caa5afb8bf9f3a2652760ce7d4f62d21c4d5a423e68466fca30df82f2330164" +checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19" dependencies = [ "cfg-if", "windows-targets 0.52.4", @@ -1046,18 +1052,13 @@ checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" [[package]] name = "miette" -version = "5.10.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e" +checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ - "is-terminal", + "cfg-if", "miette-derive", - "once_cell", - "owo-colors 3.5.0", - "supports-color", - "supports-hyperlinks", - "supports-unicode", - "terminal_size", + "owo-colors", "textwrap", "thiserror", "unicode-width", @@ -1065,9 +1066,9 @@ dependencies = [ [[package]] name = "miette-derive" -version = "5.10.0" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" +checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", @@ -1179,6 +1180,20 @@ dependencies = [ "libloading", ] +[[package]] +name = "nodejs_package_json" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "770813a997f38d144fa7c0e0b15012b8d1ff863f16d875c9ac8ab1b3ebd5cff8" +dependencies = [ + "indexmap", + "rustc-hash", + "semver 1.0.22", + "serde", + "serde_json", + "thiserror", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1301,12 +1316,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" -[[package]] -name = "owo-colors" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" - [[package]] name = "owo-colors" version = "4.0.0" @@ -1444,9 +1453,8 @@ dependencies = [ name = "oxc_diagnostics" version = "0.9.0" dependencies = [ - "is-terminal", "miette", - "owo-colors 3.5.0", + "owo-colors", "textwrap", "thiserror", "unicode-width", @@ -1651,20 +1659,21 @@ dependencies = [ [[package]] name = "oxc_resolver" -version = "1.5.4" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2033cc3b0e72446d3321866db0954804b9ca559ad692480205053f6aea4bfc15" +checksum = "2255c105f844bd9793db2fa57ee443ac82668d7d86e5601d0245de5efd7686df" dependencies = [ "dashmap", "dunce", - "indexmap", "json-strip-comments", + "nodejs_package_json", "once_cell", "rustc-hash", "serde", "serde_json", "thiserror", "tracing", + "typescript_tsconfig_json", ] [[package]] @@ -1917,18 +1926,18 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "pin-project" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", @@ -1986,7 +1995,7 @@ dependencies = [ "quote", "syn 2.0.52", "version_check", - "yansi 1.0.0-rc.1", + "yansi 1.0.0", ] [[package]] @@ -2242,6 +2251,9 @@ name = "semver" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" +dependencies = [ + "serde", +] [[package]] name = "semver-parser" @@ -2455,24 +2467,6 @@ dependencies = [ "is_ci", ] -[[package]] -name = "supports-hyperlinks" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d" -dependencies = [ - "is-terminal", -] - -[[package]] -name = "supports-unicode" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a" -dependencies = [ - "is-terminal", -] - [[package]] name = "syn" version = "1.0.109" @@ -2495,21 +2489,11 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "terminal_size" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" -dependencies = [ - "libc", - "winapi", -] - [[package]] name = "textwrap" -version = "0.15.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" dependencies = [ "smawk", "unicode-linebreak", @@ -2784,6 +2768,19 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "typescript_tsconfig_json" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7891656ce99f05b8866301d3a97abc9b260fb80ac962aad3d31616947b068646" +dependencies = [ + "clean-path", + "indexmap", + "rustc-hash", + "serde", + "serde_json", +] + [[package]] name = "ucd-trie" version = "0.1.6" @@ -3188,9 +3185,9 @@ checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" [[package]] name = "yansi" -version = "1.0.0-rc.1" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1367295b8f788d371ce2dbc842c7b709c73ee1364d30351dd300ec2203b12377" +checksum = "6c2861d76f58ec8fc95708b9b1e417f7b12fd72ad33c01fa6886707092dea0d3" [[package]] name = "zeroize" diff --git a/Cargo.toml b/Cargo.toml index 127a5a6d3..0a4d464fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,7 +97,7 @@ ignore = { version = "0.4.22" } itertools = { version = "0.12.1" } jemallocator = { version = "0.5.4" } lazy_static = { version = "1.4.0" } -miette = { version = "5.10.0", features = ["fancy-no-backtrace"] } +miette = { version = "7.2.0", features = ["fancy-no-syscall"] } mimalloc = { version = "0.1.39" } num-bigint = { version = "0.4.4" } num-traits = { version = "0.2.18" } diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml index fa0cbabbb..65b8d6eab 100644 --- a/crates/oxc_diagnostics/Cargo.toml +++ b/crates/oxc_diagnostics/Cargo.toml @@ -19,9 +19,8 @@ doctest = false [dependencies] thiserror = { workspace = true } -miette = { workspace = true, features = ["fancy-no-backtrace"] } +miette = { workspace = true } unicode-width = "0.1.11" -owo-colors = { version = "3.5.0" } -textwrap = { version = "=0.15.0" } # pinned to 0.15.0 to sync with miette -is-terminal = { version = "0.4.12" } +owo-colors = { version = "4.0.0" } +textwrap = { version = "0.16.0" } diff --git a/crates/oxc_diagnostics/src/graphic_reporter.rs b/crates/oxc_diagnostics/src/graphic_reporter.rs index b642d1ef7..0c7d8de79 100644 --- a/crates/oxc_diagnostics/src/graphic_reporter.rs +++ b/crates/oxc_diagnostics/src/graphic_reporter.rs @@ -1,29 +1,22 @@ -#![allow(clippy::must_use_candidate)] +#![allow(clippy::restriction)] +#![allow(clippy::style)] #![allow(clippy::pedantic)] #![allow(clippy::nursery)] #![allow(dead_code)] -/// origin file: https://github.com/zkat/miette/blob/78fe18e6990feacc8bdaeeb10e1439a12c111e6e/src/handlers/graphical.rs +/// origin file: https://github.com/zkat/miette/blob/75fea0935e495d0215518c80d32dd820910982e3/src/handlers/graphical.rs#L1 use std::fmt::{self, Write}; -// use miette::diagnostic_chain::DiagnosticChain; -use miette::{ - Diagnostic, LabeledSpan, MietteError, ReportHandler, Severity, SourceCode, SourceSpan, - SpanContents, -}; use owo_colors::{OwoColorize, Style}; use unicode_width::UnicodeWidthChar; -pub use crate::graphical_theme::GraphicalTheme; +use miette::{ + Diagnostic, LabeledSpan, ReportHandler, Severity, SourceCode, SourceSpan, SpanContents, + ThemeCharacters, +}; + +use crate::graphical_theme::GraphicalTheme; -/** -A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a -quasi-graphical way, using terminal colors, unicode drawing characters, and -other such things. -This is the default reporter bundled with `miette`. -See [`set_hook()`](miette::set_hook) for more details on customizing your global -printer. -*/ #[derive(Debug, Clone)] pub struct GraphicalReportHandler { pub(crate) links: LinkStyle, @@ -33,6 +26,12 @@ pub struct GraphicalReportHandler { pub(crate) context_lines: usize, pub(crate) tab_width: usize, pub(crate) with_cause_chain: bool, + pub(crate) wrap_lines: bool, + pub(crate) break_words: bool, + pub(crate) word_separator: Option, + pub(crate) word_splitter: Option, + // pub(crate) highlighter: MietteHighlighter, + pub(crate) link_display_text: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -41,9 +40,6 @@ pub(crate) enum LinkStyle { Link, Text, } -fn style() -> Style { - Style::new() -} impl GraphicalReportHandler { /// Create a new `GraphicalReportHandler` with the default @@ -51,12 +47,18 @@ impl GraphicalReportHandler { pub fn new() -> Self { Self { links: LinkStyle::Link, - termwidth: 400, // Changed: origin: 200 + termwidth: 400, theme: GraphicalTheme::default(), footer: None, context_lines: 1, tab_width: 4, with_cause_chain: false, + wrap_lines: true, + break_words: true, + word_separator: None, + word_splitter: None, + // highlighter: MietteHighlighter::default(), + link_display_text: None, } } @@ -111,6 +113,30 @@ impl GraphicalReportHandler { self } + /// Enables or disables wrapping of lines to fit the width. + pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self { + self.wrap_lines = wrap_lines; + self + } + + /// Enables or disables breaking of words during wrapping. + pub fn with_break_words(mut self, break_words: bool) -> Self { + self.break_words = break_words; + self + } + + /// Sets the word separator to use when wrapping. + pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self { + self.word_separator = Some(word_separator); + self + } + + /// Sets the word splitter to usewhen wrapping. + pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self { + self.word_splitter = Some(word_splitter); + self + } + /// Sets the 'global' footer for this handler. pub fn with_footer(mut self, footer: String) -> Self { self.footer = Some(footer); @@ -122,6 +148,30 @@ impl GraphicalReportHandler { self.context_lines = lines; self } + + // /// Enable syntax highlighting for source code snippets, using the given + // /// [`Highlighter`]. See the [crate::highlighters] crate for more details. + // pub fn with_syntax_highlighting( + // mut self, + // highlighter: impl Highlighter + Send + Sync + 'static, + // ) -> Self { + // self.highlighter = MietteHighlighter::from(highlighter); + // self + // } + + // /// Disable syntax highlighting. This uses the + // /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter. + // pub fn without_syntax_highlighting(mut self) -> Self { + // self.highlighter = MietteHighlighter::nocolor(); + // self + // } + + /// Sets the display text for links. + /// Miette displays `(link)` if this option is not set. + pub fn with_link_display_text(mut self, text: impl Into) -> Self { + self.link_display_text = Some(text.into()); + self + } } impl Default for GraphicalReportHandler { @@ -134,9 +184,9 @@ impl GraphicalReportHandler { /// Render a [`Diagnostic`]. This function is mostly internal and meant to /// be called by the toplevel [`ReportHandler`] handler, but is made public /// to make it easier (possible) to test in isolation from global state. - pub fn render_report( + pub fn render_report( &self, - f: &mut T, + f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result { self.render_header(f, diagnostic)?; @@ -149,8 +199,18 @@ impl GraphicalReportHandler { if let Some(footer) = &self.footer { writeln!(f)?; let width = self.termwidth.saturating_sub(4); - let opts = textwrap::Options::new(width).initial_indent(" ").subsequent_indent(" "); - writeln!(f, "{}", textwrap::fill(footer, opts))?; + let mut opts = textwrap::Options::new(width) + .initial_indent(" ") + .subsequent_indent(" ") + .break_words(self.break_words); + if let Some(word_separator) = self.word_separator { + opts = opts.word_separator(word_separator); + } + if let Some(word_splitter) = self.word_splitter.clone() { + opts = opts.word_splitter(word_splitter); + } + + writeln!(f, "{}", self.wrap(footer, opts))?; } Ok(()) } @@ -169,14 +229,16 @@ impl GraphicalReportHandler { } else { "".to_string() }; + let display_text = self.link_display_text.as_deref().unwrap_or("(link)"); let link = format!( "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\", url, code.style(severity_style), - "(link)".style(self.theme.styles.link) + display_text.style(self.theme.styles.link) ); write!(header, "{}", link)?; writeln!(f, "{}", header)?; + writeln!(f)?; } else if let Some(code) = diagnostic.code() { write!(header, "{}", code.style(severity_style),)?; if self.links == LinkStyle::Text && diagnostic.url().is_some() { @@ -184,6 +246,7 @@ impl GraphicalReportHandler { write!(header, " ({})", url.style(self.theme.styles.link))?; } writeln!(f, "{}", header)?; + writeln!(f)?; } Ok(()) } @@ -198,47 +261,77 @@ impl GraphicalReportHandler { let initial_indent = format!(" {} ", severity_icon.style(severity_style)); let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style)); let width = self.termwidth.saturating_sub(2); - let opts = textwrap::Options::new(width) + let mut opts = textwrap::Options::new(width) .initial_indent(&initial_indent) - .subsequent_indent(&rest_indent); + .subsequent_indent(&rest_indent) + .break_words(self.break_words); + if let Some(word_separator) = self.word_separator { + opts = opts.word_separator(word_separator); + } + if let Some(word_splitter) = self.word_splitter.clone() { + opts = opts.word_splitter(word_splitter); + } let title = format!("{}", diagnostic.to_string().style(severity_style)); let title = textwrap::fill(&title, opts); writeln!(f, "{}", title)?; - // CHANGED: REMOVED // if !self.with_cause_chain { - // return Ok(()); + // return Ok(()); // } // if let Some(mut cause_iter) = diagnostic - // .diagnostic_source() - // .map(DiagnosticChain::from_diagnostic) - // .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) - // .map(|it| it.peekable()) + // .diagnostic_source() + // .map(DiagnosticChain::from_diagnostic) + // .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror)) + // .map(|it| it.peekable()) // { - // while let Some(error) = cause_iter.next() { - // let is_last = cause_iter.peek().is_none(); - // let char = if !is_last { - // self.theme.characters.lcross - // } else { - // self.theme.characters.lbot - // }; - // let initial_indent = format!( - // " {}{}{} ", - // char, self.theme.characters.hbar, self.theme.characters.rarrow - // ) - // .style(severity_style) - // .to_string(); - // let rest_indent = - // format!(" {} ", if is_last { ' ' } else { self.theme.characters.vbar }) - // .style(severity_style) - // .to_string(); - // let opts = textwrap::Options::new(width) - // .initial_indent(&initial_indent) - // .subsequent_indent(&rest_indent); - // writeln!(f, "{}", textwrap::fill(&error.to_string(), opts))?; - // } + // while let Some(error) = cause_iter.next() { + // let is_last = cause_iter.peek().is_none(); + // let char = if !is_last { + // self.theme.characters.lcross + // } else { + // self.theme.characters.lbot + // }; + // let initial_indent = format!( + // " {}{}{} ", + // char, self.theme.characters.hbar, self.theme.characters.rarrow + // ) + // .style(severity_style) + // .to_string(); + // let rest_indent = + // format!(" {} ", if is_last { ' ' } else { self.theme.characters.vbar }) + // .style(severity_style) + // .to_string(); + // let mut opts = textwrap::Options::new(width) + // .initial_indent(&initial_indent) + // .subsequent_indent(&rest_indent) + // .break_words(self.break_words); + // if let Some(word_separator) = self.word_separator { + // opts = opts.word_separator(word_separator); + // } + // if let Some(word_splitter) = self.word_splitter.clone() { + // opts = opts.word_splitter(word_splitter); + // } + + // match error { + // ErrorKind::Diagnostic(diag) => { + // let mut inner = String::new(); + + // let mut inner_renderer = self.clone(); + // // Don't print footer for inner errors + // inner_renderer.footer = None; + // // Cause chains are already flattened, so don't double-print the nested error + // inner_renderer.with_cause_chain = false; + // inner_renderer.render_report(&mut inner, diag)?; + + // writeln!(f, "{}", self.wrap(&inner, opts))?; + // } + // ErrorKind::StdError(err) => { + // writeln!(f, "{}", self.wrap(&err.to_string(), opts))?; + // } + // } + // } // } Ok(()) @@ -248,10 +341,18 @@ impl GraphicalReportHandler { if let Some(help) = diagnostic.help() { let width = self.termwidth.saturating_sub(4); let initial_indent = " help: ".style(self.theme.styles.help).to_string(); - let opts = textwrap::Options::new(width) + let mut opts = textwrap::Options::new(width) .initial_indent(&initial_indent) - .subsequent_indent(" "); - writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?; + .subsequent_indent(" ") + .break_words(self.break_words); + if let Some(word_separator) = self.word_separator { + opts = opts.word_separator(word_separator); + } + if let Some(word_splitter) = self.word_splitter.clone() { + opts = opts.word_splitter(word_splitter); + } + + writeln!(f, "{}", self.wrap(&help.to_string(), opts))?; } Ok(()) } @@ -263,6 +364,9 @@ impl GraphicalReportHandler { parent_src: Option<&dyn SourceCode>, ) -> fmt::Result { if let Some(related) = diagnostic.related() { + let mut inner_renderer = self.clone(); + // Re-enable the printing of nested cause chains for related errors + inner_renderer.with_cause_chain = true; writeln!(f)?; for rel in related { match rel.severity() { @@ -270,13 +374,12 @@ impl GraphicalReportHandler { Some(Severity::Warning) => write!(f, "Warning: ")?, Some(Severity::Advice) => write!(f, "Advice: ")?, }; - self.render_header(f, rel)?; - writeln!(f)?; - self.render_causes(f, rel)?; + inner_renderer.render_header(f, rel)?; + inner_renderer.render_causes(f, rel)?; let src = rel.source_code().or(parent_src); - self.render_snippets(f, rel, src)?; - self.render_footer(f, rel)?; - self.render_related(f, rel, src)?; + inner_renderer.render_snippets(f, rel, src)?; + inner_renderer.render_footer(f, rel)?; + inner_renderer.render_related(f, rel, src)?; } } Ok(()) @@ -288,66 +391,58 @@ impl GraphicalReportHandler { diagnostic: &(dyn Diagnostic), opt_source: Option<&dyn SourceCode>, ) -> fmt::Result { - if let Some(source) = opt_source { - 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 mut contexts = Vec::new(); - for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) { - if contexts.is_empty() { - contexts.push((right, right_conts)); - } else { - let (left, left_conts) = contexts.last().unwrap().clone(); - 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() - { - contexts.pop(); - contexts.push(( - // We'll throw this away later - new_span, left_conts, - )); - } else { - contexts.push((right, right_conts)); - } - } else { - contexts.push((right, right_conts)); - } - } - } - for (ctx, _) in contexts { - self.render_context(f, source, &ctx, &labels[..])?; - } + let source = match opt_source { + Some(source) => source, + None => return Ok(()), + }; + let labels = match diagnostic.labels() { + Some(labels) => labels, + None => return Ok(()), + }; + + let mut labels = labels.collect::>(); + labels.sort_unstable_by_key(|l| l.inner().offset()); + + let mut contexts = Vec::with_capacity(labels.len()); + for right in labels.iter().cloned() { + let right_conts = source + .read_span(right.inner(), self.context_lines, self.context_lines) + .map_err(|_| fmt::Error)?; + + if contexts.is_empty() { + contexts.push((right, right_conts)); + continue; + } + + let (left, left_conts) = contexts.last().unwrap(); + if left_conts.line() + left_conts.line_count() >= right_conts.line() { + // The snippets will overlap, so we create one Big Chunky Boi + let left_end = left.offset() + left.len(); + let right_end = right.offset() + right.len(); + let new_end = std::cmp::max(left_end, right_end); + + let new_span = LabeledSpan::new( + left.label().map(String::from), + left.offset(), + new_end - left.offset(), + ); + // Check that the two contexts can be combined + if let Ok(new_conts) = + source.read_span(new_span.inner(), self.context_lines, self.context_lines) + { + contexts.pop(); + // We'll throw the contents away later + contexts.push((new_span, new_conts)); + continue; } } + + contexts.push((right, right_conts)); } + for (ctx, _) in contexts { + self.render_context(f, source, &ctx, &labels[..])?; + } + Ok(()) } @@ -360,6 +455,15 @@ impl GraphicalReportHandler { ) -> fmt::Result { let (contents, lines) = self.get_lines(source, context.inner())?; + // only consider labels from the context as primary label + let ctx_labels = labels.iter().filter(|l| { + context.inner().offset() <= l.inner().offset() + && l.inner().offset() + l.inner().len() + <= context.inner().offset() + context.inner().len() + }); + let primary_label = + ctx_labels.clone().find(|label| label.primary()).or_else(|| ctx_labels.clone().next()); + // sorting is your friend let labels = labels .iter() @@ -367,6 +471,8 @@ impl GraphicalReportHandler { .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) .collect::>(); + // let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents); + // The max number of gutter-lines that will be active at any given // point. We need this to figure out indentation, so we do one loop // over the lines to see what the damage is gonna be. @@ -374,7 +480,7 @@ impl GraphicalReportHandler { for line in &lines { let mut num_highlights = 0; for hl in &labels { - if !line.span_line_only(hl) && line.span_applies(hl) { + if !line.span_line_only(hl) && line.span_applies_gutter(hl) { num_highlights += 1; } } @@ -400,23 +506,26 @@ impl GraphicalReportHandler { self.theme.characters.hbar, )?; - let (source_line, source_col) = labels - .iter() - .find_map(|hl| { - lines - .iter() - .find(|line| line.span_applies(hl)) - .map(|line| (line.line_number, self.visual_offset(line, hl.offset()) + 1)) - }) - .unwrap_or_else(|| (contents.line() + 1, contents.column() + 1)); + // If there is a primary label, then use its span + // as the reference point for line/column information. + let primary_contents = match primary_label { + Some(label) => source.read_span(label.inner(), 0, 0).map_err(|_| fmt::Error)?, + None => contents, + }; - if let Some(source_name) = contents.name() { + if let Some(source_name) = primary_contents.name() { let source_name = source_name.style(self.theme.styles.link); - writeln!(f, "[{}:{}:{}]", source_name, source_line, source_col)?; + writeln!( + f, + "[{}:{}:{}]", + source_name, + primary_contents.line() + 1, + primary_contents.column() + 1 + )?; } else if lines.len() <= 1 { writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?; } else { - writeln!(f, "[{}:{}]", source_line, source_col)?; + writeln!(f, "[{}:{}]", primary_contents.line() + 1, primary_contents.column() + 1)?; } // Now it's time for the fun part--actually rendering everything! @@ -430,7 +539,10 @@ impl GraphicalReportHandler { self.render_line_gutter(f, max_gutter, line, &labels)?; // And _now_ we can print out the line text itself! - self.render_line_text(f, &line.text)?; + // let styled_text = + // StyledList::from(highlighter_state.highlight_line(&line.text)).to_string(); + let styled_text = &line.text; + self.render_line_text(f, styled_text)?; // Next, we write all the highlights that apply to this particular line. let (single_line, multi_line): (Vec<_>, Vec<_>) = labels @@ -441,7 +553,13 @@ impl GraphicalReportHandler { // no line number! self.write_no_linum(f, linum_width)?; // gutter _again_ - self.render_highlight_gutter(f, max_gutter, line, &labels)?; + self.render_highlight_gutter( + f, + max_gutter, + line, + &labels, + LabelRenderMode::SingleLine, + )?; self.render_single_line_highlights( f, line, @@ -453,11 +571,7 @@ impl GraphicalReportHandler { } for hl in multi_line { if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { - // no line number! - self.write_no_linum(f, linum_width)?; - // gutter _again_ - self.render_highlight_gutter(f, max_gutter, line, &labels)?; - self.render_multi_line_end(f, hl)?; + self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?; } } } @@ -471,6 +585,85 @@ impl GraphicalReportHandler { Ok(()) } + fn render_multi_line_end( + &self, + f: &mut impl fmt::Write, + labels: &[FancySpan], + max_gutter: usize, + linum_width: usize, + line: &Line, + label: &FancySpan, + ) -> fmt::Result { + // no line number! + self.write_no_linum(f, linum_width)?; + + if let Some(label_parts) = label.label_parts() { + // if it has a label, how long is it? + let (first, rest) = label_parts + .split_first() + .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan"); + + if rest.is_empty() { + // gutter _again_ + self.render_highlight_gutter( + f, + max_gutter, + line, + labels, + LabelRenderMode::SingleLine, + )?; + + self.render_multi_line_end_single( + f, + first, + label.style, + LabelRenderMode::SingleLine, + )?; + } else { + // gutter _again_ + self.render_highlight_gutter( + f, + max_gutter, + line, + labels, + LabelRenderMode::MultiLineFirst, + )?; + + self.render_multi_line_end_single( + f, + first, + label.style, + LabelRenderMode::MultiLineFirst, + )?; + for label_line in rest { + // no line number! + self.write_no_linum(f, linum_width)?; + // gutter _again_ + self.render_highlight_gutter( + f, + max_gutter, + line, + labels, + LabelRenderMode::MultiLineRest, + )?; + self.render_multi_line_end_single( + f, + label_line, + label.style, + LabelRenderMode::MultiLineRest, + )?; + } + } + } else { + // gutter _again_ + self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?; + // has no label + writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?; + } + + Ok(()) + } + fn render_line_gutter( &self, f: &mut impl fmt::Write, @@ -483,7 +676,7 @@ impl GraphicalReportHandler { } let chars = &self.theme.characters; let mut gutter = String::new(); - let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); + let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); let mut arrow = false; for (i, hl) in applicable.enumerate() { if line.span_starts(hl) { @@ -539,33 +732,121 @@ impl GraphicalReportHandler { max_gutter: usize, line: &Line, highlights: &[FancySpan], + render_mode: LabelRenderMode, ) -> fmt::Result { if max_gutter == 0 { return Ok(()); } + + // keeps track of how many columns wide the gutter is + // important for ansi since simply measuring the size of the final string + // gives the wrong result when the string contains ansi codes. + let mut gutter_cols = 0; + let chars = &self.theme.characters; let mut gutter = String::new(); - let applicable = highlights.iter().filter(|hl| line.span_applies(hl)); + let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl)); for (i, hl) in applicable.enumerate() { if !line.span_line_only(hl) && line.span_ends(hl) { - gutter.push_str(&chars.lbot.style(hl.style).to_string()); - gutter.push_str( - &chars - .hbar - .to_string() - .repeat(max_gutter.saturating_sub(i) + 2) - .style(hl.style) - .to_string(), - ); + if render_mode == LabelRenderMode::MultiLineRest { + // this is to make multiline labels work. We want to make the right amount + // of horizontal space for them, but not actually draw the lines + let horizontal_space = max_gutter.saturating_sub(i) + 2; + for _ in 0..horizontal_space { + gutter.push(' '); + } + // account for one more horizontal space, since in multiline mode + // we also add in the vertical line before the label like this: + // 2 │ ╭─▶ text + // 3 │ ├─▶ here + // · ╰──┤ these two lines + // · │ are the problem + // ^this + gutter_cols += horizontal_space + 1; + } else { + let num_repeat = max_gutter.saturating_sub(i) + 2; + + gutter.push_str(&chars.lbot.style(hl.style).to_string()); + + gutter.push_str( + &chars + .hbar + .to_string() + .repeat( + num_repeat + // if we are rendering a multiline label, then leave a bit of space for the + // rcross character + - if render_mode == LabelRenderMode::MultiLineFirst { + 1 + } else { + 0 + }, + ) + .style(hl.style) + .to_string(), + ); + + // we count 1 for the lbot char, and then a few more, the same number + // as we just repeated for. For each repeat we only add 1, even though + // due to ansi escape codes the number of bytes in the string could grow + // a lot each time. + gutter_cols += num_repeat + 1; + } break; } else { gutter.push_str(&chars.vbar.style(hl.style).to_string()); + + // we may push many bytes for the ansi escape codes style adds, + // but we still only add a single character-width to the string in a terminal + gutter_cols += 1; } } - write!(f, "{:width$}", gutter, width = max_gutter + 1)?; + + // now calculate how many spaces to add based on how many columns we just created. + // it's the max width of the gutter, minus how many character-widths we just generated + // capped at 0 (though this should never go below in reality), and then we add 3 to + // account for arrowheads when a gutter line ends + let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols); + // we then write the gutter and as many spaces as we need + write!(f, "{}{:width$}", gutter, "", width = num_spaces)?; Ok(()) } + fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String { + if self.wrap_lines { + textwrap::fill(text, opts) + } else { + // Format without wrapping, but retain the indentation options + // Implementation based on `textwrap::indent` + let mut result = String::with_capacity(2 * text.len()); + let trimmed_indent = opts.subsequent_indent.trim_end(); + for (idx, line) in text.split_terminator('\n').enumerate() { + if idx > 0 { + result.push('\n'); + } + if idx == 0 { + if line.trim().is_empty() { + result.push_str(opts.initial_indent.trim_end()); + } else { + result.push_str(opts.initial_indent); + } + } else { + if line.trim().is_empty() { + result.push_str(trimmed_indent); + } else { + result.push_str(opts.subsequent_indent); + } + } + result.push_str(line); + } + if text.ends_with('\n') { + // split_terminator will have eaten the final '\n'. + result.push('\n'); + } + result + } + } + fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result { write!( f, @@ -585,13 +866,26 @@ impl GraphicalReportHandler { /// 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 mut escaped = false; let tab_width = self.tab_width; text.chars().map(move |c| { - let width = if c == '\t' { + let width = match (escaped, c) { // Round up to the next multiple of tab_width - tab_width - column % tab_width - } else { - c.width().unwrap_or(0) + (false, '\t') => tab_width - column % tab_width, + // start of ANSI escape + (false, '\x1b') => { + escaped = true; + 0 + } + // use Unicode width for all other characters + (false, c) => c.width().unwrap_or(0), + // end of ANSI escape + (true, 'm') => { + escaped = false; + 0 + } + // characters are zero width within escape sequence + (true, _) => 0, }; column += width; width @@ -599,11 +893,22 @@ impl GraphicalReportHandler { } /// Returns the visual column position of a byte offset on a specific line. - fn visual_offset(&self, line: &Line, offset: usize) -> usize { + /// + /// If the offset occurs in the middle of a character, the returned column + /// corresponds to that character's first column in `start` is true, or its + /// last column if `start` is false. + fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize { let line_range = line.offset..=(line.offset + line.length); assert!(line_range.contains(&offset)); - let text_index = offset - line.offset; + let mut text_index = offset - line.offset; + while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) { + if start { + text_index -= 1; + } else { + text_index += 1; + } + } let text = &line.text[..text_index.min(line.text.len())]; let text_width = self.line_visual_char_width(text).sum(); if text_index > line.text.len() { @@ -626,10 +931,10 @@ impl GraphicalReportHandler { for (c, width) in text.chars().zip(self.line_visual_char_width(text)) { if c == '\t' { for _ in 0..width { - f.write_char(' ')? + f.write_char(' ')?; } } else { - f.write_char(c)? + f.write_char(c)?; } } f.write_char('\n')?; @@ -654,32 +959,34 @@ impl GraphicalReportHandler { .map(|hl| { let byte_start = hl.offset(); let byte_end = hl.offset() + hl.len(); - let start = self.visual_offset(line, byte_start).max(highest); - let end = self.visual_offset(line, byte_end).max(start + 1); + let start = self.visual_offset(line, byte_start, true).max(highest); + let end = if hl.len() == 0 { + start + 1 + } else { + self.visual_offset(line, byte_end, false).max(start + 1) + }; let vbar_offset = (start + end) / 2; let num_left = vbar_offset - start; let num_right = end - vbar_offset - 1; - 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 = start.saturating_sub(highest), - ) - .style(hl.style) - .to_string(), - ); - } + 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) @@ -688,27 +995,40 @@ impl GraphicalReportHandler { writeln!(f, "{}", underlines)?; for hl in single_liners.iter().rev() { - if let Some(label) = hl.label() { - self.write_no_linum(f, linum_width)?; - self.render_highlight_gutter(f, max_gutter, line, all_highlights)?; - let mut curr_offset = 1usize; - for (offset_hl, vbar_offset) in &vbar_offsets { - while curr_offset < *vbar_offset + 1 { - write!(f, " ")?; - curr_offset += 1; - } - if *offset_hl != hl { - write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; - curr_offset += 1; - } else { - let lines = format!( - "{}{} {}", - chars.lbot, - chars.hbar.to_string().repeat(2), - label, - ); - writeln!(f, "{}", lines.style(hl.style))?; - break; + if let Some(label) = hl.label_parts() { + if label.len() == 1 { + self.write_label_text( + f, + line, + linum_width, + max_gutter, + all_highlights, + chars, + &vbar_offsets, + hl, + &label[0], + LabelRenderMode::SingleLine, + )?; + } else { + let mut first = true; + for label_line in &label { + self.write_label_text( + f, + line, + linum_width, + max_gutter, + all_highlights, + chars, + &vbar_offsets, + hl, + label_line, + if first { + LabelRenderMode::MultiLineFirst + } else { + LabelRenderMode::MultiLineRest + }, + )?; + first = false; } } } @@ -716,13 +1036,77 @@ impl GraphicalReportHandler { Ok(()) } - fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result { - writeln!( + // I know it's not good practice, but making this a function makes a lot of sense + // and making a struct for this does not... + #[allow(clippy::too_many_arguments)] + fn write_label_text( + &self, + f: &mut impl fmt::Write, + line: &Line, + linum_width: usize, + max_gutter: usize, + all_highlights: &[FancySpan], + chars: &ThemeCharacters, + vbar_offsets: &[(&&FancySpan, usize)], + hl: &&FancySpan, + label: &str, + render_mode: LabelRenderMode, + ) -> fmt::Result { + self.write_no_linum(f, linum_width)?; + self.render_highlight_gutter( f, - "{} {}", - self.theme.characters.hbar.style(hl.style), - hl.label().unwrap_or_else(|| "".into()), + max_gutter, + line, + all_highlights, + LabelRenderMode::SingleLine, )?; + let mut curr_offset = 1usize; + for (offset_hl, vbar_offset) in vbar_offsets { + while curr_offset < *vbar_offset + 1 { + write!(f, " ")?; + curr_offset += 1; + } + if *offset_hl != hl { + write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?; + curr_offset += 1; + } else { + let lines = match render_mode { + LabelRenderMode::SingleLine => { + format!("{}{} {}", chars.lbot, chars.hbar.to_string().repeat(2), label,) + } + LabelRenderMode::MultiLineFirst => { + format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,) + } + LabelRenderMode::MultiLineRest => { + format!(" {} {}", chars.vbar, label,) + } + }; + writeln!(f, "{}", lines.style(hl.style))?; + break; + } + } + Ok(()) + } + + fn render_multi_line_end_single( + &self, + f: &mut impl fmt::Write, + label: &str, + style: Style, + render_mode: LabelRenderMode, + ) -> fmt::Result { + match render_mode { + LabelRenderMode::SingleLine => { + writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?; + } + LabelRenderMode::MultiLineFirst => { + writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?; + } + LabelRenderMode::MultiLineRest => { + writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?; + } + } + Ok(()) } @@ -801,6 +1185,16 @@ impl ReportHandler for GraphicalReportHandler { Support types */ +#[derive(PartialEq, Debug)] +enum LabelRenderMode { + /// we're rendering a single line label (or not rendering in any special way) + SingleLine, + /// we're rendering a multiline label + MultiLineFirst, + /// we're rendering the rest of a multiline label + MultiLineRest, +} + #[derive(Debug)] struct Line { line_number: usize, @@ -814,14 +1208,31 @@ impl Line { span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length } + /// Returns whether `span` should be visible on this line, either in the gutter or under the + /// text on this line fn span_applies(&self, span: &FancySpan) -> bool { let spanlen = if span.len() == 0 { 1 } else { span.len() }; // Span starts in this line + (span.offset() >= self.offset && span.offset() < self.offset + self.length) - // Span passes through this line - || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo - // Span ends on this line - || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) + // Span passes through this line + || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo + // Span ends on this line + || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length) + } + + /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans + /// that are only visible on this line and do not span multiple lines) + fn span_applies_gutter(&self, span: &FancySpan) -> bool { + let spanlen = if span.len() == 0 { 1 } else { span.len() }; + // Span starts in this line + self.span_applies(span) + && !( + // as long as it doesn't start *and* end on this line + (span.offset() >= self.offset && span.offset() < self.offset + self.length) + && (span.offset() + spanlen > self.offset + && span.offset() + spanlen <= self.offset + self.length) + ) } // A 'flyby' is a multi-line span that technically covers this line, but @@ -851,7 +1262,10 @@ impl Line { #[derive(Debug, Clone)] struct FancySpan { - label: Option, + /// this is deliberately an option of a vec because I wanted to be very explicit + /// that there can also be *no* label. If there is a label, it can have multiple + /// lines which is what the vec is for. + label: Option>, span: SourceSpan, style: Style, } @@ -862,9 +1276,13 @@ impl PartialEq for FancySpan { } } +fn split_label(v: String) -> Vec { + v.split('\n').map(|i| i.to_string()).collect() +} + impl FancySpan { fn new(label: Option, span: SourceSpan, style: Style) -> Self { - FancySpan { label, span, style } + FancySpan { label: label.map(split_label), span, style } } fn style(&self) -> Style { @@ -872,7 +1290,11 @@ impl FancySpan { } fn label(&self) -> Option { - self.label.as_ref().map(|l| l.style(self.style()).to_string()) + self.label.as_ref().map(|l| l.join("\n").style(self.style()).to_string()) + } + + fn label_parts(&self) -> Option> { + self.label.as_ref().map(|l| l.iter().map(|i| i.style(self.style()).to_string()).collect()) } fn offset(&self) -> usize { diff --git a/crates/oxc_diagnostics/src/graphical_theme.rs b/crates/oxc_diagnostics/src/graphical_theme.rs index 210b60649..49a4ee122 100644 --- a/crates/oxc_diagnostics/src/graphical_theme.rs +++ b/crates/oxc_diagnostics/src/graphical_theme.rs @@ -3,16 +3,20 @@ #![allow(clippy::nursery)] #![allow(dead_code)] -/// origin file: https://github.com/zkat/miette/blob/78fe18e6990feacc8bdaeeb10e1439a12c111e6e/src/handlers/theme.rs -use is_terminal::IsTerminal; -use owo_colors::{style, Style}; +/// origin file: https://github.com/zkat/miette/blob/75fea0935e495d0215518c80d32dd820910982e3/src/handlers/theme.rs +use std::io::IsTerminal; + +use miette::ThemeCharacters; +use owo_colors::Style; /** Theme used by [`GraphicalReportHandler`](crate::GraphicalReportHandler) to -render fancy [`Diagnostic`](miette::Diagnostic) reports. +render fancy [`Diagnostic`](crate::Diagnostic) reports. + A theme consists of two things: the set of characters to be used for drawing, and the [`owo_colors::Style`](https://docs.rs/owo-colors/latest/owo_colors/struct.Style.html)s to be used to paint various items. + You can create your own custom graphical theme using this type, or you can use one of the predefined ones using the methods below. */ @@ -32,6 +36,12 @@ impl GraphicalTheme { /// Graphical theme that draws using both ansi colors and unicode /// characters. + /// + /// Note that full rgb colors aren't enabled by default because they're + /// an accessibility hazard, especially in the context of terminal themes + /// that can change the background color and make hardcoded colors illegible. + /// Such themes typically remap ansi codes properly, treating them more + /// like CSS classes than specific colors. pub fn unicode() -> Self { Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::rgb() } } @@ -44,9 +54,7 @@ impl GraphicalTheme { /// A "basic" graphical theme that skips colors and unicode characters and /// just does monochrome ascii art. If you want a completely non-graphical - /// rendering of your `Diagnostic`s, check out - /// [miette::NarratableReportHandler], or write your own - /// [miette::ReportHandler]! + /// rendering of your [`Diagnostic`](crate::Diagnostic)s pub fn none() -> Self { Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() } } @@ -65,7 +73,8 @@ impl Default for GraphicalTheme { } /** -Styles for various parts of graphical rendering for the [crate::GraphicalReportHandler]. +Styles for various parts of graphical rendering for the +[`GraphicalReportHandler`](crate::GraphicalReportHandler). */ #[derive(Debug, Clone)] pub struct ThemeStyles { @@ -86,6 +95,10 @@ pub struct ThemeStyles { pub highlights: Vec