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