miette/handlers/
graphical.rs

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