diff --git a/.github/renovate.json b/.github/renovate.json index f05509dc92545..ec0fd0dca1663 100644 --- a/.github/renovate.json +++ b/.github/renovate.json @@ -8,7 +8,7 @@ { "groupName": "rust crates", "matchManagers": ["cargo"], - "ignoreDeps": ["syn", "ureq", "unicode-width"] + "ignoreDeps": ["syn", "ureq"] }, { "groupName": "vscode npm packages", diff --git a/Cargo.lock b/Cargo.lock index 55e04838dec71..4ddc8e8e90835 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1150,31 +1150,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "miette" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" -dependencies = [ - "cfg-if", - "miette-derive", - "owo-colors", - "textwrap", - "thiserror", - "unicode-width 0.1.14", -] - -[[package]] -name = "miette-derive" -version = "7.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "mimalloc" version = "0.1.43" @@ -1429,6 +1404,31 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "oxc-miette" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1ef4c8027e4a7f932b83ca96b7c65274b887c9a3161c25ca0e5ae2c555c9a7" +dependencies = [ + "cfg-if", + "owo-colors", + "oxc-miette-derive", + "textwrap", + "thiserror", + "unicode-width 0.2.0", +] + +[[package]] +name = "oxc-miette-derive" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d83d93ebb57b593f858fb85ccb7053a2b25c24320e18221c308461ae9989b47b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "oxc_allocator" version = "0.34.0" @@ -1576,11 +1576,8 @@ dependencies = [ name = "oxc_diagnostics" version = "0.34.0" dependencies = [ - "miette", - "owo-colors", + "oxc-miette", "rustc-hash", - "textwrap", - "unicode-width 0.2.0", ] [[package]] @@ -1929,7 +1926,7 @@ name = "oxc_span" version = "0.34.0" dependencies = [ "compact_str", - "miette", + "oxc-miette", "oxc_allocator", "oxc_ast_macros", "oxc_estree", @@ -2076,8 +2073,8 @@ dependencies = [ "glob", "ignore", "jemallocator", - "miette", "mimalloc", + "oxc-miette", "oxc_diagnostics", "oxc_linter", "oxc_span", @@ -2331,9 +2328,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.88" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -2925,9 +2922,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.82" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83540f837a8afc019423a8edb95b52a8effe46957ee402287f4292fae35be021" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -2971,18 +2968,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.64" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index a8d8413ab2304..f4564b1c26145 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,14 +160,13 @@ log = "0.4.22" markdown = "1.0.0-alpha.21" memchr = "2.7.4" memoffset = "0.9.1" -miette = { version = "7.2.0", features = ["fancy-no-syscall"] } +miette = { package = "oxc-miette", version = "1.0.1", features = ["fancy-no-syscall"] } mimalloc = "0.1.43" mime_guess = "2.0.5" nonmax = "0.5.5" num-bigint = "0.4.6" num-traits = "0.2.19" once_cell = "1.20.2" -owo-colors = "4.1.0" oxc-browserslist = "1.0.3" oxc_resolver = "2.0.0" petgraph = "0.6.5" @@ -192,12 +191,10 @@ sha1 = "0.10.6" simdutf8 = { version = "0.1.5", features = ["aarch64_neon"] } similar = "2.6.0" tempfile = "3.13.0" -textwrap = "0.16.1" tokio = "1.40.0" tower-lsp = "0.20.0" tracing-subscriber = "0.3.18" tsify = "0.4.5" -unicode-width = "0.2.0" ureq = { version = "2.10.1", default-features = false } url = "2.5.2" walkdir = "2.5.0" diff --git a/crates/oxc_diagnostics/Cargo.toml b/crates/oxc_diagnostics/Cargo.toml index e0c136df0ee91..cdd6d75d094ff 100644 --- a/crates/oxc_diagnostics/Cargo.toml +++ b/crates/oxc_diagnostics/Cargo.toml @@ -20,8 +20,4 @@ doctest = false [dependencies] miette = { workspace = true } - -owo-colors = { workspace = true } rustc-hash = { workspace = true } -textwrap = { workspace = true } -unicode-width = { workspace = true } diff --git a/crates/oxc_diagnostics/src/graphic_reporter.rs b/crates/oxc_diagnostics/src/graphic_reporter.rs deleted file mode 100644 index 0a9bade367cd7..0000000000000 --- a/crates/oxc_diagnostics/src/graphic_reporter.rs +++ /dev/null @@ -1,1347 +0,0 @@ -#![allow(clippy::restriction)] -#![allow(clippy::style)] -#![allow(clippy::pedantic)] -#![allow(clippy::nursery)] -#![allow(dead_code)] - -/// origin file: https://github.com/zkat/miette/blob/75fea0935e495d0215518c80d32dd820910982e3/src/handlers/graphical.rs#L1 -use std::fmt::{self, Write}; -use std::io::IsTerminal; - -use miette::{ - Diagnostic, LabeledSpan, ReportHandler, Severity, SourceCode, SourceSpan, SpanContents, - ThemeCharacters, -}; -use owo_colors::{OwoColorize, Style}; -use unicode_width::UnicodeWidthChar; - -use crate::graphical_theme::GraphicalTheme; - -#[derive(Debug, Clone)] -pub struct GraphicalReportHandler { - /// How to render links. - /// - /// Default: [`LinkStyle::Link`] - pub(crate) links: LinkStyle, - /// Terminal width to wrap at. - /// - /// Default: `400` - pub(crate) termwidth: usize, - /// How to style reports - pub(crate) theme: GraphicalTheme, - pub(crate) footer: Option, - /// Number of source lines to render before/after the line(s) covered by errors. - /// - /// Default: `1` - pub(crate) context_lines: usize, - /// Tab print width - /// - /// Default: `4` - pub(crate) tab_width: usize, - /// Unused. - pub(crate) with_cause_chain: bool, - /// Whether to wrap lines to fit the width. - /// - /// Default: `true` - pub(crate) wrap_lines: bool, - /// Whether to break words during wrapping. - /// - /// When `false`, line breaks will happen before the first word that would overflow `termwidth`. - /// - /// Default: `true` - 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)] -pub(crate) enum LinkStyle { - None, - Link, - Text, -} - -impl GraphicalReportHandler { - /// Create a new `GraphicalReportHandler` with the default - /// [`GraphicalTheme`]. This will use both unicode characters and colors. - pub fn new() -> Self { - let is_terminal = std::io::stdout().is_terminal() && std::io::stderr().is_terminal(); - Self { - links: if is_terminal { LinkStyle::Link } else { LinkStyle::Text }, - termwidth: 400, - theme: GraphicalTheme::new(is_terminal), - 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, - } - } - - /// Set the displayed tab width in spaces. - pub fn tab_width(mut self, width: usize) -> Self { - self.tab_width = width; - self - } - - /// Whether to enable error code linkification using [`Diagnostic::url()`]. - pub fn with_links(mut self, links: bool) -> Self { - self.links = if links { LinkStyle::Link } else { LinkStyle::Text }; - self - } - - /// Include the cause chain of the top-level error in the graphical output, - /// if available. - pub fn with_cause_chain(mut self) -> Self { - self.with_cause_chain = true; - self - } - - /// Do not include the cause chain of the top-level error in the graphical - /// output. - pub fn without_cause_chain(mut self) -> Self { - self.with_cause_chain = false; - self - } - - /// Whether to include [`Diagnostic::url()`] in the output. - /// - /// Disabling this is not recommended, but can be useful for more easily - /// reproducible tests, as `url(docsrs)` links are version-dependent. - pub fn with_urls(mut self, urls: bool) -> Self { - self.links = match (self.links, urls) { - (_, false) => LinkStyle::None, - (LinkStyle::None, true) => LinkStyle::Link, - (links, true) => links, - }; - self - } - - /// Set a theme for this handler. - pub fn with_theme(mut self, theme: GraphicalTheme) -> Self { - self.theme = theme; - self - } - - /// Sets the width to wrap the report at. - pub fn with_width(mut self, width: usize) -> Self { - self.termwidth = width; - 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); - self - } - - /// Sets the number of lines of context to show around each error. - pub fn with_context_lines(mut self, lines: usize) -> Self { - 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 { - fn default() -> Self { - Self::new() - } -} - -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( - &self, - f: &mut impl fmt::Write, - diagnostic: &(dyn Diagnostic), - ) -> fmt::Result { - // self.render_header(f, diagnostic)?; - writeln!(f)?; - self.render_causes(f, diagnostic)?; - let src = diagnostic.source_code(); - self.render_snippets(f, diagnostic, src)?; - self.render_footer(f, diagnostic)?; - self.render_related(f, diagnostic, src)?; - if let Some(footer) = &self.footer { - writeln!(f)?; - let width = self.termwidth.saturating_sub(4); - 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(()) - } - - fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { - let severity_style = match diagnostic.severity() { - Some(Severity::Error) | None => self.theme.styles.error, - Some(Severity::Warning) => self.theme.styles.warning, - Some(Severity::Advice) => self.theme.styles.advice, - }; - let mut header = String::new(); - if self.links == LinkStyle::Link && diagnostic.url().is_some() { - let url = diagnostic.url().unwrap(); // safe - let code = if let Some(code) = diagnostic.code() { - format!("{} ", code) - } 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), - 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() { - let url = diagnostic.url().unwrap(); // safe - write!(header, " ({})", url.style(self.theme.styles.link))?; - } - writeln!(f, "{}", header)?; - writeln!(f)?; - } - Ok(()) - } - - fn render_causes(&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 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 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); - } - - let title = match (self.links, diagnostic.url(), diagnostic.code()) { - (LinkStyle::Link, Some(url), Some(code)) => { - // magic unicode escape sequences to make the terminal print a hyperlink - const CTL: &str = "\u{1b}]8;;"; - const END: &str = "\u{1b}]8;;\u{1b}\\"; - let code = code.style(severity_style); - let message = diagnostic.to_string(); - let title = message.style(severity_style); - format!("{CTL}{url}\u{1b}\\{code}{END}: {title}",) - } - (_, _, Some(code)) => { - let title = format!("{code}: {}", diagnostic); - format!("{}", title.style(severity_style)) - } - _ => { - format!("{}", diagnostic.to_string().style(severity_style)) - } - }; - let title = textwrap::fill(&title, opts); - writeln!(f, "{}", title)?; - - // if !self.with_cause_chain { - // 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()) - // { - // 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(()) - } - - fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result { - 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 mut opts = textwrap::Options::new(width) - .initial_indent(&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(&help.to_string(), opts))?; - } - Ok(()) - } - - fn render_related( - &self, - f: &mut impl fmt::Write, - diagnostic: &(dyn Diagnostic), - 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() { - Some(Severity::Error) | None => write!(f, "Error: ")?, - Some(Severity::Warning) => write!(f, "Warning: ")?, - Some(Severity::Advice) => write!(f, "Advice: ")?, - }; - inner_renderer.render_header(f, rel)?; - inner_renderer.render_causes(f, rel)?; - let src = rel.source_code().or(parent_src); - inner_renderer.render_snippets(f, rel, src)?; - inner_renderer.render_footer(f, rel)?; - inner_renderer.render_related(f, rel, src)?; - } - } - Ok(()) - } - - fn render_snippets( - &self, - f: &mut impl fmt::Write, - diagnostic: &(dyn Diagnostic), - opt_source: Option<&dyn SourceCode>, - ) -> fmt::Result { - 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(()) - } - - fn render_context( - &self, - f: &mut impl fmt::Write, - source: &dyn SourceCode, - context: &LabeledSpan, - labels: &[LabeledSpan], - ) -> 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() - .zip(self.theme.styles.highlights.iter().cloned().cycle()) - .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. - let mut max_gutter = 0usize; - for line in &lines { - let mut num_highlights = 0; - for hl in &labels { - if !line.span_line_only(hl) && line.span_applies_gutter(hl) { - num_highlights += 1; - } - } - max_gutter = std::cmp::max(max_gutter, num_highlights); - } - - // Oh and one more thing: We need to figure out how much room our line - // numbers need! - let linum_width = lines[..] - .last() - .map(|line| line.line_number) - // It's possible for the source to be an empty string. - .unwrap_or(0) - .to_string() - .len(); - - // Header - write!( - f, - "{}{}{}", - " ".repeat(linum_width + 2), - self.theme.characters.ltop, - self.theme.characters.hbar, - )?; - - // 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) = primary_contents.name() { - let source_name = source_name.style(self.theme.styles.link); - 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, "[{}:{}]", primary_contents.line() + 1, primary_contents.column() + 1)?; - } - - // Now it's time for the fun part--actually rendering everything! - for line in &lines { - // Line number, appropriately padded. - self.write_linum(f, linum_width, line.line_number)?; - - // Then, we need to print the gutter, along with any fly-bys We - // have separate gutters depending on whether we're on the actual - // line, or on one of the "highlight lines" below it. - self.render_line_gutter(f, max_gutter, line, &labels)?; - - // And _now_ we can print out the line text itself! - // 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 - .iter() - .filter(|hl| line.span_applies(hl)) - .partition(|hl| line.span_line_only(hl)); - if !single_line.is_empty() { - // no line number! - self.write_no_linum(f, linum_width)?; - // gutter _again_ - self.render_highlight_gutter( - f, - max_gutter, - line, - &labels, - LabelRenderMode::SingleLine, - )?; - self.render_single_line_highlights( - f, - line, - linum_width, - max_gutter, - &single_line, - &labels, - )?; - } - for hl in multi_line { - if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) { - self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?; - } - } - } - writeln!( - f, - "{}{}{}", - " ".repeat(linum_width + 2), - self.theme.characters.lbot, - self.theme.characters.hbar.to_string().repeat(4), - )?; - 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::BlockFirst, - )?; - - self.render_multi_line_end_single( - f, - first, - label.style, - LabelRenderMode::BlockFirst, - )?; - 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::BlockRest, - )?; - self.render_multi_line_end_single( - f, - label_line, - label.style, - LabelRenderMode::BlockRest, - )?; - } - } - } 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, - max_gutter: usize, - line: &Line, - highlights: &[FancySpan], - ) -> fmt::Result { - if max_gutter == 0 { - return Ok(()); - } - let chars = &self.theme.characters; - let mut gutter = String::new(); - 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) { - gutter.push_str(&chars.ltop.style(hl.style).to_string()); - gutter.push_str( - &chars - .hbar - .to_string() - .repeat(max_gutter.saturating_sub(i)) - .style(hl.style) - .to_string(), - ); - gutter.push_str(&chars.rarrow.style(hl.style).to_string()); - arrow = true; - break; - } else if line.span_ends(hl) { - if hl.label().is_some() { - gutter.push_str(&chars.lcross.style(hl.style).to_string()); - } else { - gutter.push_str(&chars.lbot.style(hl.style).to_string()); - } - gutter.push_str( - &chars - .hbar - .to_string() - .repeat(max_gutter.saturating_sub(i)) - .style(hl.style) - .to_string(), - ); - gutter.push_str(&chars.rarrow.style(hl.style).to_string()); - arrow = true; - break; - } else if line.span_flyby(hl) { - gutter.push_str(&chars.vbar.style(hl.style).to_string()); - } else { - gutter.push(' '); - } - } - write!( - f, - "{}{}", - gutter, - " ".repeat( - if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count()) - ) - )?; - Ok(()) - } - - fn render_highlight_gutter( - &self, - f: &mut impl fmt::Write, - 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_gutter(hl)); - for (i, hl) in applicable.enumerate() { - if !line.span_line_only(hl) && line.span_ends(hl) { - if render_mode == LabelRenderMode::BlockRest { - // 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::BlockFirst { - 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; - } - } - - // 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, - " {:width$} {} ", - linum.style(self.theme.styles.linum), - self.theme.characters.vbar, - width = width - )?; - Ok(()) - } - - fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result { - write!(f, " {:width$} {} ", "", self.theme.characters.vbar_break, width = width)?; - 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 mut escaped = false; - let tab_width = self.tab_width; - text.chars().map(move |c| { - let width = match (escaped, c) { - // Round up to the next multiple of tab_width - (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 - }) - } - - /// Returns the visual column position of a byte offset on a specific line. - /// - /// 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 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() { - // Spans extending past the end of the line are always rendered as - // one column past the end of the visible line. - // - // This doesn't necessarily correspond to a specific byte-offset, - // since a span extending past the end of the line could contain: - // - an actual \n character (1 byte) - // - a CRLF (2 bytes) - // - EOF (0 bytes) - text_width + 1 - } else { - text_width - } - } - - /// 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( - &self, - f: &mut impl fmt::Write, - line: &Line, - linum_width: usize, - max_gutter: usize, - single_liners: &[&FancySpan], - all_highlights: &[FancySpan], - ) -> fmt::Result { - let mut underlines = String::new(); - let mut highest = 0; - - let chars = &self.theme.characters; - let vbar_offsets: Vec<_> = single_liners - .iter() - .map(|hl| { - let byte_start = hl.offset(); - let byte_end = hl.offset() + hl.len(); - 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; - 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(); - writeln!(f, "{}", underlines)?; - - for hl in single_liners.iter().rev() { - 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::BlockFirst - } else { - LabelRenderMode::BlockRest - }, - )?; - first = false; - } - } - } - } - Ok(()) - } - - // 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, - 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::BlockFirst => { - format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,) - } - LabelRenderMode::BlockRest => { - 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::BlockFirst => { - writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?; - } - LabelRenderMode::BlockRest => { - writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?; - } - } - - Ok(()) - } - - fn get_lines<'a>( - &'a self, - source: &'a dyn SourceCode, - context_span: &'a SourceSpan, - ) -> Result<(Box + 'a>, Vec), fmt::Error> { - let context_data = source - .read_span(context_span, self.context_lines, self.context_lines) - .map_err(|_| fmt::Error)?; - let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected"); - let mut line = context_data.line(); - let mut column = context_data.column(); - let mut offset = context_data.span().offset(); - let mut line_offset = offset; - let mut line_str = String::with_capacity(context.len()); - let mut lines = Vec::with_capacity(1); - let mut iter = context.chars().peekable(); - while let Some(char) = iter.next() { - offset += char.len_utf8(); - let mut at_end_of_file = false; - match char { - '\r' => { - if iter.next_if_eq(&'\n').is_some() { - offset += 1; - line += 1; - column = 0; - } else { - line_str.push(char); - column += 1; - } - at_end_of_file = iter.peek().is_none(); - } - '\n' => { - at_end_of_file = iter.peek().is_none(); - line += 1; - column = 0; - } - _ => { - line_str.push(char); - column += 1; - } - } - - if iter.peek().is_none() && !at_end_of_file { - line += 1; - } - - if column == 0 || iter.peek().is_none() { - lines.push(Line { - line_number: line, - offset: line_offset, - length: offset - line_offset, - text: line_str.clone(), - }); - line_str.clear(); - line_offset = offset; - } - } - Ok((context_data, lines)) - } -} - -impl ReportHandler for GraphicalReportHandler { - fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result { - if f.alternate() { - return fmt::Debug::fmt(diagnostic, f); - } - - self.render_report(f, diagnostic) - } -} - -/* -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 - BlockFirst, - /// we're rendering the rest of a multiline label - BlockRest, -} - -#[derive(Debug)] -struct Line { - line_number: usize, - offset: usize, - length: usize, - text: String, -} - -impl Line { - fn span_line_only(&self, span: &FancySpan) -> bool { - 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) - } - - /// 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 - // does not begin or end within the line itself. This method is used to - // calculate gutters. - fn span_flyby(&self, span: &FancySpan) -> bool { - // The span itself starts before this line's starting offset (so, in a - // prev line). - span.offset() < self.offset - // ...and it stops after this line's end. - && span.offset() + span.len() > self.offset + self.length - } - - // Does this line contain the *beginning* of this multiline span? - // This assumes self.span_applies() is true already. - fn span_starts(&self, span: &FancySpan) -> bool { - span.offset() >= self.offset - } - - // Does this line contain the *end* of this multiline span? - // This assumes self.span_applies() is true already. - fn span_ends(&self, span: &FancySpan) -> bool { - span.offset() + span.len() >= self.offset - && span.offset() + span.len() <= self.offset + self.length - } -} - -#[derive(Debug, Clone)] -struct FancySpan { - /// 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, -} - -impl PartialEq for FancySpan { - fn eq(&self, other: &Self) -> bool { - self.label == other.label && self.span == other.span - } -} - -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: label.map(split_label), span, style } - } - - fn style(&self) -> Style { - self.style - } - - fn label(&self) -> Option { - 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 { - self.span.offset() - } - - fn len(&self) -> usize { - self.span.len() - } -} diff --git a/crates/oxc_diagnostics/src/graphical_theme.rs b/crates/oxc_diagnostics/src/graphical_theme.rs deleted file mode 100644 index e74bdfcc65e80..0000000000000 --- a/crates/oxc_diagnostics/src/graphical_theme.rs +++ /dev/null @@ -1,147 +0,0 @@ -#![allow(clippy::must_use_candidate)] -#![allow(clippy::pedantic)] -#![allow(clippy::nursery)] -#![allow(dead_code)] - -/// origin file: https://github.com/zkat/miette/blob/75fea0935e495d0215518c80d32dd820910982e3/src/handlers/theme.rs -use miette::ThemeCharacters; -use owo_colors::Style; - -/** -Theme used by [`GraphicalReportHandler`](crate::GraphicalReportHandler) to -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. - -When created by [`Default::default`], themes are automatically selected based on the `NO_COLOR` -environment variable and whether the process is running in a terminal. -*/ -#[derive(Debug, Clone)] -pub struct GraphicalTheme { - /// Characters to be used for drawing. - pub characters: ThemeCharacters, - /// Styles to be used for painting. - pub styles: ThemeStyles, -} - -impl GraphicalTheme { - pub fn new(is_terminal: bool) -> Self { - match std::env::var("NO_COLOR") { - _ if !is_terminal => Self::none(), - Ok(string) if string != "0" => Self::unicode_nocolor(), - _ => Self::unicode(), - } - } - - /// ASCII-art-based graphical drawing, with ANSI styling. - pub fn ascii() -> Self { - Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::ansi() } - } - - /// 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() } - } - - /// Graphical theme that draws in monochrome, while still using unicode - /// characters. - pub fn unicode_nocolor() -> Self { - Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::none() } - } - - /// 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`](crate::Diagnostic)s - pub fn none() -> Self { - Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() } - } -} - -/** -Styles for various parts of graphical rendering for the -[`GraphicalReportHandler`](crate::GraphicalReportHandler). -*/ -#[derive(Debug, Clone)] -pub struct ThemeStyles { - /// Style to apply to things highlighted as "error". - pub error: Style, - /// Style to apply to things highlighted as "warning". - pub warning: Style, - /// Style to apply to things highlighted as "advice". - pub advice: Style, - /// Style to apply to the help text. - pub help: Style, - /// Style to apply to filenames/links/URLs. - pub link: Style, - /// Style to apply to line numbers. - pub linum: Style, - /// Styles to cycle through (using `.iter().cycle()`), to render the lines - /// and text for diagnostic highlights. - pub highlights: Vec