1use std::{
2 borrow::Cow,
3 fmt::{self, Write},
4 io::IsTerminal,
5};
6
7use owo_colors::{OwoColorize, Style};
8use unicode_segmentation::UnicodeSegmentation;
9use unicode_width::UnicodeWidthStr;
10
11use crate::{
12 Diagnostic, GraphicalTheme, LabeledSpan, MietteSpanContents, ReportHandler, Severity,
13 SourceCode, SourceSpan, SpanContents, ThemeCharacters,
14};
15
16#[derive(Debug, Clone)]
17pub struct GraphicalReportHandler {
18 pub(crate) links: LinkStyle,
22 pub(crate) termwidth: usize,
26 pub(crate) theme: GraphicalTheme,
28 pub(crate) footer: Option<String>,
29 pub(crate) context_lines: usize,
33 pub(crate) tab_width: usize,
37 pub(crate) with_cause_chain: bool,
39 pub(crate) wrap_lines: bool,
43 pub(crate) break_words: bool,
49 pub(crate) word_separator: Option<textwrap::WordSeparator>,
50 pub(crate) word_splitter: Option<textwrap::WordSplitter>,
51 pub(crate) link_display_text: Option<String>,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) enum LinkStyle {
57 None,
58 Link,
59 Text,
60}
61
62impl GraphicalReportHandler {
63 pub fn new() -> Self {
66 let is_terminal = std::io::stdout().is_terminal() && std::io::stderr().is_terminal();
67 Self {
68 links: if is_terminal { LinkStyle::Link } else { LinkStyle::Text },
69 termwidth: 400,
70 theme: GraphicalTheme::new(is_terminal),
71 footer: None,
72 context_lines: 1,
73 tab_width: 4,
74 with_cause_chain: false,
75 wrap_lines: true,
76 break_words: true,
77 word_separator: None,
78 word_splitter: None,
79 link_display_text: None,
81 }
82 }
83
84 pub fn new_themed(theme: GraphicalTheme) -> Self {
86 Self {
87 links: LinkStyle::Link,
88 termwidth: 200,
89 theme,
90 footer: None,
91 context_lines: 1,
92 tab_width: 4,
93 wrap_lines: true,
94 with_cause_chain: true,
95 break_words: true,
96 word_separator: None,
97 word_splitter: None,
98 link_display_text: None,
100 }
101 }
102
103 pub fn tab_width(mut self, width: usize) -> Self {
105 self.tab_width = width;
106 self
107 }
108
109 pub fn with_links(mut self, links: bool) -> Self {
111 self.links = if links { LinkStyle::Link } else { LinkStyle::Text };
112 self
113 }
114
115 pub fn with_cause_chain(mut self) -> Self {
118 self.with_cause_chain = true;
119 self
120 }
121
122 pub fn without_cause_chain(mut self) -> Self {
125 self.with_cause_chain = false;
126 self
127 }
128
129 pub fn with_urls(mut self, urls: bool) -> Self {
134 self.links = match (self.links, urls) {
135 (_, false) => LinkStyle::None,
136 (LinkStyle::None, true) => LinkStyle::Link,
137 (links, true) => links,
138 };
139 self
140 }
141
142 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
144 self.theme = theme;
145 self
146 }
147
148 pub fn with_width(mut self, width: usize) -> Self {
150 self.termwidth = width;
151 self
152 }
153
154 pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
156 self.wrap_lines = wrap_lines;
157 self
158 }
159
160 pub fn with_break_words(mut self, break_words: bool) -> Self {
162 self.break_words = break_words;
163 self
164 }
165
166 pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
168 self.word_separator = Some(word_separator);
169 self
170 }
171
172 pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
174 self.word_splitter = Some(word_splitter);
175 self
176 }
177
178 pub fn with_footer(mut self, footer: String) -> Self {
180 self.footer = Some(footer);
181 self
182 }
183
184 pub fn with_context_lines(mut self, lines: usize) -> Self {
186 self.context_lines = lines;
187 self
188 }
189
190 pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
210 self.link_display_text = Some(text.into());
211 self
212 }
213}
214
215impl Default for GraphicalReportHandler {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221impl GraphicalReportHandler {
222 pub fn render_report(
226 &self,
227 f: &mut impl fmt::Write,
228 diagnostic: &dyn Diagnostic,
229 ) -> fmt::Result {
230 writeln!(f)?;
232 self.render_causes(f, diagnostic)?;
233 let src = diagnostic.source_code();
234 self.render_snippets(f, diagnostic, src)?;
235 self.render_footer(f, diagnostic)?;
236 self.render_related(f, diagnostic, src)?;
237 if let Some(footer) = &self.footer {
238 writeln!(f)?;
239 let width = self.termwidth.saturating_sub(4);
240 let mut opts = textwrap::Options::new(width)
241 .initial_indent(" ")
242 .subsequent_indent(" ")
243 .break_words(self.break_words);
244 if let Some(word_separator) = self.word_separator {
245 opts = opts.word_separator(word_separator);
246 }
247 if let Some(word_splitter) = self.word_splitter.clone() {
248 opts = opts.word_splitter(word_splitter);
249 }
250
251 writeln!(f, "{}", self.wrap(footer, opts))?;
252 }
253 Ok(())
254 }
255
256 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
257 let severity_style = match diagnostic.severity() {
258 Some(Severity::Error) | None => self.theme.styles.error,
259 Some(Severity::Warning) => self.theme.styles.warning,
260 Some(Severity::Advice) => self.theme.styles.advice,
261 };
262 let mut header = String::new();
263 if self.links == LinkStyle::Link && diagnostic.url().is_some() {
264 let url = diagnostic.url().unwrap(); let code = match diagnostic.code() {
266 Some(code) => {
267 format!("{code} ")
268 }
269 _ => "".to_string(),
270 };
271 let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
272 let link = format!(
273 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
274 url,
275 code.style(severity_style),
276 display_text.style(self.theme.styles.link)
277 );
278 write!(header, "{link}")?;
279 writeln!(f, "{header}")?;
280 writeln!(f)?;
281 } else if let Some(code) = diagnostic.code() {
282 write!(header, "{}", code.style(severity_style),)?;
283 if self.links == LinkStyle::Text && diagnostic.url().is_some() {
284 let url = diagnostic.url().unwrap(); write!(header, " ({})", url.style(self.theme.styles.link))?;
286 }
287 writeln!(f, "{header}")?;
288 writeln!(f)?;
289 }
290 Ok(())
291 }
292
293 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
294 let (severity_style, severity_icon) = match diagnostic.severity() {
295 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
296 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
297 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
298 };
299
300 let initial_indent = format!(" {} ", severity_icon.style(severity_style));
301 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
302 let width = self.termwidth.saturating_sub(2);
303 let mut opts = textwrap::Options::new(width)
304 .initial_indent(&initial_indent)
305 .subsequent_indent(&rest_indent)
306 .break_words(self.break_words);
307 if let Some(word_separator) = self.word_separator {
308 opts = opts.word_separator(word_separator);
309 }
310 if let Some(word_splitter) = self.word_splitter.clone() {
311 opts = opts.word_splitter(word_splitter);
312 }
313
314 let title = match (self.links, diagnostic.url(), diagnostic.code()) {
315 (LinkStyle::Link, Some(url), Some(code)) => {
316 const CTL: &str = "\u{1b}]8;;";
318 const END: &str = "\u{1b}]8;;\u{1b}\\";
319 let code = code.style(severity_style);
320 let message = diagnostic.to_string();
321 let title = message.style(severity_style);
322 format!("{CTL}{url}\u{1b}\\{code}{END}: {title}",)
323 }
324 (_, _, Some(code)) => {
325 let title = format!("{code}: {diagnostic}");
326 format!("{}", title.style(severity_style))
327 }
328 _ => {
329 format!("{}", diagnostic.to_string().style(severity_style))
330 }
331 };
332 let title = textwrap::fill(&title, opts);
333 writeln!(f, "{title}")?;
334
335 Ok(())
394 }
395
396 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
397 if let Some(help) = diagnostic.help() {
398 let width = self.termwidth.saturating_sub(4);
399 let initial_indent = " help: ".style(self.theme.styles.help).to_string();
400 let mut opts = textwrap::Options::new(width)
401 .initial_indent(&initial_indent)
402 .subsequent_indent(" ")
403 .break_words(self.break_words);
404 if let Some(word_separator) = self.word_separator {
405 opts = opts.word_separator(word_separator);
406 }
407 if let Some(word_splitter) = self.word_splitter.clone() {
408 opts = opts.word_splitter(word_splitter);
409 }
410
411 writeln!(f, "{}", self.wrap(&help, opts))?;
412 }
413 if let Some(note) = diagnostic.note() {
414 let width = self.termwidth.saturating_sub(4);
417 let initial_indent = " note: ".style(self.theme.styles.note).to_string();
418 let mut opts = textwrap::Options::new(width)
419 .initial_indent(&initial_indent)
420 .subsequent_indent(" ")
421 .break_words(self.break_words);
422 if let Some(word_separator) = self.word_separator {
423 opts = opts.word_separator(word_separator);
424 }
425 if let Some(word_splitter) = self.word_splitter.clone() {
426 opts = opts.word_splitter(word_splitter);
427 }
428
429 writeln!(f, "{}", self.wrap(¬e, opts))?;
430 }
431 Ok(())
432 }
433
434 fn render_related(
435 &self,
436 f: &mut impl fmt::Write,
437 diagnostic: &dyn Diagnostic,
438 parent_src: Option<&dyn SourceCode>,
439 ) -> fmt::Result {
440 let related = diagnostic.related();
441 if !related.is_empty() {
442 let mut inner_renderer = self.clone();
443 inner_renderer.with_cause_chain = true;
445 writeln!(f)?;
446 for rel in related.iter().copied() {
447 match rel.severity() {
448 Some(Severity::Error) | None => write!(f, "Error: ")?,
449 Some(Severity::Warning) => write!(f, "Warning: ")?,
450 Some(Severity::Advice) => write!(f, "Advice: ")?,
451 };
452 inner_renderer.render_header(f, rel)?;
453 inner_renderer.render_causes(f, rel)?;
454 let src = rel.source_code().or(parent_src);
455 inner_renderer.render_snippets(f, rel, src)?;
456 inner_renderer.render_footer(f, rel)?;
457 inner_renderer.render_related(f, rel, src)?;
458 }
459 }
460 Ok(())
461 }
462
463 fn render_snippets(
464 &self,
465 f: &mut impl fmt::Write,
466 diagnostic: &dyn Diagnostic,
467 opt_source: Option<&dyn SourceCode>,
468 ) -> fmt::Result {
469 let source = match opt_source {
470 Some(source) => source,
471 None => return Ok(()),
472 };
473 let mut labels = diagnostic.labels();
474 if labels.is_empty() {
475 return Ok(());
476 }
477 labels.sort_unstable_by_key(|l| l.inner().offset());
478
479 let mut contexts: Vec<(Cow<'_, LabeledSpan>, _)> = Vec::with_capacity(labels.len());
480 for right in labels.iter() {
481 let right_conts = source
482 .read_span(right.inner(), self.context_lines, self.context_lines)
483 .map_err(|_| fmt::Error)?;
484
485 if contexts.is_empty() {
486 contexts.push((Cow::Borrowed(right), right_conts));
487 continue;
488 }
489
490 let (left, left_conts) = contexts.last().unwrap();
491 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
492 let left_end = left.offset() + left.len();
494 let right_end = right.offset() + right.len();
495 let new_end = std::cmp::max(left_end, right_end);
496
497 let new_span = LabeledSpan::new(
498 left.label().map(String::from),
499 left.offset(),
500 new_end - left.offset(),
501 );
502 if let Ok(new_conts) =
504 source.read_span(new_span.inner(), self.context_lines, self.context_lines)
505 {
506 contexts.pop();
507 contexts.push((Cow::Owned(new_span), new_conts));
509 continue;
510 }
511 }
512
513 contexts.push((Cow::Borrowed(right), right_conts));
514 }
515 for (ctx, _) in contexts {
516 self.render_context(f, source, &ctx, &labels[..])?;
517 }
518
519 Ok(())
520 }
521
522 fn render_context(
523 &self,
524 f: &mut impl fmt::Write,
525 source: &dyn SourceCode,
526 context: &LabeledSpan,
527 labels: &[LabeledSpan],
528 ) -> fmt::Result {
529 let (contents, lines) = self.get_lines(source, context.inner())?;
530
531 let ctx_labels = labels.iter().filter(|l| {
533 context.inner().offset() <= l.inner().offset()
534 && l.inner().offset() + l.inner().len()
535 <= context.inner().offset() + context.inner().len()
536 });
537 let primary_label =
538 ctx_labels.clone().find(|label| label.primary()).or_else(|| ctx_labels.clone().next());
539
540 let labels = labels
542 .iter()
543 .zip(self.theme.styles.highlights.iter().cloned().cycle())
544 .map(|(label, st)| FancySpan::new(label.label(), *label.inner(), st))
545 .collect::<Vec<_>>();
546
547 let mut max_gutter = 0usize;
553 for line in &lines {
554 let mut num_highlights = 0;
555 for hl in &labels {
556 if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
557 num_highlights += 1;
558 }
559 }
560 max_gutter = std::cmp::max(max_gutter, num_highlights);
561 }
562
563 let linum_width = lines[..]
566 .last()
567 .map(|line| line.line_number)
568 .unwrap_or(0)
570 .to_string()
571 .len();
572
573 write!(
575 f,
576 "{}{}{}",
577 " ".repeat(linum_width + 2),
578 self.theme.characters.ltop,
579 self.theme.characters.hbar,
580 )?;
581
582 let primary_contents = match primary_label {
585 Some(label) => source.read_span(label.inner(), 0, 0).map_err(|_| fmt::Error)?,
586 None => contents,
587 };
588
589 match primary_contents.name() {
590 Some(source_name) => {
591 let source_name = source_name.style(self.theme.styles.link);
592 writeln!(
593 f,
594 "[{}:{}:{}]",
595 source_name,
596 primary_contents.line() + 1,
597 primary_contents.column() + 1
598 )?;
599 }
600 _ => {
601 if lines.len() <= 1 {
602 writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
603 } else {
604 writeln!(
605 f,
606 "[{}:{}]",
607 primary_contents.line() + 1,
608 primary_contents.column() + 1
609 )?;
610 }
611 }
612 }
613
614 for line in &lines {
616 self.write_linum(f, linum_width, line.line_number)?;
618
619 self.render_line_gutter(f, max_gutter, line, &labels)?;
623
624 let styled_text = &line.text;
628 self.render_line_text(f, styled_text)?;
629
630 let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
632 .iter()
633 .filter(|hl| line.span_applies(hl))
634 .partition(|hl| line.span_line_only(hl));
635 if !single_line.is_empty() {
636 self.write_no_linum(f, linum_width)?;
638 self.render_highlight_gutter(
640 f,
641 max_gutter,
642 line,
643 &labels,
644 LabelRenderMode::SingleLine,
645 )?;
646 self.render_single_line_highlights(
647 f,
648 line,
649 linum_width,
650 max_gutter,
651 &single_line,
652 &labels,
653 )?;
654 }
655 for hl in multi_line {
656 if hl.has_label() && line.span_ends(hl) && !line.span_starts(hl) {
657 self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
658 }
659 }
660 }
661 writeln!(
662 f,
663 "{}{}{}",
664 " ".repeat(linum_width + 2),
665 self.theme.characters.lbot,
666 self.theme.characters.hbar.to_string().repeat(4),
667 )?;
668 Ok(())
669 }
670
671 fn render_multi_line_end(
672 &self,
673 f: &mut impl fmt::Write,
674 labels: &[FancySpan],
675 max_gutter: usize,
676 linum_width: usize,
677 line: &Line<'_>,
678 label: &FancySpan,
679 ) -> fmt::Result {
680 self.write_no_linum(f, linum_width)?;
682
683 if let Some(label_parts) = label.label_parts() {
684 let (first, rest) = label_parts
686 .split_first()
687 .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
688
689 if rest.is_empty() {
690 self.render_highlight_gutter(
692 f,
693 max_gutter,
694 line,
695 labels,
696 LabelRenderMode::SingleLine,
697 )?;
698
699 self.render_multi_line_end_single(
700 f,
701 first,
702 label.style,
703 LabelRenderMode::SingleLine,
704 )?;
705 } else {
706 self.render_highlight_gutter(
708 f,
709 max_gutter,
710 line,
711 labels,
712 LabelRenderMode::BlockFirst,
713 )?;
714
715 self.render_multi_line_end_single(
716 f,
717 first,
718 label.style,
719 LabelRenderMode::BlockFirst,
720 )?;
721 for label_line in rest {
722 self.write_no_linum(f, linum_width)?;
724 self.render_highlight_gutter(
726 f,
727 max_gutter,
728 line,
729 labels,
730 LabelRenderMode::BlockRest,
731 )?;
732 self.render_multi_line_end_single(
733 f,
734 label_line,
735 label.style,
736 LabelRenderMode::BlockRest,
737 )?;
738 }
739 }
740 } else {
741 self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?;
743 writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
745 }
746
747 Ok(())
748 }
749
750 fn render_line_gutter(
751 &self,
752 f: &mut impl fmt::Write,
753 max_gutter: usize,
754 line: &Line<'_>,
755 highlights: &[FancySpan],
756 ) -> fmt::Result {
757 if max_gutter == 0 {
758 return Ok(());
759 }
760 let chars = &self.theme.characters;
761 let mut gutter = String::new();
762 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
763 let mut arrow = false;
764 for (i, hl) in applicable.enumerate() {
765 if line.span_starts(hl) {
766 write!(gutter, "{}", chars.ltop.style(hl.style))?;
767 write!(
768 gutter,
769 "{}",
770 chars.hbar.to_string().repeat(max_gutter.saturating_sub(i)).style(hl.style)
771 )?;
772 write!(gutter, "{}", chars.rarrow.style(hl.style))?;
773 arrow = true;
774 break;
775 } else if line.span_ends(hl) {
776 if hl.has_label() {
777 write!(gutter, "{}", chars.lcross.style(hl.style))?;
778 } else {
779 write!(gutter, "{}", chars.lbot.style(hl.style))?;
780 }
781 write!(
782 gutter,
783 "{}",
784 chars.hbar.to_string().repeat(max_gutter.saturating_sub(i)).style(hl.style)
785 )?;
786 write!(gutter, "{}", chars.rarrow.style(hl.style))?;
787 arrow = true;
788 break;
789 } else if line.span_flyby(hl) {
790 write!(gutter, "{}", chars.vbar.style(hl.style))?;
791 } else {
792 gutter.push(' ');
793 }
794 }
795 write!(
796 f,
797 "{}{}",
798 gutter,
799 " ".repeat(
800 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
801 )
802 )?;
803 Ok(())
804 }
805
806 fn render_highlight_gutter(
807 &self,
808 f: &mut impl fmt::Write,
809 max_gutter: usize,
810 line: &Line<'_>,
811 highlights: &[FancySpan],
812 render_mode: LabelRenderMode,
813 ) -> fmt::Result {
814 if max_gutter == 0 {
815 return Ok(());
816 }
817
818 let mut gutter_cols = 0;
822
823 let chars = &self.theme.characters;
824 let mut gutter = String::new();
825 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
826 for (i, hl) in applicable.enumerate() {
827 if !line.span_line_only(hl) && line.span_ends(hl) {
828 if render_mode == LabelRenderMode::BlockRest {
829 let horizontal_space = max_gutter.saturating_sub(i) + 2;
832 for _ in 0..horizontal_space {
833 gutter.push(' ');
834 }
835 gutter_cols += horizontal_space + 1;
843 } else {
844 let num_repeat = max_gutter.saturating_sub(i) + 2;
845
846 write!(gutter, "{}", chars.lbot.style(hl.style))?;
847
848 write!(
849 gutter,
850 "{}",
851 chars
852 .hbar
853 .to_string()
854 .repeat(
855 num_repeat
856 - if render_mode == LabelRenderMode::BlockFirst {
859 1
860 } else {
861 0
862 },
863 )
864 .style(hl.style)
865 )?;
866
867 gutter_cols += num_repeat + 1;
872 }
873 break;
874 } else {
875 write!(gutter, "{}", chars.vbar.style(hl.style))?;
876
877 gutter_cols += 1;
880 }
881 }
882
883 let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
888 write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
890 Ok(())
891 }
892
893 fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
894 if self.wrap_lines {
895 textwrap::fill(text, opts)
896 } else {
897 let mut result = String::with_capacity(2 * text.len());
900 let trimmed_indent = opts.subsequent_indent.trim_end();
901 for (idx, line) in text.split_terminator('\n').enumerate() {
902 if idx > 0 {
903 result.push('\n');
904 }
905 if idx == 0 {
906 if line.trim().is_empty() {
907 result.push_str(opts.initial_indent.trim_end());
908 } else {
909 result.push_str(opts.initial_indent);
910 }
911 } else if line.trim().is_empty() {
912 result.push_str(trimmed_indent);
913 } else {
914 result.push_str(opts.subsequent_indent);
915 }
916 result.push_str(line);
917 }
918 if text.ends_with('\n') {
919 result.push('\n');
921 }
922 result
923 }
924 }
925
926 fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
927 write!(
928 f,
929 " {:width$} {} ",
930 linum.style(self.theme.styles.linum),
931 self.theme.characters.vbar,
932 width = width
933 )?;
934 Ok(())
935 }
936
937 fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
938 write!(f, " {:width$} {} ", "", self.theme.characters.vbar_break, width = width)?;
939 Ok(())
940 }
941
942 fn line_visual_char_width<'a>(
944 &self,
945 text: &'a str,
946 ) -> impl Iterator<Item = usize> + 'a + use<'a> {
947 struct CharWidthIterator<'a> {
949 chars: std::str::CharIndices<'a>,
950 grapheme_boundaries: Option<Vec<(usize, usize)>>, current_grapheme_idx: usize,
952 column: usize,
953 escaped: bool,
954 tab_width: usize,
955 }
956
957 impl<'a> Iterator for CharWidthIterator<'a> {
958 type Item = usize;
959
960 fn next(&mut self) -> Option<Self::Item> {
961 let (byte_pos, c) = self.chars.next()?;
962
963 let width = match (self.escaped, c) {
964 (false, '\t') => self.tab_width - self.column % self.tab_width,
965 (false, '\x1b') => {
966 self.escaped = true;
967 0
968 }
969 (false, _) => {
970 if let Some(ref boundaries) = self.grapheme_boundaries {
971 if self.current_grapheme_idx < boundaries.len()
973 && boundaries[self.current_grapheme_idx].0 == byte_pos
974 {
975 let width = boundaries[self.current_grapheme_idx].1;
976 self.current_grapheme_idx += 1;
977 width
978 } else {
979 0 }
981 } else {
982 1
984 }
985 }
986 (true, 'm') => {
987 self.escaped = false;
988 0
989 }
990 (true, _) => 0,
991 };
992
993 self.column += width;
994 Some(width)
995 }
996 }
997
998 let grapheme_boundaries = if text.is_ascii() {
1000 None
1001 } else {
1002 Some(
1004 text.grapheme_indices(true)
1005 .map(|(pos, grapheme)| (pos, grapheme.width()))
1006 .collect(),
1007 )
1008 };
1009
1010 CharWidthIterator {
1011 chars: text.char_indices(),
1012 grapheme_boundaries,
1013 current_grapheme_idx: 0,
1014 column: 0,
1015 escaped: false,
1016 tab_width: self.tab_width,
1017 }
1018 }
1019
1020 fn visual_offset(&self, line: &Line<'_>, offset: usize, start: bool) -> usize {
1026 let line_range = line.offset..=(line.offset + line.length);
1027 assert!(line_range.contains(&offset));
1028
1029 let mut text_index = offset - line.offset;
1030 while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
1031 if start {
1032 text_index -= 1;
1033 } else {
1034 text_index += 1;
1035 }
1036 }
1037 let text = &line.text[..text_index.min(line.text.len())];
1038 let text_width = self.line_visual_char_width(text).sum();
1039 if text_index > line.text.len() {
1040 text_width + 1
1049 } else {
1050 text_width
1051 }
1052 }
1053
1054 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
1056 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
1057 if c == '\t' {
1058 for _ in 0..width {
1059 f.write_char(' ')?;
1060 }
1061 } else {
1062 f.write_char(c)?;
1063 }
1064 }
1065 f.write_char('\n')?;
1066 Ok(())
1067 }
1068
1069 fn render_single_line_highlights(
1070 &self,
1071 f: &mut impl fmt::Write,
1072 line: &Line<'_>,
1073 linum_width: usize,
1074 max_gutter: usize,
1075 single_liners: &[&FancySpan],
1076 all_highlights: &[FancySpan],
1077 ) -> fmt::Result {
1078 let mut underlines = String::new();
1079 let mut highest = 0;
1080
1081 let chars = &self.theme.characters;
1082 let vbar_offsets: Vec<_> = single_liners
1083 .iter()
1084 .map(|hl| {
1085 let byte_start = hl.offset();
1086 let byte_end = hl.offset() + hl.len();
1087 let start = self.visual_offset(line, byte_start, true).max(highest);
1088 let end = if hl.len() == 0 {
1089 start + 1
1090 } else {
1091 self.visual_offset(line, byte_end, false).max(start + 1)
1092 };
1093
1094 let vbar_offset = (start + end) / 2;
1095 let num_left = vbar_offset - start;
1096 let num_right = end - vbar_offset - 1;
1097 let width = start.saturating_sub(highest).min(u16::MAX as usize);
1099 let _ = write!(
1100 underlines,
1101 "{}",
1102 format!(
1103 "{:width$}{}{}{}",
1104 "",
1105 chars.underline.to_string().repeat(num_left),
1106 if hl.len() == 0 {
1107 chars.uarrow
1108 } else if hl.has_label() {
1109 chars.underbar
1110 } else {
1111 chars.underline
1112 },
1113 chars.underline.to_string().repeat(num_right),
1114 )
1115 .style(hl.style)
1116 );
1117 highest = std::cmp::max(highest, end);
1118
1119 (hl, vbar_offset)
1120 })
1121 .collect();
1122 writeln!(f, "{underlines}")?;
1123
1124 for hl in single_liners.iter().rev() {
1125 if let Some(label) = hl.label_parts() {
1126 if label.len() == 1 {
1127 self.write_label_text(
1128 f,
1129 line,
1130 linum_width,
1131 max_gutter,
1132 all_highlights,
1133 chars,
1134 &vbar_offsets,
1135 hl,
1136 &label[0],
1137 LabelRenderMode::SingleLine,
1138 )?;
1139 } else {
1140 let mut first = true;
1141 for label_line in label {
1142 self.write_label_text(
1143 f,
1144 line,
1145 linum_width,
1146 max_gutter,
1147 all_highlights,
1148 chars,
1149 &vbar_offsets,
1150 hl,
1151 label_line,
1152 if first {
1153 LabelRenderMode::BlockFirst
1154 } else {
1155 LabelRenderMode::BlockRest
1156 },
1157 )?;
1158 first = false;
1159 }
1160 }
1161 }
1162 }
1163 Ok(())
1164 }
1165
1166 #[allow(clippy::too_many_arguments)]
1169 fn write_label_text(
1170 &self,
1171 f: &mut impl fmt::Write,
1172 line: &Line<'_>,
1173 linum_width: usize,
1174 max_gutter: usize,
1175 all_highlights: &[FancySpan],
1176 chars: &ThemeCharacters,
1177 vbar_offsets: &[(&&FancySpan, usize)],
1178 hl: &&FancySpan,
1179 label: &str,
1180 render_mode: LabelRenderMode,
1181 ) -> fmt::Result {
1182 self.write_no_linum(f, linum_width)?;
1183 self.render_highlight_gutter(
1184 f,
1185 max_gutter,
1186 line,
1187 all_highlights,
1188 LabelRenderMode::SingleLine,
1189 )?;
1190 let mut curr_offset = 1usize;
1191 for (offset_hl, vbar_offset) in vbar_offsets {
1192 while curr_offset < *vbar_offset + 1 {
1193 write!(f, " ")?;
1194 curr_offset += 1;
1195 }
1196 if *offset_hl != hl {
1197 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
1198 curr_offset += 1;
1199 } else {
1200 let lines = match render_mode {
1201 LabelRenderMode::SingleLine => {
1202 format!("{}{} {}", chars.lbot, chars.hbar.to_string().repeat(2), label,)
1203 }
1204 LabelRenderMode::BlockFirst => {
1205 format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1206 }
1207 LabelRenderMode::BlockRest => {
1208 format!(" {} {}", chars.vbar, label,)
1209 }
1210 };
1211 writeln!(f, "{}", lines.style(hl.style))?;
1212 break;
1213 }
1214 }
1215 Ok(())
1216 }
1217
1218 fn render_multi_line_end_single(
1219 &self,
1220 f: &mut impl fmt::Write,
1221 label: &str,
1222 style: Style,
1223 render_mode: LabelRenderMode,
1224 ) -> fmt::Result {
1225 match render_mode {
1226 LabelRenderMode::SingleLine => {
1227 writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1228 }
1229 LabelRenderMode::BlockFirst => {
1230 writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1231 }
1232 LabelRenderMode::BlockRest => {
1233 writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1234 }
1235 }
1236
1237 Ok(())
1238 }
1239
1240 fn get_lines<'a>(
1241 &'a self,
1242 source: &'a dyn SourceCode,
1243 context_span: &'a SourceSpan,
1244 ) -> Result<(MietteSpanContents<'a>, Vec<Line<'a>>), fmt::Error> {
1245 let context_data = source
1246 .read_span(context_span, self.context_lines, self.context_lines)
1247 .map_err(|_| fmt::Error)?;
1248 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
1249 let mut line = context_data.line();
1250 let mut column = context_data.column();
1251 let mut offset = context_data.span().offset() as usize;
1253 let base = offset;
1256 let mut line_offset = offset;
1257 let mut line_len = 0usize;
1260 let mut lines = Vec::with_capacity(1);
1261 let mut iter = context.chars().peekable();
1262 while let Some(char) = iter.next() {
1263 offset += char.len_utf8();
1264 let mut at_end_of_file = false;
1265 match char {
1266 '\r' => {
1267 if iter.next_if_eq(&'\n').is_some() {
1268 offset += 1;
1269 line += 1;
1270 column = 0;
1271 } else {
1272 line_len += char.len_utf8();
1273 column += 1;
1274 }
1275 at_end_of_file = iter.peek().is_none();
1276 }
1277 '\n' => {
1278 at_end_of_file = iter.peek().is_none();
1279 line += 1;
1280 column = 0;
1281 }
1282 _ => {
1283 line_len += char.len_utf8();
1284 column += 1;
1285 }
1286 }
1287
1288 if iter.peek().is_none() && !at_end_of_file {
1289 line += 1;
1290 }
1291
1292 if column == 0 || iter.peek().is_none() {
1293 let text_start = line_offset - base;
1296 lines.push(Line {
1297 line_number: line,
1298 offset: line_offset,
1299 length: offset - line_offset,
1300 text: &context[text_start..text_start + line_len],
1301 });
1302 line_len = 0;
1303 line_offset = offset;
1304 }
1305 }
1306 Ok((context_data, lines))
1307 }
1308}
1309
1310impl ReportHandler for GraphicalReportHandler {
1311 fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1312 if f.alternate() {
1313 return fmt::Debug::fmt(diagnostic, f);
1314 }
1315
1316 self.render_report(f, diagnostic)
1317 }
1318}
1319
1320#[derive(PartialEq, Debug)]
1325enum LabelRenderMode {
1326 SingleLine,
1328 BlockFirst,
1330 BlockRest,
1332}
1333
1334#[derive(Debug)]
1335struct Line<'a> {
1336 line_number: usize,
1337 offset: usize,
1338 length: usize,
1339 text: &'a str,
1340}
1341
1342impl Line<'_> {
1343 fn span_line_only(&self, span: &FancySpan) -> bool {
1344 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
1345 }
1346
1347 fn span_applies(&self, span: &FancySpan) -> bool {
1350 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1351 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1354 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1358 }
1359
1360 fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1363 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1364 self.span_applies(span)
1366 && !(
1367 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1369 && (span.offset() + spanlen > self.offset
1370 && span.offset() + spanlen <= self.offset + self.length)
1371 )
1372 }
1373
1374 fn span_flyby(&self, span: &FancySpan) -> bool {
1378 span.offset() < self.offset
1381 && span.offset() + span.len() > self.offset + self.length
1383 }
1384
1385 fn span_starts(&self, span: &FancySpan) -> bool {
1388 span.offset() >= self.offset
1389 }
1390
1391 fn span_ends(&self, span: &FancySpan) -> bool {
1394 span.offset() + span.len() >= self.offset
1395 && span.offset() + span.len() <= self.offset + self.length
1396 }
1397}
1398
1399#[derive(Debug, Clone)]
1400struct FancySpan {
1401 label: Option<Vec<String>>,
1405 span: SourceSpan,
1406 style: Style,
1407}
1408
1409impl PartialEq for FancySpan {
1410 fn eq(&self, other: &Self) -> bool {
1411 self.label == other.label && self.span == other.span
1412 }
1413}
1414
1415fn split_label(v: &str, style: Style) -> Vec<String> {
1416 v.split('\n').map(|i| i.style(style).to_string()).collect()
1417}
1418
1419impl FancySpan {
1420 fn new(label: Option<&str>, span: SourceSpan, style: Style) -> Self {
1421 FancySpan { label: label.map(|l| split_label(l, style)), span, style }
1422 }
1423
1424 fn has_label(&self) -> bool {
1425 self.label.is_some()
1426 }
1427
1428 fn label_parts(&self) -> Option<&[String]> {
1429 self.label.as_deref()
1430 }
1431
1432 fn offset(&self) -> usize {
1433 self.span.offset() as usize
1434 }
1435
1436 fn len(&self) -> usize {
1437 self.span.len() as usize
1438 }
1439}