miette/handlers/
graphical.rs

1use std::fmt::{self, Write};
2
3use owo_colors::{OwoColorize, Style, StyledList};
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
7use crate::handlers::theme::*;
8use crate::highlighters::{Highlighter, MietteHighlighter};
9use crate::protocol::{Diagnostic, Severity};
10use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
11
12/**
13A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
14quasi-graphical way, using terminal colors, unicode drawing characters, and
15other such things.
16
17This is the default reporter bundled with `miette`.
18
19This printer can be customized by using [`new_themed()`](GraphicalReportHandler::new_themed) and handing it a
20[`GraphicalTheme`] of your own creation (or using one of its own defaults!)
21
22See [`set_hook()`](crate::set_hook) for more details on customizing your global
23printer.
24*/
25#[derive(Debug, Clone)]
26pub struct GraphicalReportHandler {
27    pub(crate) links: LinkStyle,
28    pub(crate) termwidth: usize,
29    pub(crate) theme: GraphicalTheme,
30    pub(crate) footer: Option<String>,
31    pub(crate) context_lines: usize,
32    pub(crate) tab_width: usize,
33    pub(crate) with_cause_chain: bool,
34    pub(crate) wrap_lines: bool,
35    pub(crate) break_words: bool,
36    pub(crate) word_separator: Option<textwrap::WordSeparator>,
37    pub(crate) word_splitter: Option<textwrap::WordSplitter>,
38    pub(crate) highlighter: MietteHighlighter,
39    pub(crate) link_display_text: Option<String>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub(crate) enum LinkStyle {
44    None,
45    Link,
46    Text,
47}
48
49impl GraphicalReportHandler {
50    /// Create a new `GraphicalReportHandler` with the default
51    /// [`GraphicalTheme`]. This will use both unicode characters and colors.
52    pub fn new() -> Self {
53        Self {
54            links: LinkStyle::Link,
55            termwidth: 200,
56            theme: GraphicalTheme::default(),
57            footer: None,
58            context_lines: 1,
59            tab_width: 4,
60            with_cause_chain: true,
61            wrap_lines: true,
62            break_words: true,
63            word_separator: None,
64            word_splitter: None,
65            highlighter: MietteHighlighter::default(),
66            link_display_text: None,
67        }
68    }
69
70    ///Create a new `GraphicalReportHandler` with a given [`GraphicalTheme`].
71    pub fn new_themed(theme: GraphicalTheme) -> Self {
72        Self {
73            links: LinkStyle::Link,
74            termwidth: 200,
75            theme,
76            footer: None,
77            context_lines: 1,
78            tab_width: 4,
79            wrap_lines: true,
80            with_cause_chain: true,
81            break_words: true,
82            word_separator: None,
83            word_splitter: None,
84            highlighter: MietteHighlighter::default(),
85            link_display_text: None,
86        }
87    }
88
89    /// Set the displayed tab width in spaces.
90    pub fn tab_width(mut self, width: usize) -> Self {
91        self.tab_width = width;
92        self
93    }
94
95    /// Whether to enable error code linkification using [`Diagnostic::url()`].
96    pub fn with_links(mut self, links: bool) -> Self {
97        self.links = if links {
98            LinkStyle::Link
99        } else {
100            LinkStyle::Text
101        };
102        self
103    }
104
105    /// Include the cause chain of the top-level error in the graphical output,
106    /// if available.
107    pub fn with_cause_chain(mut self) -> Self {
108        self.with_cause_chain = true;
109        self
110    }
111
112    /// Do not include the cause chain of the top-level error in the graphical
113    /// output.
114    pub fn without_cause_chain(mut self) -> Self {
115        self.with_cause_chain = false;
116        self
117    }
118
119    /// Whether to include [`Diagnostic::url()`] in the output.
120    ///
121    /// Disabling this is not recommended, but can be useful for more easily
122    /// reproducible tests, as `url(docsrs)` links are version-dependent.
123    pub fn with_urls(mut self, urls: bool) -> Self {
124        self.links = match (self.links, urls) {
125            (_, false) => LinkStyle::None,
126            (LinkStyle::None, true) => LinkStyle::Link,
127            (links, true) => links,
128        };
129        self
130    }
131
132    /// Set a theme for this handler.
133    pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
134        self.theme = theme;
135        self
136    }
137
138    /// Sets the width to wrap the report at.
139    pub fn with_width(mut self, width: usize) -> Self {
140        self.termwidth = width;
141        self
142    }
143
144    /// Enables or disables wrapping of lines to fit the width.
145    pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
146        self.wrap_lines = wrap_lines;
147        self
148    }
149
150    /// Enables or disables breaking of words during wrapping.
151    pub fn with_break_words(mut self, break_words: bool) -> Self {
152        self.break_words = break_words;
153        self
154    }
155
156    /// Sets the word separator to use when wrapping.
157    pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
158        self.word_separator = Some(word_separator);
159        self
160    }
161
162    /// Sets the word splitter to use when wrapping.
163    pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
164        self.word_splitter = Some(word_splitter);
165        self
166    }
167
168    /// Sets the 'global' footer for this handler.
169    pub fn with_footer(mut self, footer: String) -> Self {
170        self.footer = Some(footer);
171        self
172    }
173
174    /// Sets the number of lines of context to show around each error.
175    pub fn with_context_lines(mut self, lines: usize) -> Self {
176        self.context_lines = lines;
177        self
178    }
179
180    /// Enable syntax highlighting for source code snippets, using the given
181    /// [`Highlighter`]. See the [highlighters](crate::highlighters) crate
182    /// for more details.
183    pub fn with_syntax_highlighting(
184        mut self,
185        highlighter: impl Highlighter + Send + Sync + 'static,
186    ) -> Self {
187        self.highlighter = MietteHighlighter::from(highlighter);
188        self
189    }
190
191    /// Disable syntax highlighting. This uses the
192    /// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter.
193    pub fn without_syntax_highlighting(mut self) -> Self {
194        self.highlighter = MietteHighlighter::nocolor();
195        self
196    }
197
198    /// Sets the display text for links.
199    /// Miette displays `(link)` if this option is not set.
200    pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
201        self.link_display_text = Some(text.into());
202        self
203    }
204}
205
206impl Default for GraphicalReportHandler {
207    fn default() -> Self {
208        Self::new()
209    }
210}
211
212impl GraphicalReportHandler {
213    /// Render a [`Diagnostic`]. This function is mostly internal and meant to
214    /// be called by the toplevel [`ReportHandler`] handler, but is made public
215    /// to make it easier (possible) to test in isolation from global state.
216    pub fn render_report(
217        &self,
218        f: &mut impl fmt::Write,
219        diagnostic: &(dyn Diagnostic),
220    ) -> fmt::Result {
221        self.render_report_inner(f, diagnostic, diagnostic.source_code())
222    }
223
224    fn render_report_inner(
225        &self,
226        f: &mut impl fmt::Write,
227        diagnostic: &(dyn Diagnostic),
228        parent_src: Option<&dyn SourceCode>,
229    ) -> fmt::Result {
230        let src = diagnostic.source_code().or(parent_src);
231        self.render_header(f, diagnostic)?;
232        self.render_causes(f, diagnostic, src)?;
233        self.render_snippets(f, diagnostic, src)?;
234        self.render_footer(f, diagnostic)?;
235        self.render_related(f, diagnostic, src)?;
236        if let Some(footer) = &self.footer {
237            writeln!(f)?;
238            let width = self.termwidth.saturating_sub(2);
239            let mut opts = textwrap::Options::new(width)
240                .initial_indent("  ")
241                .subsequent_indent("  ")
242                .break_words(self.break_words);
243            if let Some(word_separator) = self.word_separator {
244                opts = opts.word_separator(word_separator);
245            }
246            if let Some(word_splitter) = self.word_splitter.clone() {
247                opts = opts.word_splitter(word_splitter);
248            }
249
250            writeln!(f, "{}", self.wrap(footer, opts))?;
251        }
252        Ok(())
253    }
254
255    fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
256        let severity_style = match diagnostic.severity() {
257            Some(Severity::Error) | None => self.theme.styles.error,
258            Some(Severity::Warning) => self.theme.styles.warning,
259            Some(Severity::Advice) => self.theme.styles.advice,
260        };
261        let mut header = String::new();
262        if self.links == LinkStyle::Link && diagnostic.url().is_some() {
263            let url = diagnostic.url().unwrap(); // safe
264            let code = if let Some(code) = diagnostic.code() {
265                format!("{} ", code)
266            } else {
267                "".to_string()
268            };
269            let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
270            let link = format!(
271                "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
272                url,
273                code.style(severity_style),
274                display_text.style(self.theme.styles.link)
275            );
276            write!(header, "{}", link)?;
277            writeln!(f, "{}", header)?;
278        } else if let Some(code) = diagnostic.code() {
279            write!(header, "{}", code.style(severity_style),)?;
280            if self.links == LinkStyle::Text && diagnostic.url().is_some() {
281                let url = diagnostic.url().unwrap(); // safe
282                write!(header, " ({})", url.style(self.theme.styles.link))?;
283            }
284            writeln!(f, "{}", header)?;
285        }
286        writeln!(f)?;
287        Ok(())
288    }
289
290    fn render_causes(
291        &self,
292        f: &mut impl fmt::Write,
293        diagnostic: &(dyn Diagnostic),
294        parent_src: Option<&dyn SourceCode>,
295    ) -> fmt::Result {
296        let src = diagnostic.source_code().or(parent_src);
297
298        let (severity_style, severity_icon) = match diagnostic.severity() {
299            Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
300            Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
301            Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
302        };
303
304        let initial_indent = format!("  {} ", severity_icon.style(severity_style));
305        let rest_indent = format!("  {} ", self.theme.characters.vbar.style(severity_style));
306        let width = self.termwidth.saturating_sub(2);
307        let mut opts = textwrap::Options::new(width)
308            .initial_indent(&initial_indent)
309            .subsequent_indent(&rest_indent)
310            .break_words(self.break_words);
311        if let Some(word_separator) = self.word_separator {
312            opts = opts.word_separator(word_separator);
313        }
314        if let Some(word_splitter) = self.word_splitter.clone() {
315            opts = opts.word_splitter(word_splitter);
316        }
317
318        writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
319
320        if !self.with_cause_chain {
321            return Ok(());
322        }
323
324        if let Some(mut cause_iter) = diagnostic
325            .diagnostic_source()
326            .map(DiagnosticChain::from_diagnostic)
327            .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
328            .map(|it| it.peekable())
329        {
330            while let Some(error) = cause_iter.next() {
331                let is_last = cause_iter.peek().is_none();
332                let char = if !is_last {
333                    self.theme.characters.lcross
334                } else {
335                    self.theme.characters.lbot
336                };
337                let initial_indent = format!(
338                    "  {}{}{} ",
339                    char, self.theme.characters.hbar, self.theme.characters.rarrow
340                )
341                .style(severity_style)
342                .to_string();
343                let rest_indent = format!(
344                    "  {}   ",
345                    if is_last {
346                        ' '
347                    } else {
348                        self.theme.characters.vbar
349                    }
350                )
351                .style(severity_style)
352                .to_string();
353                let mut opts = textwrap::Options::new(width)
354                    .initial_indent(&initial_indent)
355                    .subsequent_indent(&rest_indent)
356                    .break_words(self.break_words);
357                if let Some(word_separator) = self.word_separator {
358                    opts = opts.word_separator(word_separator);
359                }
360                if let Some(word_splitter) = self.word_splitter.clone() {
361                    opts = opts.word_splitter(word_splitter);
362                }
363
364                match error {
365                    ErrorKind::Diagnostic(diag) => {
366                        let mut inner = String::new();
367
368                        let mut inner_renderer = self.clone();
369                        // Don't print footer for inner errors
370                        inner_renderer.footer = None;
371                        // Cause chains are already flattened, so don't double-print the nested error
372                        inner_renderer.with_cause_chain = false;
373                        // Since everything from here on is indented, shrink the virtual terminal
374                        inner_renderer.termwidth -= rest_indent.width();
375                        inner_renderer.render_report_inner(&mut inner, diag, src)?;
376
377                        // If there was no header, remove the leading newline
378                        let inner = inner.trim_start_matches('\n');
379                        writeln!(f, "{}", self.wrap(inner, opts))?;
380                    }
381                    ErrorKind::StdError(err) => {
382                        writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
383                    }
384                }
385            }
386        }
387
388        Ok(())
389    }
390
391    fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
392        if let Some(help) = diagnostic.help() {
393            let width = self.termwidth.saturating_sub(2);
394            let initial_indent = "  help: ".style(self.theme.styles.help).to_string();
395            let mut opts = textwrap::Options::new(width)
396                .initial_indent(&initial_indent)
397                .subsequent_indent("        ")
398                .break_words(self.break_words);
399            if let Some(word_separator) = self.word_separator {
400                opts = opts.word_separator(word_separator);
401            }
402            if let Some(word_splitter) = self.word_splitter.clone() {
403                opts = opts.word_splitter(word_splitter);
404            }
405
406            writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
407        }
408        Ok(())
409    }
410
411    fn render_related(
412        &self,
413        f: &mut impl fmt::Write,
414        diagnostic: &(dyn Diagnostic),
415        parent_src: Option<&dyn SourceCode>,
416    ) -> fmt::Result {
417        if let Some(related) = diagnostic.related() {
418            let mut inner_renderer = self.clone();
419            // Re-enable the printing of nested cause chains for related errors
420            inner_renderer.with_cause_chain = true;
421            for rel in related {
422                writeln!(f)?;
423                match rel.severity() {
424                    Some(Severity::Error) | None => write!(f, "Error: ")?,
425                    Some(Severity::Warning) => write!(f, "Warning: ")?,
426                    Some(Severity::Advice) => write!(f, "Advice: ")?,
427                };
428                inner_renderer.render_header(f, rel)?;
429                let src = rel.source_code().or(parent_src);
430                inner_renderer.render_causes(f, rel, src)?;
431                inner_renderer.render_snippets(f, rel, src)?;
432                inner_renderer.render_footer(f, rel)?;
433                inner_renderer.render_related(f, rel, src)?;
434            }
435        }
436        Ok(())
437    }
438
439    fn render_snippets(
440        &self,
441        f: &mut impl fmt::Write,
442        diagnostic: &(dyn Diagnostic),
443        opt_source: Option<&dyn SourceCode>,
444    ) -> fmt::Result {
445        let source = match opt_source {
446            Some(source) => source,
447            None => return Ok(()),
448        };
449        let labels = match diagnostic.labels() {
450            Some(labels) => labels,
451            None => return Ok(()),
452        };
453
454        let mut labels = labels.collect::<Vec<_>>();
455        labels.sort_unstable_by_key(|l| l.inner().offset());
456
457        let mut contexts = Vec::with_capacity(labels.len());
458        for right in labels.iter().cloned() {
459            let right_conts =
460                match source.read_span(right.inner(), self.context_lines, self.context_lines) {
461                    Ok(cont) => cont,
462                    Err(err) => {
463                        writeln!(
464                            f,
465                            "  [{} `{}` (offset: {}, length: {}): {:?}]",
466                            "Failed to read contents for label".style(self.theme.styles.error),
467                            right
468                                .label()
469                                .unwrap_or("<none>")
470                                .style(self.theme.styles.link),
471                            right.offset().style(self.theme.styles.link),
472                            right.len().style(self.theme.styles.link),
473                            err.style(self.theme.styles.warning)
474                        )?;
475                        return Ok(());
476                    }
477                };
478
479            if contexts.is_empty() {
480                contexts.push((right, right_conts));
481                continue;
482            }
483
484            let (left, left_conts) = contexts.last().unwrap();
485            if left_conts.line() + left_conts.line_count() >= right_conts.line() {
486                // The snippets will overlap, so we create one Big Chunky Boi
487                let left_end = left.offset() + left.len();
488                let right_end = right.offset() + right.len();
489                let new_end = std::cmp::max(left_end, right_end);
490
491                let new_span = LabeledSpan::new(
492                    left.label().map(String::from),
493                    left.offset(),
494                    new_end - left.offset(),
495                );
496                // Check that the two contexts can be combined
497                if let Ok(new_conts) =
498                    source.read_span(new_span.inner(), self.context_lines, self.context_lines)
499                {
500                    contexts.pop();
501                    // We'll throw the contents away later
502                    contexts.push((new_span, new_conts));
503                    continue;
504                }
505            }
506
507            contexts.push((right, right_conts));
508        }
509        for (ctx, _) in contexts {
510            self.render_context(f, source, &ctx, &labels[..])?;
511        }
512
513        Ok(())
514    }
515
516    fn render_context(
517        &self,
518        f: &mut impl fmt::Write,
519        source: &dyn SourceCode,
520        context: &LabeledSpan,
521        labels: &[LabeledSpan],
522    ) -> fmt::Result {
523        let (contents, lines) = self.get_lines(source, context.inner())?;
524
525        // only consider labels from the context as primary label
526        let ctx_labels = labels.iter().filter(|l| {
527            context.inner().offset() <= l.inner().offset()
528                && l.inner().offset() + l.inner().len()
529                    <= context.inner().offset() + context.inner().len()
530        });
531        let primary_label = ctx_labels
532            .clone()
533            .find(|label| label.primary())
534            .or_else(|| ctx_labels.clone().next());
535
536        // sorting is your friend
537        let labels = labels
538            .iter()
539            .zip(self.theme.styles.highlights.iter().cloned().cycle())
540            .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
541            .collect::<Vec<_>>();
542
543        let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
544
545        // The max number of gutter-lines that will be active at any given
546        // point. We need this to figure out indentation, so we do one loop
547        // over the lines to see what the damage is gonna be.
548        let mut max_gutter = 0usize;
549        for line in &lines {
550            let mut num_highlights = 0;
551            for hl in &labels {
552                if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
553                    num_highlights += 1;
554                }
555            }
556            max_gutter = std::cmp::max(max_gutter, num_highlights);
557        }
558
559        // Oh and one more thing: We need to figure out how much room our line
560        // numbers need!
561        let linum_width = lines[..]
562            .last()
563            .map(|line| line.line_number)
564            // It's possible for the source to be an empty string.
565            .unwrap_or(0)
566            .to_string()
567            .len();
568
569        // Header
570        write!(
571            f,
572            "{}{}{}",
573            " ".repeat(linum_width + 2),
574            self.theme.characters.ltop,
575            self.theme.characters.hbar,
576        )?;
577
578        // If there is a primary label, then use its span
579        // as the reference point for line/column information.
580        let primary_contents = match primary_label {
581            Some(label) => source
582                .read_span(label.inner(), 0, 0)
583                .map_err(|_| fmt::Error)?,
584            None => contents,
585        };
586
587        if let Some(source_name) = primary_contents.name() {
588            writeln!(
589                f,
590                "[{}]",
591                format_args!(
592                    "{}:{}:{}",
593                    source_name,
594                    primary_contents.line() + 1,
595                    primary_contents.column() + 1
596                )
597                .style(self.theme.styles.link)
598            )?;
599        } else if lines.len() <= 1 {
600            writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
601        } else {
602            writeln!(
603                f,
604                "[{}:{}]",
605                primary_contents.line() + 1,
606                primary_contents.column() + 1
607            )?;
608        }
609
610        // Now it's time for the fun part--actually rendering everything!
611        for line in &lines {
612            // Line number, appropriately padded.
613            self.write_linum(f, linum_width, line.line_number)?;
614
615            // Then, we need to print the gutter, along with any fly-bys We
616            // have separate gutters depending on whether we're on the actual
617            // line, or on one of the "highlight lines" below it.
618            self.render_line_gutter(f, max_gutter, line, &labels)?;
619
620            // And _now_ we can print out the line text itself!
621            let styled_text =
622                StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
623            self.render_line_text(f, &styled_text)?;
624
625            // Next, we write all the highlights that apply to this particular line.
626            let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
627                .iter()
628                .filter(|hl| line.span_applies(hl))
629                .partition(|hl| line.span_line_only(hl));
630            if !single_line.is_empty() {
631                // no line number!
632                self.write_no_linum(f, linum_width)?;
633                // gutter _again_
634                self.render_highlight_gutter(
635                    f,
636                    max_gutter,
637                    line,
638                    &labels,
639                    LabelRenderMode::SingleLine,
640                )?;
641                self.render_single_line_highlights(
642                    f,
643                    line,
644                    linum_width,
645                    max_gutter,
646                    &single_line,
647                    &labels,
648                )?;
649            }
650            for hl in multi_line {
651                if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
652                    self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
653                }
654            }
655        }
656        writeln!(
657            f,
658            "{}{}{}",
659            " ".repeat(linum_width + 2),
660            self.theme.characters.lbot,
661            self.theme.characters.hbar.to_string().repeat(4),
662        )?;
663        Ok(())
664    }
665
666    fn render_multi_line_end(
667        &self,
668        f: &mut impl fmt::Write,
669        labels: &[FancySpan],
670        max_gutter: usize,
671        linum_width: usize,
672        line: &Line,
673        label: &FancySpan,
674    ) -> fmt::Result {
675        // no line number!
676        self.write_no_linum(f, linum_width)?;
677
678        if let Some(label_parts) = label.label_parts() {
679            // if it has a label, how long is it?
680            let (first, rest) = label_parts
681                .split_first()
682                .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
683
684            if rest.is_empty() {
685                // gutter _again_
686                self.render_highlight_gutter(
687                    f,
688                    max_gutter,
689                    line,
690                    labels,
691                    LabelRenderMode::SingleLine,
692                )?;
693
694                self.render_multi_line_end_single(
695                    f,
696                    first,
697                    label.style,
698                    LabelRenderMode::SingleLine,
699                )?;
700            } else {
701                // gutter _again_
702                self.render_highlight_gutter(
703                    f,
704                    max_gutter,
705                    line,
706                    labels,
707                    LabelRenderMode::MultiLineFirst,
708                )?;
709
710                self.render_multi_line_end_single(
711                    f,
712                    first,
713                    label.style,
714                    LabelRenderMode::MultiLineFirst,
715                )?;
716                for label_line in rest {
717                    // no line number!
718                    self.write_no_linum(f, linum_width)?;
719                    // gutter _again_
720                    self.render_highlight_gutter(
721                        f,
722                        max_gutter,
723                        line,
724                        labels,
725                        LabelRenderMode::MultiLineRest,
726                    )?;
727                    self.render_multi_line_end_single(
728                        f,
729                        label_line,
730                        label.style,
731                        LabelRenderMode::MultiLineRest,
732                    )?;
733                }
734            }
735        } else {
736            // gutter _again_
737            self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?;
738            // has no label
739            writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
740        }
741
742        Ok(())
743    }
744
745    fn render_line_gutter(
746        &self,
747        f: &mut impl fmt::Write,
748        max_gutter: usize,
749        line: &Line,
750        highlights: &[FancySpan],
751    ) -> fmt::Result {
752        if max_gutter == 0 {
753            return Ok(());
754        }
755        let chars = &self.theme.characters;
756        let mut gutter = String::new();
757        let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
758        let mut arrow = false;
759        for (i, hl) in applicable.enumerate() {
760            if line.span_starts(hl) {
761                gutter.push_str(&chars.ltop.style(hl.style).to_string());
762                gutter.push_str(
763                    &chars
764                        .hbar
765                        .to_string()
766                        .repeat(max_gutter.saturating_sub(i))
767                        .style(hl.style)
768                        .to_string(),
769                );
770                gutter.push_str(&chars.rarrow.style(hl.style).to_string());
771                arrow = true;
772                break;
773            } else if line.span_ends(hl) {
774                if hl.label().is_some() {
775                    gutter.push_str(&chars.lcross.style(hl.style).to_string());
776                } else {
777                    gutter.push_str(&chars.lbot.style(hl.style).to_string());
778                }
779                gutter.push_str(
780                    &chars
781                        .hbar
782                        .to_string()
783                        .repeat(max_gutter.saturating_sub(i))
784                        .style(hl.style)
785                        .to_string(),
786                );
787                gutter.push_str(&chars.rarrow.style(hl.style).to_string());
788                arrow = true;
789                break;
790            } else if line.span_flyby(hl) {
791                gutter.push_str(&chars.vbar.style(hl.style).to_string());
792            } else {
793                gutter.push(' ');
794            }
795        }
796        write!(
797            f,
798            "{}{}",
799            gutter,
800            " ".repeat(
801                if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
802            )
803        )?;
804        Ok(())
805    }
806
807    fn render_highlight_gutter(
808        &self,
809        f: &mut impl fmt::Write,
810        max_gutter: usize,
811        line: &Line,
812        highlights: &[FancySpan],
813        render_mode: LabelRenderMode,
814    ) -> fmt::Result {
815        if max_gutter == 0 {
816            return Ok(());
817        }
818
819        // keeps track of how many columns wide the gutter is
820        // important for ansi since simply measuring the size of the final string
821        // gives the wrong result when the string contains ansi codes.
822        let mut gutter_cols = 0;
823
824        let chars = &self.theme.characters;
825        let mut gutter = String::new();
826        let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
827        for (i, hl) in applicable.enumerate() {
828            if !line.span_line_only(hl) && line.span_ends(hl) {
829                if render_mode == LabelRenderMode::MultiLineRest {
830                    // this is to make multiline labels work. We want to make the right amount
831                    // of horizontal space for them, but not actually draw the lines
832                    let horizontal_space = max_gutter.saturating_sub(i) + 2;
833                    for _ in 0..horizontal_space {
834                        gutter.push(' ');
835                    }
836                    // account for one more horizontal space, since in multiline mode
837                    // we also add in the vertical line before the label like this:
838                    // 2 │ ╭─▶   text
839                    // 3 │ ├─▶     here
840                    //   · ╰──┤ these two lines
841                    //   ·    │ are the problem
842                    //        ^this
843                    gutter_cols += horizontal_space + 1;
844                } else {
845                    let num_repeat = max_gutter.saturating_sub(i) + 2;
846
847                    gutter.push_str(&chars.lbot.style(hl.style).to_string());
848
849                    gutter.push_str(
850                        &chars
851                            .hbar
852                            .to_string()
853                            .repeat(
854                                num_repeat
855                                    // if we are rendering a multiline label, then leave a bit of space for the
856                                    // rcross character
857                                    - if render_mode == LabelRenderMode::MultiLineFirst {
858                                        1
859                                    } else {
860                                        0
861                                    },
862                            )
863                            .style(hl.style)
864                            .to_string(),
865                    );
866
867                    // we count 1 for the lbot char, and then a few more, the same number
868                    // as we just repeated for. For each repeat we only add 1, even though
869                    // due to ansi escape codes the number of bytes in the string could grow
870                    // a lot each time.
871                    gutter_cols += num_repeat + 1;
872                }
873                break;
874            } else {
875                gutter.push_str(&chars.vbar.style(hl.style).to_string());
876
877                // we may push many bytes for the ansi escape codes style adds,
878                // but we still only add a single character-width to the string in a terminal
879                gutter_cols += 1;
880            }
881        }
882
883        // now calculate how many spaces to add based on how many columns we just created.
884        // it's the max width of the gutter, minus how many character-widths we just generated
885        // capped at 0 (though this should never go below in reality), and then we add 3 to
886        // account for arrowheads when a gutter line ends
887        let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
888        // we then write the gutter and as many spaces as we need
889        write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
890        Ok(())
891    }
892
893    fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
894        if self.wrap_lines {
895            textwrap::fill(text, opts)
896        } else {
897            // Format without wrapping, but retain the indentation options
898            // Implementation based on `textwrap::indent`
899            let mut result = String::with_capacity(2 * text.len());
900            let trimmed_indent = opts.subsequent_indent.trim_end();
901            for (idx, line) in text.split_terminator('\n').enumerate() {
902                if idx > 0 {
903                    result.push('\n');
904                }
905                if idx == 0 {
906                    if line.trim().is_empty() {
907                        result.push_str(opts.initial_indent.trim_end());
908                    } else {
909                        result.push_str(opts.initial_indent);
910                    }
911                } else if line.trim().is_empty() {
912                    result.push_str(trimmed_indent);
913                } else {
914                    result.push_str(opts.subsequent_indent);
915                }
916                result.push_str(line);
917            }
918            if text.ends_with('\n') {
919                // split_terminator will have eaten the final '\n'.
920                result.push('\n');
921            }
922            result
923        }
924    }
925
926    fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
927        write!(
928            f,
929            " {:width$} {} ",
930            linum.style(self.theme.styles.linum),
931            self.theme.characters.vbar,
932            width = width
933        )?;
934        Ok(())
935    }
936
937    fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
938        write!(
939            f,
940            " {:width$} {} ",
941            "",
942            self.theme.characters.vbar_break,
943            width = width
944        )?;
945        Ok(())
946    }
947
948    /// Returns an iterator over the visual width of each character in a line.
949    fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
950        let mut column = 0;
951        let mut escaped = false;
952        let tab_width = self.tab_width;
953        text.chars().map(move |c| {
954            let width = match (escaped, c) {
955                // Round up to the next multiple of tab_width
956                (false, '\t') => tab_width - column % tab_width,
957                // start of ANSI escape
958                (false, '\x1b') => {
959                    escaped = true;
960                    0
961                }
962                // use Unicode width for all other characters
963                (false, c) => c.width().unwrap_or(0),
964                // end of ANSI escape
965                (true, 'm') => {
966                    escaped = false;
967                    0
968                }
969                // characters are zero width within escape sequence
970                (true, _) => 0,
971            };
972            column += width;
973            width
974        })
975    }
976
977    /// Returns the visual column position of a byte offset on a specific line.
978    ///
979    /// If the offset occurs in the middle of a character, the returned column
980    /// corresponds to that character's first column in `start` is true, or its
981    /// last column if `start` is false.
982    fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
983        let line_range = line.offset..=(line.offset + line.length);
984        assert!(line_range.contains(&offset));
985
986        let mut text_index = offset - line.offset;
987        while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
988            if start {
989                text_index -= 1;
990            } else {
991                text_index += 1;
992            }
993        }
994        let text = &line.text[..text_index.min(line.text.len())];
995        let text_width = self.line_visual_char_width(text).sum();
996        if text_index > line.text.len() {
997            // Spans extending past the end of the line are always rendered as
998            // one column past the end of the visible line.
999            //
1000            // This doesn't necessarily correspond to a specific byte-offset,
1001            // since a span extending past the end of the line could contain:
1002            //  - an actual \n character (1 byte)
1003            //  - a CRLF (2 bytes)
1004            //  - EOF (0 bytes)
1005            text_width + 1
1006        } else {
1007            text_width
1008        }
1009    }
1010
1011    /// Renders a line to the output formatter, replacing tabs with spaces.
1012    fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
1013        for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
1014            if c == '\t' {
1015                for _ in 0..width {
1016                    f.write_char(' ')?;
1017                }
1018            } else {
1019                f.write_char(c)?;
1020            }
1021        }
1022        f.write_char('\n')?;
1023        Ok(())
1024    }
1025
1026    fn render_single_line_highlights(
1027        &self,
1028        f: &mut impl fmt::Write,
1029        line: &Line,
1030        linum_width: usize,
1031        max_gutter: usize,
1032        single_liners: &[&FancySpan],
1033        all_highlights: &[FancySpan],
1034    ) -> fmt::Result {
1035        let mut underlines = String::new();
1036        let mut highest = 0;
1037
1038        let chars = &self.theme.characters;
1039        let vbar_offsets: Vec<_> = single_liners
1040            .iter()
1041            .map(|hl| {
1042                let byte_start = hl.offset();
1043                let byte_end = hl.offset() + hl.len();
1044                let start = self.visual_offset(line, byte_start, true).max(highest);
1045                let end = if hl.len() == 0 {
1046                    start + 1
1047                } else {
1048                    self.visual_offset(line, byte_end, false).max(start + 1)
1049                };
1050
1051                let vbar_offset = (start + end) / 2;
1052                let num_left = vbar_offset - start;
1053                let num_right = end - vbar_offset - 1;
1054                underlines.push_str(
1055                    &format!(
1056                        "{:width$}{}{}{}",
1057                        "",
1058                        chars.underline.to_string().repeat(num_left),
1059                        if hl.len() == 0 {
1060                            chars.uarrow
1061                        } else if hl.label().is_some() {
1062                            chars.underbar
1063                        } else {
1064                            chars.underline
1065                        },
1066                        chars.underline.to_string().repeat(num_right),
1067                        width = start.saturating_sub(highest),
1068                    )
1069                    .style(hl.style)
1070                    .to_string(),
1071                );
1072                highest = std::cmp::max(highest, end);
1073
1074                (hl, vbar_offset)
1075            })
1076            .collect();
1077        writeln!(f, "{}", underlines)?;
1078
1079        for hl in single_liners.iter().rev() {
1080            if let Some(label) = hl.label_parts() {
1081                if label.len() == 1 {
1082                    self.write_label_text(
1083                        f,
1084                        line,
1085                        linum_width,
1086                        max_gutter,
1087                        all_highlights,
1088                        chars,
1089                        &vbar_offsets,
1090                        hl,
1091                        &label[0],
1092                        LabelRenderMode::SingleLine,
1093                    )?;
1094                } else {
1095                    let mut first = true;
1096                    for label_line in &label {
1097                        self.write_label_text(
1098                            f,
1099                            line,
1100                            linum_width,
1101                            max_gutter,
1102                            all_highlights,
1103                            chars,
1104                            &vbar_offsets,
1105                            hl,
1106                            label_line,
1107                            if first {
1108                                LabelRenderMode::MultiLineFirst
1109                            } else {
1110                                LabelRenderMode::MultiLineRest
1111                            },
1112                        )?;
1113                        first = false;
1114                    }
1115                }
1116            }
1117        }
1118        Ok(())
1119    }
1120
1121    // I know it's not good practice, but making this a function makes a lot of sense
1122    // and making a struct for this does not...
1123    #[allow(clippy::too_many_arguments)]
1124    fn write_label_text(
1125        &self,
1126        f: &mut impl fmt::Write,
1127        line: &Line,
1128        linum_width: usize,
1129        max_gutter: usize,
1130        all_highlights: &[FancySpan],
1131        chars: &ThemeCharacters,
1132        vbar_offsets: &[(&&FancySpan, usize)],
1133        hl: &&FancySpan,
1134        label: &str,
1135        render_mode: LabelRenderMode,
1136    ) -> fmt::Result {
1137        self.write_no_linum(f, linum_width)?;
1138        self.render_highlight_gutter(
1139            f,
1140            max_gutter,
1141            line,
1142            all_highlights,
1143            LabelRenderMode::SingleLine,
1144        )?;
1145        let mut curr_offset = 1usize;
1146        for (offset_hl, vbar_offset) in vbar_offsets {
1147            while curr_offset < *vbar_offset + 1 {
1148                write!(f, " ")?;
1149                curr_offset += 1;
1150            }
1151            if *offset_hl != hl {
1152                write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
1153                curr_offset += 1;
1154            } else {
1155                let lines = match render_mode {
1156                    LabelRenderMode::SingleLine => format!(
1157                        "{}{} {}",
1158                        chars.lbot,
1159                        chars.hbar.to_string().repeat(2),
1160                        label,
1161                    ),
1162                    LabelRenderMode::MultiLineFirst => {
1163                        format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1164                    }
1165                    LabelRenderMode::MultiLineRest => {
1166                        format!("  {} {}", chars.vbar, label,)
1167                    }
1168                };
1169                writeln!(f, "{}", lines.style(hl.style))?;
1170                break;
1171            }
1172        }
1173        Ok(())
1174    }
1175
1176    fn render_multi_line_end_single(
1177        &self,
1178        f: &mut impl fmt::Write,
1179        label: &str,
1180        style: Style,
1181        render_mode: LabelRenderMode,
1182    ) -> fmt::Result {
1183        match render_mode {
1184            LabelRenderMode::SingleLine => {
1185                writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1186            }
1187            LabelRenderMode::MultiLineFirst => {
1188                writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1189            }
1190            LabelRenderMode::MultiLineRest => {
1191                writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1192            }
1193        }
1194
1195        Ok(())
1196    }
1197
1198    fn get_lines<'a>(
1199        &'a self,
1200        source: &'a dyn SourceCode,
1201        context_span: &'a SourceSpan,
1202    ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
1203        let context_data = source
1204            .read_span(context_span, self.context_lines, self.context_lines)
1205            .map_err(|_| fmt::Error)?;
1206        let context = String::from_utf8_lossy(context_data.data());
1207        let mut line = context_data.line();
1208        let mut column = context_data.column();
1209        let mut offset = context_data.span().offset();
1210        let mut line_offset = offset;
1211        let mut line_str = String::with_capacity(context.len());
1212        let mut lines = Vec::with_capacity(1);
1213        let mut iter = context.chars().peekable();
1214        while let Some(char) = iter.next() {
1215            offset += char.len_utf8();
1216            let mut at_end_of_file = false;
1217            match char {
1218                '\r' => {
1219                    if iter.next_if_eq(&'\n').is_some() {
1220                        offset += 1;
1221                        line += 1;
1222                        column = 0;
1223                    } else {
1224                        line_str.push(char);
1225                        column += 1;
1226                    }
1227                    at_end_of_file = iter.peek().is_none();
1228                }
1229                '\n' => {
1230                    at_end_of_file = iter.peek().is_none();
1231                    line += 1;
1232                    column = 0;
1233                }
1234                _ => {
1235                    line_str.push(char);
1236                    column += 1;
1237                }
1238            }
1239
1240            if iter.peek().is_none() && !at_end_of_file {
1241                line += 1;
1242            }
1243
1244            if column == 0 || iter.peek().is_none() {
1245                lines.push(Line {
1246                    line_number: line,
1247                    offset: line_offset,
1248                    length: offset - line_offset,
1249                    text: line_str.clone(),
1250                });
1251                line_str.clear();
1252                line_offset = offset;
1253            }
1254        }
1255        Ok((context_data, lines))
1256    }
1257}
1258
1259impl ReportHandler for GraphicalReportHandler {
1260    fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
1261        if f.alternate() {
1262            return fmt::Debug::fmt(diagnostic, f);
1263        }
1264
1265        self.render_report(f, diagnostic)
1266    }
1267}
1268
1269/*
1270Support types
1271*/
1272
1273#[derive(PartialEq, Debug)]
1274enum LabelRenderMode {
1275    /// we're rendering a single line label (or not rendering in any special way)
1276    SingleLine,
1277    /// we're rendering a multiline label
1278    MultiLineFirst,
1279    /// we're rendering the rest of a multiline label
1280    MultiLineRest,
1281}
1282
1283#[derive(Debug)]
1284struct Line {
1285    line_number: usize,
1286    offset: usize,
1287    length: usize,
1288    text: String,
1289}
1290
1291impl Line {
1292    fn span_line_only(&self, span: &FancySpan) -> bool {
1293        span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
1294    }
1295
1296    /// Returns whether `span` should be visible on this line, either in the gutter or under the
1297    /// text on this line
1298    fn span_applies(&self, span: &FancySpan) -> bool {
1299        let spanlen = if span.len() == 0 { 1 } else { span.len() };
1300        // Span starts in this line
1301
1302        (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1303            // Span passes through this line
1304            || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
1305            // Span ends on this line
1306            || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1307    }
1308
1309    /// Returns whether `span` should be visible on this line in the gutter (so this excludes spans
1310    /// that are only visible on this line and do not span multiple lines)
1311    fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1312        let spanlen = if span.len() == 0 { 1 } else { span.len() };
1313        // Span starts in this line
1314        self.span_applies(span)
1315            && !(
1316                // as long as it doesn't start *and* end on this line
1317                (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1318                    && (span.offset() + spanlen > self.offset
1319                        && span.offset() + spanlen <= self.offset + self.length)
1320            )
1321    }
1322
1323    // A 'flyby' is a multi-line span that technically covers this line, but
1324    // does not begin or end within the line itself. This method is used to
1325    // calculate gutters.
1326    fn span_flyby(&self, span: &FancySpan) -> bool {
1327        // The span itself starts before this line's starting offset (so, in a
1328        // prev line).
1329        span.offset() < self.offset
1330            // ...and it stops after this line's end.
1331            && span.offset() + span.len() > self.offset + self.length
1332    }
1333
1334    // Does this line contain the *beginning* of this multiline span?
1335    // This assumes self.span_applies() is true already.
1336    fn span_starts(&self, span: &FancySpan) -> bool {
1337        span.offset() >= self.offset
1338    }
1339
1340    // Does this line contain the *end* of this multiline span?
1341    // This assumes self.span_applies() is true already.
1342    fn span_ends(&self, span: &FancySpan) -> bool {
1343        span.offset() + span.len() >= self.offset
1344            && span.offset() + span.len() <= self.offset + self.length
1345    }
1346}
1347
1348#[derive(Debug, Clone)]
1349struct FancySpan {
1350    /// this is deliberately an option of a vec because I wanted to be very explicit
1351    /// that there can also be *no* label. If there is a label, it can have multiple
1352    /// lines which is what the vec is for.
1353    label: Option<Vec<String>>,
1354    span: SourceSpan,
1355    style: Style,
1356}
1357
1358impl PartialEq for FancySpan {
1359    fn eq(&self, other: &Self) -> bool {
1360        self.label == other.label && self.span == other.span
1361    }
1362}
1363
1364fn split_label(v: String) -> Vec<String> {
1365    v.split('\n').map(|i| i.to_string()).collect()
1366}
1367
1368impl FancySpan {
1369    fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
1370        FancySpan {
1371            label: label.map(split_label),
1372            span,
1373            style,
1374        }
1375    }
1376
1377    fn style(&self) -> Style {
1378        self.style
1379    }
1380
1381    fn label(&self) -> Option<String> {
1382        self.label
1383            .as_ref()
1384            .map(|l| l.join("\n").style(self.style()).to_string())
1385    }
1386
1387    fn label_parts(&self) -> Option<Vec<String>> {
1388        self.label.as_ref().map(|l| {
1389            l.iter()
1390                .map(|i| i.style(self.style()).to_string())
1391                .collect()
1392        })
1393    }
1394
1395    fn offset(&self) -> usize {
1396        self.span.offset()
1397    }
1398
1399    fn len(&self) -> usize {
1400        self.span.len()
1401    }
1402}