Skip to main content

miette/handlers/
graphical.rs

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