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