1use std::fmt::{self, Write};
2
3use owo_colors::{OwoColorize, Style, StyledList};
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
7use crate::handlers::theme::*;
8use crate::highlighters::{Highlighter, MietteHighlighter};
9use crate::protocol::{Diagnostic, Severity};
10use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
11
12#[derive(Debug, Clone)]
26pub 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) with_primary_span_start: bool,
37 pub(crate) word_separator: Option<textwrap::WordSeparator>,
38 pub(crate) word_splitter: Option<textwrap::WordSplitter>,
39 pub(crate) highlighter: MietteHighlighter,
40 pub(crate) link_display_text: Option<String>,
41 pub(crate) show_related_as_nested: bool,
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub(crate) enum LinkStyle {
46 None,
47 Link,
48 Text,
49}
50
51impl GraphicalReportHandler {
52 pub fn new() -> Self {
55 Self {
56 links: LinkStyle::Link,
57 termwidth: 200,
58 theme: GraphicalTheme::default(),
59 footer: None,
60 context_lines: 1,
61 tab_width: 4,
62 with_cause_chain: true,
63 wrap_lines: true,
64 break_words: true,
65 with_primary_span_start: true,
66 word_separator: None,
67 word_splitter: None,
68 highlighter: MietteHighlighter::default(),
69 link_display_text: None,
70 show_related_as_nested: false,
71 }
72 }
73
74 pub fn new_themed(theme: GraphicalTheme) -> Self {
76 Self {
77 links: LinkStyle::Link,
78 termwidth: 200,
79 theme,
80 footer: None,
81 context_lines: 1,
82 tab_width: 4,
83 wrap_lines: true,
84 with_cause_chain: true,
85 with_primary_span_start: true,
86 break_words: true,
87 word_separator: None,
88 word_splitter: None,
89 highlighter: MietteHighlighter::default(),
90 link_display_text: None,
91 show_related_as_nested: false,
92 }
93 }
94
95 pub fn tab_width(mut self, width: usize) -> Self {
97 self.tab_width = width;
98 self
99 }
100
101 pub fn with_links(mut self, links: bool) -> Self {
103 self.links = if links {
104 LinkStyle::Link
105 } else {
106 LinkStyle::Text
107 };
108 self
109 }
110
111 pub fn with_cause_chain(mut self) -> Self {
114 self.with_cause_chain = true;
115 self
116 }
117
118 pub fn without_cause_chain(mut self) -> Self {
121 self.with_cause_chain = false;
122 self
123 }
124
125 pub fn with_primary_span_start(mut self) -> Self {
128 self.with_primary_span_start = true;
129 self
130 }
131
132 pub fn without_primary_span_start(mut self) -> Self {
135 self.with_primary_span_start = false;
136 self
137 }
138
139 pub fn with_urls(mut self, urls: bool) -> Self {
144 self.links = match (self.links, urls) {
145 (_, false) => LinkStyle::None,
146 (LinkStyle::None, true) => LinkStyle::Link,
147 (links, true) => links,
148 };
149 self
150 }
151
152 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
154 self.theme = theme;
155 self
156 }
157
158 pub fn with_width(mut self, width: usize) -> Self {
160 self.termwidth = width;
161 self
162 }
163
164 pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
166 self.wrap_lines = wrap_lines;
167 self
168 }
169
170 pub fn with_break_words(mut self, break_words: bool) -> Self {
172 self.break_words = break_words;
173 self
174 }
175
176 pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
178 self.word_separator = Some(word_separator);
179 self
180 }
181
182 pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
184 self.word_splitter = Some(word_splitter);
185 self
186 }
187
188 pub fn with_footer(mut self, footer: String) -> Self {
190 self.footer = Some(footer);
191 self
192 }
193
194 pub fn with_context_lines(mut self, lines: usize) -> Self {
196 self.context_lines = lines;
197 self
198 }
199
200 pub fn with_show_related_as_nested(mut self, show_related_as_nested: bool) -> Self {
202 self.show_related_as_nested = show_related_as_nested;
203 self
204 }
205
206 pub fn with_syntax_highlighting(
210 mut self,
211 highlighter: impl Highlighter + Send + Sync + 'static,
212 ) -> Self {
213 self.highlighter = MietteHighlighter::from(highlighter);
214 self
215 }
216
217 pub fn without_syntax_highlighting(mut self) -> Self {
220 self.highlighter = MietteHighlighter::nocolor();
221 self
222 }
223
224 pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
227 self.link_display_text = Some(text.into());
228 self
229 }
230}
231
232impl Default for GraphicalReportHandler {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238impl GraphicalReportHandler {
239 pub fn render_report(
243 &self,
244 f: &mut impl fmt::Write,
245 diagnostic: &(dyn Diagnostic),
246 ) -> fmt::Result {
247 self.render_report_inner(f, diagnostic, diagnostic.source_code())
248 }
249
250 fn render_report_inner(
251 &self,
252 f: &mut impl fmt::Write,
253 diagnostic: &(dyn Diagnostic),
254 parent_src: Option<&dyn SourceCode>,
255 ) -> fmt::Result {
256 let src = diagnostic.source_code().or(parent_src);
257 self.render_header(f, diagnostic, false)?;
258 self.render_causes(f, diagnostic, src)?;
259 self.render_snippets(f, diagnostic, src)?;
260 self.render_footer(f, diagnostic)?;
261 self.render_related(f, diagnostic, src)?;
262 if let Some(footer) = &self.footer {
263 writeln!(f)?;
264 let width = self.termwidth.saturating_sub(2);
265 let mut opts = textwrap::Options::new(width)
266 .initial_indent(" ")
267 .subsequent_indent(" ")
268 .break_words(self.break_words);
269 if let Some(word_separator) = self.word_separator {
270 opts = opts.word_separator(word_separator);
271 }
272 if let Some(word_splitter) = self.word_splitter.clone() {
273 opts = opts.word_splitter(word_splitter);
274 }
275
276 writeln!(f, "{}", self.wrap(footer, opts))?;
277 }
278 Ok(())
279 }
280
281 fn render_header(
282 &self,
283 f: &mut impl fmt::Write,
284 diagnostic: &(dyn Diagnostic),
285 is_nested: bool,
286 ) -> fmt::Result {
287 let severity_style = match diagnostic.severity() {
288 Some(Severity::Error) | None => self.theme.styles.error,
289 Some(Severity::Warning) => self.theme.styles.warning,
290 Some(Severity::Advice) => self.theme.styles.advice,
291 };
292 let mut header = String::new();
293 let mut need_newline = is_nested;
294 if self.links == LinkStyle::Link && diagnostic.url().is_some() {
295 let url = diagnostic.url().unwrap(); let code = if let Some(code) = diagnostic.code() {
297 format!("{} ", code)
298 } else {
299 "".to_string()
300 };
301 let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
302 let link = format!(
303 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
304 url,
305 code.style(severity_style),
306 display_text.style(self.theme.styles.link)
307 );
308 write!(header, "{}", link)?;
309 writeln!(f, "{}", header)?;
310 need_newline = true;
311 } else if let Some(code) = diagnostic.code() {
312 write!(header, "{}", code.style(severity_style),)?;
313 if self.links == LinkStyle::Text && diagnostic.url().is_some() {
314 let url = diagnostic.url().unwrap(); write!(header, " ({})", url.style(self.theme.styles.link))?;
316 }
317 writeln!(f, "{}", header)?;
318 need_newline = true;
319 }
320 if need_newline {
321 writeln!(f)?;
322 }
323 Ok(())
324 }
325
326 fn render_causes(
327 &self,
328 f: &mut impl fmt::Write,
329 diagnostic: &(dyn Diagnostic),
330 parent_src: Option<&dyn SourceCode>,
331 ) -> fmt::Result {
332 let src = diagnostic.source_code().or(parent_src);
333
334 let (severity_style, severity_icon) = match diagnostic.severity() {
335 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
336 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
337 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
338 };
339
340 let initial_indent = format!(" {} ", severity_icon.style(severity_style));
341 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
342 let width = self.termwidth.saturating_sub(2);
343 let mut opts = textwrap::Options::new(width)
344 .initial_indent(&initial_indent)
345 .subsequent_indent(&rest_indent)
346 .break_words(self.break_words);
347 if let Some(word_separator) = self.word_separator {
348 opts = opts.word_separator(word_separator);
349 }
350 if let Some(word_splitter) = self.word_splitter.clone() {
351 opts = opts.word_splitter(word_splitter);
352 }
353
354 writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
355
356 if !self.with_cause_chain {
357 return Ok(());
358 }
359
360 if let Some(mut cause_iter) = diagnostic
361 .diagnostic_source()
362 .map(DiagnosticChain::from_diagnostic)
363 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
364 .map(|it| it.peekable())
365 {
366 while let Some(error) = cause_iter.next() {
367 let is_last = cause_iter.peek().is_none();
368 let char = if !is_last {
369 self.theme.characters.lcross
370 } else {
371 self.theme.characters.lbot
372 };
373 let initial_indent = format!(
374 " {}{}{} ",
375 char, self.theme.characters.hbar, self.theme.characters.rarrow
376 )
377 .style(severity_style)
378 .to_string();
379 let rest_indent = format!(
380 " {} ",
381 if is_last {
382 ' '
383 } else {
384 self.theme.characters.vbar
385 }
386 )
387 .style(severity_style)
388 .to_string();
389 let mut opts = textwrap::Options::new(width)
390 .initial_indent(&initial_indent)
391 .subsequent_indent(&rest_indent)
392 .break_words(self.break_words);
393 if let Some(word_separator) = self.word_separator {
394 opts = opts.word_separator(word_separator);
395 }
396 if let Some(word_splitter) = self.word_splitter.clone() {
397 opts = opts.word_splitter(word_splitter);
398 }
399
400 match error {
401 ErrorKind::Diagnostic(diag) => {
402 let mut inner = String::new();
403
404 let mut inner_renderer = self.clone();
405 inner_renderer.footer = None;
407 inner_renderer.with_cause_chain = false;
409 inner_renderer.termwidth -= rest_indent.width();
411 inner_renderer.render_report_inner(&mut inner, diag, src)?;
412
413 let inner = inner.trim_start_matches('\n');
415 writeln!(f, "{}", self.wrap(inner, opts))?;
416 }
417 ErrorKind::StdError(err) => {
418 writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
419 }
420 }
421 }
422 }
423
424 Ok(())
425 }
426
427 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
428 if let Some(help) = diagnostic.help() {
429 let width = self.termwidth.saturating_sub(2);
430 let initial_indent = " help: ".style(self.theme.styles.help).to_string();
431 let mut opts = textwrap::Options::new(width)
432 .initial_indent(&initial_indent)
433 .subsequent_indent(" ")
434 .break_words(self.break_words);
435 if let Some(word_separator) = self.word_separator {
436 opts = opts.word_separator(word_separator);
437 }
438 if let Some(word_splitter) = self.word_splitter.clone() {
439 opts = opts.word_splitter(word_splitter);
440 }
441
442 writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
443 }
444 Ok(())
445 }
446
447 fn render_related(
448 &self,
449 f: &mut impl fmt::Write,
450 diagnostic: &(dyn Diagnostic),
451 parent_src: Option<&dyn SourceCode>,
452 ) -> fmt::Result {
453 let src = diagnostic.source_code().or(parent_src);
454
455 if let Some(related) = diagnostic.related() {
456 let severity_style = match diagnostic.severity() {
457 Some(Severity::Error) | None => self.theme.styles.error,
458 Some(Severity::Warning) => self.theme.styles.warning,
459 Some(Severity::Advice) => self.theme.styles.advice,
460 };
461
462 let mut inner_renderer = self.clone();
463 inner_renderer.with_cause_chain = true;
465 if self.show_related_as_nested {
466 let width = self.termwidth.saturating_sub(2);
467 let mut related = related.peekable();
468 while let Some(rel) = related.next() {
469 let is_last = related.peek().is_none();
470 let char = if !is_last {
471 self.theme.characters.lcross
472 } else {
473 self.theme.characters.lbot
474 };
475 let initial_indent = format!(
476 " {}{}{} ",
477 char, self.theme.characters.hbar, self.theme.characters.rarrow
478 )
479 .style(severity_style)
480 .to_string();
481 let rest_indent = format!(
482 " {} ",
483 if is_last {
484 ' '
485 } else {
486 self.theme.characters.vbar
487 }
488 )
489 .style(severity_style)
490 .to_string();
491
492 let mut opts = textwrap::Options::new(width)
493 .initial_indent(&initial_indent)
494 .subsequent_indent(&rest_indent)
495 .break_words(self.break_words);
496 if let Some(word_separator) = self.word_separator {
497 opts = opts.word_separator(word_separator);
498 }
499 if let Some(word_splitter) = self.word_splitter.clone() {
500 opts = opts.word_splitter(word_splitter);
501 }
502
503 let mut inner = String::new();
504
505 let mut inner_renderer = self.clone();
506 inner_renderer.footer = None;
507 inner_renderer.with_cause_chain = false;
508 inner_renderer.termwidth -= rest_indent.width();
509 inner_renderer.render_report_inner(&mut inner, rel, src)?;
510
511 let inner = inner.trim_matches('\n');
513 writeln!(f, "{}", self.wrap(inner, opts))?;
514 }
515 } else {
516 for rel in related {
517 writeln!(f)?;
518 match rel.severity() {
519 Some(Severity::Error) | None => write!(f, "Error: ")?,
520 Some(Severity::Warning) => write!(f, "Warning: ")?,
521 Some(Severity::Advice) => write!(f, "Advice: ")?,
522 };
523 inner_renderer.render_header(f, rel, true)?;
524 let src = rel.source_code().or(parent_src);
525 inner_renderer.render_causes(f, rel, src)?;
526 inner_renderer.render_snippets(f, rel, src)?;
527 inner_renderer.render_footer(f, rel)?;
528 inner_renderer.render_related(f, rel, src)?;
529 }
530 }
531 }
532 Ok(())
533 }
534
535 fn render_snippets(
536 &self,
537 f: &mut impl fmt::Write,
538 diagnostic: &(dyn Diagnostic),
539 opt_source: Option<&dyn SourceCode>,
540 ) -> fmt::Result {
541 let source = match opt_source {
542 Some(source) => source,
543 None => return Ok(()),
544 };
545 let labels = match diagnostic.labels() {
546 Some(labels) => labels,
547 None => return Ok(()),
548 };
549
550 let mut labels = labels.collect::<Vec<_>>();
551 labels.sort_unstable_by_key(|l| l.inner().offset());
552
553 let mut contexts = Vec::with_capacity(labels.len());
554 for right in labels.iter().cloned() {
555 let right_conts =
556 match source.read_span(right.inner(), self.context_lines, self.context_lines) {
557 Ok(cont) => cont,
558 Err(err) => {
559 writeln!(
560 f,
561 " [{} `{}` (offset: {}, length: {}): {:?}]",
562 "Failed to read contents for label".style(self.theme.styles.error),
563 right
564 .label()
565 .unwrap_or("<none>")
566 .style(self.theme.styles.link),
567 right.offset().style(self.theme.styles.link),
568 right.len().style(self.theme.styles.link),
569 err.style(self.theme.styles.warning)
570 )?;
571 return Ok(());
572 }
573 };
574
575 if contexts.is_empty() {
576 contexts.push((right, right_conts));
577 continue;
578 }
579
580 let (left, left_conts) = contexts.last().unwrap();
581 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
582 let left_end = left.offset() + left.len();
584 let right_end = right.offset() + right.len();
585 let new_end = std::cmp::max(left_end, right_end);
586
587 let new_span = LabeledSpan::new(
588 left.label().map(String::from),
589 left.offset(),
590 new_end - left.offset(),
591 );
592 if let Ok(new_conts) =
594 source.read_span(new_span.inner(), self.context_lines, self.context_lines)
595 {
596 contexts.pop();
597 contexts.push((new_span, new_conts));
599 continue;
600 }
601 }
602
603 contexts.push((right, right_conts));
604 }
605 for (ctx, _) in contexts {
606 self.render_context(f, source, &ctx, &labels[..])?;
607 }
608
609 Ok(())
610 }
611
612 fn render_context(
613 &self,
614 f: &mut impl fmt::Write,
615 source: &dyn SourceCode,
616 context: &LabeledSpan,
617 labels: &[LabeledSpan],
618 ) -> fmt::Result {
619 let (contents, lines) = self.get_lines(source, context.inner())?;
620
621 let ctx_labels = labels.iter().filter(|l| {
623 context.inner().offset() <= l.inner().offset()
624 && l.inner().offset() + l.inner().len()
625 <= context.inner().offset() + context.inner().len()
626 });
627 let primary_label = ctx_labels
628 .clone()
629 .find(|label| label.primary())
630 .or_else(|| ctx_labels.clone().next());
631
632 let labels = labels
634 .iter()
635 .zip(self.theme.styles.highlights.iter().cloned().cycle())
636 .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
637 .collect::<Vec<_>>();
638
639 let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
640
641 let mut max_gutter = 0usize;
645 for line in &lines {
646 let mut num_highlights = 0;
647 for hl in &labels {
648 if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
649 num_highlights += 1;
650 }
651 }
652 max_gutter = std::cmp::max(max_gutter, num_highlights);
653 }
654
655 let linum_width = lines[..]
658 .last()
659 .map(|line| line.line_number)
660 .unwrap_or(0)
662 .to_string()
663 .len();
664
665 write!(
667 f,
668 "{}{}{}",
669 " ".repeat(linum_width + 2),
670 self.theme.characters.ltop,
671 self.theme.characters.hbar,
672 )?;
673
674 let primary_contents = match primary_label {
677 Some(label) => source
678 .read_span(label.inner(), 0, 0)
679 .map_err(|_| fmt::Error)?,
680 None => contents,
681 };
682
683 if let Some(source_name) = primary_contents.name() {
684 if self.with_primary_span_start {
685 writeln!(
686 f,
687 "[{}]",
688 format_args!(
689 "{}:{}:{}",
690 source_name,
691 primary_contents.line() + 1,
692 primary_contents.column() + 1
693 )
694 .style(self.theme.styles.link)
695 )?;
696 } else {
697 writeln!(
698 f,
699 "[{}]",
700 format_args!("{}", source_name,).style(self.theme.styles.link)
701 )?;
702 }
703 } else if self.with_primary_span_start && lines.len() > 1 {
704 writeln!(
705 f,
706 "[{}:{}]",
707 primary_contents.line() + 1,
708 primary_contents.column() + 1
709 )?;
710 } else {
711 writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
712 }
713
714 for line in &lines {
716 self.write_linum(f, linum_width, line.line_number)?;
718
719 self.render_line_gutter(f, max_gutter, line, &labels)?;
723
724 let styled_text =
726 StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
727 self.render_line_text(f, &styled_text)?;
728
729 let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
731 .iter()
732 .filter(|hl| line.span_applies(hl))
733 .partition(|hl| line.span_line_only(hl));
734 if !single_line.is_empty() {
735 self.write_no_linum(f, linum_width)?;
737 self.render_highlight_gutter(
739 f,
740 max_gutter,
741 line,
742 &labels,
743 LabelRenderMode::SingleLine,
744 )?;
745 self.render_single_line_highlights(
746 f,
747 line,
748 linum_width,
749 max_gutter,
750 &single_line,
751 &labels,
752 )?;
753 }
754 for hl in multi_line {
755 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
756 self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
757 }
758 }
759 }
760 writeln!(
761 f,
762 "{}{}{}",
763 " ".repeat(linum_width + 2),
764 self.theme.characters.lbot,
765 self.theme.characters.hbar.to_string().repeat(4),
766 )?;
767 Ok(())
768 }
769
770 fn render_multi_line_end(
771 &self,
772 f: &mut impl fmt::Write,
773 labels: &[FancySpan],
774 max_gutter: usize,
775 linum_width: usize,
776 line: &Line,
777 label: &FancySpan,
778 ) -> fmt::Result {
779 self.write_no_linum(f, linum_width)?;
781
782 if let Some(label_parts) = label.label_parts() {
783 let (first, rest) = label_parts
785 .split_first()
786 .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
787
788 if rest.is_empty() {
789 self.render_highlight_gutter(
791 f,
792 max_gutter,
793 line,
794 labels,
795 LabelRenderMode::SingleLine,
796 )?;
797
798 self.render_multi_line_end_single(
799 f,
800 first,
801 label.style,
802 LabelRenderMode::SingleLine,
803 )?;
804 } else {
805 self.render_highlight_gutter(
807 f,
808 max_gutter,
809 line,
810 labels,
811 LabelRenderMode::MultiLineFirst,
812 )?;
813
814 self.render_multi_line_end_single(
815 f,
816 first,
817 label.style,
818 LabelRenderMode::MultiLineFirst,
819 )?;
820 for label_line in rest {
821 self.write_no_linum(f, linum_width)?;
823 self.render_highlight_gutter(
825 f,
826 max_gutter,
827 line,
828 labels,
829 LabelRenderMode::MultiLineRest,
830 )?;
831 self.render_multi_line_end_single(
832 f,
833 label_line,
834 label.style,
835 LabelRenderMode::MultiLineRest,
836 )?;
837 }
838 }
839 } else {
840 self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?;
842 writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
844 }
845
846 Ok(())
847 }
848
849 fn render_line_gutter(
850 &self,
851 f: &mut impl fmt::Write,
852 max_gutter: usize,
853 line: &Line,
854 highlights: &[FancySpan],
855 ) -> fmt::Result {
856 if max_gutter == 0 {
857 return Ok(());
858 }
859 let chars = &self.theme.characters;
860 let mut gutter = String::new();
861 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
862 let mut arrow = false;
863 for (i, hl) in applicable.enumerate() {
864 if line.span_starts(hl) {
865 gutter.push_str(&chars.ltop.style(hl.style).to_string());
866 gutter.push_str(
867 &chars
868 .hbar
869 .to_string()
870 .repeat(max_gutter.saturating_sub(i))
871 .style(hl.style)
872 .to_string(),
873 );
874 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
875 arrow = true;
876 break;
877 } else if line.span_ends(hl) {
878 if hl.label().is_some() {
879 gutter.push_str(&chars.lcross.style(hl.style).to_string());
880 } else {
881 gutter.push_str(&chars.lbot.style(hl.style).to_string());
882 }
883 gutter.push_str(
884 &chars
885 .hbar
886 .to_string()
887 .repeat(max_gutter.saturating_sub(i))
888 .style(hl.style)
889 .to_string(),
890 );
891 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
892 arrow = true;
893 break;
894 } else if line.span_flyby(hl) {
895 gutter.push_str(&chars.vbar.style(hl.style).to_string());
896 } else {
897 gutter.push(' ');
898 }
899 }
900 write!(
901 f,
902 "{}{}",
903 gutter,
904 " ".repeat(
905 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
906 )
907 )?;
908 Ok(())
909 }
910
911 fn render_highlight_gutter(
912 &self,
913 f: &mut impl fmt::Write,
914 max_gutter: usize,
915 line: &Line,
916 highlights: &[FancySpan],
917 render_mode: LabelRenderMode,
918 ) -> fmt::Result {
919 if max_gutter == 0 {
920 return Ok(());
921 }
922
923 let mut gutter_cols = 0;
927
928 let chars = &self.theme.characters;
929 let mut gutter = String::new();
930 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
931 for (i, hl) in applicable.enumerate() {
932 if !line.span_line_only(hl) && line.span_ends(hl) {
933 if render_mode == LabelRenderMode::MultiLineRest {
934 let horizontal_space = max_gutter.saturating_sub(i) + 2;
937 for _ in 0..horizontal_space {
938 gutter.push(' ');
939 }
940 gutter_cols += horizontal_space + 1;
948 } else {
949 let num_repeat = max_gutter.saturating_sub(i) + 2;
950
951 gutter.push_str(&chars.lbot.style(hl.style).to_string());
952
953 gutter.push_str(
954 &chars
955 .hbar
956 .to_string()
957 .repeat(
958 num_repeat
959 - if render_mode == LabelRenderMode::MultiLineFirst {
962 1
963 } else {
964 0
965 },
966 )
967 .style(hl.style)
968 .to_string(),
969 );
970
971 gutter_cols += num_repeat + 1;
976 }
977 break;
978 } else {
979 gutter.push_str(&chars.vbar.style(hl.style).to_string());
980
981 gutter_cols += 1;
984 }
985 }
986
987 let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
992 write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
994 Ok(())
995 }
996
997 fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
998 if self.wrap_lines {
999 textwrap::fill(text, opts)
1000 } else {
1001 let mut result = String::with_capacity(2 * text.len());
1004 let trimmed_indent = opts.subsequent_indent.trim_end();
1005 for (idx, line) in text.split_terminator('\n').enumerate() {
1006 if idx > 0 {
1007 result.push('\n');
1008 }
1009 if idx == 0 {
1010 if line.trim().is_empty() {
1011 result.push_str(opts.initial_indent.trim_end());
1012 } else {
1013 result.push_str(opts.initial_indent);
1014 }
1015 } else if line.trim().is_empty() {
1016 result.push_str(trimmed_indent);
1017 } else {
1018 result.push_str(opts.subsequent_indent);
1019 }
1020 result.push_str(line);
1021 }
1022 if text.ends_with('\n') {
1023 result.push('\n');
1025 }
1026 result
1027 }
1028 }
1029
1030 fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
1031 write!(
1032 f,
1033 " {:width$} {} ",
1034 linum.style(self.theme.styles.linum),
1035 self.theme.characters.vbar,
1036 width = width
1037 )?;
1038 Ok(())
1039 }
1040
1041 fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
1042 write!(
1043 f,
1044 " {:width$} {} ",
1045 "",
1046 self.theme.characters.vbar_break,
1047 width = width
1048 )?;
1049 Ok(())
1050 }
1051
1052 fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
1054 let mut column = 0;
1055 let mut escaped = false;
1056 let tab_width = self.tab_width;
1057 text.chars().map(move |c| {
1058 let width = match (escaped, c) {
1059 (false, '\t') => tab_width - column % tab_width,
1061 (false, '\x1b') => {
1063 escaped = true;
1064 0
1065 }
1066 (false, c) => c.width().unwrap_or(0),
1068 (true, 'm') => {
1070 escaped = false;
1071 0
1072 }
1073 (true, _) => 0,
1075 };
1076 column += width;
1077 width
1078 })
1079 }
1080
1081 fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
1087 let line_range = line.offset..=(line.offset + line.length);
1088 assert!(line_range.contains(&offset));
1089
1090 let mut text_index = offset - line.offset;
1091 while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
1092 if start {
1093 text_index -= 1;
1094 } else {
1095 text_index += 1;
1096 }
1097 }
1098 let text = &line.text[..text_index.min(line.text.len())];
1099 let text_width = self.line_visual_char_width(text).sum();
1100 if text_index > line.text.len() {
1101 text_width + 1
1110 } else {
1111 text_width
1112 }
1113 }
1114
1115 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
1117 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
1118 if c == '\t' {
1119 for _ in 0..width {
1120 f.write_char(' ')?;
1121 }
1122 } else {
1123 f.write_char(c)?;
1124 }
1125 }
1126 f.write_char('\n')?;
1127 Ok(())
1128 }
1129
1130 fn render_single_line_highlights(
1131 &self,
1132 f: &mut impl fmt::Write,
1133 line: &Line,
1134 linum_width: usize,
1135 max_gutter: usize,
1136 single_liners: &[&FancySpan],
1137 all_highlights: &[FancySpan],
1138 ) -> fmt::Result {
1139 let mut underlines = String::new();
1140 let mut highest = 0;
1141
1142 let chars = &self.theme.characters;
1143 let vbar_offsets: Vec<_> = single_liners
1144 .iter()
1145 .map(|hl| {
1146 let byte_start = hl.offset();
1147 let byte_end = hl.offset() + hl.len();
1148 let start = self.visual_offset(line, byte_start, true).max(highest);
1149 let end = if hl.len() == 0 {
1150 start + 1
1151 } else {
1152 self.visual_offset(line, byte_end, false).max(start + 1)
1153 };
1154
1155 let vbar_offset = (start + end) / 2;
1156 let num_left = vbar_offset - start;
1157 let num_right = end - vbar_offset - 1;
1158 underlines.push_str(
1159 &format!(
1160 "{:width$}{}{}{}",
1161 "",
1162 chars.underline.to_string().repeat(num_left),
1163 if hl.len() == 0 {
1164 chars.uarrow
1165 } else if hl.label().is_some() {
1166 chars.underbar
1167 } else {
1168 chars.underline
1169 },
1170 chars.underline.to_string().repeat(num_right),
1171 width = start.saturating_sub(highest),
1172 )
1173 .style(hl.style)
1174 .to_string(),
1175 );
1176 highest = std::cmp::max(highest, end);
1177
1178 (hl, vbar_offset)
1179 })
1180 .collect();
1181 writeln!(f, "{}", underlines)?;
1182
1183 for hl in single_liners.iter().rev() {
1184 if let Some(label) = hl.label_parts() {
1185 if label.len() == 1 {
1186 self.write_label_text(
1187 f,
1188 line,
1189 linum_width,
1190 max_gutter,
1191 all_highlights,
1192 chars,
1193 &vbar_offsets,
1194 hl,
1195 &label[0],
1196 LabelRenderMode::SingleLine,
1197 )?;
1198 } else {
1199 let mut first = true;
1200 for label_line in &label {
1201 self.write_label_text(
1202 f,
1203 line,
1204 linum_width,
1205 max_gutter,
1206 all_highlights,
1207 chars,
1208 &vbar_offsets,
1209 hl,
1210 label_line,
1211 if first {
1212 LabelRenderMode::MultiLineFirst
1213 } else {
1214 LabelRenderMode::MultiLineRest
1215 },
1216 )?;
1217 first = false;
1218 }
1219 }
1220 }
1221 }
1222 Ok(())
1223 }
1224
1225 #[allow(clippy::too_many_arguments)]
1228 fn write_label_text(
1229 &self,
1230 f: &mut impl fmt::Write,
1231 line: &Line,
1232 linum_width: usize,
1233 max_gutter: usize,
1234 all_highlights: &[FancySpan],
1235 chars: &ThemeCharacters,
1236 vbar_offsets: &[(&&FancySpan, usize)],
1237 hl: &&FancySpan,
1238 label: &str,
1239 render_mode: LabelRenderMode,
1240 ) -> fmt::Result {
1241 self.write_no_linum(f, linum_width)?;
1242 self.render_highlight_gutter(
1243 f,
1244 max_gutter,
1245 line,
1246 all_highlights,
1247 LabelRenderMode::SingleLine,
1248 )?;
1249 let mut curr_offset = 1usize;
1250 for (offset_hl, vbar_offset) in vbar_offsets {
1251 while curr_offset < *vbar_offset + 1 {
1252 write!(f, " ")?;
1253 curr_offset += 1;
1254 }
1255 if *offset_hl != hl {
1256 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
1257 curr_offset += 1;
1258 } else {
1259 let lines = match render_mode {
1260 LabelRenderMode::SingleLine => format!(
1261 "{}{} {}",
1262 chars.lbot,
1263 chars.hbar.to_string().repeat(2),
1264 label,
1265 ),
1266 LabelRenderMode::MultiLineFirst => {
1267 format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1268 }
1269 LabelRenderMode::MultiLineRest => {
1270 format!(" {} {}", chars.vbar, label,)
1271 }
1272 };
1273 writeln!(f, "{}", lines.style(hl.style))?;
1274 break;
1275 }
1276 }
1277 Ok(())
1278 }
1279
1280 fn render_multi_line_end_single(
1281 &self,
1282 f: &mut impl fmt::Write,
1283 label: &str,
1284 style: Style,
1285 render_mode: LabelRenderMode,
1286 ) -> fmt::Result {
1287 match render_mode {
1288 LabelRenderMode::SingleLine => {
1289 writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1290 }
1291 LabelRenderMode::MultiLineFirst => {
1292 writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1293 }
1294 LabelRenderMode::MultiLineRest => {
1295 writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1296 }
1297 }
1298
1299 Ok(())
1300 }
1301
1302 fn get_lines<'a>(
1303 &'a self,
1304 source: &'a dyn SourceCode,
1305 context_span: &'a SourceSpan,
1306 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
1307 let context_data = source
1308 .read_span(context_span, self.context_lines, self.context_lines)
1309 .map_err(|_| fmt::Error)?;
1310 let context = String::from_utf8_lossy(context_data.data());
1311 let mut line = context_data.line();
1312 let mut column = context_data.column();
1313 let mut offset = context_data.span().offset();
1314 let mut line_offset = offset;
1315 let mut line_str = String::with_capacity(context.len());
1316 let mut lines = Vec::with_capacity(1);
1317 let mut iter = context.chars().peekable();
1318 while let Some(char) = iter.next() {
1319 offset += char.len_utf8();
1320 let mut at_end_of_file = false;
1321 match char {
1322 '\r' => {
1323 if iter.next_if_eq(&'\n').is_some() {
1324 offset += 1;
1325 line += 1;
1326 column = 0;
1327 } else {
1328 line_str.push(char);
1329 column += 1;
1330 }
1331 at_end_of_file = iter.peek().is_none();
1332 }
1333 '\n' => {
1334 at_end_of_file = iter.peek().is_none();
1335 line += 1;
1336 column = 0;
1337 }
1338 _ => {
1339 line_str.push(char);
1340 column += 1;
1341 }
1342 }
1343
1344 if iter.peek().is_none() && !at_end_of_file {
1345 line += 1;
1346 }
1347
1348 if column == 0 || iter.peek().is_none() {
1349 lines.push(Line {
1350 line_number: line,
1351 offset: line_offset,
1352 length: offset - line_offset,
1353 text: line_str.clone(),
1354 });
1355 line_str.clear();
1356 line_offset = offset;
1357 }
1358 }
1359 Ok((context_data, lines))
1360 }
1361}
1362
1363impl ReportHandler for GraphicalReportHandler {
1364 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
1365 if f.alternate() {
1366 return fmt::Debug::fmt(diagnostic, f);
1367 }
1368
1369 self.render_report(f, diagnostic)
1370 }
1371}
1372
1373#[derive(PartialEq, Debug)]
1378enum LabelRenderMode {
1379 SingleLine,
1381 MultiLineFirst,
1383 MultiLineRest,
1385}
1386
1387#[derive(Debug)]
1388struct Line {
1389 line_number: usize,
1390 offset: usize,
1391 length: usize,
1392 text: String,
1393}
1394
1395impl Line {
1396 fn span_line_only(&self, span: &FancySpan) -> bool {
1397 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
1398 }
1399
1400 fn span_applies(&self, span: &FancySpan) -> bool {
1403 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1404 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1407 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1411 }
1412
1413 fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1416 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1417 self.span_applies(span)
1419 && !(
1420 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1422 && (span.offset() + spanlen > self.offset
1423 && span.offset() + spanlen <= self.offset + self.length)
1424 )
1425 }
1426
1427 fn span_flyby(&self, span: &FancySpan) -> bool {
1431 span.offset() < self.offset
1434 && span.offset() + span.len() > self.offset + self.length
1436 }
1437
1438 fn span_starts(&self, span: &FancySpan) -> bool {
1441 span.offset() >= self.offset
1442 }
1443
1444 fn span_ends(&self, span: &FancySpan) -> bool {
1447 span.offset() + span.len() >= self.offset
1448 && span.offset() + span.len() <= self.offset + self.length
1449 }
1450}
1451
1452#[derive(Debug, Clone)]
1453struct FancySpan {
1454 label: Option<Vec<String>>,
1458 span: SourceSpan,
1459 style: Style,
1460}
1461
1462impl PartialEq for FancySpan {
1463 fn eq(&self, other: &Self) -> bool {
1464 self.label == other.label && self.span == other.span
1465 }
1466}
1467
1468fn split_label(v: String) -> Vec<String> {
1469 v.split('\n').map(|i| i.to_string()).collect()
1470}
1471
1472impl FancySpan {
1473 fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
1474 FancySpan {
1475 label: label.map(split_label),
1476 span,
1477 style,
1478 }
1479 }
1480
1481 fn style(&self) -> Style {
1482 self.style
1483 }
1484
1485 fn label(&self) -> Option<String> {
1486 self.label
1487 .as_ref()
1488 .map(|l| l.join("\n").style(self.style()).to_string())
1489 }
1490
1491 fn label_parts(&self) -> Option<Vec<String>> {
1492 self.label.as_ref().map(|l| {
1493 l.iter()
1494 .map(|i| i.style(self.style()).to_string())
1495 .collect()
1496 })
1497 }
1498
1499 fn offset(&self) -> usize {
1500 self.span.offset()
1501 }
1502
1503 fn len(&self) -> usize {
1504 self.span.len()
1505 }
1506}