1 use std::fmt::{self, Write};
2 
3 use owo_colors::{OwoColorize, Style};
4 use unicode_width::UnicodeWidthChar;
5 
6 use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
7 use crate::handlers::theme::*;
8 use crate::protocol::{Diagnostic, Severity};
9 use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
10 
11 /**
12 A [`ReportHandler`] that displays a given [`Report`](crate::Report) in a
13 quasi-graphical way, using terminal colors, unicode drawing characters, and
14 other such things.
15 
16 This is the default reporter bundled with `miette`.
17 
18 This printer can be customized by using [`new_themed()`](GraphicalReportHandler::new_themed) and handing it a
19 [`GraphicalTheme`] of your own creation (or using one of its own defaults!)
20 
21 See [`set_hook()`](crate::set_hook) for more details on customizing your global
22 printer.
23 */
24 #[derive(Debug, Clone)]
25 pub struct GraphicalReportHandler {
26     pub(crate) links: LinkStyle,
27     pub(crate) termwidth: usize,
28     pub(crate) theme: GraphicalTheme,
29     pub(crate) footer: Option<String>,
30     pub(crate) context_lines: usize,
31     pub(crate) tab_width: usize,
32     pub(crate) with_cause_chain: bool,
33 }
34 
35 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
36 pub(crate) enum LinkStyle {
37     None,
38     Link,
39     Text,
40 }
41 
42 impl GraphicalReportHandler {
43     /// Create a new `GraphicalReportHandler` with the default
44     /// [`GraphicalTheme`]. This will use both unicode characters and colors.
new() -> Self45     pub fn new() -> Self {
46         Self {
47             links: LinkStyle::Link,
48             termwidth: 200,
49             theme: GraphicalTheme::default(),
50             footer: None,
51             context_lines: 1,
52             tab_width: 4,
53             with_cause_chain: true,
54         }
55     }
56 
57     ///Create a new `GraphicalReportHandler` with a given [`GraphicalTheme`].
new_themed(theme: GraphicalTheme) -> Self58     pub fn new_themed(theme: GraphicalTheme) -> Self {
59         Self {
60             links: LinkStyle::Link,
61             termwidth: 200,
62             theme,
63             footer: None,
64             context_lines: 1,
65             tab_width: 4,
66             with_cause_chain: true,
67         }
68     }
69 
70     /// Set the displayed tab width in spaces.
tab_width(mut self, width: usize) -> Self71     pub fn tab_width(mut self, width: usize) -> Self {
72         self.tab_width = width;
73         self
74     }
75 
76     /// Whether to enable error code linkification using [`Diagnostic::url()`].
with_links(mut self, links: bool) -> Self77     pub fn with_links(mut self, links: bool) -> Self {
78         self.links = if links {
79             LinkStyle::Link
80         } else {
81             LinkStyle::Text
82         };
83         self
84     }
85 
86     /// Include the cause chain of the top-level error in the graphical output,
87     /// if available.
with_cause_chain(mut self) -> Self88     pub fn with_cause_chain(mut self) -> Self {
89         self.with_cause_chain = true;
90         self
91     }
92 
93     /// Do not include the cause chain of the top-level error in the graphical
94     /// output.
without_cause_chain(mut self) -> Self95     pub fn without_cause_chain(mut self) -> Self {
96         self.with_cause_chain = false;
97         self
98     }
99 
100     /// Whether to include [`Diagnostic::url()`] in the output.
101     ///
102     /// Disabling this is not recommended, but can be useful for more easily
103     /// reproducible tests, as `url(docsrs)` links are version-dependent.
with_urls(mut self, urls: bool) -> Self104     pub fn with_urls(mut self, urls: bool) -> Self {
105         self.links = match (self.links, urls) {
106             (_, false) => LinkStyle::None,
107             (LinkStyle::None, true) => LinkStyle::Link,
108             (links, true) => links,
109         };
110         self
111     }
112 
113     /// Set a theme for this handler.
with_theme(mut self, theme: GraphicalTheme) -> Self114     pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
115         self.theme = theme;
116         self
117     }
118 
119     /// Sets the width to wrap the report at.
with_width(mut self, width: usize) -> Self120     pub fn with_width(mut self, width: usize) -> Self {
121         self.termwidth = width;
122         self
123     }
124 
125     /// Sets the 'global' footer for this handler.
with_footer(mut self, footer: String) -> Self126     pub fn with_footer(mut self, footer: String) -> Self {
127         self.footer = Some(footer);
128         self
129     }
130 
131     /// Sets the number of lines of context to show around each error.
with_context_lines(mut self, lines: usize) -> Self132     pub fn with_context_lines(mut self, lines: usize) -> Self {
133         self.context_lines = lines;
134         self
135     }
136 }
137 
138 impl Default for GraphicalReportHandler {
default() -> Self139     fn default() -> Self {
140         Self::new()
141     }
142 }
143 
144 impl GraphicalReportHandler {
145     /// Render a [`Diagnostic`]. This function is mostly internal and meant to
146     /// be called by the toplevel [`ReportHandler`] handler, but is made public
147     /// to make it easier (possible) to test in isolation from global state.
render_report( &self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic), ) -> fmt::Result148     pub fn render_report(
149         &self,
150         f: &mut impl fmt::Write,
151         diagnostic: &(dyn Diagnostic),
152     ) -> fmt::Result {
153         self.render_header(f, diagnostic)?;
154         self.render_causes(f, diagnostic)?;
155         let src = diagnostic.source_code();
156         self.render_snippets(f, diagnostic, src)?;
157         self.render_footer(f, diagnostic)?;
158         self.render_related(f, diagnostic, src)?;
159         if let Some(footer) = &self.footer {
160             writeln!(f)?;
161             let width = self.termwidth.saturating_sub(4);
162             let opts = textwrap::Options::new(width)
163                 .initial_indent("  ")
164                 .subsequent_indent("  ");
165             writeln!(f, "{}", textwrap::fill(footer, opts))?;
166         }
167         Ok(())
168     }
169 
render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result170     fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
171         let severity_style = match diagnostic.severity() {
172             Some(Severity::Error) | None => self.theme.styles.error,
173             Some(Severity::Warning) => self.theme.styles.warning,
174             Some(Severity::Advice) => self.theme.styles.advice,
175         };
176         let mut header = String::new();
177         if self.links == LinkStyle::Link && diagnostic.url().is_some() {
178             let url = diagnostic.url().unwrap(); // safe
179             let code = if let Some(code) = diagnostic.code() {
180                 format!("{} ", code)
181             } else {
182                 "".to_string()
183             };
184             let link = format!(
185                 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
186                 url,
187                 code.style(severity_style),
188                 "(link)".style(self.theme.styles.link)
189             );
190             write!(header, "{}", link)?;
191             writeln!(f, "{}", header)?;
192             writeln!(f)?;
193         } else if let Some(code) = diagnostic.code() {
194             write!(header, "{}", code.style(severity_style),)?;
195             if self.links == LinkStyle::Text && diagnostic.url().is_some() {
196                 let url = diagnostic.url().unwrap(); // safe
197                 write!(header, " ({})", url.style(self.theme.styles.link))?;
198             }
199             writeln!(f, "{}", header)?;
200             writeln!(f)?;
201         }
202         Ok(())
203     }
204 
205     fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
206         let (severity_style, severity_icon) = match diagnostic.severity() {
207             Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
208             Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
209             Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
210         };
211 
212         let initial_indent = format!("  {} ", severity_icon.style(severity_style));
213         let rest_indent = format!("  {} ", self.theme.characters.vbar.style(severity_style));
214         let width = self.termwidth.saturating_sub(2);
215         let opts = textwrap::Options::new(width)
216             .initial_indent(&initial_indent)
217             .subsequent_indent(&rest_indent);
218 
219         writeln!(f, "{}", textwrap::fill(&diagnostic.to_string(), opts))?;
220 
221         if !self.with_cause_chain {
222             return Ok(());
223         }
224 
225         if let Some(mut cause_iter) = diagnostic
226             .diagnostic_source()
227             .map(DiagnosticChain::from_diagnostic)
228             .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
229             .map(|it| it.peekable())
230         {
231             while let Some(error) = cause_iter.next() {
232                 let is_last = cause_iter.peek().is_none();
233                 let char = if !is_last {
234                     self.theme.characters.lcross
235                 } else {
236                     self.theme.characters.lbot
237                 };
238                 let initial_indent = format!(
239                     "  {}{}{} ",
240                     char, self.theme.characters.hbar, self.theme.characters.rarrow
241                 )
242                 .style(severity_style)
243                 .to_string();
244                 let rest_indent = format!(
245                     "  {}   ",
246                     if is_last {
247                         ' '
248                     } else {
249                         self.theme.characters.vbar
250                     }
251                 )
252                 .style(severity_style)
253                 .to_string();
254                 let opts = textwrap::Options::new(width)
255                     .initial_indent(&initial_indent)
256                     .subsequent_indent(&rest_indent);
257                 match error {
258                     ErrorKind::Diagnostic(diag) => {
259                         let mut inner = String::new();
260 
261                         // Don't print footer for inner errors
262                         let mut inner_renderer = self.clone();
263                         inner_renderer.footer = None;
264                         inner_renderer.with_cause_chain = false;
265                         inner_renderer.render_report(&mut inner, diag)?;
266 
267                         writeln!(f, "{}", textwrap::fill(&inner, opts))?;
268                     }
269                     ErrorKind::StdError(err) => {
270                         writeln!(f, "{}", textwrap::fill(&err.to_string(), opts))?;
271                     }
272                 }
273             }
274         }
275 
276         Ok(())
277     }
278 
279     fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
280         if let Some(help) = diagnostic.help() {
281             let width = self.termwidth.saturating_sub(4);
282             let initial_indent = "  help: ".style(self.theme.styles.help).to_string();
283             let opts = textwrap::Options::new(width)
284                 .initial_indent(&initial_indent)
285                 .subsequent_indent("        ");
286             writeln!(f, "{}", textwrap::fill(&help.to_string(), opts))?;
287         }
288         Ok(())
289     }
290 
291     fn render_related(
292         &self,
293         f: &mut impl fmt::Write,
294         diagnostic: &(dyn Diagnostic),
295         parent_src: Option<&dyn SourceCode>,
296     ) -> fmt::Result {
297         if let Some(related) = diagnostic.related() {
298             writeln!(f)?;
299             for rel in related {
300                 match rel.severity() {
301                     Some(Severity::Error) | None => write!(f, "Error: ")?,
302                     Some(Severity::Warning) => write!(f, "Warning: ")?,
303                     Some(Severity::Advice) => write!(f, "Advice: ")?,
304                 };
305                 self.render_header(f, rel)?;
306                 self.render_causes(f, rel)?;
307                 let src = rel.source_code().or(parent_src);
308                 self.render_snippets(f, rel, src)?;
309                 self.render_footer(f, rel)?;
310                 self.render_related(f, rel, src)?;
311             }
312         }
313         Ok(())
314     }
315 
316     fn render_snippets(
317         &self,
318         f: &mut impl fmt::Write,
319         diagnostic: &(dyn Diagnostic),
320         opt_source: Option<&dyn SourceCode>,
321     ) -> fmt::Result {
322         if let Some(source) = opt_source {
323             if let Some(labels) = diagnostic.labels() {
324                 let mut labels = labels.collect::<Vec<_>>();
325                 labels.sort_unstable_by_key(|l| l.inner().offset());
326                 if !labels.is_empty() {
327                     let contents = labels
328                         .iter()
329                         .map(|label| {
330                             source.read_span(label.inner(), self.context_lines, self.context_lines)
331                         })
332                         .collect::<Result<Vec<Box<dyn SpanContents<'_>>>, MietteError>>()
333                         .map_err(|_| fmt::Error)?;
334                     let mut contexts = Vec::with_capacity(contents.len());
335                     for (right, right_conts) in labels.iter().cloned().zip(contents.iter()) {
336                         if contexts.is_empty() {
337                             contexts.push((right, right_conts));
338                         } else {
339                             let (left, left_conts) = contexts.last().unwrap().clone();
340                             let left_end = left.offset() + left.len();
341                             let right_end = right.offset() + right.len();
342                             if left_conts.line() + left_conts.line_count() >= right_conts.line() {
343                                 // The snippets will overlap, so we create one Big Chunky Boi
344                                 let new_span = LabeledSpan::new(
345                                     left.label().map(String::from),
346                                     left.offset(),
347                                     if right_end >= left_end {
348                                         // Right end goes past left end
349                                         right_end - left.offset()
350                                     } else {
351                                         // right is contained inside left
352                                         left.len()
353                                     },
354                                 );
355                                 if source
356                                     .read_span(
357                                         new_span.inner(),
358                                         self.context_lines,
359                                         self.context_lines,
360                                     )
361                                     .is_ok()
362                                 {
363                                     contexts.pop();
364                                     contexts.push((
365                                         // We'll throw this away later
366                                         new_span, left_conts,
367                                     ));
368                                 } else {
369                                     contexts.push((right, right_conts));
370                                 }
371                             } else {
372                                 contexts.push((right, right_conts));
373                             }
374                         }
375                     }
376                     for (ctx, _) in contexts {
377                         self.render_context(f, source, &ctx, &labels[..])?;
378                     }
379                 }
380             }
381         }
382         Ok(())
383     }
384 
385     fn render_context<'a>(
386         &self,
387         f: &mut impl fmt::Write,
388         source: &'a dyn SourceCode,
389         context: &LabeledSpan,
390         labels: &[LabeledSpan],
391     ) -> fmt::Result {
392         let (contents, lines) = self.get_lines(source, context.inner())?;
393 
394         // sorting is your friend
395         let labels = labels
396             .iter()
397             .zip(self.theme.styles.highlights.iter().cloned().cycle())
398             .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
399             .collect::<Vec<_>>();
400 
401         // The max number of gutter-lines that will be active at any given
402         // point. We need this to figure out indentation, so we do one loop
403         // over the lines to see what the damage is gonna be.
404         let mut max_gutter = 0usize;
405         for line in &lines {
406             let mut num_highlights = 0;
407             for hl in &labels {
408                 if !line.span_line_only(hl) && line.span_applies(hl) {
409                     num_highlights += 1;
410                 }
411             }
412             max_gutter = std::cmp::max(max_gutter, num_highlights);
413         }
414 
415         // Oh and one more thing: We need to figure out how much room our line
416         // numbers need!
417         let linum_width = lines[..]
418             .last()
419             .map(|line| line.line_number)
420             // It's possible for the source to be an empty string.
421             .unwrap_or(0)
422             .to_string()
423             .len();
424 
425         // Header
426         write!(
427             f,
428             "{}{}{}",
429             " ".repeat(linum_width + 2),
430             self.theme.characters.ltop,
431             self.theme.characters.hbar,
432         )?;
433 
434         if let Some(source_name) = contents.name() {
435             let source_name = source_name.style(self.theme.styles.link);
436             writeln!(
437                 f,
438                 "[{}:{}:{}]",
439                 source_name,
440                 contents.line() + 1,
441                 contents.column() + 1
442             )?;
443         } else if lines.len() <= 1 {
444             writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
445         } else {
446             writeln!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
447         }
448 
449         // Now it's time for the fun part--actually rendering everything!
450         for line in &lines {
451             // Line number, appropriately padded.
452             self.write_linum(f, linum_width, line.line_number)?;
453 
454             // Then, we need to print the gutter, along with any fly-bys We
455             // have separate gutters depending on whether we're on the actual
456             // line, or on one of the "highlight lines" below it.
457             self.render_line_gutter(f, max_gutter, line, &labels)?;
458 
459             // And _now_ we can print out the line text itself!
460             self.render_line_text(f, &line.text)?;
461 
462             // Next, we write all the highlights that apply to this particular line.
463             let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
464                 .iter()
465                 .filter(|hl| line.span_applies(hl))
466                 .partition(|hl| line.span_line_only(hl));
467             if !single_line.is_empty() {
468                 // no line number!
469                 self.write_no_linum(f, linum_width)?;
470                 // gutter _again_
471                 self.render_highlight_gutter(f, max_gutter, line, &labels)?;
472                 self.render_single_line_highlights(
473                     f,
474                     line,
475                     linum_width,
476                     max_gutter,
477                     &single_line,
478                     &labels,
479                 )?;
480             }
481             for hl in multi_line {
482                 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
483                     // no line number!
484                     self.write_no_linum(f, linum_width)?;
485                     // gutter _again_
486                     self.render_highlight_gutter(f, max_gutter, line, &labels)?;
487                     self.render_multi_line_end(f, hl)?;
488                 }
489             }
490         }
491         writeln!(
492             f,
493             "{}{}{}",
494             " ".repeat(linum_width + 2),
495             self.theme.characters.lbot,
496             self.theme.characters.hbar.to_string().repeat(4),
497         )?;
498         Ok(())
499     }
500 
501     fn render_line_gutter(
502         &self,
503         f: &mut impl fmt::Write,
504         max_gutter: usize,
505         line: &Line,
506         highlights: &[FancySpan],
507     ) -> fmt::Result {
508         if max_gutter == 0 {
509             return Ok(());
510         }
511         let chars = &self.theme.characters;
512         let mut gutter = String::new();
513         let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
514         let mut arrow = false;
515         for (i, hl) in applicable.enumerate() {
516             if line.span_starts(hl) {
517                 gutter.push_str(&chars.ltop.style(hl.style).to_string());
518                 gutter.push_str(
519                     &chars
520                         .hbar
521                         .to_string()
522                         .repeat(max_gutter.saturating_sub(i))
523                         .style(hl.style)
524                         .to_string(),
525                 );
526                 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
527                 arrow = true;
528                 break;
529             } else if line.span_ends(hl) {
530                 if hl.label().is_some() {
531                     gutter.push_str(&chars.lcross.style(hl.style).to_string());
532                 } else {
533                     gutter.push_str(&chars.lbot.style(hl.style).to_string());
534                 }
535                 gutter.push_str(
536                     &chars
537                         .hbar
538                         .to_string()
539                         .repeat(max_gutter.saturating_sub(i))
540                         .style(hl.style)
541                         .to_string(),
542                 );
543                 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
544                 arrow = true;
545                 break;
546             } else if line.span_flyby(hl) {
547                 gutter.push_str(&chars.vbar.style(hl.style).to_string());
548             } else {
549                 gutter.push(' ');
550             }
551         }
552         write!(
553             f,
554             "{}{}",
555             gutter,
556             " ".repeat(
557                 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
558             )
559         )?;
560         Ok(())
561     }
562 
563     fn render_highlight_gutter(
564         &self,
565         f: &mut impl fmt::Write,
566         max_gutter: usize,
567         line: &Line,
568         highlights: &[FancySpan],
569     ) -> fmt::Result {
570         if max_gutter == 0 {
571             return Ok(());
572         }
573         let chars = &self.theme.characters;
574         let mut gutter = String::new();
575         let applicable = highlights.iter().filter(|hl| line.span_applies(hl));
576         for (i, hl) in applicable.enumerate() {
577             if !line.span_line_only(hl) && line.span_ends(hl) {
578                 gutter.push_str(&chars.lbot.style(hl.style).to_string());
579                 gutter.push_str(
580                     &chars
581                         .hbar
582                         .to_string()
583                         .repeat(max_gutter.saturating_sub(i) + 2)
584                         .style(hl.style)
585                         .to_string(),
586                 );
587                 break;
588             } else {
589                 gutter.push_str(&chars.vbar.style(hl.style).to_string());
590             }
591         }
592         write!(f, "{:width$}", gutter, width = max_gutter + 1)?;
593         Ok(())
594     }
595 
596     fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
597         write!(
598             f,
599             " {:width$} {} ",
600             linum.style(self.theme.styles.linum),
601             self.theme.characters.vbar,
602             width = width
603         )?;
604         Ok(())
605     }
606 
607     fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
608         write!(
609             f,
610             " {:width$} {} ",
611             "",
612             self.theme.characters.vbar_break,
613             width = width
614         )?;
615         Ok(())
616     }
617 
618     /// Returns an iterator over the visual width of each character in a line.
619     fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
620         let mut column = 0;
621         let tab_width = self.tab_width;
622         text.chars().map(move |c| {
623             let width = if c == '\t' {
624                 // Round up to the next multiple of tab_width
625                 tab_width - column % tab_width
626             } else {
627                 c.width().unwrap_or(0)
628             };
629             column += width;
630             width
631         })
632     }
633 
634     /// Returns the visual column position of a byte offset on a specific line.
635     fn visual_offset(&self, line: &Line, offset: usize) -> usize {
636         let line_range = line.offset..=(line.offset + line.length);
637         assert!(line_range.contains(&offset));
638 
639         let text_index = offset - line.offset;
640         let text = &line.text[..text_index.min(line.text.len())];
641         let text_width = self.line_visual_char_width(text).sum();
642         if text_index > line.text.len() {
643             // Spans extending past the end of the line are always rendered as
644             // one column past the end of the visible line.
645             //
646             // This doesn't necessarily correspond to a specific byte-offset,
647             // since a span extending past the end of the line could contain:
648             //  - an actual \n character (1 byte)
649             //  - a CRLF (2 bytes)
650             //  - EOF (0 bytes)
651             text_width + 1
652         } else {
653             text_width
654         }
655     }
656 
657     /// Renders a line to the output formatter, replacing tabs with spaces.
658     fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
659         for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
660             if c == '\t' {
661                 for _ in 0..width {
662                     f.write_char(' ')?
663                 }
664             } else {
665                 f.write_char(c)?
666             }
667         }
668         f.write_char('\n')?;
669         Ok(())
670     }
671 
672     fn render_single_line_highlights(
673         &self,
674         f: &mut impl fmt::Write,
675         line: &Line,
676         linum_width: usize,
677         max_gutter: usize,
678         single_liners: &[&FancySpan],
679         all_highlights: &[FancySpan],
680     ) -> fmt::Result {
681         let mut underlines = String::new();
682         let mut highest = 0;
683 
684         let chars = &self.theme.characters;
685         let vbar_offsets: Vec<_> = single_liners
686             .iter()
687             .map(|hl| {
688                 let byte_start = hl.offset();
689                 let byte_end = hl.offset() + hl.len();
690                 let start = self.visual_offset(line, byte_start).max(highest);
691                 let end = self.visual_offset(line, byte_end).max(start + 1);
692 
693                 let vbar_offset = (start + end) / 2;
694                 let num_left = vbar_offset - start;
695                 let num_right = end - vbar_offset - 1;
696                 if start < end {
697                     underlines.push_str(
698                         &format!(
699                             "{:width$}{}{}{}",
700                             "",
701                             chars.underline.to_string().repeat(num_left),
702                             if hl.len() == 0 {
703                                 chars.uarrow
704                             } else if hl.label().is_some() {
705                                 chars.underbar
706                             } else {
707                                 chars.underline
708                             },
709                             chars.underline.to_string().repeat(num_right),
710                             width = start.saturating_sub(highest),
711                         )
712                         .style(hl.style)
713                         .to_string(),
714                     );
715                 }
716                 highest = std::cmp::max(highest, end);
717 
718                 (hl, vbar_offset)
719             })
720             .collect();
721         writeln!(f, "{}", underlines)?;
722 
723         for hl in single_liners.iter().rev() {
724             if let Some(label) = hl.label() {
725                 self.write_no_linum(f, linum_width)?;
726                 self.render_highlight_gutter(f, max_gutter, line, all_highlights)?;
727                 let mut curr_offset = 1usize;
728                 for (offset_hl, vbar_offset) in &vbar_offsets {
729                     while curr_offset < *vbar_offset + 1 {
730                         write!(f, " ")?;
731                         curr_offset += 1;
732                     }
733                     if *offset_hl != hl {
734                         write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
735                         curr_offset += 1;
736                     } else {
737                         let lines = format!(
738                             "{}{} {}",
739                             chars.lbot,
740                             chars.hbar.to_string().repeat(2),
741                             label,
742                         );
743                         writeln!(f, "{}", lines.style(hl.style))?;
744                         break;
745                     }
746                 }
747             }
748         }
749         Ok(())
750     }
751 
752     fn render_multi_line_end(&self, f: &mut impl fmt::Write, hl: &FancySpan) -> fmt::Result {
753         writeln!(
754             f,
755             "{} {}",
756             self.theme.characters.hbar.style(hl.style),
757             hl.label().unwrap_or_else(|| "".into()),
758         )?;
759         Ok(())
760     }
761 
762     fn get_lines<'a>(
763         &'a self,
764         source: &'a dyn SourceCode,
765         context_span: &'a SourceSpan,
766     ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
767         let context_data = source
768             .read_span(context_span, self.context_lines, self.context_lines)
769             .map_err(|_| fmt::Error)?;
770         let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
771         let mut line = context_data.line();
772         let mut column = context_data.column();
773         let mut offset = context_data.span().offset();
774         let mut line_offset = offset;
775         let mut iter = context.chars().peekable();
776         let mut line_str = String::new();
777         let mut lines = Vec::new();
778         while let Some(char) = iter.next() {
779             offset += char.len_utf8();
780             let mut at_end_of_file = false;
781             match char {
782                 '\r' => {
783                     if iter.next_if_eq(&'\n').is_some() {
784                         offset += 1;
785                         line += 1;
786                         column = 0;
787                     } else {
788                         line_str.push(char);
789                         column += 1;
790                     }
791                     at_end_of_file = iter.peek().is_none();
792                 }
793                 '\n' => {
794                     at_end_of_file = iter.peek().is_none();
795                     line += 1;
796                     column = 0;
797                 }
798                 _ => {
799                     line_str.push(char);
800                     column += 1;
801                 }
802             }
803 
804             if iter.peek().is_none() && !at_end_of_file {
805                 line += 1;
806             }
807 
808             if column == 0 || iter.peek().is_none() {
809                 lines.push(Line {
810                     line_number: line,
811                     offset: line_offset,
812                     length: offset - line_offset,
813                     text: line_str.clone(),
814                 });
815                 line_str.clear();
816                 line_offset = offset;
817             }
818         }
819         Ok((context_data, lines))
820     }
821 }
822 
823 impl ReportHandler for GraphicalReportHandler {
824     fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
825         if f.alternate() {
826             return fmt::Debug::fmt(diagnostic, f);
827         }
828 
829         self.render_report(f, diagnostic)
830     }
831 }
832 
833 /*
834 Support types
835 */
836 
837 #[derive(Debug)]
838 struct Line {
839     line_number: usize,
840     offset: usize,
841     length: usize,
842     text: String,
843 }
844 
845 impl Line {
846     fn span_line_only(&self, span: &FancySpan) -> bool {
847         span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
848     }
849 
850     fn span_applies(&self, span: &FancySpan) -> bool {
851         let spanlen = if span.len() == 0 { 1 } else { span.len() };
852         // Span starts in this line
853         (span.offset() >= self.offset && span.offset() < self.offset + self.length)
854         // Span passes through this line
855         || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) //todo
856         // Span ends on this line
857         || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
858     }
859 
860     // A 'flyby' is a multi-line span that technically covers this line, but
861     // does not begin or end within the line itself. This method is used to
862     // calculate gutters.
863     fn span_flyby(&self, span: &FancySpan) -> bool {
864         // The span itself starts before this line's starting offset (so, in a
865         // prev line).
866         span.offset() < self.offset
867             // ...and it stops after this line's end.
868             && span.offset() + span.len() > self.offset + self.length
869     }
870 
871     // Does this line contain the *beginning* of this multiline span?
872     // This assumes self.span_applies() is true already.
873     fn span_starts(&self, span: &FancySpan) -> bool {
874         span.offset() >= self.offset
875     }
876 
877     // Does this line contain the *end* of this multiline span?
878     // This assumes self.span_applies() is true already.
879     fn span_ends(&self, span: &FancySpan) -> bool {
880         span.offset() + span.len() >= self.offset
881             && span.offset() + span.len() <= self.offset + self.length
882     }
883 }
884 
885 #[derive(Debug, Clone)]
886 struct FancySpan {
887     label: Option<String>,
888     span: SourceSpan,
889     style: Style,
890 }
891 
892 impl PartialEq for FancySpan {
893     fn eq(&self, other: &Self) -> bool {
894         self.label == other.label && self.span == other.span
895     }
896 }
897 
898 impl FancySpan {
899     fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
900         FancySpan { label, span, style }
901     }
902 
903     fn style(&self) -> Style {
904         self.style
905     }
906 
907     fn label(&self) -> Option<String> {
908         self.label
909             .as_ref()
910             .map(|l| l.style(self.style()).to_string())
911     }
912 
913     fn offset(&self) -> usize {
914         self.span.offset()
915     }
916 
917     fn len(&self) -> usize {
918         self.span.len()
919     }
920 }
921