1use std::fmt::{self, Write};
2
3use owo_colors::{OwoColorize, Style, StyledList};
4use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
5
6use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
7use crate::handlers::theme::*;
8use crate::highlighters::{Highlighter, MietteHighlighter};
9use crate::protocol::{Diagnostic, Severity};
10use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
11
12#[derive(Debug, Clone)]
26pub struct GraphicalReportHandler {
27 pub(crate) links: LinkStyle,
28 pub(crate) termwidth: usize,
29 pub(crate) theme: GraphicalTheme,
30 pub(crate) footer: Option<String>,
31 pub(crate) context_lines: usize,
32 pub(crate) tab_width: usize,
33 pub(crate) with_cause_chain: bool,
34 pub(crate) wrap_lines: bool,
35 pub(crate) break_words: bool,
36 pub(crate) word_separator: Option<textwrap::WordSeparator>,
37 pub(crate) word_splitter: Option<textwrap::WordSplitter>,
38 pub(crate) highlighter: MietteHighlighter,
39 pub(crate) link_display_text: Option<String>,
40}
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq)]
43pub(crate) enum LinkStyle {
44 None,
45 Link,
46 Text,
47}
48
49impl GraphicalReportHandler {
50 pub fn new() -> Self {
53 Self {
54 links: LinkStyle::Link,
55 termwidth: 200,
56 theme: GraphicalTheme::default(),
57 footer: None,
58 context_lines: 1,
59 tab_width: 4,
60 with_cause_chain: true,
61 wrap_lines: true,
62 break_words: true,
63 word_separator: None,
64 word_splitter: None,
65 highlighter: MietteHighlighter::default(),
66 link_display_text: None,
67 }
68 }
69
70 pub fn new_themed(theme: GraphicalTheme) -> Self {
72 Self {
73 links: LinkStyle::Link,
74 termwidth: 200,
75 theme,
76 footer: None,
77 context_lines: 1,
78 tab_width: 4,
79 wrap_lines: true,
80 with_cause_chain: true,
81 break_words: true,
82 word_separator: None,
83 word_splitter: None,
84 highlighter: MietteHighlighter::default(),
85 link_display_text: None,
86 }
87 }
88
89 pub fn tab_width(mut self, width: usize) -> Self {
91 self.tab_width = width;
92 self
93 }
94
95 pub fn with_links(mut self, links: bool) -> Self {
97 self.links = if links {
98 LinkStyle::Link
99 } else {
100 LinkStyle::Text
101 };
102 self
103 }
104
105 pub fn with_cause_chain(mut self) -> Self {
108 self.with_cause_chain = true;
109 self
110 }
111
112 pub fn without_cause_chain(mut self) -> Self {
115 self.with_cause_chain = false;
116 self
117 }
118
119 pub fn with_urls(mut self, urls: bool) -> Self {
124 self.links = match (self.links, urls) {
125 (_, false) => LinkStyle::None,
126 (LinkStyle::None, true) => LinkStyle::Link,
127 (links, true) => links,
128 };
129 self
130 }
131
132 pub fn with_theme(mut self, theme: GraphicalTheme) -> Self {
134 self.theme = theme;
135 self
136 }
137
138 pub fn with_width(mut self, width: usize) -> Self {
140 self.termwidth = width;
141 self
142 }
143
144 pub fn with_wrap_lines(mut self, wrap_lines: bool) -> Self {
146 self.wrap_lines = wrap_lines;
147 self
148 }
149
150 pub fn with_break_words(mut self, break_words: bool) -> Self {
152 self.break_words = break_words;
153 self
154 }
155
156 pub fn with_word_separator(mut self, word_separator: textwrap::WordSeparator) -> Self {
158 self.word_separator = Some(word_separator);
159 self
160 }
161
162 pub fn with_word_splitter(mut self, word_splitter: textwrap::WordSplitter) -> Self {
164 self.word_splitter = Some(word_splitter);
165 self
166 }
167
168 pub fn with_footer(mut self, footer: String) -> Self {
170 self.footer = Some(footer);
171 self
172 }
173
174 pub fn with_context_lines(mut self, lines: usize) -> Self {
176 self.context_lines = lines;
177 self
178 }
179
180 pub fn with_syntax_highlighting(
184 mut self,
185 highlighter: impl Highlighter + Send + Sync + 'static,
186 ) -> Self {
187 self.highlighter = MietteHighlighter::from(highlighter);
188 self
189 }
190
191 pub fn without_syntax_highlighting(mut self) -> Self {
194 self.highlighter = MietteHighlighter::nocolor();
195 self
196 }
197
198 pub fn with_link_display_text(mut self, text: impl Into<String>) -> Self {
201 self.link_display_text = Some(text.into());
202 self
203 }
204}
205
206impl Default for GraphicalReportHandler {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl GraphicalReportHandler {
213 pub fn render_report(
217 &self,
218 f: &mut impl fmt::Write,
219 diagnostic: &(dyn Diagnostic),
220 ) -> fmt::Result {
221 self.render_report_inner(f, diagnostic, diagnostic.source_code())
222 }
223
224 fn render_report_inner(
225 &self,
226 f: &mut impl fmt::Write,
227 diagnostic: &(dyn Diagnostic),
228 parent_src: Option<&dyn SourceCode>,
229 ) -> fmt::Result {
230 let src = diagnostic.source_code().or(parent_src);
231 self.render_header(f, diagnostic)?;
232 self.render_causes(f, diagnostic, src)?;
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(2);
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 = if let Some(code) = diagnostic.code() {
265 format!("{} ", code)
266 } else {
267 "".to_string()
268 };
269 let display_text = self.link_display_text.as_deref().unwrap_or("(link)");
270 let link = format!(
271 "\u{1b}]8;;{}\u{1b}\\{}{}\u{1b}]8;;\u{1b}\\",
272 url,
273 code.style(severity_style),
274 display_text.style(self.theme.styles.link)
275 );
276 write!(header, "{}", link)?;
277 writeln!(f, "{}", header)?;
278 } else if let Some(code) = diagnostic.code() {
279 write!(header, "{}", code.style(severity_style),)?;
280 if self.links == LinkStyle::Text && diagnostic.url().is_some() {
281 let url = diagnostic.url().unwrap(); write!(header, " ({})", url.style(self.theme.styles.link))?;
283 }
284 writeln!(f, "{}", header)?;
285 }
286 writeln!(f)?;
287 Ok(())
288 }
289
290 fn render_causes(
291 &self,
292 f: &mut impl fmt::Write,
293 diagnostic: &(dyn Diagnostic),
294 parent_src: Option<&dyn SourceCode>,
295 ) -> fmt::Result {
296 let src = diagnostic.source_code().or(parent_src);
297
298 let (severity_style, severity_icon) = match diagnostic.severity() {
299 Some(Severity::Error) | None => (self.theme.styles.error, &self.theme.characters.error),
300 Some(Severity::Warning) => (self.theme.styles.warning, &self.theme.characters.warning),
301 Some(Severity::Advice) => (self.theme.styles.advice, &self.theme.characters.advice),
302 };
303
304 let initial_indent = format!(" {} ", severity_icon.style(severity_style));
305 let rest_indent = format!(" {} ", self.theme.characters.vbar.style(severity_style));
306 let width = self.termwidth.saturating_sub(2);
307 let mut opts = textwrap::Options::new(width)
308 .initial_indent(&initial_indent)
309 .subsequent_indent(&rest_indent)
310 .break_words(self.break_words);
311 if let Some(word_separator) = self.word_separator {
312 opts = opts.word_separator(word_separator);
313 }
314 if let Some(word_splitter) = self.word_splitter.clone() {
315 opts = opts.word_splitter(word_splitter);
316 }
317
318 writeln!(f, "{}", self.wrap(&diagnostic.to_string(), opts))?;
319
320 if !self.with_cause_chain {
321 return Ok(());
322 }
323
324 if let Some(mut cause_iter) = diagnostic
325 .diagnostic_source()
326 .map(DiagnosticChain::from_diagnostic)
327 .or_else(|| diagnostic.source().map(DiagnosticChain::from_stderror))
328 .map(|it| it.peekable())
329 {
330 while let Some(error) = cause_iter.next() {
331 let is_last = cause_iter.peek().is_none();
332 let char = if !is_last {
333 self.theme.characters.lcross
334 } else {
335 self.theme.characters.lbot
336 };
337 let initial_indent = format!(
338 " {}{}{} ",
339 char, self.theme.characters.hbar, self.theme.characters.rarrow
340 )
341 .style(severity_style)
342 .to_string();
343 let rest_indent = format!(
344 " {} ",
345 if is_last {
346 ' '
347 } else {
348 self.theme.characters.vbar
349 }
350 )
351 .style(severity_style)
352 .to_string();
353 let mut opts = textwrap::Options::new(width)
354 .initial_indent(&initial_indent)
355 .subsequent_indent(&rest_indent)
356 .break_words(self.break_words);
357 if let Some(word_separator) = self.word_separator {
358 opts = opts.word_separator(word_separator);
359 }
360 if let Some(word_splitter) = self.word_splitter.clone() {
361 opts = opts.word_splitter(word_splitter);
362 }
363
364 match error {
365 ErrorKind::Diagnostic(diag) => {
366 let mut inner = String::new();
367
368 let mut inner_renderer = self.clone();
369 inner_renderer.footer = None;
371 inner_renderer.with_cause_chain = false;
373 inner_renderer.termwidth -= rest_indent.width();
375 inner_renderer.render_report_inner(&mut inner, diag, src)?;
376
377 let inner = inner.trim_start_matches('\n');
379 writeln!(f, "{}", self.wrap(inner, opts))?;
380 }
381 ErrorKind::StdError(err) => {
382 writeln!(f, "{}", self.wrap(&err.to_string(), opts))?;
383 }
384 }
385 }
386 }
387
388 Ok(())
389 }
390
391 fn render_footer(&self, f: &mut impl fmt::Write, diagnostic: &(dyn Diagnostic)) -> fmt::Result {
392 if let Some(help) = diagnostic.help() {
393 let width = self.termwidth.saturating_sub(2);
394 let initial_indent = " help: ".style(self.theme.styles.help).to_string();
395 let mut opts = textwrap::Options::new(width)
396 .initial_indent(&initial_indent)
397 .subsequent_indent(" ")
398 .break_words(self.break_words);
399 if let Some(word_separator) = self.word_separator {
400 opts = opts.word_separator(word_separator);
401 }
402 if let Some(word_splitter) = self.word_splitter.clone() {
403 opts = opts.word_splitter(word_splitter);
404 }
405
406 writeln!(f, "{}", self.wrap(&help.to_string(), opts))?;
407 }
408 Ok(())
409 }
410
411 fn render_related(
412 &self,
413 f: &mut impl fmt::Write,
414 diagnostic: &(dyn Diagnostic),
415 parent_src: Option<&dyn SourceCode>,
416 ) -> fmt::Result {
417 if let Some(related) = diagnostic.related() {
418 let mut inner_renderer = self.clone();
419 inner_renderer.with_cause_chain = true;
421 for rel in related {
422 writeln!(f)?;
423 match rel.severity() {
424 Some(Severity::Error) | None => write!(f, "Error: ")?,
425 Some(Severity::Warning) => write!(f, "Warning: ")?,
426 Some(Severity::Advice) => write!(f, "Advice: ")?,
427 };
428 inner_renderer.render_header(f, rel)?;
429 let src = rel.source_code().or(parent_src);
430 inner_renderer.render_causes(f, rel, src)?;
431 inner_renderer.render_snippets(f, rel, src)?;
432 inner_renderer.render_footer(f, rel)?;
433 inner_renderer.render_related(f, rel, src)?;
434 }
435 }
436 Ok(())
437 }
438
439 fn render_snippets(
440 &self,
441 f: &mut impl fmt::Write,
442 diagnostic: &(dyn Diagnostic),
443 opt_source: Option<&dyn SourceCode>,
444 ) -> fmt::Result {
445 let source = match opt_source {
446 Some(source) => source,
447 None => return Ok(()),
448 };
449 let labels = match diagnostic.labels() {
450 Some(labels) => labels,
451 None => return Ok(()),
452 };
453
454 let mut labels = labels.collect::<Vec<_>>();
455 labels.sort_unstable_by_key(|l| l.inner().offset());
456
457 let mut contexts = Vec::with_capacity(labels.len());
458 for right in labels.iter().cloned() {
459 let right_conts =
460 match source.read_span(right.inner(), self.context_lines, self.context_lines) {
461 Ok(cont) => cont,
462 Err(err) => {
463 writeln!(
464 f,
465 " [{} `{}` (offset: {}, length: {}): {:?}]",
466 "Failed to read contents for label".style(self.theme.styles.error),
467 right
468 .label()
469 .unwrap_or("<none>")
470 .style(self.theme.styles.link),
471 right.offset().style(self.theme.styles.link),
472 right.len().style(self.theme.styles.link),
473 err.style(self.theme.styles.warning)
474 )?;
475 return Ok(());
476 }
477 };
478
479 if contexts.is_empty() {
480 contexts.push((right, right_conts));
481 continue;
482 }
483
484 let (left, left_conts) = contexts.last().unwrap();
485 if left_conts.line() + left_conts.line_count() >= right_conts.line() {
486 let left_end = left.offset() + left.len();
488 let right_end = right.offset() + right.len();
489 let new_end = std::cmp::max(left_end, right_end);
490
491 let new_span = LabeledSpan::new(
492 left.label().map(String::from),
493 left.offset(),
494 new_end - left.offset(),
495 );
496 if let Ok(new_conts) =
498 source.read_span(new_span.inner(), self.context_lines, self.context_lines)
499 {
500 contexts.pop();
501 contexts.push((new_span, new_conts));
503 continue;
504 }
505 }
506
507 contexts.push((right, right_conts));
508 }
509 for (ctx, _) in contexts {
510 self.render_context(f, source, &ctx, &labels[..])?;
511 }
512
513 Ok(())
514 }
515
516 fn render_context(
517 &self,
518 f: &mut impl fmt::Write,
519 source: &dyn SourceCode,
520 context: &LabeledSpan,
521 labels: &[LabeledSpan],
522 ) -> fmt::Result {
523 let (contents, lines) = self.get_lines(source, context.inner())?;
524
525 let ctx_labels = labels.iter().filter(|l| {
527 context.inner().offset() <= l.inner().offset()
528 && l.inner().offset() + l.inner().len()
529 <= context.inner().offset() + context.inner().len()
530 });
531 let primary_label = ctx_labels
532 .clone()
533 .find(|label| label.primary())
534 .or_else(|| ctx_labels.clone().next());
535
536 let labels = labels
538 .iter()
539 .zip(self.theme.styles.highlights.iter().cloned().cycle())
540 .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
541 .collect::<Vec<_>>();
542
543 let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents);
544
545 let mut max_gutter = 0usize;
549 for line in &lines {
550 let mut num_highlights = 0;
551 for hl in &labels {
552 if !line.span_line_only(hl) && line.span_applies_gutter(hl) {
553 num_highlights += 1;
554 }
555 }
556 max_gutter = std::cmp::max(max_gutter, num_highlights);
557 }
558
559 let linum_width = lines[..]
562 .last()
563 .map(|line| line.line_number)
564 .unwrap_or(0)
566 .to_string()
567 .len();
568
569 write!(
571 f,
572 "{}{}{}",
573 " ".repeat(linum_width + 2),
574 self.theme.characters.ltop,
575 self.theme.characters.hbar,
576 )?;
577
578 let primary_contents = match primary_label {
581 Some(label) => source
582 .read_span(label.inner(), 0, 0)
583 .map_err(|_| fmt::Error)?,
584 None => contents,
585 };
586
587 if let Some(source_name) = primary_contents.name() {
588 writeln!(
589 f,
590 "[{}]",
591 format_args!(
592 "{}:{}:{}",
593 source_name,
594 primary_contents.line() + 1,
595 primary_contents.column() + 1
596 )
597 .style(self.theme.styles.link)
598 )?;
599 } else if lines.len() <= 1 {
600 writeln!(f, "{}", self.theme.characters.hbar.to_string().repeat(3))?;
601 } else {
602 writeln!(
603 f,
604 "[{}:{}]",
605 primary_contents.line() + 1,
606 primary_contents.column() + 1
607 )?;
608 }
609
610 for line in &lines {
612 self.write_linum(f, linum_width, line.line_number)?;
614
615 self.render_line_gutter(f, max_gutter, line, &labels)?;
619
620 let styled_text =
622 StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
623 self.render_line_text(f, &styled_text)?;
624
625 let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
627 .iter()
628 .filter(|hl| line.span_applies(hl))
629 .partition(|hl| line.span_line_only(hl));
630 if !single_line.is_empty() {
631 self.write_no_linum(f, linum_width)?;
633 self.render_highlight_gutter(
635 f,
636 max_gutter,
637 line,
638 &labels,
639 LabelRenderMode::SingleLine,
640 )?;
641 self.render_single_line_highlights(
642 f,
643 line,
644 linum_width,
645 max_gutter,
646 &single_line,
647 &labels,
648 )?;
649 }
650 for hl in multi_line {
651 if hl.label().is_some() && line.span_ends(hl) && !line.span_starts(hl) {
652 self.render_multi_line_end(f, &labels, max_gutter, linum_width, line, hl)?;
653 }
654 }
655 }
656 writeln!(
657 f,
658 "{}{}{}",
659 " ".repeat(linum_width + 2),
660 self.theme.characters.lbot,
661 self.theme.characters.hbar.to_string().repeat(4),
662 )?;
663 Ok(())
664 }
665
666 fn render_multi_line_end(
667 &self,
668 f: &mut impl fmt::Write,
669 labels: &[FancySpan],
670 max_gutter: usize,
671 linum_width: usize,
672 line: &Line,
673 label: &FancySpan,
674 ) -> fmt::Result {
675 self.write_no_linum(f, linum_width)?;
677
678 if let Some(label_parts) = label.label_parts() {
679 let (first, rest) = label_parts
681 .split_first()
682 .expect("cannot crash because rest would have been None, see docs on the `label` field of FancySpan");
683
684 if rest.is_empty() {
685 self.render_highlight_gutter(
687 f,
688 max_gutter,
689 line,
690 labels,
691 LabelRenderMode::SingleLine,
692 )?;
693
694 self.render_multi_line_end_single(
695 f,
696 first,
697 label.style,
698 LabelRenderMode::SingleLine,
699 )?;
700 } else {
701 self.render_highlight_gutter(
703 f,
704 max_gutter,
705 line,
706 labels,
707 LabelRenderMode::MultiLineFirst,
708 )?;
709
710 self.render_multi_line_end_single(
711 f,
712 first,
713 label.style,
714 LabelRenderMode::MultiLineFirst,
715 )?;
716 for label_line in rest {
717 self.write_no_linum(f, linum_width)?;
719 self.render_highlight_gutter(
721 f,
722 max_gutter,
723 line,
724 labels,
725 LabelRenderMode::MultiLineRest,
726 )?;
727 self.render_multi_line_end_single(
728 f,
729 label_line,
730 label.style,
731 LabelRenderMode::MultiLineRest,
732 )?;
733 }
734 }
735 } else {
736 self.render_highlight_gutter(f, max_gutter, line, labels, LabelRenderMode::SingleLine)?;
738 writeln!(f, "{}", self.theme.characters.hbar.style(label.style))?;
740 }
741
742 Ok(())
743 }
744
745 fn render_line_gutter(
746 &self,
747 f: &mut impl fmt::Write,
748 max_gutter: usize,
749 line: &Line,
750 highlights: &[FancySpan],
751 ) -> fmt::Result {
752 if max_gutter == 0 {
753 return Ok(());
754 }
755 let chars = &self.theme.characters;
756 let mut gutter = String::new();
757 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
758 let mut arrow = false;
759 for (i, hl) in applicable.enumerate() {
760 if line.span_starts(hl) {
761 gutter.push_str(&chars.ltop.style(hl.style).to_string());
762 gutter.push_str(
763 &chars
764 .hbar
765 .to_string()
766 .repeat(max_gutter.saturating_sub(i))
767 .style(hl.style)
768 .to_string(),
769 );
770 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
771 arrow = true;
772 break;
773 } else if line.span_ends(hl) {
774 if hl.label().is_some() {
775 gutter.push_str(&chars.lcross.style(hl.style).to_string());
776 } else {
777 gutter.push_str(&chars.lbot.style(hl.style).to_string());
778 }
779 gutter.push_str(
780 &chars
781 .hbar
782 .to_string()
783 .repeat(max_gutter.saturating_sub(i))
784 .style(hl.style)
785 .to_string(),
786 );
787 gutter.push_str(&chars.rarrow.style(hl.style).to_string());
788 arrow = true;
789 break;
790 } else if line.span_flyby(hl) {
791 gutter.push_str(&chars.vbar.style(hl.style).to_string());
792 } else {
793 gutter.push(' ');
794 }
795 }
796 write!(
797 f,
798 "{}{}",
799 gutter,
800 " ".repeat(
801 if arrow { 1 } else { 3 } + max_gutter.saturating_sub(gutter.chars().count())
802 )
803 )?;
804 Ok(())
805 }
806
807 fn render_highlight_gutter(
808 &self,
809 f: &mut impl fmt::Write,
810 max_gutter: usize,
811 line: &Line,
812 highlights: &[FancySpan],
813 render_mode: LabelRenderMode,
814 ) -> fmt::Result {
815 if max_gutter == 0 {
816 return Ok(());
817 }
818
819 let mut gutter_cols = 0;
823
824 let chars = &self.theme.characters;
825 let mut gutter = String::new();
826 let applicable = highlights.iter().filter(|hl| line.span_applies_gutter(hl));
827 for (i, hl) in applicable.enumerate() {
828 if !line.span_line_only(hl) && line.span_ends(hl) {
829 if render_mode == LabelRenderMode::MultiLineRest {
830 let horizontal_space = max_gutter.saturating_sub(i) + 2;
833 for _ in 0..horizontal_space {
834 gutter.push(' ');
835 }
836 gutter_cols += horizontal_space + 1;
844 } else {
845 let num_repeat = max_gutter.saturating_sub(i) + 2;
846
847 gutter.push_str(&chars.lbot.style(hl.style).to_string());
848
849 gutter.push_str(
850 &chars
851 .hbar
852 .to_string()
853 .repeat(
854 num_repeat
855 - if render_mode == LabelRenderMode::MultiLineFirst {
858 1
859 } else {
860 0
861 },
862 )
863 .style(hl.style)
864 .to_string(),
865 );
866
867 gutter_cols += num_repeat + 1;
872 }
873 break;
874 } else {
875 gutter.push_str(&chars.vbar.style(hl.style).to_string());
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!(
939 f,
940 " {:width$} {} ",
941 "",
942 self.theme.characters.vbar_break,
943 width = width
944 )?;
945 Ok(())
946 }
947
948 fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
950 let mut column = 0;
951 let mut escaped = false;
952 let tab_width = self.tab_width;
953 text.chars().map(move |c| {
954 let width = match (escaped, c) {
955 (false, '\t') => tab_width - column % tab_width,
957 (false, '\x1b') => {
959 escaped = true;
960 0
961 }
962 (false, c) => c.width().unwrap_or(0),
964 (true, 'm') => {
966 escaped = false;
967 0
968 }
969 (true, _) => 0,
971 };
972 column += width;
973 width
974 })
975 }
976
977 fn visual_offset(&self, line: &Line, offset: usize, start: bool) -> usize {
983 let line_range = line.offset..=(line.offset + line.length);
984 assert!(line_range.contains(&offset));
985
986 let mut text_index = offset - line.offset;
987 while text_index <= line.text.len() && !line.text.is_char_boundary(text_index) {
988 if start {
989 text_index -= 1;
990 } else {
991 text_index += 1;
992 }
993 }
994 let text = &line.text[..text_index.min(line.text.len())];
995 let text_width = self.line_visual_char_width(text).sum();
996 if text_index > line.text.len() {
997 text_width + 1
1006 } else {
1007 text_width
1008 }
1009 }
1010
1011 fn render_line_text(&self, f: &mut impl fmt::Write, text: &str) -> fmt::Result {
1013 for (c, width) in text.chars().zip(self.line_visual_char_width(text)) {
1014 if c == '\t' {
1015 for _ in 0..width {
1016 f.write_char(' ')?;
1017 }
1018 } else {
1019 f.write_char(c)?;
1020 }
1021 }
1022 f.write_char('\n')?;
1023 Ok(())
1024 }
1025
1026 fn render_single_line_highlights(
1027 &self,
1028 f: &mut impl fmt::Write,
1029 line: &Line,
1030 linum_width: usize,
1031 max_gutter: usize,
1032 single_liners: &[&FancySpan],
1033 all_highlights: &[FancySpan],
1034 ) -> fmt::Result {
1035 let mut underlines = String::new();
1036 let mut highest = 0;
1037
1038 let chars = &self.theme.characters;
1039 let vbar_offsets: Vec<_> = single_liners
1040 .iter()
1041 .map(|hl| {
1042 let byte_start = hl.offset();
1043 let byte_end = hl.offset() + hl.len();
1044 let start = self.visual_offset(line, byte_start, true).max(highest);
1045 let end = if hl.len() == 0 {
1046 start + 1
1047 } else {
1048 self.visual_offset(line, byte_end, false).max(start + 1)
1049 };
1050
1051 let vbar_offset = (start + end) / 2;
1052 let num_left = vbar_offset - start;
1053 let num_right = end - vbar_offset - 1;
1054 underlines.push_str(
1055 &format!(
1056 "{:width$}{}{}{}",
1057 "",
1058 chars.underline.to_string().repeat(num_left),
1059 if hl.len() == 0 {
1060 chars.uarrow
1061 } else if hl.label().is_some() {
1062 chars.underbar
1063 } else {
1064 chars.underline
1065 },
1066 chars.underline.to_string().repeat(num_right),
1067 width = start.saturating_sub(highest),
1068 )
1069 .style(hl.style)
1070 .to_string(),
1071 );
1072 highest = std::cmp::max(highest, end);
1073
1074 (hl, vbar_offset)
1075 })
1076 .collect();
1077 writeln!(f, "{}", underlines)?;
1078
1079 for hl in single_liners.iter().rev() {
1080 if let Some(label) = hl.label_parts() {
1081 if label.len() == 1 {
1082 self.write_label_text(
1083 f,
1084 line,
1085 linum_width,
1086 max_gutter,
1087 all_highlights,
1088 chars,
1089 &vbar_offsets,
1090 hl,
1091 &label[0],
1092 LabelRenderMode::SingleLine,
1093 )?;
1094 } else {
1095 let mut first = true;
1096 for label_line in &label {
1097 self.write_label_text(
1098 f,
1099 line,
1100 linum_width,
1101 max_gutter,
1102 all_highlights,
1103 chars,
1104 &vbar_offsets,
1105 hl,
1106 label_line,
1107 if first {
1108 LabelRenderMode::MultiLineFirst
1109 } else {
1110 LabelRenderMode::MultiLineRest
1111 },
1112 )?;
1113 first = false;
1114 }
1115 }
1116 }
1117 }
1118 Ok(())
1119 }
1120
1121 #[allow(clippy::too_many_arguments)]
1124 fn write_label_text(
1125 &self,
1126 f: &mut impl fmt::Write,
1127 line: &Line,
1128 linum_width: usize,
1129 max_gutter: usize,
1130 all_highlights: &[FancySpan],
1131 chars: &ThemeCharacters,
1132 vbar_offsets: &[(&&FancySpan, usize)],
1133 hl: &&FancySpan,
1134 label: &str,
1135 render_mode: LabelRenderMode,
1136 ) -> fmt::Result {
1137 self.write_no_linum(f, linum_width)?;
1138 self.render_highlight_gutter(
1139 f,
1140 max_gutter,
1141 line,
1142 all_highlights,
1143 LabelRenderMode::SingleLine,
1144 )?;
1145 let mut curr_offset = 1usize;
1146 for (offset_hl, vbar_offset) in vbar_offsets {
1147 while curr_offset < *vbar_offset + 1 {
1148 write!(f, " ")?;
1149 curr_offset += 1;
1150 }
1151 if *offset_hl != hl {
1152 write!(f, "{}", chars.vbar.to_string().style(offset_hl.style))?;
1153 curr_offset += 1;
1154 } else {
1155 let lines = match render_mode {
1156 LabelRenderMode::SingleLine => format!(
1157 "{}{} {}",
1158 chars.lbot,
1159 chars.hbar.to_string().repeat(2),
1160 label,
1161 ),
1162 LabelRenderMode::MultiLineFirst => {
1163 format!("{}{}{} {}", chars.lbot, chars.hbar, chars.rcross, label,)
1164 }
1165 LabelRenderMode::MultiLineRest => {
1166 format!(" {} {}", chars.vbar, label,)
1167 }
1168 };
1169 writeln!(f, "{}", lines.style(hl.style))?;
1170 break;
1171 }
1172 }
1173 Ok(())
1174 }
1175
1176 fn render_multi_line_end_single(
1177 &self,
1178 f: &mut impl fmt::Write,
1179 label: &str,
1180 style: Style,
1181 render_mode: LabelRenderMode,
1182 ) -> fmt::Result {
1183 match render_mode {
1184 LabelRenderMode::SingleLine => {
1185 writeln!(f, "{} {}", self.theme.characters.hbar.style(style), label)?;
1186 }
1187 LabelRenderMode::MultiLineFirst => {
1188 writeln!(f, "{} {}", self.theme.characters.rcross.style(style), label)?;
1189 }
1190 LabelRenderMode::MultiLineRest => {
1191 writeln!(f, "{} {}", self.theme.characters.vbar.style(style), label)?;
1192 }
1193 }
1194
1195 Ok(())
1196 }
1197
1198 fn get_lines<'a>(
1199 &'a self,
1200 source: &'a dyn SourceCode,
1201 context_span: &'a SourceSpan,
1202 ) -> Result<(Box<dyn SpanContents<'a> + 'a>, Vec<Line>), fmt::Error> {
1203 let context_data = source
1204 .read_span(context_span, self.context_lines, self.context_lines)
1205 .map_err(|_| fmt::Error)?;
1206 let context = String::from_utf8_lossy(context_data.data());
1207 let mut line = context_data.line();
1208 let mut column = context_data.column();
1209 let mut offset = context_data.span().offset();
1210 let mut line_offset = offset;
1211 let mut line_str = String::with_capacity(context.len());
1212 let mut lines = Vec::with_capacity(1);
1213 let mut iter = context.chars().peekable();
1214 while let Some(char) = iter.next() {
1215 offset += char.len_utf8();
1216 let mut at_end_of_file = false;
1217 match char {
1218 '\r' => {
1219 if iter.next_if_eq(&'\n').is_some() {
1220 offset += 1;
1221 line += 1;
1222 column = 0;
1223 } else {
1224 line_str.push(char);
1225 column += 1;
1226 }
1227 at_end_of_file = iter.peek().is_none();
1228 }
1229 '\n' => {
1230 at_end_of_file = iter.peek().is_none();
1231 line += 1;
1232 column = 0;
1233 }
1234 _ => {
1235 line_str.push(char);
1236 column += 1;
1237 }
1238 }
1239
1240 if iter.peek().is_none() && !at_end_of_file {
1241 line += 1;
1242 }
1243
1244 if column == 0 || iter.peek().is_none() {
1245 lines.push(Line {
1246 line_number: line,
1247 offset: line_offset,
1248 length: offset - line_offset,
1249 text: line_str.clone(),
1250 });
1251 line_str.clear();
1252 line_offset = offset;
1253 }
1254 }
1255 Ok((context_data, lines))
1256 }
1257}
1258
1259impl ReportHandler for GraphicalReportHandler {
1260 fn debug(&self, diagnostic: &(dyn Diagnostic), f: &mut fmt::Formatter<'_>) -> fmt::Result {
1261 if f.alternate() {
1262 return fmt::Debug::fmt(diagnostic, f);
1263 }
1264
1265 self.render_report(f, diagnostic)
1266 }
1267}
1268
1269#[derive(PartialEq, Debug)]
1274enum LabelRenderMode {
1275 SingleLine,
1277 MultiLineFirst,
1279 MultiLineRest,
1281}
1282
1283#[derive(Debug)]
1284struct Line {
1285 line_number: usize,
1286 offset: usize,
1287 length: usize,
1288 text: String,
1289}
1290
1291impl Line {
1292 fn span_line_only(&self, span: &FancySpan) -> bool {
1293 span.offset() >= self.offset && span.offset() + span.len() <= self.offset + self.length
1294 }
1295
1296 fn span_applies(&self, span: &FancySpan) -> bool {
1299 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1300 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1303 || (span.offset() < self.offset && span.offset() + spanlen > self.offset + self.length) || (span.offset() + spanlen > self.offset && span.offset() + spanlen <= self.offset + self.length)
1307 }
1308
1309 fn span_applies_gutter(&self, span: &FancySpan) -> bool {
1312 let spanlen = if span.len() == 0 { 1 } else { span.len() };
1313 self.span_applies(span)
1315 && !(
1316 (span.offset() >= self.offset && span.offset() < self.offset + self.length)
1318 && (span.offset() + spanlen > self.offset
1319 && span.offset() + spanlen <= self.offset + self.length)
1320 )
1321 }
1322
1323 fn span_flyby(&self, span: &FancySpan) -> bool {
1327 span.offset() < self.offset
1330 && span.offset() + span.len() > self.offset + self.length
1332 }
1333
1334 fn span_starts(&self, span: &FancySpan) -> bool {
1337 span.offset() >= self.offset
1338 }
1339
1340 fn span_ends(&self, span: &FancySpan) -> bool {
1343 span.offset() + span.len() >= self.offset
1344 && span.offset() + span.len() <= self.offset + self.length
1345 }
1346}
1347
1348#[derive(Debug, Clone)]
1349struct FancySpan {
1350 label: Option<Vec<String>>,
1354 span: SourceSpan,
1355 style: Style,
1356}
1357
1358impl PartialEq for FancySpan {
1359 fn eq(&self, other: &Self) -> bool {
1360 self.label == other.label && self.span == other.span
1361 }
1362}
1363
1364fn split_label(v: String) -> Vec<String> {
1365 v.split('\n').map(|i| i.to_string()).collect()
1366}
1367
1368impl FancySpan {
1369 fn new(label: Option<String>, span: SourceSpan, style: Style) -> Self {
1370 FancySpan {
1371 label: label.map(split_label),
1372 span,
1373 style,
1374 }
1375 }
1376
1377 fn style(&self) -> Style {
1378 self.style
1379 }
1380
1381 fn label(&self) -> Option<String> {
1382 self.label
1383 .as_ref()
1384 .map(|l| l.join("\n").style(self.style()).to_string())
1385 }
1386
1387 fn label_parts(&self) -> Option<Vec<String>> {
1388 self.label.as_ref().map(|l| {
1389 l.iter()
1390 .map(|i| i.style(self.style()).to_string())
1391 .collect()
1392 })
1393 }
1394
1395 fn offset(&self) -> usize {
1396 self.span.offset()
1397 }
1398
1399 fn len(&self) -> usize {
1400 self.span.len()
1401 }
1402}