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