Skip to main content

miette/handlers/
graphical.rs

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