1use std::{
2 fmt::{self, Write},
3 io::IsTerminal,
4};
5
6use owo_colors::{OwoColorize, Style};
7use unicode_segmentation::UnicodeSegmentation;
8use unicode_width::UnicodeWidthStr;
9
10use crate::{
11 Diagnostic, GraphicalTheme, LabeledSpan, ReportHandler, Severity, SourceCode, SourceSpan,
12 SpanContents, ThemeCharacters,
13};
14
15#[derive(Debug, Clone)]
16pub struct GraphicalReportHandler {
17 pub(crate) links: LinkStyle,
21 pub(crate) termwidth: usize,
25 pub(crate) theme: GraphicalTheme,
27 pub(crate) footer: Option<String>,
28 pub(crate) context_lines: usize,
32 pub(crate) tab_width: usize,
36 pub(crate) with_cause_chain: bool,
38 pub(crate) wrap_lines: bool,
42 pub(crate) break_words: bool,
48 pub(crate) word_separator: Option<textwrap::WordSeparator>,
49 pub(crate) word_splitter: Option<textwrap::WordSplitter>,
50 pub(crate) link_display_text: Option<String>,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55pub(crate) enum LinkStyle {
56 None,
57 Link,
58 Text,
59}
60
61impl GraphicalReportHandler {
62 pub fn new() -> Self {
65 let is_terminal = std::io::stdout().is_terminal() && std::io::stderr().is_terminal();
66 Self {
67 links: if is_terminal { LinkStyle::Link } else { LinkStyle::Text },
68 termwidth: 400,
69 theme: GraphicalTheme::new(is_terminal),
70 footer: None,
71 context_lines: 1,
72 tab_width: 4,
73 with_cause_chain: false,
74 wrap_lines: true,
75 break_words: true,
76 word_separator: None,
77 word_splitter: None,
78 link_display_text: None,
80 }
81 }
82
83 pub fn new_themed(theme: GraphicalTheme) -> Self {
85 Self {
86 links: LinkStyle::Link,
87 termwidth: 200,
88 theme,
89 footer: None,
90 context_lines: 1,
91 tab_width: 4,
92 wrap_lines: true,
93 with_cause_chain: true,
94 break_words: true,
95 word_separator: None,
96 word_splitter: None,
97 link_display_text: None,
99 }
100 }
101
102 pub fn tab_width(mut self, width: usize) -> Self {
104 self.tab_width = width;
105 self
106 }
107
108 pub fn with_links(mut self, links: bool) -> Self {
110 self.links = if links { LinkStyle::Link } else { LinkStyle::Text };
111 self
112 }
113
114 pub fn with_cause_chain(mut self) -> Self {
117 self.with_cause_chain = true;
118 self
119 }
120
121 pub fn without_cause_chain(mut self) -> Self {
124 self.with_cause_chain = false;
125 self
126 }
127
128 pub fn with_urls(mut self, urls: bool) -> Self {
133 self.links = match (self.links, urls) {
134 (_, false) => LinkStyle::None,
135 (LinkStyle::None, true) => LinkStyle::Link,
136 (links, true) => links,
137 };
138 self
139 }
140
141 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
143 self.theme = theme;
144 self
145 }
146
147 pub fn with_width(mut self, width: usize) -> Self {
149 self.termwidth = width;
150 self
151 }
152
153 pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
155 self.wrap_lines = wrap_lines;
156 self
157 }
158
159 pub fn with_break_words(mut self, break_words: bool) -> Self {
161 self.break_words = break_words;
162 self
163 }
164
165 pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
167 self.word_separator = Some(word_separator);
168 self
169 }
170
171 pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
173 self.word_splitter = Some(word_splitter);
174 self
175 }
176
177 pub fn with_footer(mut self, footer: String) -> Self {
179 self.footer = Some(footer);
180 self
181 }
182
183 pub fn with_context_lines(mut self, lines: usize) -> Self {
185 self.context_lines = lines;
186 self
187 }
188
189 pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
209 self.link_display_text = Some(text.into());
210 self
211 }
212}
213
214impl Default for GraphicalReportHandler {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220impl GraphicalReportHandler {
221 pub fn render_report(
225 &self,
226 f: &mut impl fmt::Write,
227 diagnostic: &dyn Diagnostic,
228 ) -> fmt::Result {
229 writeln!(f)?;
231 self.render_causes(f, diagnostic)?;
232 let src = diagnostic.source_code();
233 self.render_snippets(f, diagnostic, src)?;
234 self.render_footer(f, diagnostic)?;
235 self.render_related(f, diagnostic, src)?;
236 if let Some(footer) = &self.footer {
237 writeln!(f)?;
238 let width = self.termwidth.saturating_sub(4);
239 let mut opts = textwrap::Options::new(width)
240 .initial_indent(" ")
241 .subsequent_indent(" ")
242 .break_words(self.break_words);
243 if let Some(word_separator) = self.word_separator {
244 opts = opts.word_separator(word_separator);
245 }
246 if let Some(word_splitter) = self.word_splitter.clone() {
247 opts = opts.word_splitter(word_splitter);
248 }
249
250 writeln!(f, "{}", self.wrap(footer, opts))?;
251 }
252 Ok(())
253 }
254
255 fn render_header(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
256 let severity_style = match diagnostic.severity() {
257 Some(Severity::Error) | None => self.theme.styles.error,
258 Some(Severity::Warning) => self.theme.styles.warning,
259 Some(Severity::Advice) => self.theme.styles.advice,
260 };
261 let mut header = String::new();
262 if self.links == LinkStyle::Link && diagnostic.url().is_some() {
263 let url = diagnostic.url().unwrap(); let code = match diagnostic.code() {
265 Some(code) => {
266 format!("{code} ")
267 }
268 _ => "".to_string(),
269 };
270 let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
271 let link = format!(
272 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
273 url,
274 code.style(severity_style),
275 display_text.style(self.theme.styles.link)
276 );
277 write!(header, "{link}")?;
278 writeln!(f, "{header}")?;
279 writeln!(f)?;
280 } else if let Some(code) = diagnostic.code() {
281 write!(header, "{}", code.style(severity_style),)?;
282 if self.links == LinkStyle::Text && diagnostic.url().is_some() {
283 let url = diagnostic.url().unwrap(); write!(header, " ({})", url.style(self.theme.styles.link))?;
285 }
286 writeln!(f, "{header}")?;
287 writeln!(f)?;
288 }
289 Ok(())
290 }
291
292 fn render_causes(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
293 let (severity_style, severity_icon) = match diagnostic.severity() {
294 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
295 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
296 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
297 };
298
299 let initial_indent = format!(" {} ", severity_icon.style(severity_style));
300 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
301 let width = self.termwidth.saturating_sub(2);
302 let mut opts = textwrap::Options::new(width)
303 .initial_indent(&initial_indent)
304 .subsequent_indent(&rest_indent)
305 .break_words(self.break_words);
306 if let Some(word_separator) = self.word_separator {
307 opts = opts.word_separator(word_separator);
308 }
309 if let Some(word_splitter) = self.word_splitter.clone() {
310 opts = opts.word_splitter(word_splitter);
311 }
312
313 let title = match (self.links, diagnostic.url(), diagnostic.code()) {
314 (LinkStyle::Link, Some(url), Some(code)) => {
315 const CTL: &str = "\u{1b}]8;;";
317 const END: &str = "\u{1b}]8;;\u{1b}\\";
318 let code = code.style(severity_style);
319 let message = diagnostic.to_string();
320 let title = message.style(severity_style);
321 format!("{CTL}{url}\u{1b}\\{code}{END}: {title}",)
322 }
323 (_, _, Some(code)) => {
324 let title = format!("{code}: {diagnostic}");
325 format!("{}", title.style(severity_style))
326 }
327 _ => {
328 format!("{}", diagnostic.to_string().style(severity_style))
329 }
330 };
331 let title = textwrap::fill(&title, opts);
332 writeln!(f, "{title}")?;
333
334 Ok(())
393 }
394
395 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &dyn Diagnostic) -> fmt::Result {
396 if let Some(help) = diagnostic.help() {
397 let width = self.termwidth.saturating_sub(4);
398 let initial_indent = " help: ".style(self.theme.styles.help).to_string();
399 let mut opts = textwrap::Options::new(width)
400 .initial_indent(&initial_indent)
401 .subsequent_indent(" ")
402 .break_words(self.break_words);
403 if let Some(word_separator) = self.word_separator {
404 opts = opts.word_separator(word_separator);
405 }
406 if let Some(word_splitter) = self.word_splitter.clone() {
407 opts = opts.word_splitter(word_splitter);
408 }
409
410 writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
411 }
412 if let Some(note) = diagnostic.note() {
413 let width = self.termwidth.saturating_sub(4);
416 let initial_indent = " note: ".style(self.theme.styles.note).to_string();
417 let mut opts = textwrap::Options::new(width)
418 .initial_indent(&initial_indent)
419 .subsequent_indent(" ")
420 .break_words(self.break_words);
421 if let Some(word_separator) = self.word_separator {
422 opts = opts.word_separator(word_separator);
423 }
424 if let Some(word_splitter) = self.word_splitter.clone() {
425 opts = opts.word_splitter(word_splitter);
426 }
427
428 writeln!(f, "{}", self.wrap(¬e.to_string(), opts))?;
429 }
430 Ok(())
431 }
432
433 fn render_related(
434 &self,
435 f: &mut impl fmt::Write,
436 diagnostic: &dyn Diagnostic,
437 parent_src: Option<&dyn SourceCode>,
438 ) -> fmt::Result {
439 if let Some(related) = diagnostic.related() {
440 let mut inner_renderer = self.clone();
441 inner_renderer.with_cause_chain = true;
443 writeln!(f)?;
444 for rel in related {
445 match rel.severity() {
446 Some(Severity::Error) | None => write!(f, "Error: ")?,
447 Some(Severity::Warning) => write!(f, "Warning: ")?,
448 Some(Severity::Advice) => write!(f, "Advice: ")?,
449 };
450 inner_renderer.render_header(f, rel)?;
451 inner_renderer.render_causes(f, rel)?;
452 let src = rel.source_code().or(parent_src);
453 inner_renderer.render_snippets(f, rel, src)?;
454 inner_renderer.render_footer(f, rel)?;
455 inner_renderer.render_related(f, rel, src)?;
456 }
457 }
458 Ok(())
459 }
460
461 fn render_snippets(
462 &self,
463 f: &mut impl fmt::Write,
464 diagnostic: &dyn Diagnostic,
465 opt_source: Option<&dyn SourceCode>,
466 ) -> fmt::Result {
467 let source = match opt_source {
468 Some(source) => source,
469 None => return Ok(()),
470 };
471 let labels = match diagnostic.labels() {
472 Some(labels) => labels,
473 None => return Ok(()),
474 };
475
476 let mut labels = labels.collect::<Vec<_>>();
477 labels.sort_unstable_by_key(|l| l.inner().offset());
478
479 let mut contexts = Vec::with_capacity(labels.len());
480 for right in labels.iter().cloned() {
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((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((new_span, new_conts));
509 continue;
510 }
511 }
512
513 contexts.push((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().map(String::from), *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.label().is_some() && 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 gutter.push_str(&chars.ltop.style(hl.style).to_string());
767 gutter.push_str(
768 &chars
769 .hbar
770 .to_string()
771 .repeat(max_gutter.saturating_sub(i))
772 .style(hl.style)
773 .to_string(),
774 );
775 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
776 arrow = true;
777 break;
778 } else if line.span_ends(hl) {
779 if hl.label().is_some() {
780 gutter.push_str(&chars.lcross.style(hl.style).to_string());
781 } else {
782 gutter.push_str(&chars.lbot.style(hl.style).to_string());
783 }
784 gutter.push_str(
785 &chars
786 .hbar
787 .to_string()
788 .repeat(max_gutter.saturating_sub(i))
789 .style(hl.style)
790 .to_string(),
791 );
792 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
793 arrow = true;
794 break;
795 } else if line.span_flyby(hl) {
796 gutter.push_str(&chars.vbar.style(hl.style).to_string());
797 } else {
798 gutter.push(' ');
799 }
800 }
801 write!(
802 f,
803 "{}{}",
804 gutter,
805 " ".repeat(
806 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
807 )
808 )?;
809 Ok(())
810 }
811
812 fn render_highlight_gutter(
813 &self,
814 f: &mut impl fmt::Write,
815 max_gutter: usize,
816 line: &Line,
817 highlights: &[FancySpan],
818 render_mode: LabelRenderMode,
819 ) -> fmt::Result {
820 if max_gutter == 0 {
821 return Ok(());
822 }
823
824 let mut gutter_cols = 0;
828
829 let chars = &self.theme.characters;
830 let mut gutter = String::new();
831 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
832 for (i, hl) in applicable.enumerate() {
833 if !line.span_line_only(hl) && line.span_ends(hl) {
834 if render_mode == LabelRenderMode::BlockRest {
835 let horizontal_space = max_gutter.saturating_sub(i) + 2;
838 for _ in 0..horizontal_space {
839 gutter.push(' ');
840 }
841 gutter_cols += horizontal_space + 1;
849 } else {
850 let num_repeat = max_gutter.saturating_sub(i) + 2;
851
852 gutter.push_str(&chars.lbot.style(hl.style).to_string());
853
854 gutter.push_str(
855 &chars
856 .hbar
857 .to_string()
858 .repeat(
859 num_repeat
860 - if render_mode == LabelRenderMode::BlockFirst {
863 1
864 } else {
865 0
866 },
867 )
868 .style(hl.style)
869 .to_string(),
870 );
871
872 gutter_cols += num_repeat + 1;
877 }
878 break;
879 } else {
880 gutter.push_str(&chars.vbar.style(hl.style).to_string());
881
882 gutter_cols += 1;
885 }
886 }
887
888 let num_spaces = (max_gutter + 3).saturating_sub(gutter_cols);
893 write!(f, "{}{:width$}", gutter, "", width = num_spaces)?;
895 Ok(())
896 }
897
898 fn wrap(&self, text: &str, opts: textwrap::Options<'_>) -> String {
899 if self.wrap_lines {
900 textwrap::fill(text, opts)
901 } else {
902 let mut result = String::with_capacity(2 * text.len());
905 let trimmed_indent = opts.subsequent_indent.trim_end();
906 for (idx, line) in text.split_terminator('\n').enumerate() {
907 if idx > 0 {
908 result.push('\n');
909 }
910 if idx == 0 {
911 if line.trim().is_empty() {
912 result.push_str(opts.initial_indent.trim_end());
913 } else {
914 result.push_str(opts.initial_indent);
915 }
916 } else if line.trim().is_empty() {
917 result.push_str(trimmed_indent);
918 } else {
919 result.push_str(opts.subsequent_indent);
920 }
921 result.push_str(line);
922 }
923 if text.ends_with('\n') {
924 result.push('\n');
926 }
927 result
928 }
929 }
930
931 fn write_linum(&self, f: &mut impl fmt::Write, width: usize, linum: usize) -> fmt::Result {
932 write!(
933 f,
934 " {:width$} {} ",
935 linum.style(self.theme.styles.linum),
936 self.theme.characters.vbar,
937 width = width
938 )?;
939 Ok(())
940 }
941
942 fn write_no_linum(&self, f: &mut impl fmt::Write, width: usize) -> fmt::Result {
943 write!(f, " {:width$} {} ", "", self.theme.characters.vbar_break, width = width)?;
944 Ok(())
945 }
946
947 fn line_visual_char_width<'a>(
949 &self,
950 text: &'a str,
951 ) -> impl Iterator<Item = usize> + 'a + use<'a> {
952 struct CharWidthIterator<'a> {
954 chars: std::str::CharIndices<'a>,
955 grapheme_boundaries: Option<Vec<(usize, usize)>>, current_grapheme_idx: usize,
957 column: usize,
958 escaped: bool,
959 tab_width: usize,
960 }
961
962 impl<'a> Iterator for CharWidthIterator<'a> {
963 type Item = usize;
964
965 fn next(&mut self) -> Option<Self::Item> {
966 let (byte_pos, c) = self.chars.next()?;
967
968 let width = match (self.escaped, c) {
969 (false, '\t') => self.tab_width - self.column % self.tab_width,
970 (false, '\x1b') => {
971 self.escaped = true;
972 0
973 }
974 (false, _) => {
975 if let Some(ref boundaries) = self.grapheme_boundaries {
976 if self.current_grapheme_idx < boundaries.len()
978 && boundaries[self.current_grapheme_idx].0 == byte_pos
979 {
980 let width = boundaries[self.current_grapheme_idx].1;
981 self.current_grapheme_idx += 1;
982 width
983 } else {
984 0 }
986 } else {
987 1
989 }
990 }
991 (true, 'm') => {
992 self.escaped = false;
993 0
994 }
995 (true, _) => 0,
996 };
997
998 self.column += width;
999 Some(width)
1000 }
1001 }
1002
1003 let grapheme_boundaries = if text.is_ascii() {
1005 None
1006 } else {
1007 Some(
1009 text.grapheme_indices(true)
1010 .map(|(pos, grapheme)| (pos, grapheme.width()))
1011 .collect(),
1012 )
1013 };
1014
1015 CharWidthIterator {
1016 chars: text.char_indices(),
1017 grapheme_boundaries,
1018 current_grapheme_idx: 0,
1019 column: 0,
1020 escaped: false,
1021 tab_width: self.tab_width,
1022 }
1023 }
1024
1025 fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
1031 let line_range = line.offset..=(line.offset + line.length);
1032 assert!(line_range.contains(&offset));
1033
1034 let mut text_index = offset - line.offset;
1035 while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
1036 if start {
1037 text_index -= 1;
1038 } else {
1039 text_index += 1;
1040 }
1041 }
1042 let text = &line.text[..text_index.min(line.text.len())];
1043 let text_width = self.line_visual_char_width(text).sum();
1044 if text_index > line.text.len() {
1045 text_width + 1
1054 } else {
1055 text_width
1056 }
1057 }
1058
1059 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
1061 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
1062 if c == '\t' {
1063 for _ in 0..width {
1064 f.write_char(' ')?;
1065 }
1066 } else {
1067 f.write_char(c)?;
1068 }
1069 }
1070 f.write_char('\n')?;
1071 Ok(())
1072 }
1073
1074 fn render_single_line_highlights(
1075 &self,
1076 f: &mut impl fmt::Write,
1077 line: &Line,
1078 linum_width: usize,
1079 max_gutter: usize,
1080 single_liners: &[&FancySpan],
1081 all_highlights: &[FancySpan],
1082 ) -> fmt::Result {
1083 let mut underlines = String::new();
1084 let mut highest = 0;
1085
1086 let chars = &self.theme.characters;
1087 let vbar_offsets: Vec<_> = single_liners
1088 .iter()
1089 .map(|hl| {
1090 let byte_start = hl.offset();
1091 let byte_end = hl.offset() + hl.len();
1092 let start = self.visual_offset(line, byte_start, true).max(highest);
1093 let end = if hl.len() == 0 {
1094 start + 1
1095 } else {
1096 self.visual_offset(line, byte_end, false).max(start + 1)
1097 };
1098
1099 let vbar_offset = (start + end) / 2;
1100 let num_left = vbar_offset - start;
1101 let num_right = end - vbar_offset - 1;
1102 let width = start.saturating_sub(highest).min(u16::MAX as usize);
1104 underlines.push_str(
1105 &format!(
1106 "{:width$}{}{}{}",
1107 "",
1108 chars.underline.to_string().repeat(num_left),
1109 if hl.len() == 0 {
1110 chars.uarrow
1111 } else if hl.label().is_some() {
1112 chars.underbar
1113 } else {
1114 chars.underline
1115 },
1116 chars.underline.to_string().repeat(num_right),
1117 )
1118 .style(hl.style)
1119 .to_string(),
1120 );
1121 highest = std::cmp::max(highest, end);
1122
1123 (hl, vbar_offset)
1124 })
1125 .collect();
1126 writeln!(f, "{underlines}")?;
1127
1128 for hl in single_liners.iter().rev() {
1129 if let Some(label) = hl.label_parts() {
1130 if label.len() == 1 {
1131 self.write_label_text(
1132 f,
1133 line,
1134 linum_width,
1135 max_gutter,
1136 all_highlights,
1137 chars,
1138 &vbar_offsets,
1139 hl,
1140 &label[0],
1141 LabelRenderMode::SingleLine,
1142 )?;
1143 } else {
1144 let mut first = true;
1145 for label_line in &label {
1146 self.write_label_text(
1147 f,
1148 line,
1149 linum_width,
1150 max_gutter,
1151 all_highlights,
1152 chars,
1153 &vbar_offsets,
1154 hl,
1155 label_line,
1156 if first {
1157 LabelRenderMode::BlockFirst
1158 } else {
1159 LabelRenderMode::BlockRest
1160 },
1161 )?;
1162 first = false;
1163 }
1164 }
1165 }
1166 }
1167 Ok(())
1168 }
1169
1170 #[allow(clippy::too_many_arguments)]
1173 fn write_label_text(
1174 &self,
1175 f: &mut impl fmt::Write,
1176 line: &Line,
1177 linum_width: usize,
1178 max_gutter: usize,
1179 all_highlights: &[FancySpan],
1180 chars: &ThemeCharacters,
1181 vbar_offsets: &[(&&FancySpan, usize)],
1182 hl: &&FancySpan,
1183 label: &str,
1184 render_mode: LabelRenderMode,
1185 ) -> fmt::Result {
1186 self.write_no_linum(f, linum_width)?;
1187 self.render_highlight_gutter(
1188 f,
1189 max_gutter,
1190 line,
1191 all_highlights,
1192 LabelRenderMode::SingleLine,
1193 )?;
1194 let mut curr_offset = 1usize;
1195 for (offset_hl, vbar_offset) in vbar_offsets {
1196 while curr_offset < *vbar_offset + 1 {
1197 write!(f, " ")?;
1198 curr_offset += 1;
1199 }
1200 if *offset_hl != hl {
1201 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
1202 curr_offset += 1;
1203 } else {
1204 let lines = match render_mode {
1205 LabelRenderMode::SingleLine => {
1206 format!("{}{} {}", chars.lbot, chars.hbar.to_string().repeat(2), label,)
1207 }
1208 LabelRenderMode::BlockFirst => {
1209 format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1210 }
1211 LabelRenderMode::BlockRest => {
1212 format!(" {} {}", chars.vbar, label,)
1213 }
1214 };
1215 writeln!(f, "{}", lines.style(hl.style))?;
1216 break;
1217 }
1218 }
1219 Ok(())
1220 }
1221
1222 fn render_multi_line_end_single(
1223 &self,
1224 f: &mut impl fmt::Write,
1225 label: &str,
1226 style: Style,
1227 render_mode: LabelRenderMode,
1228 ) -> fmt::Result {
1229 match render_mode {
1230 LabelRenderMode::SingleLine => {
1231 writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1232 }
1233 LabelRenderMode::BlockFirst => {
1234 writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1235 }
1236 LabelRenderMode::BlockRest => {
1237 writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1238 }
1239 }
1240
1241 Ok(())
1242 }
1243
1244 fn get_lines<'a>(
1245 &'a self,
1246 source: &'a dyn SourceCode,
1247 context_span: &'a SourceSpan,
1248 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
1249 let context_data = source
1250 .read_span(context_span, self.context_lines, self.context_lines)
1251 .map_err(|_| fmt::Error)?;
1252 let context = std::str::from_utf8(context_data.data()).expect("Bad utf8 detected");
1253 let mut line = context_data.line();
1254 let mut column = context_data.column();
1255 let mut offset = context_data.span().offset();
1256 let mut line_offset = offset;
1257 let mut line_str = String::with_capacity(context.len());
1258 let mut lines = Vec::with_capacity(1);
1259 let mut iter = context.chars().peekable();
1260 while let Some(char) = iter.next() {
1261 offset += char.len_utf8();
1262 let mut at_end_of_file = false;
1263 match char {
1264 '\r' => {
1265 if iter.next_if_eq(&'\n').is_some() {
1266 offset += 1;
1267 line += 1;
1268 column = 0;
1269 } else {
1270 line_str.push(char);
1271 column += 1;
1272 }
1273 at_end_of_file = iter.peek().is_none();
1274 }
1275 '\n' => {
1276 at_end_of_file = iter.peek().is_none();
1277 line += 1;
1278 column = 0;
1279 }
1280 _ => {
1281 line_str.push(char);
1282 column += 1;
1283 }
1284 }
1285
1286 if iter.peek().is_none() && !at_end_of_file {
1287 line += 1;
1288 }
1289
1290 if column == 0 || iter.peek().is_none() {
1291 lines.push(Line {
1292 line_number: line,
1293 offset: line_offset,
1294 length: offset - line_offset,
1295 text: line_str.clone(),
1296 });
1297 line_str.clear();
1298 line_offset = offset;
1299 }
1300 }
1301 Ok((context_data, lines))
1302 }
1303}
1304
1305impl ReportHandler for GraphicalReportHandler {
1306 fn debug(&self, diagnostic: &dyn Diagnostic, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1307 if f.alternate() {
1308 return fmt::Debug::fmt(diagnostic, f);
1309 }
1310
1311 self.render_report(f, diagnostic)
1312 }
1313}
1314
1315#[derive(PartialEq, Debug)]
1320enum LabelRenderMode {
1321 SingleLine,
1323 BlockFirst,
1325 BlockRest,
1327}
1328
1329#[derive(Debug)]
1330struct Line {
1331 line_number: usize,
1332 offset: usize,
1333 length: usize,
1334 text: String,
1335}
1336
1337impl Line {
1338 fn span_line_only(&self, span: &FancySpan) -> bool {
1339 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
1340 }
1341
1342 fn span_applies(&self, span: &FancySpan) -> bool {
1345 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1346 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1349 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1353 }
1354
1355 fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1358 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1359 self.span_applies(span)
1361 && !(
1362 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1364 && (span.offset() + spanlen > self.offset
1365 && span.offset() + spanlen <= self.offset + self.length)
1366 )
1367 }
1368
1369 fn span_flyby(&self, span: &FancySpan) -> bool {
1373 span.offset() < self.offset
1376 && span.offset() + span.len() > self.offset + self.length
1378 }
1379
1380 fn span_starts(&self, span: &FancySpan) -> bool {
1383 span.offset() >= self.offset
1384 }
1385
1386 fn span_ends(&self, span: &FancySpan) -> bool {
1389 span.offset() + span.len() >= self.offset
1390 && span.offset() + span.len() <= self.offset + self.length
1391 }
1392}
1393
1394#[derive(Debug, Clone)]
1395struct FancySpan {
1396 label: Option<Vec<String>>,
1400 span: SourceSpan,
1401 style: Style,
1402}
1403
1404impl PartialEq for FancySpan {
1405 fn eq(&self, other: &Self) -> bool {
1406 self.label == other.label && self.span == other.span
1407 }
1408}
1409
1410fn split_label(v: String) -> Vec<String> {
1411 v.split('\n').map(|i| i.to_string()).collect()
1412}
1413
1414impl FancySpan {
1415 fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
1416 FancySpan { label: label.map(split_label), span, style }
1417 }
1418
1419 fn style(&self) -> Style {
1420 self.style
1421 }
1422
1423 fn label(&self) -> Option<String> {
1424 self.label.as_ref().map(|l| l.join("\n").style(self.style()).to_string())
1425 }
1426
1427 fn label_parts(&self) -> Option<Vec<String>> {
1428 self.label.as_ref().map(|l| l.iter().map(|i| i.style(self.style()).to_string()).collect())
1429 }
1430
1431 fn offset(&self) -> usize {
1432 self.span.offset()
1433 }
1434
1435 fn len(&self) -> usize {
1436 self.span.len()
1437 }
1438}