annotate_snippets/renderer/mod.rs
1// Most of this file is adapted from https://github.com/rust-lang/rust/blob/160905b6253f42967ed4aef4b98002944c7df24c/compiler/rustc_errors/src/emitter.rs
2
3//! The [Renderer] and its settings
4//!
5//! # Example
6//!
7//! ```
8//! # use annotate_snippets::*;
9//! # use annotate_snippets::renderer::*;
10//! # use annotate_snippets::Level;
11//! let report = // ...
12//! # &[Group::with_title(
13//! # Level::ERROR
14//! # .primary_title("unresolved import `baz::zed`")
15//! # .id("E0432")
16//! # )];
17//!
18//! let renderer = Renderer::styled().decor_style(DecorStyle::Unicode);
19//! let output = renderer.render(report);
20//! anstream::println!("{output}");
21//! ```
22
23mod margin;
24pub(crate) mod source_map;
25mod styled_buffer;
26pub(crate) mod stylesheet;
27
28use crate::level::{Level, LevelInner};
29use crate::renderer::source_map::{
30 AnnotatedLineInfo, LineInfo, Loc, SourceMap, SubstitutionHighlight,
31};
32use crate::renderer::styled_buffer::StyledBuffer;
33use crate::snippet::Id;
34use crate::{
35 Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Report, Snippet, Title,
36};
37pub use anstyle::*;
38use margin::Margin;
39use std::borrow::Cow;
40use std::cmp::{max, min, Ordering, Reverse};
41use std::collections::{HashMap, VecDeque};
42use std::fmt;
43use stylesheet::Stylesheet;
44
45const ANONYMIZED_LINE_NUM: &str = "LL";
46
47/// See [`Renderer::term_width`]
48pub const DEFAULT_TERM_WIDTH: usize = 140;
49
50/// The [Renderer] for a [`Report`]
51///
52/// The caller is expected to detect any relevant terminal features and configure the renderer,
53/// including
54/// - ANSI Escape code support (always outputted with [`Renderer::styled`])
55/// - Terminal width ([`Renderer::term_width`])
56/// - Unicode support ([`Renderer::decor_style`])
57///
58/// # Example
59///
60/// ```
61/// # use annotate_snippets::*;
62/// # use annotate_snippets::renderer::*;
63/// # use annotate_snippets::Level;
64/// let report = // ...
65/// # &[Group::with_title(
66/// # Level::ERROR
67/// # .primary_title("unresolved import `baz::zed`")
68/// # .id("E0432")
69/// # )];
70///
71/// let renderer = Renderer::styled();
72/// let output = renderer.render(report);
73/// anstream::println!("{output}");
74/// ```
75#[derive(Clone, Debug)]
76pub struct Renderer {
77 anonymized_line_numbers: bool,
78 term_width: usize,
79 decor_style: DecorStyle,
80 stylesheet: Stylesheet,
81 short_message: bool,
82}
83
84impl Renderer {
85 /// No terminal styling
86 pub const fn plain() -> Self {
87 Self {
88 anonymized_line_numbers: false,
89 term_width: DEFAULT_TERM_WIDTH,
90 decor_style: DecorStyle::Ascii,
91 stylesheet: Stylesheet::plain(),
92 short_message: false,
93 }
94 }
95
96 /// Default terminal styling
97 ///
98 /// If ANSI escape codes are not supported, either
99 /// - Call [`Renderer::plain`] instead
100 /// - Strip them after the fact, like with [`anstream`](https://docs.rs/anstream/latest/anstream/)
101 ///
102 /// # Note
103 ///
104 /// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
105 pub const fn styled() -> Self {
106 const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors");
107 const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS {
108 AnsiColor::BrightCyan.on_default()
109 } else {
110 AnsiColor::BrightBlue.on_default()
111 };
112 Self {
113 stylesheet: Stylesheet {
114 error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
115 warning: if USE_WINDOWS_COLORS {
116 AnsiColor::BrightYellow.on_default()
117 } else {
118 AnsiColor::Yellow.on_default()
119 }
120 .effects(Effects::BOLD),
121 info: BRIGHT_BLUE.effects(Effects::BOLD),
122 note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD),
123 help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD),
124 line_num: BRIGHT_BLUE.effects(Effects::BOLD),
125 emphasis: if USE_WINDOWS_COLORS {
126 AnsiColor::BrightWhite.on_default()
127 } else {
128 Style::new()
129 }
130 .effects(Effects::BOLD),
131 none: Style::new(),
132 context: BRIGHT_BLUE.effects(Effects::BOLD),
133 addition: AnsiColor::BrightGreen.on_default(),
134 removal: AnsiColor::BrightRed.on_default(),
135 },
136 ..Self::plain()
137 }
138 }
139
140 /// Abbreviate the message
141 pub const fn short_message(mut self, short_message: bool) -> Self {
142 self.short_message = short_message;
143 self
144 }
145
146 /// Set the width to render within
147 ///
148 /// Affects the rendering of [`Snippet`]s
149 pub const fn term_width(mut self, term_width: usize) -> Self {
150 self.term_width = term_width;
151 self
152 }
153
154 /// Set the character set used for rendering decor
155 pub const fn decor_style(mut self, decor_style: DecorStyle) -> Self {
156 self.decor_style = decor_style;
157 self
158 }
159
160 /// Anonymize line numbers
161 ///
162 /// When enabled, line numbers are replaced with `LL` which is useful for tests.
163 ///
164 /// # Example
165 ///
166 /// ```text
167 /// --> $DIR/whitespace-trimming.rs:4:193
168 /// |
169 /// LL | ... let _: () = 42;
170 /// | ^^ expected (), found integer
171 /// |
172 /// ```
173 pub const fn anonymized_line_numbers(mut self, anonymized_line_numbers: bool) -> Self {
174 self.anonymized_line_numbers = anonymized_line_numbers;
175 self
176 }
177}
178
179impl Renderer {
180 /// Render a diagnostic [`Report`]
181 pub fn render(&self, groups: Report<'_>) -> String {
182 if self.short_message {
183 self.render_short_message(groups).unwrap()
184 } else {
185 let max_line_num_len = if self.anonymized_line_numbers {
186 ANONYMIZED_LINE_NUM.len()
187 } else {
188 num_decimal_digits(max_line_number(groups))
189 };
190 let mut out_string = String::new();
191 let group_len = groups.len();
192 let mut og_primary_path = None;
193 for (g, group) in groups.iter().enumerate() {
194 let mut buffer = StyledBuffer::new();
195 let primary_path = group
196 .elements
197 .iter()
198 .find_map(|s| match &s {
199 Element::Cause(cause) => Some(cause.path.as_ref()),
200 Element::Origin(origin) => Some(Some(&origin.path)),
201 _ => None,
202 })
203 .unwrap_or_default();
204 if og_primary_path.is_none() && primary_path.is_some() {
205 og_primary_path = primary_path;
206 }
207 let level = group.primary_level.clone();
208 let mut source_map_annotated_lines = VecDeque::new();
209 let mut max_depth = 0;
210 for e in &group.elements {
211 if let Element::Cause(cause) = e {
212 let source_map = SourceMap::new(&cause.source, cause.line_start);
213 let (depth, annotated_lines) =
214 source_map.annotated_lines(cause.markers.clone(), cause.fold);
215 max_depth = max(max_depth, depth);
216 source_map_annotated_lines.push_back((source_map, annotated_lines));
217 }
218 }
219 let mut message_iter = group.elements.iter().enumerate().peekable();
220 if let Some(title) = &group.title {
221 let peek = message_iter.peek().map(|(_, s)| s).copied();
222 let title_style = if title.allows_styling {
223 TitleStyle::Header
224 } else {
225 TitleStyle::MainHeader
226 };
227 let buffer_msg_line_offset = buffer.num_lines();
228 self.render_title(
229 &mut buffer,
230 title,
231 max_line_num_len,
232 title_style,
233 matches!(peek, Some(Element::Message(_))),
234 buffer_msg_line_offset,
235 );
236 let buffer_msg_line_offset = buffer.num_lines();
237
238 if matches!(peek, Some(Element::Message(_))) {
239 self.draw_col_separator_no_space(
240 &mut buffer,
241 buffer_msg_line_offset,
242 max_line_num_len + 1,
243 );
244 }
245 if peek.is_none() && g == 0 && group_len > 1 {
246 self.draw_col_separator_end(
247 &mut buffer,
248 buffer_msg_line_offset,
249 max_line_num_len + 1,
250 );
251 }
252 }
253 let mut seen_primary = false;
254 let mut last_suggestion_path = None;
255 while let Some((i, section)) = message_iter.next() {
256 let peek = message_iter.peek().map(|(_, s)| s).copied();
257 let is_first = i == 0;
258 match §ion {
259 Element::Message(title) => {
260 let title_style = TitleStyle::Secondary;
261 let buffer_msg_line_offset = buffer.num_lines();
262 self.render_title(
263 &mut buffer,
264 title,
265 max_line_num_len,
266 title_style,
267 peek.is_some(),
268 buffer_msg_line_offset,
269 );
270 }
271 Element::Cause(cause) => {
272 if let Some((source_map, annotated_lines)) =
273 source_map_annotated_lines.pop_front()
274 {
275 let is_primary =
276 primary_path == cause.path.as_ref() && !seen_primary;
277 seen_primary |= is_primary;
278 self.render_snippet_annotations(
279 &mut buffer,
280 max_line_num_len,
281 cause,
282 is_primary,
283 &source_map,
284 &annotated_lines,
285 max_depth,
286 peek.is_some() || (g == 0 && group_len > 1),
287 is_first,
288 );
289
290 if g == 0 {
291 let current_line = buffer.num_lines();
292 match peek {
293 Some(Element::Message(_)) => {
294 self.draw_col_separator_no_space(
295 &mut buffer,
296 current_line,
297 max_line_num_len + 1,
298 );
299 }
300 None if group_len > 1 => self.draw_col_separator_end(
301 &mut buffer,
302 current_line,
303 max_line_num_len + 1,
304 ),
305 _ => {}
306 }
307 }
308 }
309 }
310 Element::Suggestion(suggestion) => {
311 let source_map =
312 SourceMap::new(&suggestion.source, suggestion.line_start);
313 let matches_previous_suggestion =
314 last_suggestion_path == Some(suggestion.path.as_ref());
315 self.emit_suggestion_default(
316 &mut buffer,
317 suggestion,
318 max_line_num_len,
319 &source_map,
320 primary_path.or(og_primary_path),
321 matches_previous_suggestion,
322 is_first,
323 //matches!(peek, Some(Element::Message(_) | Element::Padding(_))),
324 peek.is_some(),
325 );
326
327 if matches!(peek, Some(Element::Suggestion(_))) {
328 last_suggestion_path = Some(suggestion.path.as_ref());
329 } else {
330 last_suggestion_path = None;
331 }
332 }
333
334 Element::Origin(origin) => {
335 let buffer_msg_line_offset = buffer.num_lines();
336 let is_primary = primary_path == Some(&origin.path) && !seen_primary;
337 seen_primary |= is_primary;
338 self.render_origin(
339 &mut buffer,
340 max_line_num_len,
341 origin,
342 is_primary,
343 is_first,
344 buffer_msg_line_offset,
345 );
346 let current_line = buffer.num_lines();
347 if g == 0 && peek.is_none() && group_len > 1 {
348 self.draw_col_separator_end(
349 &mut buffer,
350 current_line,
351 max_line_num_len + 1,
352 );
353 }
354 }
355 Element::Padding(_) => {
356 let current_line = buffer.num_lines();
357 if peek.is_none() {
358 self.draw_col_separator_end(
359 &mut buffer,
360 current_line,
361 max_line_num_len + 1,
362 );
363 } else {
364 self.draw_col_separator_no_space(
365 &mut buffer,
366 current_line,
367 max_line_num_len + 1,
368 );
369 }
370 }
371 }
372 }
373 buffer
374 .render(&level, &self.stylesheet, &mut out_string)
375 .unwrap();
376 if g != group_len - 1 {
377 use std::fmt::Write;
378
379 writeln!(out_string).unwrap();
380 }
381 }
382 out_string
383 }
384 }
385
386 fn render_short_message(&self, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
387 let mut buffer = StyledBuffer::new();
388 let mut labels = None;
389 let group = groups.first().expect("Expected at least one group");
390
391 let Some(title) = &group.title else {
392 panic!("Expected a Title");
393 };
394
395 if let Some(Element::Cause(cause)) = group
396 .elements
397 .iter()
398 .find(|e| matches!(e, Element::Cause(_)))
399 {
400 let labels_inner = cause
401 .markers
402 .iter()
403 .filter_map(|ann| match &ann.label {
404 Some(msg) if ann.kind.is_primary() => {
405 if !msg.trim().is_empty() {
406 Some(msg.to_string())
407 } else {
408 None
409 }
410 }
411 _ => None,
412 })
413 .collect::<Vec<_>>()
414 .join(", ");
415 if !labels_inner.is_empty() {
416 labels = Some(labels_inner);
417 }
418
419 if let Some(path) = &cause.path {
420 let mut origin = Origin::path(path.as_ref());
421
422 let source_map = SourceMap::new(&cause.source, cause.line_start);
423 let (_depth, annotated_lines) =
424 source_map.annotated_lines(cause.markers.clone(), cause.fold);
425
426 if let Some(primary_line) = annotated_lines
427 .iter()
428 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
429 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
430 {
431 origin.line = Some(primary_line.line_index);
432 if let Some(first_annotation) = primary_line
433 .annotations
434 .iter()
435 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
436 {
437 origin.char_column = Some(first_annotation.start.char + 1);
438 }
439 }
440
441 self.render_origin(&mut buffer, 0, &origin, true, true, 0);
442 buffer.append(0, ": ", ElementStyle::LineAndColumn);
443 }
444 }
445
446 self.render_title(
447 &mut buffer,
448 title,
449 0, // No line numbers in short messages
450 TitleStyle::MainHeader,
451 false,
452 0,
453 );
454
455 if let Some(labels) = labels {
456 buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
457 }
458
459 let mut out_string = String::new();
460 buffer.render(&title.level, &self.stylesheet, &mut out_string)?;
461
462 Ok(out_string)
463 }
464
465 #[allow(clippy::too_many_arguments)]
466 fn render_title(
467 &self,
468 buffer: &mut StyledBuffer,
469 title: &dyn MessageOrTitle,
470 max_line_num_len: usize,
471 title_style: TitleStyle,
472 is_cont: bool,
473 buffer_msg_line_offset: usize,
474 ) {
475 let (label_style, title_element_style) = match title_style {
476 TitleStyle::MainHeader => (
477 ElementStyle::Level(title.level().level),
478 if self.short_message {
479 ElementStyle::NoStyle
480 } else {
481 ElementStyle::MainHeaderMsg
482 },
483 ),
484 TitleStyle::Header => (
485 ElementStyle::Level(title.level().level),
486 ElementStyle::HeaderMsg,
487 ),
488 TitleStyle::Secondary => {
489 for _ in 0..max_line_num_len {
490 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
491 }
492
493 self.draw_note_separator(
494 buffer,
495 buffer_msg_line_offset,
496 max_line_num_len + 1,
497 is_cont,
498 );
499 (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
500 }
501 };
502 let mut label_width = 0;
503
504 if title.level().name != Some(None) {
505 buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
506 label_width += title.level().as_str().len();
507 if let Some(Id { id: Some(id), url }) = &title.id() {
508 buffer.append(buffer_msg_line_offset, "[", label_style);
509 if let Some(url) = url.as_ref() {
510 buffer.append(
511 buffer_msg_line_offset,
512 &format!("\x1B]8;;{url}\x1B\\"),
513 label_style,
514 );
515 }
516 buffer.append(buffer_msg_line_offset, id, label_style);
517 if url.is_some() {
518 buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
519 }
520 buffer.append(buffer_msg_line_offset, "]", label_style);
521 label_width += 2 + id.len();
522 }
523 buffer.append(buffer_msg_line_offset, ": ", title_element_style);
524 label_width += 2;
525 }
526
527 let padding = " ".repeat(if title_style == TitleStyle::Secondary {
528 // The extra 3 ` ` is padding that's always needed to align to the
529 // label i.e. `note: `:
530 //
531 // error: message
532 // --> file.rs:13:20
533 // |
534 // 13 | <CODE>
535 // | ^^^^
536 // |
537 // = note: multiline
538 // message
539 // ++^^^------
540 // | | |
541 // | | |
542 // | | width of label
543 // | magic `3`
544 // `max_line_num_len`
545 max_line_num_len + 3 + label_width
546 } else {
547 label_width
548 });
549
550 let (title_str, style) = if title.allows_styling() {
551 (title.text().to_owned(), ElementStyle::NoStyle)
552 } else {
553 (normalize_whitespace(title.text()), title_element_style)
554 };
555 for (i, text) in title_str.split('\n').enumerate() {
556 if i != 0 {
557 buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
558 if title_style == TitleStyle::Secondary
559 && is_cont
560 && matches!(self.decor_style, DecorStyle::Unicode)
561 {
562 // There's another note after this one, associated to the subwindow above.
563 // We write additional vertical lines to join them:
564 // ╭▸ test.rs:3:3
565 // │
566 // 3 │ code
567 // │ ━━━━
568 // │
569 // ├ note: foo
570 // │ bar
571 // ╰ note: foo
572 // bar
573 self.draw_col_separator_no_space(
574 buffer,
575 buffer_msg_line_offset + i,
576 max_line_num_len + 1,
577 );
578 }
579 }
580 buffer.append(buffer_msg_line_offset + i, text, style);
581 }
582 }
583
584 fn render_origin(
585 &self,
586 buffer: &mut StyledBuffer,
587 max_line_num_len: usize,
588 origin: &Origin<'_>,
589 is_primary: bool,
590 is_first: bool,
591 buffer_msg_line_offset: usize,
592 ) {
593 if is_primary && !self.short_message {
594 buffer.prepend(
595 buffer_msg_line_offset,
596 self.file_start(is_first),
597 ElementStyle::LineNumber,
598 );
599 } else if !self.short_message {
600 // if !origin.standalone {
601 // // Add spacing line, as shown:
602 // // --> $DIR/file:54:15
603 // // |
604 // // LL | code
605 // // | ^^^^
606 // // | (<- It prints *this* line)
607 // // ::: $DIR/other_file.rs:15:5
608 // // |
609 // // LL | code
610 // // | ----
611 // self.draw_col_separator_no_space(
612 // buffer,
613 // buffer_msg_line_offset,
614 // max_line_num_len + 1,
615 // );
616 //
617 // buffer_msg_line_offset += 1;
618 // }
619 // Then, the secondary file indicator
620 buffer.prepend(
621 buffer_msg_line_offset,
622 self.secondary_file_start(),
623 ElementStyle::LineNumber,
624 );
625 }
626
627 let str = match (&origin.line, &origin.char_column) {
628 (Some(line), Some(col)) => {
629 format!("{}:{}:{}", origin.path, line, col)
630 }
631 (Some(line), None) => format!("{}:{}", origin.path, line),
632 _ => origin.path.to_string(),
633 };
634
635 buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
636 if !self.short_message {
637 for _ in 0..max_line_num_len {
638 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
639 }
640 }
641 }
642
643 #[allow(clippy::too_many_arguments)]
644 fn render_snippet_annotations(
645 &self,
646 buffer: &mut StyledBuffer,
647 max_line_num_len: usize,
648 snippet: &Snippet<'_, Annotation<'_>>,
649 is_primary: bool,
650 sm: &SourceMap<'_>,
651 annotated_lines: &[AnnotatedLineInfo<'_>],
652 multiline_depth: usize,
653 is_cont: bool,
654 is_first: bool,
655 ) {
656 if let Some(path) = &snippet.path {
657 let mut origin = Origin::path(path.as_ref());
658 // print out the span location and spacer before we print the annotated source
659 // to do this, we need to know if this span will be primary
660 //let is_primary = primary_path == Some(&origin.path);
661
662 if is_primary {
663 if let Some(primary_line) = annotated_lines
664 .iter()
665 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
666 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
667 {
668 origin.line = Some(primary_line.line_index);
669 if let Some(first_annotation) = primary_line
670 .annotations
671 .iter()
672 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
673 {
674 origin.char_column = Some(first_annotation.start.char + 1);
675 }
676 }
677 } else {
678 let buffer_msg_line_offset = buffer.num_lines();
679 // Add spacing line, as shown:
680 // --> $DIR/file:54:15
681 // |
682 // LL | code
683 // | ^^^^
684 // | (<- It prints *this* line)
685 // ::: $DIR/other_file.rs:15:5
686 // |
687 // LL | code
688 // | ----
689 self.draw_col_separator_no_space(
690 buffer,
691 buffer_msg_line_offset,
692 max_line_num_len + 1,
693 );
694 if let Some(first_line) = annotated_lines.first() {
695 origin.line = Some(first_line.line_index);
696 if let Some(first_annotation) = first_line.annotations.first() {
697 origin.char_column = Some(first_annotation.start.char + 1);
698 }
699 }
700 }
701 let buffer_msg_line_offset = buffer.num_lines();
702 self.render_origin(
703 buffer,
704 max_line_num_len,
705 &origin,
706 is_primary,
707 is_first,
708 buffer_msg_line_offset,
709 );
710 // Put in the spacer between the location and annotated source
711 self.draw_col_separator_no_space(
712 buffer,
713 buffer_msg_line_offset + 1,
714 max_line_num_len + 1,
715 );
716 } else {
717 let buffer_msg_line_offset = buffer.num_lines();
718 if is_primary {
719 if self.decor_style == DecorStyle::Unicode {
720 buffer.puts(
721 buffer_msg_line_offset,
722 max_line_num_len,
723 self.file_start(is_first),
724 ElementStyle::LineNumber,
725 );
726 } else {
727 self.draw_col_separator_no_space(
728 buffer,
729 buffer_msg_line_offset,
730 max_line_num_len + 1,
731 );
732 }
733 } else {
734 // Add spacing line, as shown:
735 // --> $DIR/file:54:15
736 // |
737 // LL | code
738 // | ^^^^
739 // | (<- It prints *this* line)
740 // ::: $DIR/other_file.rs:15:5
741 // |
742 // LL | code
743 // | ----
744 self.draw_col_separator_no_space(
745 buffer,
746 buffer_msg_line_offset,
747 max_line_num_len + 1,
748 );
749
750 buffer.puts(
751 buffer_msg_line_offset + 1,
752 max_line_num_len,
753 self.secondary_file_start(),
754 ElementStyle::LineNumber,
755 );
756 }
757 }
758
759 // Contains the vertical lines' positions for active multiline annotations
760 let mut multilines = Vec::new();
761
762 // Get the left-side margin to remove it
763 let mut whitespace_margin = usize::MAX;
764 for line_info in annotated_lines {
765 // Whitespace can only be removed (aka considered leading)
766 // if the lexer considers it whitespace.
767 // non-rustc_lexer::is_whitespace() chars are reported as an
768 // error (ex. no-break-spaces \u{a0}), and thus can't be considered
769 // for removal during error reporting.
770 let leading_whitespace = line_info
771 .line
772 .chars()
773 .take_while(|c| c.is_whitespace())
774 .map(|c| {
775 match c {
776 // Tabs are displayed as 4 spaces
777 '\t' => 4,
778 _ => 1,
779 }
780 })
781 .sum();
782 if line_info.line.chars().any(|c| !c.is_whitespace()) {
783 whitespace_margin = min(whitespace_margin, leading_whitespace);
784 }
785 }
786 if whitespace_margin == usize::MAX {
787 whitespace_margin = 0;
788 }
789
790 // Left-most column any visible span points at.
791 let mut span_left_margin = usize::MAX;
792 for line_info in annotated_lines {
793 for ann in &line_info.annotations {
794 span_left_margin = min(span_left_margin, ann.start.display);
795 span_left_margin = min(span_left_margin, ann.end.display);
796 }
797 }
798 if span_left_margin == usize::MAX {
799 span_left_margin = 0;
800 }
801
802 // Right-most column any visible span points at.
803 let mut span_right_margin = 0;
804 let mut label_right_margin = 0;
805 let mut max_line_len = 0;
806 for line_info in annotated_lines {
807 max_line_len = max(max_line_len, line_info.line.len());
808 for ann in &line_info.annotations {
809 span_right_margin = max(span_right_margin, ann.start.display);
810 span_right_margin = max(span_right_margin, ann.end.display);
811 // FIXME: account for labels not in the same line
812 let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1);
813 label_right_margin = max(label_right_margin, ann.end.display + label_right);
814 }
815 }
816 let width_offset = 3 + max_line_num_len;
817 let code_offset = if multiline_depth == 0 {
818 width_offset
819 } else {
820 width_offset + multiline_depth + 1
821 };
822
823 let column_width = self.term_width.saturating_sub(code_offset);
824
825 let margin = Margin::new(
826 whitespace_margin,
827 span_left_margin,
828 span_right_margin,
829 label_right_margin,
830 column_width,
831 max_line_len,
832 );
833
834 // Next, output the annotate source for this file
835 for annotated_line_idx in 0..annotated_lines.len() {
836 let previous_buffer_line = buffer.num_lines();
837
838 let depths = self.render_source_line(
839 &annotated_lines[annotated_line_idx],
840 buffer,
841 width_offset,
842 code_offset,
843 max_line_num_len,
844 margin,
845 !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
846 );
847
848 let mut to_add = HashMap::new();
849
850 for (depth, style) in depths {
851 if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
852 multilines.swap_remove(index);
853 } else {
854 to_add.insert(depth, style);
855 }
856 }
857
858 // Set the multiline annotation vertical lines to the left of
859 // the code in this line.
860 for (depth, style) in &multilines {
861 for line in previous_buffer_line..buffer.num_lines() {
862 self.draw_multiline_line(buffer, line, width_offset, *depth, *style);
863 }
864 }
865 // check to see if we need to print out or elide lines that come between
866 // this annotated line and the next one.
867 if annotated_line_idx < (annotated_lines.len() - 1) {
868 let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
869 - annotated_lines[annotated_line_idx].line_index;
870 match line_idx_delta.cmp(&2) {
871 Ordering::Greater => {
872 let last_buffer_line_num = buffer.num_lines();
873
874 self.draw_line_separator(buffer, last_buffer_line_num, width_offset);
875
876 // Set the multiline annotation vertical lines on `...` bridging line.
877 for (depth, style) in &multilines {
878 self.draw_multiline_line(
879 buffer,
880 last_buffer_line_num,
881 width_offset,
882 *depth,
883 *style,
884 );
885 }
886 if let Some(line) = annotated_lines.get(annotated_line_idx) {
887 for ann in &line.annotations {
888 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
889 {
890 // In the case where we have elided the entire start of the
891 // multispan because those lines were empty, we still need
892 // to draw the `|`s across the `...`.
893 self.draw_multiline_line(
894 buffer,
895 last_buffer_line_num,
896 width_offset,
897 pos,
898 if ann.is_primary() {
899 ElementStyle::UnderlinePrimary
900 } else {
901 ElementStyle::UnderlineSecondary
902 },
903 );
904 }
905 }
906 }
907 }
908
909 Ordering::Equal => {
910 let unannotated_line = sm
911 .get_line(annotated_lines[annotated_line_idx].line_index + 1)
912 .unwrap_or("");
913
914 let last_buffer_line_num = buffer.num_lines();
915
916 self.draw_line(
917 buffer,
918 &normalize_whitespace(unannotated_line),
919 annotated_lines[annotated_line_idx + 1].line_index - 1,
920 last_buffer_line_num,
921 width_offset,
922 code_offset,
923 max_line_num_len,
924 margin,
925 );
926
927 for (depth, style) in &multilines {
928 self.draw_multiline_line(
929 buffer,
930 last_buffer_line_num,
931 width_offset,
932 *depth,
933 *style,
934 );
935 }
936 if let Some(line) = annotated_lines.get(annotated_line_idx) {
937 for ann in &line.annotations {
938 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type
939 {
940 self.draw_multiline_line(
941 buffer,
942 last_buffer_line_num,
943 width_offset,
944 pos,
945 if ann.is_primary() {
946 ElementStyle::UnderlinePrimary
947 } else {
948 ElementStyle::UnderlineSecondary
949 },
950 );
951 }
952 }
953 }
954 }
955 Ordering::Less => {}
956 }
957 }
958
959 multilines.extend(to_add);
960 }
961 }
962
963 #[allow(clippy::too_many_arguments)]
964 fn render_source_line(
965 &self,
966 line_info: &AnnotatedLineInfo<'_>,
967 buffer: &mut StyledBuffer,
968 width_offset: usize,
969 code_offset: usize,
970 max_line_num_len: usize,
971 margin: Margin,
972 close_window: bool,
973 ) -> Vec<(usize, ElementStyle)> {
974 // Draw:
975 //
976 // LL | ... code ...
977 // | ^^-^ span label
978 // | |
979 // | secondary span label
980 //
981 // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it
982 // | | | |
983 // | | | actual code found in your source code and the spans we use to mark it
984 // | | when there's too much wasted space to the left, trim it
985 // | vertical divider between the column number and the code
986 // column number
987
988 if line_info.line_index == 0 {
989 return Vec::new();
990 }
991
992 let source_string = normalize_whitespace(line_info.line);
993
994 let line_offset = buffer.num_lines();
995
996 let left = self.draw_line(
997 buffer,
998 &source_string,
999 line_info.line_index,
1000 line_offset,
1001 width_offset,
1002 code_offset,
1003 max_line_num_len,
1004 margin,
1005 );
1006
1007 // If there are no annotations, we are done
1008 if line_info.annotations.is_empty() {
1009 // `close_window` normally gets handled later, but we are early
1010 // returning, so it needs to be handled here
1011 if close_window {
1012 self.draw_col_separator_end(buffer, line_offset + 1, width_offset - 2);
1013 }
1014 return vec![];
1015 }
1016
1017 // Special case when there's only one annotation involved, it is the start of a multiline
1018 // span and there's no text at the beginning of the code line. Instead of doing the whole
1019 // graph:
1020 //
1021 // 2 | fn foo() {
1022 // | _^
1023 // 3 | |
1024 // 4 | | }
1025 // | |_^ test
1026 //
1027 // we simplify the output to:
1028 //
1029 // 2 | / fn foo() {
1030 // 3 | |
1031 // 4 | | }
1032 // | |_^ test
1033 let mut buffer_ops = vec![];
1034 let mut annotations = vec![];
1035 let mut short_start = true;
1036 for ann in &line_info.annotations {
1037 if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
1038 if source_string
1039 .chars()
1040 .take(ann.start.display)
1041 .all(char::is_whitespace)
1042 {
1043 let uline = self.underline(ann.is_primary());
1044 let chr = uline.multiline_whole_line;
1045 annotations.push((depth, uline.style));
1046 buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
1047 } else {
1048 short_start = false;
1049 break;
1050 }
1051 } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
1052 } else {
1053 short_start = false;
1054 break;
1055 }
1056 }
1057 if short_start {
1058 for (y, x, c, s) in buffer_ops {
1059 buffer.putc(y, x, c, s);
1060 }
1061 return annotations;
1062 }
1063
1064 // We want to display like this:
1065 //
1066 // vec.push(vec.pop().unwrap());
1067 // --- ^^^ - previous borrow ends here
1068 // | |
1069 // | error occurs here
1070 // previous borrow of `vec` occurs here
1071 //
1072 // But there are some weird edge cases to be aware of:
1073 //
1074 // vec.push(vec.pop().unwrap());
1075 // -------- - previous borrow ends here
1076 // ||
1077 // |this makes no sense
1078 // previous borrow of `vec` occurs here
1079 //
1080 // For this reason, we group the lines into "highlight lines"
1081 // and "annotations lines", where the highlight lines have the `^`.
1082
1083 // Sort the annotations by (start, end col)
1084 // The labels are reversed, sort and then reversed again.
1085 // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where
1086 // the letter signifies the span. Here we are only sorting by the
1087 // span and hence, the order of the elements with the same span will
1088 // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get
1089 // (C1, C2, B1, B2, A1, A2). All the elements with the same span are
1090 // still ordered first to last, but all the elements with different
1091 // spans are ordered by their spans in last to first order. Last to
1092 // first order is important, because the jiggly lines and | are on
1093 // the left, so the rightmost span needs to be rendered first,
1094 // otherwise the lines would end up needing to go over a message.
1095
1096 let mut annotations = line_info.annotations.clone();
1097 annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
1098
1099 // First, figure out where each label will be positioned.
1100 //
1101 // In the case where you have the following annotations:
1102 //
1103 // vec.push(vec.pop().unwrap());
1104 // -------- - previous borrow ends here [C]
1105 // ||
1106 // |this makes no sense [B]
1107 // previous borrow of `vec` occurs here [A]
1108 //
1109 // `annotations_position` will hold [(2, A), (1, B), (0, C)].
1110 //
1111 // We try, when possible, to stick the rightmost annotation at the end
1112 // of the highlight line:
1113 //
1114 // vec.push(vec.pop().unwrap());
1115 // --- --- - previous borrow ends here
1116 //
1117 // But sometimes that's not possible because one of the other
1118 // annotations overlaps it. For example, from the test
1119 // `span_overlap_label`, we have the following annotations
1120 // (written on distinct lines for clarity):
1121 //
1122 // fn foo(x: u32) {
1123 // --------------
1124 // -
1125 //
1126 // In this case, we can't stick the rightmost-most label on
1127 // the highlight line, or we would get:
1128 //
1129 // fn foo(x: u32) {
1130 // -------- x_span
1131 // |
1132 // fn_span
1133 //
1134 // which is totally weird. Instead we want:
1135 //
1136 // fn foo(x: u32) {
1137 // --------------
1138 // | |
1139 // | x_span
1140 // fn_span
1141 //
1142 // which is...less weird, at least. In fact, in general, if
1143 // the rightmost span overlaps with any other span, we should
1144 // use the "hang below" version, so we can at least make it
1145 // clear where the span *starts*. There's an exception for this
1146 // logic, when the labels do not have a message:
1147 //
1148 // fn foo(x: u32) {
1149 // --------------
1150 // |
1151 // x_span
1152 //
1153 // instead of:
1154 //
1155 // fn foo(x: u32) {
1156 // --------------
1157 // | |
1158 // | x_span
1159 // <EMPTY LINE>
1160 //
1161 let mut overlap = vec![false; annotations.len()];
1162 let mut annotations_position = vec![];
1163 let mut line_len: usize = 0;
1164 let mut p = 0;
1165 for (i, annotation) in annotations.iter().enumerate() {
1166 for (j, next) in annotations.iter().enumerate() {
1167 if overlaps(next, annotation, 0) && j > 1 {
1168 overlap[i] = true;
1169 overlap[j] = true;
1170 }
1171 if overlaps(next, annotation, 0) // This label overlaps with another one and both
1172 && annotation.has_label() // take space (they have text and are not
1173 && j > i // multiline lines).
1174 && p == 0
1175 // We're currently on the first line, move the label one line down
1176 {
1177 // If we're overlapping with an un-labelled annotation with the same span
1178 // we can just merge them in the output
1179 if next.start.display == annotation.start.display
1180 && next.start.char == annotation.start.char
1181 && next.end.display == annotation.end.display
1182 && next.end.char == annotation.end.char
1183 && !next.has_label()
1184 {
1185 continue;
1186 }
1187
1188 // This annotation needs a new line in the output.
1189 p += 1;
1190 break;
1191 }
1192 }
1193 annotations_position.push((p, annotation));
1194 for (j, next) in annotations.iter().enumerate() {
1195 if j > i {
1196 let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
1197 if (overlaps(next, annotation, l) // Do not allow two labels to be in the same
1198 // line if they overlap including padding, to
1199 // avoid situations like:
1200 //
1201 // fn foo(x: u32) {
1202 // -------^------
1203 // | |
1204 // fn_spanx_span
1205 //
1206 && annotation.has_label() // Both labels must have some text, otherwise
1207 && next.has_label()) // they are not overlapping.
1208 // Do not add a new line if this annotation
1209 // or the next are vertical line placeholders.
1210 || (annotation.takes_space() // If either this or the next annotation is
1211 && next.has_label()) // multiline start/end, move it to a new line
1212 || (annotation.has_label() // so as not to overlap the horizontal lines.
1213 && next.takes_space())
1214 || (annotation.takes_space() && next.takes_space())
1215 || (overlaps(next, annotation, l)
1216 && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1217 && next.has_label()
1218 && p == 0)
1219 // Avoid #42595.
1220 {
1221 // This annotation needs a new line in the output.
1222 p += 1;
1223 break;
1224 }
1225 }
1226 }
1227 line_len = max(line_len, p);
1228 }
1229
1230 if line_len != 0 {
1231 line_len += 1;
1232 }
1233
1234 // If there are no annotations or the only annotations on this line are
1235 // MultilineLine, then there's only code being shown, stop processing.
1236 if line_info.annotations.iter().all(LineAnnotation::is_line) {
1237 return vec![];
1238 }
1239
1240 if annotations_position
1241 .iter()
1242 .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
1243 {
1244 if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
1245 // Special case the following, so that we minimize overlapping multiline spans.
1246 //
1247 // 3 │ X0 Y0 Z0
1248 // │ ┏━━━━━┛ │ │ < We are writing these lines
1249 // │ ┃┌───────┘ │ < by reverting the "depth" of
1250 // │ ┃│┌─────────┘ < their multiline spans.
1251 // 4 │ ┃││ X1 Y1 Z1
1252 // 5 │ ┃││ X2 Y2 Z2
1253 // │ ┃│└────╿──│──┘ `Z` label
1254 // │ ┃└─────│──┤
1255 // │ ┗━━━━━━┥ `Y` is a good letter too
1256 // ╰╴ `X` is a good letter
1257 for (pos, _) in &mut annotations_position {
1258 *pos = max_pos - *pos;
1259 }
1260 // We know then that we don't need an additional line for the span label, saving us
1261 // one line of vertical space.
1262 line_len = line_len.saturating_sub(1);
1263 }
1264 }
1265
1266 // Write the column separator.
1267 //
1268 // After this we will have:
1269 //
1270 // 2 | fn foo() {
1271 // |
1272 // |
1273 // |
1274 // 3 |
1275 // 4 | }
1276 // |
1277 for pos in 0..=line_len {
1278 self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2);
1279 }
1280 if close_window {
1281 self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2);
1282 }
1283 // Write the horizontal lines for multiline annotations
1284 // (only the first and last lines need this).
1285 //
1286 // After this we will have:
1287 //
1288 // 2 | fn foo() {
1289 // | __________
1290 // |
1291 // |
1292 // 3 |
1293 // 4 | }
1294 // | _
1295 for &(pos, annotation) in &annotations_position {
1296 let underline = self.underline(annotation.is_primary());
1297 let pos = pos + 1;
1298 match annotation.annotation_type {
1299 LineAnnotationType::MultilineStart(depth)
1300 | LineAnnotationType::MultilineEnd(depth) => {
1301 self.draw_range(
1302 buffer,
1303 underline.multiline_horizontal,
1304 line_offset + pos,
1305 width_offset + depth,
1306 (code_offset + annotation.start.display).saturating_sub(left),
1307 underline.style,
1308 );
1309 }
1310 _ if annotation.highlight_source => {
1311 buffer.set_style_range(
1312 line_offset,
1313 (code_offset + annotation.start.display).saturating_sub(left),
1314 (code_offset + annotation.end.display).saturating_sub(left),
1315 underline.style,
1316 annotation.is_primary(),
1317 );
1318 }
1319 _ => {}
1320 }
1321 }
1322
1323 // Write the vertical lines for labels that are on a different line as the underline.
1324 //
1325 // After this we will have:
1326 //
1327 // 2 | fn foo() {
1328 // | __________
1329 // | | |
1330 // | |
1331 // 3 | |
1332 // 4 | | }
1333 // | |_
1334 for &(pos, annotation) in &annotations_position {
1335 let underline = self.underline(annotation.is_primary());
1336 let pos = pos + 1;
1337
1338 if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
1339 for p in line_offset + 1..=line_offset + pos {
1340 buffer.putc(
1341 p,
1342 (code_offset + annotation.start.display).saturating_sub(left),
1343 match annotation.annotation_type {
1344 LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
1345 _ => underline.vertical_text_line,
1346 },
1347 underline.style,
1348 );
1349 }
1350 if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
1351 buffer.putc(
1352 line_offset + pos,
1353 (code_offset + annotation.start.display).saturating_sub(left),
1354 underline.bottom_right,
1355 underline.style,
1356 );
1357 }
1358 if matches!(
1359 annotation.annotation_type,
1360 LineAnnotationType::MultilineEnd(_)
1361 ) && annotation.has_label()
1362 {
1363 buffer.putc(
1364 line_offset + pos,
1365 (code_offset + annotation.start.display).saturating_sub(left),
1366 underline.multiline_bottom_right_with_text,
1367 underline.style,
1368 );
1369 }
1370 }
1371 match annotation.annotation_type {
1372 LineAnnotationType::MultilineStart(depth) => {
1373 buffer.putc(
1374 line_offset + pos,
1375 width_offset + depth - 1,
1376 underline.top_left,
1377 underline.style,
1378 );
1379 for p in line_offset + pos + 1..line_offset + line_len + 2 {
1380 buffer.putc(
1381 p,
1382 width_offset + depth - 1,
1383 underline.multiline_vertical,
1384 underline.style,
1385 );
1386 }
1387 }
1388 LineAnnotationType::MultilineEnd(depth) => {
1389 for p in line_offset..line_offset + pos {
1390 buffer.putc(
1391 p,
1392 width_offset + depth - 1,
1393 underline.multiline_vertical,
1394 underline.style,
1395 );
1396 }
1397 buffer.putc(
1398 line_offset + pos,
1399 width_offset + depth - 1,
1400 underline.bottom_left,
1401 underline.style,
1402 );
1403 }
1404 _ => (),
1405 }
1406 }
1407
1408 // Write the labels on the annotations that actually have a label.
1409 //
1410 // After this we will have:
1411 //
1412 // 2 | fn foo() {
1413 // | __________
1414 // | |
1415 // | something about `foo`
1416 // 3 |
1417 // 4 | }
1418 // | _ test
1419 for &(pos, annotation) in &annotations_position {
1420 let style = if annotation.is_primary() {
1421 ElementStyle::LabelPrimary
1422 } else {
1423 ElementStyle::LabelSecondary
1424 };
1425 let (pos, col) = if pos == 0 {
1426 if annotation.end.display == 0 {
1427 (pos + 1, (annotation.end.display + 2).saturating_sub(left))
1428 } else {
1429 (pos + 1, (annotation.end.display + 1).saturating_sub(left))
1430 }
1431 } else {
1432 (pos + 2, annotation.start.display.saturating_sub(left))
1433 };
1434 if let Some(label) = &annotation.label {
1435 buffer.puts(line_offset + pos, code_offset + col, label, style);
1436 }
1437 }
1438
1439 // Sort from biggest span to smallest span so that smaller spans are
1440 // represented in the output:
1441 //
1442 // x | fn foo()
1443 // | ^^^---^^
1444 // | | |
1445 // | | something about `foo`
1446 // | something about `fn foo()`
1447 annotations_position.sort_by_key(|(_, ann)| {
1448 // Decreasing order. When annotations share the same length, prefer `Primary`.
1449 (Reverse(ann.len()), ann.is_primary())
1450 });
1451
1452 // Write the underlines.
1453 //
1454 // After this we will have:
1455 //
1456 // 2 | fn foo() {
1457 // | ____-_____^
1458 // | |
1459 // | something about `foo`
1460 // 3 |
1461 // 4 | }
1462 // | _^ test
1463 for &(pos, annotation) in &annotations_position {
1464 let uline = self.underline(annotation.is_primary());
1465 for p in annotation.start.display..annotation.end.display {
1466 // The default span label underline.
1467 buffer.putc(
1468 line_offset + 1,
1469 (code_offset + p).saturating_sub(left),
1470 uline.underline,
1471 uline.style,
1472 );
1473 }
1474
1475 if pos == 0
1476 && matches!(
1477 annotation.annotation_type,
1478 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1479 )
1480 {
1481 // The beginning of a multiline span with its leftward moving line on the same line.
1482 buffer.putc(
1483 line_offset + 1,
1484 (code_offset + annotation.start.display).saturating_sub(left),
1485 match annotation.annotation_type {
1486 LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
1487 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
1488 _ => panic!("unexpected annotation type: {annotation:?}"),
1489 },
1490 uline.style,
1491 );
1492 } else if pos != 0
1493 && matches!(
1494 annotation.annotation_type,
1495 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1496 )
1497 {
1498 // The beginning of a multiline span with its leftward moving line on another line,
1499 // so we start going down first.
1500 buffer.putc(
1501 line_offset + 1,
1502 (code_offset + annotation.start.display).saturating_sub(left),
1503 match annotation.annotation_type {
1504 LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
1505 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
1506 _ => panic!("unexpected annotation type: {annotation:?}"),
1507 },
1508 uline.style,
1509 );
1510 } else if pos != 0 && annotation.has_label() {
1511 // The beginning of a span label with an actual label, we'll point down.
1512 buffer.putc(
1513 line_offset + 1,
1514 (code_offset + annotation.start.display).saturating_sub(left),
1515 uline.label_start,
1516 uline.style,
1517 );
1518 }
1519 }
1520
1521 // We look for individual *long* spans, and we trim the *middle*, so that we render
1522 // LL | ...= [0, 0, 0, ..., 0, 0];
1523 // | ^^^^^^^^^^...^^^^^^^ expected `&[u8]`, found `[{integer}; 1680]`
1524 for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
1525 // Skip cases where multiple spans overlap eachother.
1526 if overlap[i] {
1527 continue;
1528 };
1529 let LineAnnotationType::Singleline = annotation.annotation_type else {
1530 continue;
1531 };
1532 let width = annotation.end.display - annotation.start.display;
1533 if width > margin.term_width * 2 && width > 10 {
1534 // If the terminal is *too* small, we keep at least a tiny bit of the span for
1535 // display.
1536 let pad = max(margin.term_width / 3, 5);
1537 // Code line
1538 buffer.replace(
1539 line_offset,
1540 annotation.start.display + pad,
1541 annotation.end.display - pad,
1542 self.margin(),
1543 );
1544 // Underline line
1545 buffer.replace(
1546 line_offset + 1,
1547 annotation.start.display + pad,
1548 annotation.end.display - pad,
1549 self.margin(),
1550 );
1551 }
1552 }
1553 annotations_position
1554 .iter()
1555 .filter_map(|&(_, annotation)| match annotation.annotation_type {
1556 LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
1557 let style = if annotation.is_primary() {
1558 ElementStyle::LabelPrimary
1559 } else {
1560 ElementStyle::LabelSecondary
1561 };
1562 Some((p, style))
1563 }
1564 _ => None,
1565 })
1566 .collect::<Vec<_>>()
1567 }
1568
1569 #[allow(clippy::too_many_arguments)]
1570 fn emit_suggestion_default(
1571 &self,
1572 buffer: &mut StyledBuffer,
1573 suggestion: &Snippet<'_, Patch<'_>>,
1574 max_line_num_len: usize,
1575 sm: &SourceMap<'_>,
1576 primary_path: Option<&Cow<'_, str>>,
1577 matches_previous_suggestion: bool,
1578 is_first: bool,
1579 is_cont: bool,
1580 ) {
1581 let suggestions = sm.splice_lines(suggestion.markers.clone());
1582
1583 let buffer_offset = buffer.num_lines();
1584 let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
1585 for (complete, parts, highlights) in &suggestions {
1586 let has_deletion = parts
1587 .iter()
1588 .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
1589 let is_multiline = complete.lines().count() > 1;
1590
1591 if matches_previous_suggestion {
1592 buffer.puts(
1593 row_num - 1,
1594 max_line_num_len + 1,
1595 self.multi_suggestion_separator(),
1596 ElementStyle::LineNumber,
1597 );
1598 } else {
1599 self.draw_col_separator_start(buffer, row_num - 1, max_line_num_len + 1);
1600 }
1601 if suggestion.path.as_ref() != primary_path {
1602 if let Some(path) = suggestion.path.as_ref() {
1603 if !matches_previous_suggestion {
1604 let (loc, _) = sm.span_to_locations(parts[0].span.clone());
1605 // --> file.rs:line:col
1606 // |
1607 let arrow = self.file_start(is_first);
1608 buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
1609 let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
1610 let col = usize::max(max_line_num_len + 1, arrow.len());
1611 buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
1612 for _ in 0..max_line_num_len {
1613 buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
1614 }
1615 self.draw_col_separator_no_space(buffer, row_num, max_line_num_len + 1);
1616 row_num += 1;
1617 }
1618 }
1619 }
1620 let show_code_change = if has_deletion && !is_multiline {
1621 DisplaySuggestion::Diff
1622 } else if parts.len() == 1
1623 && parts.first().map_or(false, |p| {
1624 p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
1625 })
1626 {
1627 // We are adding a line(s) of code before code that was already there.
1628 DisplaySuggestion::Add
1629 } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim())
1630 && !is_multiline
1631 {
1632 DisplaySuggestion::Underline
1633 } else {
1634 DisplaySuggestion::None
1635 };
1636
1637 if let DisplaySuggestion::Diff = show_code_change {
1638 row_num += 1;
1639 }
1640
1641 let file_lines = sm.span_to_lines(parts[0].span.clone());
1642 let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone());
1643 let mut lines = complete.lines();
1644 if lines.clone().next().is_none() {
1645 // Account for a suggestion to completely remove a line(s) with whitespace (#94192).
1646 for line in line_start.line..=line_end.line {
1647 buffer.puts(
1648 row_num - 1 + line - line_start.line,
1649 0,
1650 &self.maybe_anonymized(line, max_line_num_len),
1651 ElementStyle::LineNumber,
1652 );
1653 buffer.puts(
1654 row_num - 1 + line - line_start.line,
1655 max_line_num_len + 1,
1656 "- ",
1657 ElementStyle::Removal,
1658 );
1659 buffer.puts(
1660 row_num - 1 + line - line_start.line,
1661 max_line_num_len + 3,
1662 &normalize_whitespace(sm.get_line(line).unwrap()),
1663 ElementStyle::Removal,
1664 );
1665 }
1666 row_num += line_end.line - line_start.line;
1667 }
1668 let mut last_pos = 0;
1669 let mut is_item_attribute = false;
1670 let mut unhighlighted_lines = Vec::new();
1671 for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
1672 last_pos = line_pos;
1673
1674 // Remember lines that are not highlighted to hide them if needed
1675 if highlight_parts.is_empty() {
1676 unhighlighted_lines.push((line_pos, line));
1677 continue;
1678 }
1679 if highlight_parts.len() == 1
1680 && line.trim().starts_with("#[")
1681 && line.trim().ends_with(']')
1682 {
1683 is_item_attribute = true;
1684 }
1685
1686 match unhighlighted_lines.len() {
1687 0 => (),
1688 // Since we show first line, "..." line and last line,
1689 // There is no reason to hide if there are 3 or less lines
1690 // (because then we just replace a line with ... which is
1691 // not helpful)
1692 n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
1693 self.draw_code_line(
1694 buffer,
1695 &mut row_num,
1696 &[],
1697 p + line_start.line,
1698 l,
1699 show_code_change,
1700 max_line_num_len,
1701 &file_lines,
1702 is_multiline,
1703 );
1704 }),
1705 // Print first unhighlighted line, "..." and last unhighlighted line, like so:
1706 //
1707 // LL | this line was highlighted
1708 // LL | this line is just for context
1709 // ...
1710 // LL | this line is just for context
1711 // LL | this line was highlighted
1712 _ => {
1713 let last_line = unhighlighted_lines.pop();
1714 let first_line = unhighlighted_lines.drain(..).next();
1715
1716 if let Some((p, l)) = first_line {
1717 self.draw_code_line(
1718 buffer,
1719 &mut row_num,
1720 &[],
1721 p + line_start.line,
1722 l,
1723 show_code_change,
1724 max_line_num_len,
1725 &file_lines,
1726 is_multiline,
1727 );
1728 }
1729
1730 let placeholder = self.margin();
1731 let padding = str_width(placeholder);
1732 buffer.puts(
1733 row_num,
1734 max_line_num_len.saturating_sub(padding),
1735 placeholder,
1736 ElementStyle::LineNumber,
1737 );
1738 row_num += 1;
1739
1740 if let Some((p, l)) = last_line {
1741 self.draw_code_line(
1742 buffer,
1743 &mut row_num,
1744 &[],
1745 p + line_start.line,
1746 l,
1747 show_code_change,
1748 max_line_num_len,
1749 &file_lines,
1750 is_multiline,
1751 );
1752 }
1753 }
1754 }
1755 self.draw_code_line(
1756 buffer,
1757 &mut row_num,
1758 highlight_parts,
1759 line_pos + line_start.line,
1760 line,
1761 show_code_change,
1762 max_line_num_len,
1763 &file_lines,
1764 is_multiline,
1765 );
1766 }
1767
1768 if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute {
1769 // The suggestion adds an entire line of code, ending on a newline, so we'll also
1770 // print the *following* line, to provide context of what we're advising people to
1771 // do. Otherwise you would only see contextless code that can be confused for
1772 // already existing code, despite the colors and UI elements.
1773 // We special case `#[derive(_)]\n` and other attribute suggestions, because those
1774 // are the ones where context is most useful.
1775 let file_lines = sm.span_to_lines(parts[0].span.end..parts[0].span.end);
1776 let (lo, _) = sm.span_to_locations(parts[0].span.clone());
1777 let line_num = lo.line;
1778 if let Some(line) = sm.get_line(line_num) {
1779 let line = normalize_whitespace(line);
1780 self.draw_code_line(
1781 buffer,
1782 &mut row_num,
1783 &[],
1784 line_num + last_pos + 1,
1785 &line,
1786 DisplaySuggestion::None,
1787 max_line_num_len,
1788 &file_lines,
1789 is_multiline,
1790 );
1791 }
1792 }
1793 // This offset and the ones below need to be signed to account for replacement code
1794 // that is shorter than the original code.
1795 let mut offsets: Vec<(usize, isize)> = Vec::new();
1796 // Only show an underline in the suggestions if the suggestion is not the
1797 // entirety of the code being shown and the displayed code is not multiline.
1798 if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
1799 show_code_change
1800 {
1801 for part in parts {
1802 let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
1803 let (span_start, span_end) = sm.span_to_locations(part.span.clone());
1804 let span_start_pos = span_start.display;
1805 let span_end_pos = span_end.display;
1806
1807 // If this addition is _only_ whitespace, then don't trim it,
1808 // or else we're just not rendering anything.
1809 let is_whitespace_addition = part.replacement.trim().is_empty();
1810
1811 // Do not underline the leading...
1812 let start = if is_whitespace_addition {
1813 0
1814 } else {
1815 part.replacement
1816 .len()
1817 .saturating_sub(part.replacement.trim_start().len())
1818 };
1819 // ...or trailing spaces. Account for substitutions containing unicode
1820 // characters.
1821 let sub_len: usize = str_width(if is_whitespace_addition {
1822 &part.replacement
1823 } else {
1824 part.replacement.trim()
1825 });
1826
1827 let offset: isize = offsets
1828 .iter()
1829 .filter_map(|(start, v)| {
1830 if span_start_pos < *start {
1831 None
1832 } else {
1833 Some(v)
1834 }
1835 })
1836 .sum();
1837 let underline_start = (span_start_pos + start) as isize + offset;
1838 let underline_end = (span_start_pos + start + sub_len) as isize + offset;
1839 assert!(underline_start >= 0 && underline_end >= 0);
1840 let padding: usize = max_line_num_len + 3;
1841 for p in underline_start..underline_end {
1842 if matches!(show_code_change, DisplaySuggestion::Underline) {
1843 // If this is a replacement, underline with `~`, if this is an addition
1844 // underline with `+`.
1845 buffer.putc(
1846 row_num,
1847 (padding as isize + p) as usize,
1848 if part.is_addition(sm) {
1849 '+'
1850 } else {
1851 self.diff()
1852 },
1853 ElementStyle::Addition,
1854 );
1855 }
1856 }
1857 if let DisplaySuggestion::Diff = show_code_change {
1858 // Colorize removal with red in diff format.
1859
1860 // Below, there's some tricky buffer indexing going on. `row_num` at this
1861 // point corresponds to:
1862 //
1863 // |
1864 // LL | CODE
1865 // | ++++ <- `row_num`
1866 //
1867 // in the buffer. When we have a diff format output, we end up with
1868 //
1869 // |
1870 // LL - OLDER <- row_num - 2
1871 // LL + NEWER
1872 // | <- row_num
1873 //
1874 // The `row_num - 2` is to select the buffer line that has the "old version
1875 // of the diff" at that point. When the removal is a single line, `i` is
1876 // `0`, `newlines` is `1` so `(newlines - i - 1)` ends up being `0`, so row
1877 // points at `LL - OLDER`. When the removal corresponds to multiple lines,
1878 // we end up with `newlines > 1` and `i` being `0..newlines - 1`.
1879 //
1880 // |
1881 // LL - OLDER <- row_num - 2 - (newlines - last_i - 1)
1882 // LL - CODE
1883 // LL - BEING
1884 // LL - REMOVED <- row_num - 2 - (newlines - first_i - 1)
1885 // LL + NEWER
1886 // | <- row_num
1887
1888 let newlines = snippet.lines().count();
1889 if newlines > 0 && row_num > newlines {
1890 // Account for removals where the part being removed spans multiple
1891 // lines.
1892 // FIXME: We check the number of rows because in some cases, like in
1893 // `tests/ui/lint/invalid-nan-comparison-suggestion.rs`, the rendered
1894 // suggestion will only show the first line of code being replaced. The
1895 // proper way of doing this would be to change the suggestion rendering
1896 // logic to show the whole prior snippet, but the current output is not
1897 // too bad to begin with, so we side-step that issue here.
1898 for (i, line) in snippet.lines().enumerate() {
1899 let line = normalize_whitespace(line);
1900 let row = row_num - 2 - (newlines - i - 1);
1901 // On the first line, we highlight between the start of the part
1902 // span, and the end of that line.
1903 // On the last line, we highlight between the start of the line, and
1904 // the column of the part span end.
1905 // On all others, we highlight the whole line.
1906 let start = if i == 0 {
1907 (padding as isize + span_start_pos as isize) as usize
1908 } else {
1909 padding
1910 };
1911 let end = if i == 0 {
1912 (padding as isize
1913 + span_start_pos as isize
1914 + line.len() as isize)
1915 as usize
1916 } else if i == newlines - 1 {
1917 (padding as isize + span_end_pos as isize) as usize
1918 } else {
1919 (padding as isize + line.len() as isize) as usize
1920 };
1921 buffer.set_style_range(
1922 row,
1923 start,
1924 end,
1925 ElementStyle::Removal,
1926 true,
1927 );
1928 }
1929 } else {
1930 // The removed code fits all in one line.
1931 buffer.set_style_range(
1932 row_num - 2,
1933 (padding as isize + span_start_pos as isize) as usize,
1934 (padding as isize + span_end_pos as isize) as usize,
1935 ElementStyle::Removal,
1936 true,
1937 );
1938 }
1939 }
1940
1941 // length of the code after substitution
1942 let full_sub_len = str_width(&part.replacement) as isize;
1943
1944 // length of the code to be substituted
1945 let snippet_len = span_end_pos as isize - span_start_pos as isize;
1946 // For multiple substitutions, use the position *after* the previous
1947 // substitutions have happened, only when further substitutions are
1948 // located strictly after.
1949 offsets.push((span_end_pos, full_sub_len - snippet_len));
1950 }
1951 row_num += 1;
1952 }
1953
1954 // if we elided some lines, add an ellipsis
1955 if lines.next().is_some() {
1956 let placeholder = self.margin();
1957 let padding = str_width(placeholder);
1958 buffer.puts(
1959 row_num,
1960 max_line_num_len.saturating_sub(padding),
1961 placeholder,
1962 ElementStyle::LineNumber,
1963 );
1964 } else {
1965 let row = match show_code_change {
1966 DisplaySuggestion::Diff
1967 | DisplaySuggestion::Add
1968 | DisplaySuggestion::Underline => row_num - 1,
1969 DisplaySuggestion::None => row_num,
1970 };
1971 if is_cont {
1972 self.draw_col_separator_no_space(buffer, row, max_line_num_len + 1);
1973 } else {
1974 self.draw_col_separator_end(buffer, row, max_line_num_len + 1);
1975 }
1976 row_num = row + 1;
1977 }
1978 }
1979 }
1980
1981 #[allow(clippy::too_many_arguments)]
1982 fn draw_code_line(
1983 &self,
1984 buffer: &mut StyledBuffer,
1985 row_num: &mut usize,
1986 highlight_parts: &[SubstitutionHighlight],
1987 line_num: usize,
1988 line_to_add: &str,
1989 show_code_change: DisplaySuggestion,
1990 max_line_num_len: usize,
1991 file_lines: &[&LineInfo<'_>],
1992 is_multiline: bool,
1993 ) {
1994 if let DisplaySuggestion::Diff = show_code_change {
1995 // We need to print more than one line if the span we need to remove is multiline.
1996 // For more info: https://github.com/rust-lang/rust/issues/92741
1997 let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
1998 for (index, line_to_remove) in lines_to_remove.enumerate() {
1999 buffer.puts(
2000 *row_num - 1,
2001 0,
2002 &self.maybe_anonymized(line_num + index, max_line_num_len),
2003 ElementStyle::LineNumber,
2004 );
2005 buffer.puts(
2006 *row_num - 1,
2007 max_line_num_len + 1,
2008 "- ",
2009 ElementStyle::Removal,
2010 );
2011 let line = normalize_whitespace(line_to_remove.line);
2012 buffer.puts(
2013 *row_num - 1,
2014 max_line_num_len + 3,
2015 &line,
2016 ElementStyle::NoStyle,
2017 );
2018 *row_num += 1;
2019 }
2020 // If the last line is exactly equal to the line we need to add, we can skip both of
2021 // them. This allows us to avoid output like the following:
2022 // 2 - &
2023 // 2 + if true { true } else { false }
2024 // 3 - if true { true } else { false }
2025 // If those lines aren't equal, we print their diff
2026 let last_line = &file_lines.last().unwrap();
2027 if last_line.line == line_to_add {
2028 *row_num -= 2;
2029 } else {
2030 buffer.puts(
2031 *row_num - 1,
2032 0,
2033 &self.maybe_anonymized(line_num + file_lines.len() - 1, max_line_num_len),
2034 ElementStyle::LineNumber,
2035 );
2036 buffer.puts(
2037 *row_num - 1,
2038 max_line_num_len + 1,
2039 "- ",
2040 ElementStyle::Removal,
2041 );
2042 buffer.puts(
2043 *row_num - 1,
2044 max_line_num_len + 3,
2045 &normalize_whitespace(last_line.line),
2046 ElementStyle::NoStyle,
2047 );
2048 if line_to_add.trim().is_empty() {
2049 *row_num -= 1;
2050 } else {
2051 // Check if after the removal, the line is left with only whitespace. If so, we
2052 // will not show an "addition" line, as removing the whole line is what the user
2053 // would really want.
2054 // For example, for the following:
2055 // |
2056 // 2 - .await
2057 // 2 + (note the left over whitespace)
2058 // |
2059 // We really want
2060 // |
2061 // 2 - .await
2062 // |
2063 // *row_num -= 1;
2064 buffer.puts(
2065 *row_num,
2066 0,
2067 &self.maybe_anonymized(line_num, max_line_num_len),
2068 ElementStyle::LineNumber,
2069 );
2070 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
2071 buffer.append(
2072 *row_num,
2073 &normalize_whitespace(line_to_add),
2074 ElementStyle::NoStyle,
2075 );
2076 }
2077 }
2078 } else if is_multiline {
2079 buffer.puts(
2080 *row_num,
2081 0,
2082 &self.maybe_anonymized(line_num, max_line_num_len),
2083 ElementStyle::LineNumber,
2084 );
2085 match &highlight_parts {
2086 [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
2087 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
2088 }
2089 [] => {
2090 // FIXME: needed? Doesn't get exercised in any test.
2091 self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1);
2092 }
2093 _ => {
2094 let diff = self.diff();
2095 buffer.puts(
2096 *row_num,
2097 max_line_num_len + 1,
2098 &format!("{diff} "),
2099 ElementStyle::Addition,
2100 );
2101 }
2102 }
2103 // LL | line_to_add
2104 // ++^^^
2105 // | |
2106 // | magic `3`
2107 // `max_line_num_len`
2108 buffer.puts(
2109 *row_num,
2110 max_line_num_len + 3,
2111 &normalize_whitespace(line_to_add),
2112 ElementStyle::NoStyle,
2113 );
2114 } else if let DisplaySuggestion::Add = show_code_change {
2115 buffer.puts(
2116 *row_num,
2117 0,
2118 &self.maybe_anonymized(line_num, max_line_num_len),
2119 ElementStyle::LineNumber,
2120 );
2121 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
2122 buffer.append(
2123 *row_num,
2124 &normalize_whitespace(line_to_add),
2125 ElementStyle::NoStyle,
2126 );
2127 } else {
2128 buffer.puts(
2129 *row_num,
2130 0,
2131 &self.maybe_anonymized(line_num, max_line_num_len),
2132 ElementStyle::LineNumber,
2133 );
2134 self.draw_col_separator(buffer, *row_num, max_line_num_len + 1);
2135 buffer.append(
2136 *row_num,
2137 &normalize_whitespace(line_to_add),
2138 ElementStyle::NoStyle,
2139 );
2140 }
2141
2142 // Colorize addition/replacements with green.
2143 for &SubstitutionHighlight { start, end } in highlight_parts {
2144 // This is a no-op for empty ranges
2145 if start != end {
2146 // Account for tabs when highlighting (#87972).
2147 let tabs: usize = line_to_add
2148 .chars()
2149 .take(start)
2150 .map(|ch| match ch {
2151 '\t' => 3,
2152 _ => 0,
2153 })
2154 .sum();
2155 buffer.set_style_range(
2156 *row_num,
2157 max_line_num_len + 3 + start + tabs,
2158 max_line_num_len + 3 + end + tabs,
2159 ElementStyle::Addition,
2160 true,
2161 );
2162 }
2163 }
2164 *row_num += 1;
2165 }
2166
2167 #[allow(clippy::too_many_arguments)]
2168 fn draw_line(
2169 &self,
2170 buffer: &mut StyledBuffer,
2171 source_string: &str,
2172 line_index: usize,
2173 line_offset: usize,
2174 width_offset: usize,
2175 code_offset: usize,
2176 max_line_num_len: usize,
2177 margin: Margin,
2178 ) -> usize {
2179 // Tabs are assumed to have been replaced by spaces in calling code.
2180 debug_assert!(!source_string.contains('\t'));
2181 let line_len = str_width(source_string);
2182 // Create the source line we will highlight.
2183 let mut left = margin.left(line_len);
2184 let right = margin.right(line_len);
2185 // FIXME: The following code looks fishy. See #132860.
2186 // On long lines, we strip the source line, accounting for unicode.
2187 let mut taken = 0;
2188 let mut skipped = 0;
2189 let code: String = source_string
2190 .chars()
2191 .skip_while(|ch| {
2192 skipped += char_width(*ch);
2193 skipped <= left
2194 })
2195 .take_while(|ch| {
2196 // Make sure that the trimming on the right will fall within the terminal width.
2197 taken += char_width(*ch);
2198 taken <= (right - left)
2199 })
2200 .collect();
2201
2202 let placeholder = self.margin();
2203 let padding = str_width(placeholder);
2204 let (width_taken, bytes_taken) = if margin.was_cut_left() {
2205 // We have stripped some code/whitespace from the beginning, make it clear.
2206 let mut bytes_taken = 0;
2207 let mut width_taken = 0;
2208 for ch in code.chars() {
2209 width_taken += char_width(ch);
2210 bytes_taken += ch.len_utf8();
2211
2212 if width_taken >= padding {
2213 break;
2214 }
2215 }
2216
2217 if width_taken > padding {
2218 left -= width_taken - padding;
2219 }
2220
2221 buffer.puts(
2222 line_offset,
2223 code_offset,
2224 placeholder,
2225 ElementStyle::LineNumber,
2226 );
2227 (width_taken, bytes_taken)
2228 } else {
2229 (0, 0)
2230 };
2231
2232 buffer.puts(
2233 line_offset,
2234 code_offset + width_taken,
2235 &code[bytes_taken..],
2236 ElementStyle::Quotation,
2237 );
2238
2239 if line_len > right {
2240 // We have stripped some code/whitespace from the beginning, make it clear.
2241 let mut char_taken = 0;
2242 let mut width_taken_inner = 0;
2243 for ch in code.chars().rev() {
2244 width_taken_inner += char_width(ch);
2245 char_taken += 1;
2246
2247 if width_taken_inner >= padding {
2248 break;
2249 }
2250 }
2251
2252 buffer.puts(
2253 line_offset,
2254 code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
2255 placeholder,
2256 ElementStyle::LineNumber,
2257 );
2258 }
2259
2260 buffer.puts(
2261 line_offset,
2262 0,
2263 &self.maybe_anonymized(line_index, max_line_num_len),
2264 ElementStyle::LineNumber,
2265 );
2266
2267 self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2);
2268
2269 left
2270 }
2271
2272 fn draw_range(
2273 &self,
2274 buffer: &mut StyledBuffer,
2275 symbol: char,
2276 line: usize,
2277 col_from: usize,
2278 col_to: usize,
2279 style: ElementStyle,
2280 ) {
2281 for col in col_from..col_to {
2282 buffer.putc(line, col, symbol, style);
2283 }
2284 }
2285
2286 fn draw_multiline_line(
2287 &self,
2288 buffer: &mut StyledBuffer,
2289 line: usize,
2290 offset: usize,
2291 depth: usize,
2292 style: ElementStyle,
2293 ) {
2294 let chr = match (style, self.decor_style) {
2295 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
2296 (_, DecorStyle::Ascii) => '|',
2297 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => {
2298 '┃'
2299 }
2300 (_, DecorStyle::Unicode) => '│',
2301 };
2302 buffer.putc(line, offset + depth - 1, chr, style);
2303 }
2304
2305 fn col_separator(&self) -> char {
2306 match self.decor_style {
2307 DecorStyle::Ascii => '|',
2308 DecorStyle::Unicode => '│',
2309 }
2310 }
2311
2312 fn multi_suggestion_separator(&self) -> &'static str {
2313 match self.decor_style {
2314 DecorStyle::Ascii => "|",
2315 DecorStyle::Unicode => "├╴",
2316 }
2317 }
2318
2319 fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2320 let chr = self.col_separator();
2321 buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2322 }
2323
2324 fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2325 let chr = self.col_separator();
2326 self.draw_col_separator_no_space_with_style(
2327 buffer,
2328 chr,
2329 line,
2330 col,
2331 ElementStyle::LineNumber,
2332 );
2333 }
2334
2335 fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2336 match self.decor_style {
2337 DecorStyle::Ascii => {
2338 self.draw_col_separator_no_space_with_style(
2339 buffer,
2340 '|',
2341 line,
2342 col,
2343 ElementStyle::LineNumber,
2344 );
2345 }
2346 DecorStyle::Unicode => {
2347 self.draw_col_separator_no_space_with_style(
2348 buffer,
2349 '╭',
2350 line,
2351 col,
2352 ElementStyle::LineNumber,
2353 );
2354 self.draw_col_separator_no_space_with_style(
2355 buffer,
2356 '╴',
2357 line,
2358 col + 1,
2359 ElementStyle::LineNumber,
2360 );
2361 }
2362 }
2363 }
2364
2365 fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2366 match self.decor_style {
2367 DecorStyle::Ascii => {
2368 self.draw_col_separator_no_space_with_style(
2369 buffer,
2370 '|',
2371 line,
2372 col,
2373 ElementStyle::LineNumber,
2374 );
2375 }
2376 DecorStyle::Unicode => {
2377 self.draw_col_separator_no_space_with_style(
2378 buffer,
2379 '╰',
2380 line,
2381 col,
2382 ElementStyle::LineNumber,
2383 );
2384 self.draw_col_separator_no_space_with_style(
2385 buffer,
2386 '╴',
2387 line,
2388 col + 1,
2389 ElementStyle::LineNumber,
2390 );
2391 }
2392 }
2393 }
2394
2395 fn draw_col_separator_no_space_with_style(
2396 &self,
2397 buffer: &mut StyledBuffer,
2398 chr: char,
2399 line: usize,
2400 col: usize,
2401 style: ElementStyle,
2402 ) {
2403 buffer.putc(line, col, chr, style);
2404 }
2405
2406 fn maybe_anonymized(&self, line_num: usize, max_line_num_len: usize) -> String {
2407 format!(
2408 "{:>max_line_num_len$}",
2409 if self.anonymized_line_numbers {
2410 Cow::Borrowed(ANONYMIZED_LINE_NUM)
2411 } else {
2412 Cow::Owned(line_num.to_string())
2413 }
2414 )
2415 }
2416
2417 fn file_start(&self, is_first: bool) -> &'static str {
2418 match self.decor_style {
2419 DecorStyle::Ascii => "--> ",
2420 DecorStyle::Unicode if is_first => " ╭▸ ",
2421 DecorStyle::Unicode => " ├▸ ",
2422 }
2423 }
2424
2425 fn secondary_file_start(&self) -> &'static str {
2426 match self.decor_style {
2427 DecorStyle::Ascii => "::: ",
2428 DecorStyle::Unicode => " ⸬ ",
2429 }
2430 }
2431
2432 fn draw_note_separator(
2433 &self,
2434 buffer: &mut StyledBuffer,
2435 line: usize,
2436 col: usize,
2437 is_cont: bool,
2438 ) {
2439 let chr = match self.decor_style {
2440 DecorStyle::Ascii => "= ",
2441 DecorStyle::Unicode if is_cont => "├ ",
2442 DecorStyle::Unicode => "╰ ",
2443 };
2444 buffer.puts(line, col, chr, ElementStyle::LineNumber);
2445 }
2446
2447 fn diff(&self) -> char {
2448 match self.decor_style {
2449 DecorStyle::Ascii => '~',
2450 DecorStyle::Unicode => '±',
2451 }
2452 }
2453
2454 fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) {
2455 let (column, dots) = match self.decor_style {
2456 DecorStyle::Ascii => (0, "..."),
2457 DecorStyle::Unicode => (col - 2, "‡"),
2458 };
2459 buffer.puts(line, column, dots, ElementStyle::LineNumber);
2460 }
2461
2462 fn margin(&self) -> &'static str {
2463 match self.decor_style {
2464 DecorStyle::Ascii => "...",
2465 DecorStyle::Unicode => "…",
2466 }
2467 }
2468
2469 fn underline(&self, is_primary: bool) -> UnderlineParts {
2470 // X0 Y0
2471 // label_start > ┯━━━━ < underline
2472 // │ < vertical_text_line
2473 // text
2474
2475 // multiline_start_down ⤷ X0 Y0
2476 // top_left > ┌───╿──┘ < top_right_flat
2477 // top_left > ┏│━━━┙ < top_right
2478 // multiline_vertical > ┃│
2479 // ┃│ X1 Y1
2480 // ┃│ X2 Y2
2481 // ┃└────╿──┘ < multiline_end_same_line
2482 // bottom_left > ┗━━━━━┥ < bottom_right_with_text
2483 // multiline_horizontal ^ `X` is a good letter
2484
2485 // multiline_whole_line > ┏ X0 Y0
2486 // ┃ X1 Y1
2487 // ┗━━━━┛ < multiline_end_same_line
2488
2489 // multiline_whole_line > ┏ X0 Y0
2490 // ┃ X1 Y1
2491 // ┃ ╿ < multiline_end_up
2492 // ┗━━┛ < bottom_right
2493
2494 match (self.decor_style, is_primary) {
2495 (DecorStyle::Ascii, true) => UnderlineParts {
2496 style: ElementStyle::UnderlinePrimary,
2497 underline: '^',
2498 label_start: '^',
2499 vertical_text_line: '|',
2500 multiline_vertical: '|',
2501 multiline_horizontal: '_',
2502 multiline_whole_line: '/',
2503 multiline_start_down: '^',
2504 bottom_right: '|',
2505 top_left: ' ',
2506 top_right_flat: '^',
2507 bottom_left: '|',
2508 multiline_end_up: '^',
2509 multiline_end_same_line: '^',
2510 multiline_bottom_right_with_text: '|',
2511 },
2512 (DecorStyle::Ascii, false) => UnderlineParts {
2513 style: ElementStyle::UnderlineSecondary,
2514 underline: '-',
2515 label_start: '-',
2516 vertical_text_line: '|',
2517 multiline_vertical: '|',
2518 multiline_horizontal: '_',
2519 multiline_whole_line: '/',
2520 multiline_start_down: '-',
2521 bottom_right: '|',
2522 top_left: ' ',
2523 top_right_flat: '-',
2524 bottom_left: '|',
2525 multiline_end_up: '-',
2526 multiline_end_same_line: '-',
2527 multiline_bottom_right_with_text: '|',
2528 },
2529 (DecorStyle::Unicode, true) => UnderlineParts {
2530 style: ElementStyle::UnderlinePrimary,
2531 underline: '━',
2532 label_start: '┯',
2533 vertical_text_line: '│',
2534 multiline_vertical: '┃',
2535 multiline_horizontal: '━',
2536 multiline_whole_line: '┏',
2537 multiline_start_down: '╿',
2538 bottom_right: '┙',
2539 top_left: '┏',
2540 top_right_flat: '┛',
2541 bottom_left: '┗',
2542 multiline_end_up: '╿',
2543 multiline_end_same_line: '┛',
2544 multiline_bottom_right_with_text: '┥',
2545 },
2546 (DecorStyle::Unicode, false) => UnderlineParts {
2547 style: ElementStyle::UnderlineSecondary,
2548 underline: '─',
2549 label_start: '┬',
2550 vertical_text_line: '│',
2551 multiline_vertical: '│',
2552 multiline_horizontal: '─',
2553 multiline_whole_line: '┌',
2554 multiline_start_down: '│',
2555 bottom_right: '┘',
2556 top_left: '┌',
2557 top_right_flat: '┘',
2558 bottom_left: '└',
2559 multiline_end_up: '│',
2560 multiline_end_same_line: '┘',
2561 multiline_bottom_right_with_text: '┤',
2562 },
2563 }
2564 }
2565}
2566
2567/// Customize [`Renderer::styled`]
2568impl Renderer {
2569 /// Override the output style for `error`
2570 pub const fn error(mut self, style: Style) -> Self {
2571 self.stylesheet.error = style;
2572 self
2573 }
2574
2575 /// Override the output style for `warning`
2576 pub const fn warning(mut self, style: Style) -> Self {
2577 self.stylesheet.warning = style;
2578 self
2579 }
2580
2581 /// Override the output style for `info`
2582 pub const fn info(mut self, style: Style) -> Self {
2583 self.stylesheet.info = style;
2584 self
2585 }
2586
2587 /// Override the output style for `note`
2588 pub const fn note(mut self, style: Style) -> Self {
2589 self.stylesheet.note = style;
2590 self
2591 }
2592
2593 /// Override the output style for `help`
2594 pub const fn help(mut self, style: Style) -> Self {
2595 self.stylesheet.help = style;
2596 self
2597 }
2598
2599 /// Override the output style for line numbers
2600 pub const fn line_num(mut self, style: Style) -> Self {
2601 self.stylesheet.line_num = style;
2602 self
2603 }
2604
2605 /// Override the output style for emphasis
2606 pub const fn emphasis(mut self, style: Style) -> Self {
2607 self.stylesheet.emphasis = style;
2608 self
2609 }
2610
2611 /// Override the output style for none
2612 pub const fn none(mut self, style: Style) -> Self {
2613 self.stylesheet.none = style;
2614 self
2615 }
2616
2617 /// Override the output style for [`AnnotationKind::Context`]
2618 pub const fn context(mut self, style: Style) -> Self {
2619 self.stylesheet.context = style;
2620 self
2621 }
2622
2623 /// Override the output style for additions
2624 pub const fn addition(mut self, style: Style) -> Self {
2625 self.stylesheet.addition = style;
2626 self
2627 }
2628
2629 /// Override the output style for removals
2630 pub const fn removal(mut self, style: Style) -> Self {
2631 self.stylesheet.removal = style;
2632 self
2633 }
2634}
2635
2636trait MessageOrTitle {
2637 fn level(&self) -> &Level<'_>;
2638 fn id(&self) -> Option<&Id<'_>>;
2639 fn text(&self) -> &str;
2640 fn allows_styling(&self) -> bool;
2641}
2642
2643impl MessageOrTitle for Title<'_> {
2644 fn level(&self) -> &Level<'_> {
2645 &self.level
2646 }
2647 fn id(&self) -> Option<&Id<'_>> {
2648 self.id.as_ref()
2649 }
2650 fn text(&self) -> &str {
2651 self.text.as_ref()
2652 }
2653 fn allows_styling(&self) -> bool {
2654 self.allows_styling
2655 }
2656}
2657
2658impl MessageOrTitle for Message<'_> {
2659 fn level(&self) -> &Level<'_> {
2660 &self.level
2661 }
2662 fn id(&self) -> Option<&Id<'_>> {
2663 None
2664 }
2665 fn text(&self) -> &str {
2666 self.text.as_ref()
2667 }
2668 fn allows_styling(&self) -> bool {
2669 true
2670 }
2671}
2672
2673// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until
2674// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which
2675// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway.
2676// This is also why we need the max number of decimal digits within a `usize`.
2677fn num_decimal_digits(num: usize) -> usize {
2678 #[cfg(target_pointer_width = "64")]
2679 const MAX_DIGITS: usize = 20;
2680
2681 #[cfg(target_pointer_width = "32")]
2682 const MAX_DIGITS: usize = 10;
2683
2684 #[cfg(target_pointer_width = "16")]
2685 const MAX_DIGITS: usize = 5;
2686
2687 let mut lim = 10;
2688 for num_digits in 1..MAX_DIGITS {
2689 if num < lim {
2690 return num_digits;
2691 }
2692 lim = lim.wrapping_mul(10);
2693 }
2694 MAX_DIGITS
2695}
2696
2697fn str_width(s: &str) -> usize {
2698 s.chars().map(char_width).sum()
2699}
2700
2701fn char_width(ch: char) -> usize {
2702 // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now,
2703 // just accept that sometimes the code line will be longer than desired.
2704 match ch {
2705 '\t' => 4,
2706 // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These
2707 // are control points that we replace before printing with a visible codepoint for the sake
2708 // of being able to point at them with underlines.
2709 '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
2710 | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2711 | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2712 | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2713 | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2714 | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2715 | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2716 _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
2717 }
2718}
2719
2720fn num_overlap(
2721 a_start: usize,
2722 a_end: usize,
2723 b_start: usize,
2724 b_end: usize,
2725 inclusive: bool,
2726) -> bool {
2727 let extra = usize::from(inclusive);
2728 (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
2729}
2730
2731fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
2732 num_overlap(
2733 a1.start.display,
2734 a1.end.display + padding,
2735 a2.start.display,
2736 a2.end.display,
2737 false,
2738 )
2739}
2740
2741#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2742pub(crate) enum LineAnnotationType {
2743 /// Annotation under a single line of code
2744 Singleline,
2745
2746 // The Multiline type above is replaced with the following three in order
2747 // to reuse the current label drawing code.
2748 //
2749 // Each of these corresponds to one part of the following diagram:
2750 //
2751 // x | foo(1 + bar(x,
2752 // | _________^ < MultilineStart
2753 // x | | y), < MultilineLine
2754 // | |______________^ label < MultilineEnd
2755 // x | z);
2756 /// Annotation marking the first character of a fully shown multiline span
2757 MultilineStart(usize),
2758 /// Annotation marking the last character of a fully shown multiline span
2759 MultilineEnd(usize),
2760 /// Line at the left enclosing the lines of a fully shown multiline span
2761 // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4
2762 // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in
2763 // `draw_multiline_line`.
2764 MultilineLine(usize),
2765}
2766
2767#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2768pub(crate) struct LineAnnotation<'a> {
2769 /// Start column.
2770 /// Note that it is important that this field goes
2771 /// first, so that when we sort, we sort orderings by start
2772 /// column.
2773 pub start: Loc,
2774
2775 /// End column within the line (exclusive)
2776 pub end: Loc,
2777
2778 /// level
2779 pub kind: AnnotationKind,
2780
2781 /// Optional label to display adjacent to the annotation.
2782 pub label: Option<Cow<'a, str>>,
2783
2784 /// Is this a single line, multiline or multiline span minimized down to a
2785 /// smaller span.
2786 pub annotation_type: LineAnnotationType,
2787
2788 /// Whether the source code should be highlighted
2789 pub highlight_source: bool,
2790}
2791
2792impl LineAnnotation<'_> {
2793 pub(crate) fn is_primary(&self) -> bool {
2794 self.kind == AnnotationKind::Primary
2795 }
2796
2797 /// Whether this annotation is a vertical line placeholder.
2798 pub(crate) fn is_line(&self) -> bool {
2799 matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
2800 }
2801
2802 /// Length of this annotation as displayed in the stderr output
2803 pub(crate) fn len(&self) -> usize {
2804 // Account for usize underflows
2805 self.end.display.abs_diff(self.start.display)
2806 }
2807
2808 pub(crate) fn has_label(&self) -> bool {
2809 if let Some(label) = &self.label {
2810 // Consider labels with no text as effectively not being there
2811 // to avoid weird output with unnecessary vertical lines, like:
2812 //
2813 // X | fn foo(x: u32) {
2814 // | -------^------
2815 // | | |
2816 // | |
2817 // |
2818 //
2819 // Note that this would be the complete output users would see.
2820 !label.is_empty()
2821 } else {
2822 false
2823 }
2824 }
2825
2826 pub(crate) fn takes_space(&self) -> bool {
2827 // Multiline annotations always have to keep vertical space.
2828 matches!(
2829 self.annotation_type,
2830 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2831 )
2832 }
2833}
2834
2835#[derive(Clone, Copy, Debug)]
2836pub(crate) enum DisplaySuggestion {
2837 Underline,
2838 Diff,
2839 None,
2840 Add,
2841}
2842
2843// We replace some characters so the CLI output is always consistent and underlines aligned.
2844// Keep the following list in sync with `rustc_span::char_width`.
2845const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2846 // In terminals without Unicode support the following will be garbled, but in *all* terminals
2847 // the underlying codepoint will be as well. We could gate this replacement behind a "unicode
2848 // support" gate.
2849 ('\0', "␀"),
2850 ('\u{0001}', "␁"),
2851 ('\u{0002}', "␂"),
2852 ('\u{0003}', "␃"),
2853 ('\u{0004}', "␄"),
2854 ('\u{0005}', "␅"),
2855 ('\u{0006}', "␆"),
2856 ('\u{0007}', "␇"),
2857 ('\u{0008}', "␈"),
2858 ('\t', " "), // We do our own tab replacement
2859 ('\u{000b}', "␋"),
2860 ('\u{000c}', "␌"),
2861 ('\u{000d}', "␍"),
2862 ('\u{000e}', "␎"),
2863 ('\u{000f}', "␏"),
2864 ('\u{0010}', "␐"),
2865 ('\u{0011}', "␑"),
2866 ('\u{0012}', "␒"),
2867 ('\u{0013}', "␓"),
2868 ('\u{0014}', "␔"),
2869 ('\u{0015}', "␕"),
2870 ('\u{0016}', "␖"),
2871 ('\u{0017}', "␗"),
2872 ('\u{0018}', "␘"),
2873 ('\u{0019}', "␙"),
2874 ('\u{001a}', "␚"),
2875 ('\u{001b}', "␛"),
2876 ('\u{001c}', "␜"),
2877 ('\u{001d}', "␝"),
2878 ('\u{001e}', "␞"),
2879 ('\u{001f}', "␟"),
2880 ('\u{007f}', "␡"),
2881 ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters.
2882 ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently
2883 ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk
2884 ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always.
2885 ('\u{202d}', "�"),
2886 ('\u{202e}', "�"),
2887 ('\u{2066}', "�"),
2888 ('\u{2067}', "�"),
2889 ('\u{2068}', "�"),
2890 ('\u{2069}', "�"),
2891];
2892
2893pub(crate) fn normalize_whitespace(s: &str) -> String {
2894 // Scan the input string for a character in the ordered table above.
2895 // If it's present, replace it with its alternative string (it can be more than 1 char!).
2896 // Otherwise, retain the input char.
2897 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
2898 match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
2899 Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2900 _ => s.push(c),
2901 }
2902 s
2903 })
2904}
2905
2906#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2907pub(crate) enum ElementStyle {
2908 MainHeaderMsg,
2909 HeaderMsg,
2910 LineAndColumn,
2911 LineNumber,
2912 Quotation,
2913 UnderlinePrimary,
2914 UnderlineSecondary,
2915 LabelPrimary,
2916 LabelSecondary,
2917 NoStyle,
2918 Level(LevelInner),
2919 Addition,
2920 Removal,
2921}
2922
2923impl ElementStyle {
2924 fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
2925 match self {
2926 ElementStyle::Addition => stylesheet.addition,
2927 ElementStyle::Removal => stylesheet.removal,
2928 ElementStyle::LineAndColumn => stylesheet.none,
2929 ElementStyle::LineNumber => stylesheet.line_num,
2930 ElementStyle::Quotation => stylesheet.none,
2931 ElementStyle::MainHeaderMsg => stylesheet.emphasis,
2932 ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
2933 ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
2934 ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
2935 ElementStyle::Level(lvl) => lvl.style(stylesheet),
2936 }
2937 }
2938}
2939
2940#[derive(Debug, Clone, Copy)]
2941struct UnderlineParts {
2942 style: ElementStyle,
2943 underline: char,
2944 label_start: char,
2945 vertical_text_line: char,
2946 multiline_vertical: char,
2947 multiline_horizontal: char,
2948 multiline_whole_line: char,
2949 multiline_start_down: char,
2950 bottom_right: char,
2951 top_left: char,
2952 top_right_flat: char,
2953 bottom_left: char,
2954 multiline_end_up: char,
2955 multiline_end_same_line: char,
2956 multiline_bottom_right_with_text: char,
2957}
2958
2959/// The character set for rendering for decor
2960#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2961pub enum DecorStyle {
2962 Ascii,
2963 Unicode,
2964}
2965
2966#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2967enum TitleStyle {
2968 MainHeader,
2969 Header,
2970 Secondary,
2971}
2972
2973fn max_line_number(groups: &[Group<'_>]) -> usize {
2974 groups
2975 .iter()
2976 .map(|v| {
2977 v.elements
2978 .iter()
2979 .map(|s| match s {
2980 Element::Message(_) | Element::Origin(_) | Element::Padding(_) => 0,
2981 Element::Cause(cause) => {
2982 if cause.fold {
2983 let end = cause
2984 .markers
2985 .iter()
2986 .map(|a| a.span.end)
2987 .max()
2988 .unwrap_or(cause.source.len())
2989 .min(cause.source.len());
2990
2991 cause.line_start + newline_count(&cause.source[..end])
2992 } else {
2993 cause.line_start + newline_count(&cause.source)
2994 }
2995 }
2996 Element::Suggestion(suggestion) => {
2997 if suggestion.fold {
2998 let end = suggestion
2999 .markers
3000 .iter()
3001 .map(|a| a.span.end)
3002 .max()
3003 .unwrap_or(suggestion.source.len())
3004 .min(suggestion.source.len());
3005
3006 suggestion.line_start + newline_count(&suggestion.source[..end])
3007 } else {
3008 suggestion.line_start + newline_count(&suggestion.source)
3009 }
3010 }
3011 })
3012 .max()
3013 .unwrap_or(1)
3014 })
3015 .max()
3016 .unwrap_or(1)
3017}
3018
3019fn newline_count(body: &str) -> usize {
3020 #[cfg(feature = "simd")]
3021 {
3022 memchr::memchr_iter(b'\n', body.as_bytes()).count()
3023 }
3024 #[cfg(not(feature = "simd"))]
3025 {
3026 body.lines().count().saturating_sub(1)
3027 }
3028}
3029
3030#[cfg(test)]
3031mod test {
3032 use super::{newline_count, OUTPUT_REPLACEMENTS};
3033 use snapbox::IntoData;
3034
3035 fn format_replacements(replacements: Vec<(char, &str)>) -> String {
3036 replacements
3037 .into_iter()
3038 .map(|r| format!(" {r:?}"))
3039 .collect::<Vec<_>>()
3040 .join("\n")
3041 }
3042
3043 #[test]
3044 /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to
3045 /// work) and must contain no duplicate entries
3046 fn ensure_output_replacements_is_sorted() {
3047 let mut expected = OUTPUT_REPLACEMENTS.to_owned();
3048 expected.sort_by_key(|r| r.0);
3049 expected.dedup_by_key(|r| r.0);
3050 let expected = format_replacements(expected);
3051 let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
3052 snapbox::assert_data_eq!(actual, expected.into_data().raw());
3053 }
3054
3055 #[test]
3056 fn ensure_newline_count_correct() {
3057 let source = r#"
3058 cargo-features = ["path-bases"]
3059
3060 [package]
3061 name = "foo"
3062 version = "0.5.0"
3063 authors = ["wycats@example.com"]
3064
3065 [dependencies]
3066 bar = { base = '^^not-valid^^', path = 'bar' }
3067 "#;
3068 let actual_count = newline_count(source);
3069 let expected_count = 10;
3070
3071 assert_eq!(expected_count, actual_count);
3072 }
3073}