1use std::borrow::Cow;
4use std::cmp::{max, min, Ordering, Reverse};
5use std::collections::HashMap;
6use std::fmt;
7
8use anstyle::Style;
9
10use super::margin::Margin;
11use super::stylesheet::Stylesheet;
12use super::DecorStyle;
13use super::Renderer;
14use crate::level::{Level, LevelInner};
15use crate::renderer::source_map::{
16 AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight, TrimmedPatch,
17};
18use crate::renderer::styled_buffer::StyledBuffer;
19use crate::snippet::Id;
20use crate::{
21 Annotation, AnnotationKind, Element, Group, Message, Origin, Padding, Patch, Report, Snippet,
22 Title,
23};
24
25const ANONYMIZED_LINE_NUM: &str = "LL";
26
27pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String {
28 if renderer.short_message {
29 render_short_message(renderer, groups).unwrap()
30 } else {
31 let (max_line_num, og_primary_path, groups) = pre_process(groups);
32 let max_line_num_len = if renderer.anonymized_line_numbers {
33 ANONYMIZED_LINE_NUM.len()
34 } else {
35 num_decimal_digits(max_line_num)
36 };
37 let mut out_string = String::new();
38 let group_len = groups.len();
39 for (
40 g,
41 PreProcessedGroup {
42 group,
43 elements,
44 primary_path,
45 max_depth,
46 },
47 ) in groups.into_iter().enumerate()
48 {
49 let mut buffer = StyledBuffer::new();
50 let level = group.primary_level.clone();
51 let mut message_iter = elements.into_iter().enumerate().peekable();
52 if let Some(title) = &group.title {
53 let peek = message_iter.peek().map(|(_, s)| s);
54 let title_style = if title.allows_styling {
55 TitleStyle::Header
56 } else {
57 TitleStyle::MainHeader
58 };
59 let buffer_msg_line_offset = buffer.num_lines();
60 render_title(
61 renderer,
62 &mut buffer,
63 title,
64 max_line_num_len,
65 title_style,
66 matches!(peek, Some(PreProcessedElement::Message(_))),
67 buffer_msg_line_offset,
68 );
69 let buffer_msg_line_offset = buffer.num_lines();
70
71 if matches!(peek, Some(PreProcessedElement::Message(_))) {
72 draw_col_separator_no_space(
73 renderer,
74 &mut buffer,
75 buffer_msg_line_offset,
76 max_line_num_len + 1,
77 );
78 }
79 if peek.is_none()
80 && title_style == TitleStyle::MainHeader
81 && g == 0
82 && group_len > 1
83 {
84 draw_col_separator_end(
85 renderer,
86 &mut buffer,
87 buffer_msg_line_offset,
88 max_line_num_len + 1,
89 );
90 }
91 }
92 let mut seen_primary = false;
93 let mut last_suggestion_path = None;
94 while let Some((i, section)) = message_iter.next() {
95 let peek = message_iter.peek().map(|(_, s)| s);
96 let is_first = i == 0;
97 match section {
98 PreProcessedElement::Message(title) => {
99 let title_style = TitleStyle::Secondary;
100 let buffer_msg_line_offset = buffer.num_lines();
101 render_title(
102 renderer,
103 &mut buffer,
104 title,
105 max_line_num_len,
106 title_style,
107 peek.is_some(),
108 buffer_msg_line_offset,
109 );
110 }
111 PreProcessedElement::Cause((cause, source_map, annotated_lines)) => {
112 let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
113 seen_primary |= is_primary;
114 render_snippet_annotations(
115 renderer,
116 &mut buffer,
117 max_line_num_len,
118 cause,
119 is_primary,
120 &source_map,
121 &annotated_lines,
122 max_depth,
123 peek.is_some() || (g == 0 && group_len > 1),
124 is_first,
125 );
126
127 if g == 0 {
128 let current_line = buffer.num_lines();
129 match peek {
130 Some(PreProcessedElement::Message(_)) => {
131 draw_col_separator_no_space(
132 renderer,
133 &mut buffer,
134 current_line,
135 max_line_num_len + 1,
136 );
137 }
138 None if group_len > 1 => draw_col_separator_end(
139 renderer,
140 &mut buffer,
141 current_line,
142 max_line_num_len + 1,
143 ),
144 _ => {}
145 }
146 }
147 }
148 PreProcessedElement::Suggestion((
149 suggestion,
150 source_map,
151 spliced_lines,
152 display_suggestion,
153 )) => {
154 let matches_previous_suggestion =
155 last_suggestion_path == Some(suggestion.path.as_ref());
156 emit_suggestion_default(
157 renderer,
158 &mut buffer,
159 suggestion,
160 spliced_lines,
161 display_suggestion,
162 max_line_num_len,
163 &source_map,
164 primary_path.or(og_primary_path),
165 matches_previous_suggestion,
166 is_first,
167 peek.is_some(),
169 );
170
171 if matches!(peek, Some(PreProcessedElement::Suggestion(_))) {
172 last_suggestion_path = Some(suggestion.path.as_ref());
173 } else {
174 last_suggestion_path = None;
175 }
176 }
177
178 PreProcessedElement::Origin(origin) => {
179 let buffer_msg_line_offset = buffer.num_lines();
180 let is_primary = primary_path == Some(&origin.path) && !seen_primary;
181 seen_primary |= is_primary;
182 render_origin(
183 renderer,
184 &mut buffer,
185 max_line_num_len,
186 origin,
187 is_primary,
188 is_first,
189 peek.is_none(),
190 buffer_msg_line_offset,
191 );
192 let current_line = buffer.num_lines();
193 if g == 0 && peek.is_none() && group_len > 1 {
194 draw_col_separator_end(
195 renderer,
196 &mut buffer,
197 current_line,
198 max_line_num_len + 1,
199 );
200 }
201 }
202 PreProcessedElement::Padding(_) => {
203 let current_line = buffer.num_lines();
204 if peek.is_none() {
205 draw_col_separator_end(
206 renderer,
207 &mut buffer,
208 current_line,
209 max_line_num_len + 1,
210 );
211 } else {
212 draw_col_separator_no_space(
213 renderer,
214 &mut buffer,
215 current_line,
216 max_line_num_len + 1,
217 );
218 }
219 }
220 }
221 }
222 buffer
223 .render(&level, &renderer.stylesheet, &mut out_string)
224 .unwrap();
225 if g != group_len - 1 {
226 use std::fmt::Write;
227
228 writeln!(out_string).unwrap();
229 }
230 }
231 out_string
232 }
233}
234
235fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
236 let mut buffer = StyledBuffer::new();
237 let mut labels = None;
238 let group = groups.first().expect("Expected at least one group");
239
240 let Some(title) = &group.title else {
241 panic!("Expected a Title");
242 };
243
244 if let Some(Element::Cause(cause)) = group
245 .elements
246 .iter()
247 .find(|e| matches!(e, Element::Cause(_)))
248 {
249 let labels_inner = cause
250 .markers
251 .iter()
252 .filter_map(|ann| match &ann.label {
253 Some(msg) if ann.kind.is_primary() => {
254 if !msg.trim().is_empty() {
255 Some(msg.to_string())
256 } else {
257 None
258 }
259 }
260 _ => None,
261 })
262 .collect::<Vec<_>>()
263 .join(", ");
264 if !labels_inner.is_empty() {
265 labels = Some(labels_inner);
266 }
267
268 if let Some(path) = &cause.path {
269 let mut origin = Origin::path(path.as_ref());
270
271 let source_map = SourceMap::new(&cause.source, cause.line_start);
272 let (_depth, annotated_lines) =
273 source_map.annotated_lines(cause.markers.clone(), cause.fold);
274
275 if let Some(primary_line) = annotated_lines
276 .iter()
277 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
278 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
279 {
280 origin.line = Some(primary_line.line_index);
281 if let Some(first_annotation) = primary_line
282 .annotations
283 .iter()
284 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
285 {
286 origin.char_column = Some(first_annotation.start.char + 1);
287 }
288 }
289
290 render_origin(renderer, &mut buffer, 0, &origin, true, true, true, 0);
291 buffer.append(0, ": ", ElementStyle::LineAndColumn);
292 }
293 }
294
295 render_title(
296 renderer,
297 &mut buffer,
298 title,
299 0, TitleStyle::MainHeader,
301 false,
302 0,
303 );
304
305 if let Some(labels) = labels {
306 buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
307 }
308
309 let mut out_string = String::new();
310 buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?;
311
312 Ok(out_string)
313}
314
315#[allow(clippy::too_many_arguments)]
316fn render_title(
317 renderer: &Renderer,
318 buffer: &mut StyledBuffer,
319 title: &dyn MessageOrTitle,
320 max_line_num_len: usize,
321 title_style: TitleStyle,
322 is_cont: bool,
323 buffer_msg_line_offset: usize,
324) {
325 let (label_style, title_element_style) = match title_style {
326 TitleStyle::MainHeader => (
327 ElementStyle::Level(title.level().level),
328 if renderer.short_message {
329 ElementStyle::NoStyle
330 } else {
331 ElementStyle::MainHeaderMsg
332 },
333 ),
334 TitleStyle::Header => (
335 ElementStyle::Level(title.level().level),
336 ElementStyle::HeaderMsg,
337 ),
338 TitleStyle::Secondary => {
339 for _ in 0..max_line_num_len {
340 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
341 }
342
343 draw_note_separator(
344 renderer,
345 buffer,
346 buffer_msg_line_offset,
347 max_line_num_len + 1,
348 is_cont,
349 );
350 (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
351 }
352 };
353 let mut label_width = 0;
354
355 if title.level().name != Some(None) {
356 buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
357 label_width += title.level().as_str().len();
358 if let Some(Id { id: Some(id), url }) = &title.id() {
359 buffer.append(buffer_msg_line_offset, "[", label_style);
360 if let Some(url) = url.as_ref() {
361 buffer.append(
362 buffer_msg_line_offset,
363 &format!("\x1B]8;;{url}\x1B\\"),
364 label_style,
365 );
366 }
367 buffer.append(buffer_msg_line_offset, id, label_style);
368 if url.is_some() {
369 buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
370 }
371 buffer.append(buffer_msg_line_offset, "]", label_style);
372 label_width += 2 + id.len();
373 }
374 buffer.append(buffer_msg_line_offset, ": ", title_element_style);
375 label_width += 2;
376 }
377
378 let padding = " ".repeat(if title_style == TitleStyle::Secondary {
379 max_line_num_len + 3 + label_width
397 } else {
398 label_width
399 });
400
401 let (title_str, style) = if title.allows_styling() {
402 (title.text().to_owned(), ElementStyle::NoStyle)
403 } else {
404 (normalize_whitespace(title.text()), title_element_style)
405 };
406 for (i, text) in title_str.split('\n').enumerate() {
407 if i != 0 {
408 buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
409 if title_style == TitleStyle::Secondary
410 && is_cont
411 && matches!(renderer.decor_style, DecorStyle::Unicode)
412 {
413 draw_col_separator_no_space(
425 renderer,
426 buffer,
427 buffer_msg_line_offset + i,
428 max_line_num_len + 1,
429 );
430 }
431 }
432 buffer.append(buffer_msg_line_offset + i, text, style);
433 }
434}
435
436#[allow(clippy::too_many_arguments)]
437fn render_origin(
438 renderer: &Renderer,
439 buffer: &mut StyledBuffer,
440 max_line_num_len: usize,
441 origin: &Origin<'_>,
442 is_primary: bool,
443 is_first: bool,
444 alone: bool,
445 buffer_msg_line_offset: usize,
446) {
447 if is_primary && !renderer.short_message {
448 buffer.prepend(
449 buffer_msg_line_offset,
450 renderer.decor_style.file_start(is_first, alone),
451 ElementStyle::LineNumber,
452 );
453 } else if !renderer.short_message {
454 buffer.prepend(
475 buffer_msg_line_offset,
476 renderer.decor_style.secondary_file_start(),
477 ElementStyle::LineNumber,
478 );
479 }
480
481 let str = match (&origin.line, &origin.char_column) {
482 (Some(line), Some(col)) => {
483 format!("{}:{}:{}", origin.path, line, col)
484 }
485 (Some(line), None) => format!("{}:{}", origin.path, line),
486 _ => origin.path.to_string(),
487 };
488
489 buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
490 if !renderer.short_message {
491 for _ in 0..max_line_num_len {
492 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
493 }
494 }
495}
496
497#[allow(clippy::too_many_arguments)]
498fn render_snippet_annotations(
499 renderer: &Renderer,
500 buffer: &mut StyledBuffer,
501 max_line_num_len: usize,
502 snippet: &Snippet<'_, Annotation<'_>>,
503 is_primary: bool,
504 sm: &SourceMap<'_>,
505 annotated_lines: &[AnnotatedLineInfo<'_>],
506 multiline_depth: usize,
507 is_cont: bool,
508 is_first: bool,
509) {
510 if let Some(path) = &snippet.path {
511 let mut origin = Origin::path(path.as_ref());
512 if is_primary {
517 if let Some(primary_line) = annotated_lines
518 .iter()
519 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
520 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
521 {
522 origin.line = Some(primary_line.line_index);
523 if let Some(first_annotation) = primary_line
524 .annotations
525 .iter()
526 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
527 {
528 origin.char_column = Some(first_annotation.start.char + 1);
529 }
530 }
531 } else {
532 let buffer_msg_line_offset = buffer.num_lines();
533 draw_col_separator_no_space(
544 renderer,
545 buffer,
546 buffer_msg_line_offset,
547 max_line_num_len + 1,
548 );
549 if let Some(first_line) = annotated_lines.first() {
550 origin.line = Some(first_line.line_index);
551 if let Some(first_annotation) = first_line.annotations.first() {
552 origin.char_column = Some(first_annotation.start.char + 1);
553 }
554 }
555 }
556 let buffer_msg_line_offset = buffer.num_lines();
557 render_origin(
558 renderer,
559 buffer,
560 max_line_num_len,
561 &origin,
562 is_primary,
563 is_first,
564 false,
565 buffer_msg_line_offset,
566 );
567 draw_col_separator_no_space(
569 renderer,
570 buffer,
571 buffer_msg_line_offset + 1,
572 max_line_num_len + 1,
573 );
574 } else {
575 let buffer_msg_line_offset = buffer.num_lines();
576 if is_primary {
577 if renderer.decor_style == DecorStyle::Unicode {
578 buffer.puts(
579 buffer_msg_line_offset,
580 max_line_num_len,
581 renderer.decor_style.file_start(is_first, false),
582 ElementStyle::LineNumber,
583 );
584 } else {
585 draw_col_separator_no_space(
586 renderer,
587 buffer,
588 buffer_msg_line_offset,
589 max_line_num_len + 1,
590 );
591 }
592 } else {
593 draw_col_separator_no_space(
604 renderer,
605 buffer,
606 buffer_msg_line_offset,
607 max_line_num_len + 1,
608 );
609
610 buffer.puts(
611 buffer_msg_line_offset + 1,
612 max_line_num_len,
613 renderer.decor_style.secondary_file_start(),
614 ElementStyle::LineNumber,
615 );
616 }
617 }
618
619 let mut multilines = Vec::new();
621
622 let mut whitespace_margin = usize::MAX;
624 for line_info in annotated_lines {
625 let leading_whitespace = line_info
631 .line
632 .chars()
633 .take_while(|c| c.is_whitespace())
634 .map(|c| {
635 match c {
636 '\t' => 4,
638 _ => 1,
639 }
640 })
641 .sum();
642 if line_info.line.chars().any(|c| !c.is_whitespace()) {
643 whitespace_margin = min(whitespace_margin, leading_whitespace);
644 }
645 }
646 if whitespace_margin == usize::MAX {
647 whitespace_margin = 0;
648 }
649
650 let mut span_left_margin = usize::MAX;
652 for line_info in annotated_lines {
653 for ann in &line_info.annotations {
654 span_left_margin = min(span_left_margin, ann.start.display);
655 span_left_margin = min(span_left_margin, ann.end.display);
656 }
657 }
658 if span_left_margin == usize::MAX {
659 span_left_margin = 0;
660 }
661
662 let mut span_right_margin = 0;
664 let mut label_right_margin = 0;
665 let mut max_line_len = 0;
666 for line_info in annotated_lines {
667 max_line_len = max(max_line_len, line_info.line.len());
668 for ann in &line_info.annotations {
669 span_right_margin = max(span_right_margin, ann.start.display);
670 span_right_margin = max(span_right_margin, ann.end.display);
671 let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1);
673 label_right_margin = max(label_right_margin, ann.end.display + label_right);
674 }
675 }
676 let width_offset = 3 + max_line_num_len;
677 let code_offset = if multiline_depth == 0 {
678 width_offset
679 } else {
680 width_offset + multiline_depth + 1
681 };
682
683 let column_width = renderer.term_width.saturating_sub(code_offset);
684
685 let margin = Margin::new(
686 whitespace_margin,
687 span_left_margin,
688 span_right_margin,
689 label_right_margin,
690 column_width,
691 max_line_len,
692 );
693
694 for annotated_line_idx in 0..annotated_lines.len() {
696 let previous_buffer_line = buffer.num_lines();
697
698 let depths = render_source_line(
699 renderer,
700 &annotated_lines[annotated_line_idx],
701 buffer,
702 width_offset,
703 code_offset,
704 max_line_num_len,
705 margin,
706 !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
707 );
708
709 let mut to_add = HashMap::new();
710
711 for (depth, style) in depths {
712 if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
713 multilines.swap_remove(index);
714 } else {
715 to_add.insert(depth, style);
716 }
717 }
718
719 for (depth, style) in &multilines {
722 for line in previous_buffer_line..buffer.num_lines() {
723 draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style);
724 }
725 }
726 if annotated_line_idx < (annotated_lines.len() - 1) {
729 let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
730 - annotated_lines[annotated_line_idx].line_index;
731 match line_idx_delta.cmp(&2) {
732 Ordering::Greater => {
733 let last_buffer_line_num = buffer.num_lines();
734
735 draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
736
737 for (depth, style) in &multilines {
739 draw_multiline_line(
740 renderer,
741 buffer,
742 last_buffer_line_num,
743 width_offset,
744 *depth,
745 *style,
746 );
747 }
748 if let Some(line) = annotated_lines.get(annotated_line_idx) {
749 for ann in &line.annotations {
750 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
751 draw_multiline_line(
755 renderer,
756 buffer,
757 last_buffer_line_num,
758 width_offset,
759 pos,
760 if ann.is_primary() {
761 ElementStyle::UnderlinePrimary
762 } else {
763 ElementStyle::UnderlineSecondary
764 },
765 );
766 }
767 }
768 }
769 }
770
771 Ordering::Equal => {
772 let unannotated_line = sm
773 .get_line(annotated_lines[annotated_line_idx].line_index + 1)
774 .unwrap_or("");
775
776 let last_buffer_line_num = buffer.num_lines();
777
778 draw_line(
779 renderer,
780 buffer,
781 &normalize_whitespace(unannotated_line),
782 annotated_lines[annotated_line_idx + 1].line_index - 1,
783 last_buffer_line_num,
784 width_offset,
785 code_offset,
786 max_line_num_len,
787 margin,
788 );
789
790 for (depth, style) in &multilines {
791 draw_multiline_line(
792 renderer,
793 buffer,
794 last_buffer_line_num,
795 width_offset,
796 *depth,
797 *style,
798 );
799 }
800 if let Some(line) = annotated_lines.get(annotated_line_idx) {
801 for ann in &line.annotations {
802 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
803 draw_multiline_line(
804 renderer,
805 buffer,
806 last_buffer_line_num,
807 width_offset,
808 pos,
809 if ann.is_primary() {
810 ElementStyle::UnderlinePrimary
811 } else {
812 ElementStyle::UnderlineSecondary
813 },
814 );
815 }
816 }
817 }
818 }
819 Ordering::Less => {}
820 }
821 }
822
823 multilines.extend(to_add);
824 }
825}
826
827#[allow(clippy::too_many_arguments)]
828fn render_source_line(
829 renderer: &Renderer,
830 line_info: &AnnotatedLineInfo<'_>,
831 buffer: &mut StyledBuffer,
832 width_offset: usize,
833 code_offset: usize,
834 max_line_num_len: usize,
835 margin: Margin,
836 close_window: bool,
837) -> Vec<(usize, ElementStyle)> {
838 let source_string = normalize_whitespace(line_info.line);
853
854 let line_offset = buffer.num_lines();
855
856 let left = draw_line(
857 renderer,
858 buffer,
859 &source_string,
860 line_info.line_index,
861 line_offset,
862 width_offset,
863 code_offset,
864 max_line_num_len,
865 margin,
866 );
867
868 if line_info.annotations.is_empty() {
870 if close_window {
873 draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
874 }
875 return vec![];
876 }
877
878 let mut buffer_ops = vec![];
895 let mut annotations = vec![];
896 let mut short_start = true;
897 for ann in &line_info.annotations {
898 if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
899 if source_string
900 .chars()
901 .take(ann.start.display)
902 .all(char::is_whitespace)
903 {
904 let uline = renderer.decor_style.underline(ann.is_primary());
905 let chr = uline.multiline_whole_line;
906 annotations.push((depth, uline.style));
907 buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
908 } else {
909 short_start = false;
910 break;
911 }
912 } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
913 } else {
914 short_start = false;
915 break;
916 }
917 }
918 if short_start {
919 for (y, x, c, s) in buffer_ops {
920 buffer.putc(y, x, c, s);
921 }
922 return annotations;
923 }
924
925 let mut annotations = line_info.annotations.clone();
958 annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
959
960 let mut overlap = vec![false; annotations.len()];
1023 let mut annotations_position = vec![];
1024 let mut line_len: usize = 0;
1025 let mut p = 0;
1026 for (i, annotation) in annotations.iter().enumerate() {
1027 for (j, next) in annotations.iter().enumerate() {
1028 if overlaps(next, annotation, 0) && j > 1 {
1029 overlap[i] = true;
1030 overlap[j] = true;
1031 }
1032 if overlaps(next, annotation, 0) && annotation.has_label() && j > i && p == 0
1036 {
1038 if next.start.display == annotation.start.display
1041 && next.start.char == annotation.start.char
1042 && next.end.display == annotation.end.display
1043 && next.end.char == annotation.end.char
1044 && !next.has_label()
1045 {
1046 continue;
1047 }
1048
1049 p += 1;
1051 break;
1052 }
1053 }
1054 annotations_position.push((p, annotation));
1055 for (j, next) in annotations.iter().enumerate() {
1056 if j > i {
1057 let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
1058 if (overlaps(next, annotation, l) && annotation.has_label() && next.has_label()) || (annotation.takes_space() && next.has_label()) || (annotation.has_label() && next.takes_space())
1075 || (annotation.takes_space() && next.takes_space())
1076 || (overlaps(next, annotation, l)
1077 && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1078 && next.has_label()
1079 && p == 0)
1080 {
1082 p += 1;
1084 break;
1085 }
1086 }
1087 }
1088 line_len = max(line_len, p);
1089 }
1090
1091 if line_len != 0 {
1092 line_len += 1;
1093 }
1094
1095 if line_info.annotations.iter().all(LineAnnotation::is_line) {
1098 return vec![];
1099 }
1100
1101 if annotations_position
1102 .iter()
1103 .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
1104 {
1105 if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
1106 for (pos, _) in &mut annotations_position {
1119 *pos = max_pos - *pos;
1120 }
1121 line_len = line_len.saturating_sub(1);
1124 }
1125 }
1126
1127 for pos in 0..=line_len {
1139 draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
1140 }
1141 if close_window {
1142 draw_col_separator_end(
1143 renderer,
1144 buffer,
1145 line_offset + line_len + 1,
1146 width_offset - 2,
1147 );
1148 }
1149 for &(pos, annotation) in &annotations_position {
1162 let underline = renderer.decor_style.underline(annotation.is_primary());
1163 let pos = pos + 1;
1164 match annotation.annotation_type {
1165 LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
1166 draw_range(
1167 buffer,
1168 underline.multiline_horizontal,
1169 line_offset + pos,
1170 width_offset + depth,
1171 (code_offset + annotation.start.display).saturating_sub(left),
1172 underline.style,
1173 );
1174 }
1175 _ if annotation.highlight_source => {
1176 buffer.set_style_range(
1177 line_offset,
1178 (code_offset + annotation.start.display).saturating_sub(left),
1179 (code_offset + annotation.end.display).saturating_sub(left),
1180 underline.style,
1181 annotation.is_primary(),
1182 );
1183 }
1184 _ => {}
1185 }
1186 }
1187
1188 for &(pos, annotation) in &annotations_position {
1200 let underline = renderer.decor_style.underline(annotation.is_primary());
1201 let pos = pos + 1;
1202
1203 if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
1204 for p in line_offset + 1..=line_offset + pos {
1205 buffer.putc(
1206 p,
1207 (code_offset + annotation.start.display).saturating_sub(left),
1208 match annotation.annotation_type {
1209 LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
1210 _ => underline.vertical_text_line,
1211 },
1212 underline.style,
1213 );
1214 }
1215 if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
1216 buffer.putc(
1217 line_offset + pos,
1218 (code_offset + annotation.start.display).saturating_sub(left),
1219 underline.bottom_right,
1220 underline.style,
1221 );
1222 }
1223 if matches!(
1224 annotation.annotation_type,
1225 LineAnnotationType::MultilineEnd(_)
1226 ) && annotation.has_label()
1227 {
1228 buffer.putc(
1229 line_offset + pos,
1230 (code_offset + annotation.start.display).saturating_sub(left),
1231 underline.multiline_bottom_right_with_text,
1232 underline.style,
1233 );
1234 }
1235 }
1236 match annotation.annotation_type {
1237 LineAnnotationType::MultilineStart(depth) => {
1238 buffer.putc(
1239 line_offset + pos,
1240 width_offset + depth - 1,
1241 underline.top_left,
1242 underline.style,
1243 );
1244 for p in line_offset + pos + 1..line_offset + line_len + 2 {
1245 buffer.putc(
1246 p,
1247 width_offset + depth - 1,
1248 underline.multiline_vertical,
1249 underline.style,
1250 );
1251 }
1252 }
1253 LineAnnotationType::MultilineEnd(depth) => {
1254 for p in line_offset..line_offset + pos {
1255 buffer.putc(
1256 p,
1257 width_offset + depth - 1,
1258 underline.multiline_vertical,
1259 underline.style,
1260 );
1261 }
1262 buffer.putc(
1263 line_offset + pos,
1264 width_offset + depth - 1,
1265 underline.bottom_left,
1266 underline.style,
1267 );
1268 }
1269 _ => (),
1270 }
1271 }
1272
1273 for &(pos, annotation) in &annotations_position {
1285 let style = if annotation.is_primary() {
1286 ElementStyle::LabelPrimary
1287 } else {
1288 ElementStyle::LabelSecondary
1289 };
1290 let (pos, col) = if pos == 0 {
1291 if annotation.end.display == 0 {
1292 (pos + 1, (annotation.end.display + 2).saturating_sub(left))
1293 } else {
1294 (pos + 1, (annotation.end.display + 1).saturating_sub(left))
1295 }
1296 } else {
1297 (pos + 2, annotation.start.display.saturating_sub(left))
1298 };
1299 if let Some(label) = &annotation.label {
1300 buffer.puts(line_offset + pos, code_offset + col, label, style);
1301 }
1302 }
1303
1304 annotations_position.sort_by_key(|(_, ann)| {
1313 (Reverse(ann.len()), ann.is_primary())
1315 });
1316
1317 for &(pos, annotation) in &annotations_position {
1329 let uline = renderer.decor_style.underline(annotation.is_primary());
1330 for p in annotation.start.display..annotation.end.display {
1331 buffer.putc(
1333 line_offset + 1,
1334 (code_offset + p).saturating_sub(left),
1335 uline.underline,
1336 uline.style,
1337 );
1338 }
1339
1340 if pos == 0
1341 && matches!(
1342 annotation.annotation_type,
1343 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1344 )
1345 {
1346 buffer.putc(
1348 line_offset + 1,
1349 (code_offset + annotation.start.display).saturating_sub(left),
1350 match annotation.annotation_type {
1351 LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
1352 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
1353 _ => panic!("unexpected annotation type: {annotation:?}"),
1354 },
1355 uline.style,
1356 );
1357 } else if pos != 0
1358 && matches!(
1359 annotation.annotation_type,
1360 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1361 )
1362 {
1363 buffer.putc(
1366 line_offset + 1,
1367 (code_offset + annotation.start.display).saturating_sub(left),
1368 match annotation.annotation_type {
1369 LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
1370 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
1371 _ => panic!("unexpected annotation type: {annotation:?}"),
1372 },
1373 uline.style,
1374 );
1375 } else if pos != 0 && annotation.has_label() {
1376 buffer.putc(
1378 line_offset + 1,
1379 (code_offset + annotation.start.display).saturating_sub(left),
1380 uline.label_start,
1381 uline.style,
1382 );
1383 }
1384 }
1385
1386 for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
1390 if overlap[i] {
1392 continue;
1393 };
1394 let LineAnnotationType::Singleline = annotation.annotation_type else {
1395 continue;
1396 };
1397 let width = annotation.end.display - annotation.start.display;
1398 if width > margin.term_width * 2 && width > 10 {
1399 let pad = max(margin.term_width / 3, 5);
1402 buffer.replace(
1404 line_offset,
1405 annotation.start.display + pad,
1406 annotation.end.display - pad,
1407 renderer.decor_style.margin(),
1408 );
1409 buffer.replace(
1411 line_offset + 1,
1412 annotation.start.display + pad,
1413 annotation.end.display - pad,
1414 renderer.decor_style.margin(),
1415 );
1416 }
1417 }
1418 annotations_position
1419 .iter()
1420 .filter_map(|&(_, annotation)| match annotation.annotation_type {
1421 LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
1422 let style = if annotation.is_primary() {
1423 ElementStyle::LabelPrimary
1424 } else {
1425 ElementStyle::LabelSecondary
1426 };
1427 Some((p, style))
1428 }
1429 _ => None,
1430 })
1431 .collect::<Vec<_>>()
1432}
1433
1434#[allow(clippy::too_many_arguments)]
1435fn emit_suggestion_default(
1436 renderer: &Renderer,
1437 buffer: &mut StyledBuffer,
1438 suggestion: &Snippet<'_, Patch<'_>>,
1439 spliced_lines: SplicedLines<'_>,
1440 show_code_change: DisplaySuggestion,
1441 max_line_num_len: usize,
1442 sm: &SourceMap<'_>,
1443 primary_path: Option<&Cow<'_, str>>,
1444 matches_previous_suggestion: bool,
1445 is_first: bool,
1446 is_cont: bool,
1447) {
1448 let buffer_offset = buffer.num_lines();
1449 let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
1450 let (complete, parts, highlights) = spliced_lines;
1451 let is_multiline = complete.lines().count() > 1;
1452
1453 if matches_previous_suggestion {
1454 buffer.puts(
1455 row_num - 1,
1456 max_line_num_len + 1,
1457 renderer.decor_style.multi_suggestion_separator(),
1458 ElementStyle::LineNumber,
1459 );
1460 } else {
1461 draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
1462 }
1463 if suggestion.path.as_ref() != primary_path {
1464 if let Some(path) = suggestion.path.as_ref() {
1465 if !matches_previous_suggestion {
1466 let (loc, _) = sm.span_to_locations(parts[0].span.clone());
1467 let arrow = renderer.decor_style.file_start(is_first, false);
1470 buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
1471 let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
1472 let col = usize::max(max_line_num_len + 1, arrow.len());
1473 buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
1474 for _ in 0..max_line_num_len {
1475 buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
1476 }
1477 draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
1478 row_num += 1;
1479 }
1480 }
1481 }
1482
1483 if let DisplaySuggestion::Diff = show_code_change {
1484 row_num += 1;
1485 }
1486
1487 let file_lines = sm.span_to_lines(parts[0].span.clone());
1488 let (line_start, line_end) = if suggestion.fold {
1489 sm.span_to_locations(parts[0].original_span.clone())
1491 } else {
1492 sm.span_to_locations(0..sm.source.len())
1493 };
1494 let mut lines = complete.lines();
1495 if lines.clone().next().is_none() {
1496 for line in line_start.line..=line_end.line {
1498 buffer.puts(
1499 row_num - 1 + line - line_start.line,
1500 0,
1501 &maybe_anonymized(renderer, line, max_line_num_len),
1502 ElementStyle::LineNumber,
1503 );
1504 buffer.puts(
1505 row_num - 1 + line - line_start.line,
1506 max_line_num_len + 1,
1507 "- ",
1508 ElementStyle::Removal,
1509 );
1510 buffer.puts(
1511 row_num - 1 + line - line_start.line,
1512 max_line_num_len + 3,
1513 &normalize_whitespace(sm.get_line(line).unwrap()),
1514 ElementStyle::Removal,
1515 );
1516 }
1517 row_num += line_end.line - line_start.line;
1518 }
1519 let mut unhighlighted_lines = Vec::new();
1520 for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
1521 if highlight_parts.is_empty() && suggestion.fold {
1523 unhighlighted_lines.push((line_pos, line));
1524 continue;
1525 }
1526
1527 match unhighlighted_lines.len() {
1528 0 => (),
1529 n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
1534 draw_code_line(
1535 renderer,
1536 buffer,
1537 &mut row_num,
1538 &[],
1539 p + line_start.line,
1540 l,
1541 show_code_change,
1542 max_line_num_len,
1543 &file_lines,
1544 is_multiline,
1545 );
1546 }),
1547 _ => {
1555 let last_line = unhighlighted_lines.pop();
1556 let first_line = unhighlighted_lines.drain(..).next();
1557
1558 if let Some((p, l)) = first_line {
1559 draw_code_line(
1560 renderer,
1561 buffer,
1562 &mut row_num,
1563 &[],
1564 p + line_start.line,
1565 l,
1566 show_code_change,
1567 max_line_num_len,
1568 &file_lines,
1569 is_multiline,
1570 );
1571 }
1572
1573 let placeholder = renderer.decor_style.margin();
1574 let padding = str_width(placeholder);
1575 buffer.puts(
1576 row_num,
1577 max_line_num_len.saturating_sub(padding),
1578 placeholder,
1579 ElementStyle::LineNumber,
1580 );
1581 row_num += 1;
1582
1583 if let Some((p, l)) = last_line {
1584 draw_code_line(
1585 renderer,
1586 buffer,
1587 &mut row_num,
1588 &[],
1589 p + line_start.line,
1590 l,
1591 show_code_change,
1592 max_line_num_len,
1593 &file_lines,
1594 is_multiline,
1595 );
1596 }
1597 }
1598 }
1599 draw_code_line(
1600 renderer,
1601 buffer,
1602 &mut row_num,
1603 &highlight_parts,
1604 line_pos + line_start.line,
1605 line,
1606 show_code_change,
1607 max_line_num_len,
1608 &file_lines,
1609 is_multiline,
1610 );
1611 }
1612
1613 let mut offsets: Vec<(usize, isize)> = Vec::new();
1616 if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
1619 show_code_change
1620 {
1621 for part in parts {
1622 let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
1623 let (span_start, span_end) = sm.span_to_locations(part.span.clone());
1624 let span_start_pos = span_start.display;
1625 let span_end_pos = span_end.display;
1626
1627 let is_whitespace_addition = part.replacement.trim().is_empty();
1630
1631 let start = if is_whitespace_addition {
1633 0
1634 } else {
1635 part.replacement
1636 .len()
1637 .saturating_sub(part.replacement.trim_start().len())
1638 };
1639 let sub_len: usize = str_width(if is_whitespace_addition {
1642 &part.replacement
1643 } else {
1644 part.replacement.trim()
1645 });
1646
1647 let offset: isize = offsets
1648 .iter()
1649 .filter_map(|(start, v)| {
1650 if span_start_pos < *start {
1651 None
1652 } else {
1653 Some(v)
1654 }
1655 })
1656 .sum();
1657 let underline_start = (span_start_pos + start) as isize + offset;
1658 let underline_end = (span_start_pos + start + sub_len) as isize + offset;
1659 assert!(underline_start >= 0 && underline_end >= 0);
1660 let padding: usize = max_line_num_len + 3;
1661 for p in underline_start..underline_end {
1662 if matches!(show_code_change, DisplaySuggestion::Underline) {
1663 buffer.putc(
1666 row_num,
1667 (padding as isize + p) as usize,
1668 if part.is_addition(sm) {
1669 '+'
1670 } else {
1671 renderer.decor_style.diff()
1672 },
1673 ElementStyle::Addition,
1674 );
1675 }
1676 }
1677 if let DisplaySuggestion::Diff = show_code_change {
1678 let newlines = snippet.lines().count();
1709 if newlines > 0 && row_num > newlines {
1710 for (i, line) in snippet.lines().enumerate() {
1719 let line = normalize_whitespace(line);
1720 let min_row = buffer_offset + usize::from(!matches_previous_suggestion);
1723 let row = (row_num - 2 - (newlines - i - 1)).max(min_row);
1724 let start = if i == 0 {
1730 (padding as isize + span_start_pos as isize) as usize
1731 } else {
1732 padding
1733 };
1734 let end = if i == 0 {
1735 (padding as isize + span_start_pos as isize + line.len() as isize)
1736 as usize
1737 } else if i == newlines - 1 {
1738 (padding as isize + span_end_pos as isize) as usize
1739 } else {
1740 (padding as isize + line.len() as isize) as usize
1741 };
1742 buffer.set_style_range(row, start, end, ElementStyle::Removal, true);
1743 }
1744 } else {
1745 buffer.set_style_range(
1747 row_num - 2,
1748 (padding as isize + span_start_pos as isize) as usize,
1749 (padding as isize + span_end_pos as isize) as usize,
1750 ElementStyle::Removal,
1751 true,
1752 );
1753 }
1754 }
1755
1756 let full_sub_len = str_width(&part.replacement) as isize;
1758
1759 let snippet_len = span_end_pos as isize - span_start_pos as isize;
1761 offsets.push((span_end_pos, full_sub_len - snippet_len));
1765 }
1766 row_num += 1;
1767 }
1768
1769 if lines.next().is_some() {
1771 let placeholder = renderer.decor_style.margin();
1772 let padding = str_width(placeholder);
1773 buffer.puts(
1774 row_num,
1775 max_line_num_len.saturating_sub(padding),
1776 placeholder,
1777 ElementStyle::LineNumber,
1778 );
1779 } else {
1780 let row = match show_code_change {
1781 DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1782 row_num - 1
1783 }
1784 DisplaySuggestion::None => row_num,
1785 };
1786 if is_cont {
1787 draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
1788 } else {
1789 draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
1790 }
1791 }
1792}
1793
1794#[allow(clippy::too_many_arguments)]
1795fn draw_code_line(
1796 renderer: &Renderer,
1797 buffer: &mut StyledBuffer,
1798 row_num: &mut usize,
1799 highlight_parts: &[SubstitutionHighlight],
1800 line_num: usize,
1801 line_to_add: &str,
1802 show_code_change: DisplaySuggestion,
1803 max_line_num_len: usize,
1804 file_lines: &[&LineInfo<'_>],
1805 is_multiline: bool,
1806) {
1807 if let DisplaySuggestion::Diff = show_code_change {
1808 let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
1811 for (index, line_to_remove) in lines_to_remove.enumerate() {
1812 buffer.puts(
1813 *row_num - 1,
1814 0,
1815 &maybe_anonymized(renderer, line_num + index, max_line_num_len),
1816 ElementStyle::LineNumber,
1817 );
1818 buffer.puts(
1819 *row_num - 1,
1820 max_line_num_len + 1,
1821 "- ",
1822 ElementStyle::Removal,
1823 );
1824 let line = normalize_whitespace(line_to_remove.line);
1825 buffer.puts(
1826 *row_num - 1,
1827 max_line_num_len + 3,
1828 &line,
1829 ElementStyle::NoStyle,
1830 );
1831 *row_num += 1;
1832 }
1833 let last_line = &file_lines.last().unwrap();
1840 if last_line.line == line_to_add {
1841 *row_num -= 2;
1842 } else {
1843 buffer.puts(
1844 *row_num - 1,
1845 0,
1846 &maybe_anonymized(renderer, line_num + file_lines.len() - 1, max_line_num_len),
1847 ElementStyle::LineNumber,
1848 );
1849 buffer.puts(
1850 *row_num - 1,
1851 max_line_num_len + 1,
1852 "- ",
1853 ElementStyle::Removal,
1854 );
1855 buffer.puts(
1856 *row_num - 1,
1857 max_line_num_len + 3,
1858 &normalize_whitespace(last_line.line),
1859 ElementStyle::NoStyle,
1860 );
1861 if line_to_add.trim().is_empty() {
1862 *row_num -= 1;
1863 } else {
1864 buffer.puts(
1878 *row_num,
1879 0,
1880 &maybe_anonymized(renderer, line_num, max_line_num_len),
1881 ElementStyle::LineNumber,
1882 );
1883 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1884 buffer.append(
1885 *row_num,
1886 &normalize_whitespace(line_to_add),
1887 ElementStyle::NoStyle,
1888 );
1889 }
1890 }
1891 } else if is_multiline {
1892 buffer.puts(
1893 *row_num,
1894 0,
1895 &maybe_anonymized(renderer, line_num, max_line_num_len),
1896 ElementStyle::LineNumber,
1897 );
1898 match &highlight_parts {
1899 [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
1900 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1901 }
1902 [] | [SubstitutionHighlight { start: 0, end: 0 }] => {
1903 draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1);
1905 }
1906 _ => {
1907 let diff = renderer.decor_style.diff();
1908 buffer.puts(
1909 *row_num,
1910 max_line_num_len + 1,
1911 &format!("{diff} "),
1912 ElementStyle::Addition,
1913 );
1914 }
1915 }
1916 buffer.puts(
1922 *row_num,
1923 max_line_num_len + 3,
1924 &normalize_whitespace(line_to_add),
1925 ElementStyle::NoStyle,
1926 );
1927 } else if let DisplaySuggestion::Add = show_code_change {
1928 buffer.puts(
1929 *row_num,
1930 0,
1931 &maybe_anonymized(renderer, line_num, max_line_num_len),
1932 ElementStyle::LineNumber,
1933 );
1934 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1935 buffer.append(
1936 *row_num,
1937 &normalize_whitespace(line_to_add),
1938 ElementStyle::NoStyle,
1939 );
1940 } else {
1941 buffer.puts(
1942 *row_num,
1943 0,
1944 &maybe_anonymized(renderer, line_num, max_line_num_len),
1945 ElementStyle::LineNumber,
1946 );
1947 draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1);
1948 buffer.append(
1949 *row_num,
1950 &normalize_whitespace(line_to_add),
1951 ElementStyle::NoStyle,
1952 );
1953 }
1954
1955 for &SubstitutionHighlight { start, end } in highlight_parts {
1957 if start != end {
1959 let tabs: usize = line_to_add
1961 .chars()
1962 .take(start)
1963 .map(|ch| match ch {
1964 '\t' => 3,
1965 _ => 0,
1966 })
1967 .sum();
1968 buffer.set_style_range(
1969 *row_num,
1970 max_line_num_len + 3 + start + tabs,
1971 max_line_num_len + 3 + end + tabs,
1972 ElementStyle::Addition,
1973 true,
1974 );
1975 }
1976 }
1977 *row_num += 1;
1978}
1979
1980#[allow(clippy::too_many_arguments)]
1981fn draw_line(
1982 renderer: &Renderer,
1983 buffer: &mut StyledBuffer,
1984 source_string: &str,
1985 line_index: usize,
1986 line_offset: usize,
1987 width_offset: usize,
1988 code_offset: usize,
1989 max_line_num_len: usize,
1990 margin: Margin,
1991) -> usize {
1992 debug_assert!(!source_string.contains('\t'));
1994 let line_len = str_width(source_string);
1995 let mut left = margin.left(line_len);
1997 let right = margin.right(line_len);
1998 let mut taken = 0;
2001 let mut skipped = 0;
2002 let code: String = source_string
2003 .chars()
2004 .skip_while(|ch| {
2005 skipped += char_width(*ch);
2006 skipped <= left
2007 })
2008 .take_while(|ch| {
2009 taken += char_width(*ch);
2011 taken <= (right - left)
2012 })
2013 .collect();
2014
2015 let placeholder = renderer.decor_style.margin();
2016 let padding = str_width(placeholder);
2017 let (width_taken, bytes_taken) = if margin.was_cut_left() {
2018 let mut bytes_taken = 0;
2020 let mut width_taken = 0;
2021 for ch in code.chars() {
2022 width_taken += char_width(ch);
2023 bytes_taken += ch.len_utf8();
2024
2025 if width_taken >= padding {
2026 break;
2027 }
2028 }
2029
2030 if width_taken > padding {
2031 left -= width_taken - padding;
2032 }
2033
2034 buffer.puts(
2035 line_offset,
2036 code_offset,
2037 placeholder,
2038 ElementStyle::LineNumber,
2039 );
2040 (width_taken, bytes_taken)
2041 } else {
2042 (0, 0)
2043 };
2044
2045 buffer.puts(
2046 line_offset,
2047 code_offset + width_taken,
2048 &code[bytes_taken..],
2049 ElementStyle::Quotation,
2050 );
2051
2052 if line_len > right {
2053 let mut char_taken = 0;
2055 let mut width_taken_inner = 0;
2056 for ch in code.chars().rev() {
2057 width_taken_inner += char_width(ch);
2058 char_taken += 1;
2059
2060 if width_taken_inner >= padding {
2061 break;
2062 }
2063 }
2064
2065 buffer.puts(
2066 line_offset,
2067 code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
2068 placeholder,
2069 ElementStyle::LineNumber,
2070 );
2071 }
2072
2073 buffer.puts(
2074 line_offset,
2075 0,
2076 &maybe_anonymized(renderer, line_index, max_line_num_len),
2077 ElementStyle::LineNumber,
2078 );
2079
2080 draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
2081
2082 left
2083}
2084
2085fn draw_range(
2086 buffer: &mut StyledBuffer,
2087 symbol: char,
2088 line: usize,
2089 col_from: usize,
2090 col_to: usize,
2091 style: ElementStyle,
2092) {
2093 for col in col_from..col_to {
2094 buffer.putc(line, col, symbol, style);
2095 }
2096}
2097
2098fn draw_multiline_line(
2099 renderer: &Renderer,
2100 buffer: &mut StyledBuffer,
2101 line: usize,
2102 offset: usize,
2103 depth: usize,
2104 style: ElementStyle,
2105) {
2106 let chr = match (style, renderer.decor_style) {
2107 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
2108 (_, DecorStyle::Ascii) => '|',
2109 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => '┃',
2110 (_, DecorStyle::Unicode) => '│',
2111 };
2112 buffer.putc(line, offset + depth - 1, chr, style);
2113}
2114
2115fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2116 let chr = renderer.decor_style.col_separator();
2117 buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2118}
2119
2120fn draw_col_separator_no_space(
2121 renderer: &Renderer,
2122 buffer: &mut StyledBuffer,
2123 line: usize,
2124 col: usize,
2125) {
2126 let chr = renderer.decor_style.col_separator();
2127 draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
2128}
2129
2130fn draw_col_separator_start(
2131 renderer: &Renderer,
2132 buffer: &mut StyledBuffer,
2133 line: usize,
2134 col: usize,
2135) {
2136 match renderer.decor_style {
2137 DecorStyle::Ascii => {
2138 draw_col_separator_no_space_with_style(
2139 buffer,
2140 '|',
2141 line,
2142 col,
2143 ElementStyle::LineNumber,
2144 );
2145 }
2146 DecorStyle::Unicode => {
2147 draw_col_separator_no_space_with_style(
2148 buffer,
2149 '╭',
2150 line,
2151 col,
2152 ElementStyle::LineNumber,
2153 );
2154 draw_col_separator_no_space_with_style(
2155 buffer,
2156 '╴',
2157 line,
2158 col + 1,
2159 ElementStyle::LineNumber,
2160 );
2161 }
2162 }
2163}
2164
2165fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2166 match renderer.decor_style {
2167 DecorStyle::Ascii => {
2168 draw_col_separator_no_space_with_style(
2169 buffer,
2170 '|',
2171 line,
2172 col,
2173 ElementStyle::LineNumber,
2174 );
2175 }
2176 DecorStyle::Unicode => {
2177 draw_col_separator_no_space_with_style(
2178 buffer,
2179 '╰',
2180 line,
2181 col,
2182 ElementStyle::LineNumber,
2183 );
2184 draw_col_separator_no_space_with_style(
2185 buffer,
2186 '╴',
2187 line,
2188 col + 1,
2189 ElementStyle::LineNumber,
2190 );
2191 }
2192 }
2193}
2194
2195fn draw_col_separator_no_space_with_style(
2196 buffer: &mut StyledBuffer,
2197 chr: char,
2198 line: usize,
2199 col: usize,
2200 style: ElementStyle,
2201) {
2202 buffer.putc(line, col, chr, style);
2203}
2204
2205fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String {
2206 format!(
2207 "{:>max_line_num_len$}",
2208 if renderer.anonymized_line_numbers {
2209 Cow::Borrowed(ANONYMIZED_LINE_NUM)
2210 } else {
2211 Cow::Owned(line_num.to_string())
2212 }
2213 )
2214}
2215
2216fn draw_note_separator(
2217 renderer: &Renderer,
2218 buffer: &mut StyledBuffer,
2219 line: usize,
2220 col: usize,
2221 is_cont: bool,
2222) {
2223 let chr = renderer.decor_style.note_separator(is_cont);
2224 buffer.puts(line, col, chr, ElementStyle::LineNumber);
2225}
2226
2227fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2228 let (column, dots) = match renderer.decor_style {
2229 DecorStyle::Ascii => (0, "..."),
2230 DecorStyle::Unicode => (col - 2, "‡"),
2231 };
2232 buffer.puts(line, column, dots, ElementStyle::LineNumber);
2233}
2234
2235trait MessageOrTitle {
2236 fn level(&self) -> &Level<'_>;
2237 fn id(&self) -> Option<&Id<'_>>;
2238 fn text(&self) -> &str;
2239 fn allows_styling(&self) -> bool;
2240}
2241
2242impl MessageOrTitle for Title<'_> {
2243 fn level(&self) -> &Level<'_> {
2244 &self.level
2245 }
2246 fn id(&self) -> Option<&Id<'_>> {
2247 self.id.as_ref()
2248 }
2249 fn text(&self) -> &str {
2250 self.text.as_ref()
2251 }
2252 fn allows_styling(&self) -> bool {
2253 self.allows_styling
2254 }
2255}
2256
2257impl MessageOrTitle for Message<'_> {
2258 fn level(&self) -> &Level<'_> {
2259 &self.level
2260 }
2261 fn id(&self) -> Option<&Id<'_>> {
2262 None
2263 }
2264 fn text(&self) -> &str {
2265 self.text.as_ref()
2266 }
2267 fn allows_styling(&self) -> bool {
2268 true
2269 }
2270}
2271
2272fn num_decimal_digits(num: usize) -> usize {
2277 #[cfg(target_pointer_width = "64")]
2278 const MAX_DIGITS: usize = 20;
2279
2280 #[cfg(target_pointer_width = "32")]
2281 const MAX_DIGITS: usize = 10;
2282
2283 #[cfg(target_pointer_width = "16")]
2284 const MAX_DIGITS: usize = 5;
2285
2286 let mut lim = 10;
2287 for num_digits in 1..MAX_DIGITS {
2288 if num < lim {
2289 return num_digits;
2290 }
2291 lim = lim.wrapping_mul(10);
2292 }
2293 MAX_DIGITS
2294}
2295
2296fn str_width(s: &str) -> usize {
2297 s.chars().map(char_width).sum()
2298}
2299
2300pub(crate) fn char_width(ch: char) -> usize {
2301 match ch {
2304 '\t' => 4,
2305 '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
2309 | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2310 | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2311 | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2312 | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2313 | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2314 | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2315 _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
2316 }
2317}
2318
2319pub(crate) fn num_overlap(
2320 a_start: usize,
2321 a_end: usize,
2322 b_start: usize,
2323 b_end: usize,
2324 inclusive: bool,
2325) -> bool {
2326 let extra = usize::from(inclusive);
2327 (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
2328}
2329
2330fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
2331 num_overlap(
2332 a1.start.display,
2333 a1.end.display + padding,
2334 a2.start.display,
2335 a2.end.display,
2336 false,
2337 )
2338}
2339
2340#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2341pub(crate) enum LineAnnotationType {
2342 Singleline,
2344
2345 MultilineStart(usize),
2357 MultilineEnd(usize),
2359 MultilineLine(usize),
2364}
2365
2366#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2367pub(crate) struct LineAnnotation<'a> {
2368 pub start: Loc,
2373
2374 pub end: Loc,
2376
2377 pub kind: AnnotationKind,
2379
2380 pub label: Option<Cow<'a, str>>,
2382
2383 pub annotation_type: LineAnnotationType,
2386
2387 pub highlight_source: bool,
2389}
2390
2391impl LineAnnotation<'_> {
2392 pub(crate) fn is_primary(&self) -> bool {
2393 self.kind == AnnotationKind::Primary
2394 }
2395
2396 pub(crate) fn is_line(&self) -> bool {
2398 matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
2399 }
2400
2401 pub(crate) fn len(&self) -> usize {
2403 self.end.display.abs_diff(self.start.display)
2405 }
2406
2407 pub(crate) fn has_label(&self) -> bool {
2408 if let Some(label) = &self.label {
2409 !label.is_empty()
2420 } else {
2421 false
2422 }
2423 }
2424
2425 pub(crate) fn takes_space(&self) -> bool {
2426 matches!(
2428 self.annotation_type,
2429 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2430 )
2431 }
2432}
2433
2434#[derive(Clone, Copy, Debug)]
2435pub(crate) enum DisplaySuggestion {
2436 Underline,
2437 Diff,
2438 None,
2439 Add,
2440}
2441
2442impl DisplaySuggestion {
2443 fn new(complete: &str, patches: &[TrimmedPatch<'_>], sm: &SourceMap<'_>) -> Self {
2444 let has_deletion = patches
2445 .iter()
2446 .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
2447 let is_multiline = complete.lines().count() > 1;
2448 if has_deletion && !is_multiline {
2449 DisplaySuggestion::Diff
2450 } else if patches.len() == 1
2451 && patches.first().map_or(false, |p| {
2452 p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
2453 })
2454 {
2455 DisplaySuggestion::Add
2457 } else if (patches.len() != 1 || patches[0].replacement.trim() != complete.trim())
2458 && !is_multiline
2459 {
2460 DisplaySuggestion::Underline
2461 } else {
2462 DisplaySuggestion::None
2463 }
2464 }
2465}
2466
2467const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2470 ('\0', "␀"),
2474 ('\u{0001}', "␁"),
2475 ('\u{0002}', "␂"),
2476 ('\u{0003}', "␃"),
2477 ('\u{0004}', "␄"),
2478 ('\u{0005}', "␅"),
2479 ('\u{0006}', "␆"),
2480 ('\u{0007}', "␇"),
2481 ('\u{0008}', "␈"),
2482 ('\t', " "), ('\u{000b}', "␋"),
2484 ('\u{000c}', "␌"),
2485 ('\u{000d}', "␍"),
2486 ('\u{000e}', "␎"),
2487 ('\u{000f}', "␏"),
2488 ('\u{0010}', "␐"),
2489 ('\u{0011}', "␑"),
2490 ('\u{0012}', "␒"),
2491 ('\u{0013}', "␓"),
2492 ('\u{0014}', "␔"),
2493 ('\u{0015}', "␕"),
2494 ('\u{0016}', "␖"),
2495 ('\u{0017}', "␗"),
2496 ('\u{0018}', "␘"),
2497 ('\u{0019}', "␙"),
2498 ('\u{001a}', "␚"),
2499 ('\u{001b}', "␛"),
2500 ('\u{001c}', "␜"),
2501 ('\u{001d}', "␝"),
2502 ('\u{001e}', "␞"),
2503 ('\u{001f}', "␟"),
2504 ('\u{007f}', "␡"),
2505 ('\u{200d}', ""), ('\u{202a}', "�"), ('\u{202b}', "�"), ('\u{202c}', "�"), ('\u{202d}', "�"),
2510 ('\u{202e}', "�"),
2511 ('\u{2066}', "�"),
2512 ('\u{2067}', "�"),
2513 ('\u{2068}', "�"),
2514 ('\u{2069}', "�"),
2515];
2516
2517pub(crate) fn normalize_whitespace(s: &str) -> String {
2518 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
2522 match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
2523 Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2524 _ => s.push(c),
2525 }
2526 s
2527 })
2528}
2529
2530#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2531pub(crate) enum ElementStyle {
2532 MainHeaderMsg,
2533 HeaderMsg,
2534 LineAndColumn,
2535 LineNumber,
2536 Quotation,
2537 UnderlinePrimary,
2538 UnderlineSecondary,
2539 LabelPrimary,
2540 LabelSecondary,
2541 NoStyle,
2542 Level(LevelInner),
2543 Addition,
2544 Removal,
2545}
2546
2547impl ElementStyle {
2548 pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
2549 match self {
2550 ElementStyle::Addition => stylesheet.addition,
2551 ElementStyle::Removal => stylesheet.removal,
2552 ElementStyle::LineAndColumn => stylesheet.none,
2553 ElementStyle::LineNumber => stylesheet.line_num,
2554 ElementStyle::Quotation => stylesheet.none,
2555 ElementStyle::MainHeaderMsg => stylesheet.emphasis,
2556 ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
2557 ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
2558 ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
2559 ElementStyle::Level(lvl) => lvl.style(stylesheet),
2560 }
2561 }
2562}
2563
2564#[derive(Debug, Clone, Copy)]
2565pub(crate) struct UnderlineParts {
2566 pub(crate) style: ElementStyle,
2567 pub(crate) underline: char,
2568 pub(crate) label_start: char,
2569 pub(crate) vertical_text_line: char,
2570 pub(crate) multiline_vertical: char,
2571 pub(crate) multiline_horizontal: char,
2572 pub(crate) multiline_whole_line: char,
2573 pub(crate) multiline_start_down: char,
2574 pub(crate) bottom_right: char,
2575 pub(crate) top_left: char,
2576 pub(crate) top_right_flat: char,
2577 pub(crate) bottom_left: char,
2578 pub(crate) multiline_end_up: char,
2579 pub(crate) multiline_end_same_line: char,
2580 pub(crate) multiline_bottom_right_with_text: char,
2581}
2582
2583#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2584enum TitleStyle {
2585 MainHeader,
2586 Header,
2587 Secondary,
2588}
2589
2590struct PreProcessedGroup<'a> {
2591 group: &'a Group<'a>,
2592 elements: Vec<PreProcessedElement<'a>>,
2593 primary_path: Option<&'a Cow<'a, str>>,
2594 max_depth: usize,
2595}
2596
2597enum PreProcessedElement<'a> {
2598 Message(&'a Message<'a>),
2599 Cause(
2600 (
2601 &'a Snippet<'a, Annotation<'a>>,
2602 SourceMap<'a>,
2603 Vec<AnnotatedLineInfo<'a>>,
2604 ),
2605 ),
2606 Suggestion(
2607 (
2608 &'a Snippet<'a, Patch<'a>>,
2609 SourceMap<'a>,
2610 SplicedLines<'a>,
2611 DisplaySuggestion,
2612 ),
2613 ),
2614 Origin(&'a Origin<'a>),
2615 Padding(Padding),
2616}
2617
2618fn pre_process<'a>(
2619 groups: &'a [Group<'a>],
2620) -> (usize, Option<&'a Cow<'a, str>>, Vec<PreProcessedGroup<'a>>) {
2621 let mut max_line_num = 0;
2622 let mut og_primary_path = None;
2623 let mut out = Vec::with_capacity(groups.len());
2624 for group in groups {
2625 let mut elements = Vec::with_capacity(group.elements.len());
2626 let mut primary_path = None;
2627 let mut max_depth = 0;
2628 for element in &group.elements {
2629 match element {
2630 Element::Message(message) => {
2631 elements.push(PreProcessedElement::Message(message));
2632 }
2633 Element::Cause(cause) => {
2634 let sm = SourceMap::new(&cause.source, cause.line_start);
2635 let (depth, annotated_lines) =
2636 sm.annotated_lines(cause.markers.clone(), cause.fold);
2637
2638 if cause.fold {
2639 let end = cause
2640 .markers
2641 .iter()
2642 .map(|a| a.span.end)
2643 .max()
2644 .unwrap_or(cause.source.len())
2645 .min(cause.source.len());
2646
2647 max_line_num = max(
2648 cause.line_start + newline_count(&cause.source[..end]),
2649 max_line_num,
2650 );
2651 } else {
2652 max_line_num = max(
2653 cause.line_start + newline_count(&cause.source),
2654 max_line_num,
2655 );
2656 }
2657
2658 if primary_path.is_none() {
2659 primary_path = Some(cause.path.as_ref());
2660 }
2661 max_depth = max(depth, max_depth);
2662 elements.push(PreProcessedElement::Cause((cause, sm, annotated_lines)));
2663 }
2664 Element::Suggestion(suggestion) => {
2665 let sm = SourceMap::new(&suggestion.source, suggestion.line_start);
2666 if let Some((complete, patches, highlights)) =
2667 sm.splice_lines(suggestion.markers.clone(), suggestion.fold)
2668 {
2669 let display_suggestion = DisplaySuggestion::new(&complete, &patches, &sm);
2670
2671 if suggestion.fold {
2672 if let Some(first) = patches.first() {
2673 let (l_start, _) =
2674 sm.span_to_locations(first.original_span.clone());
2675 let nc = newline_count(&complete);
2676 let sugg_max_line_num = match display_suggestion {
2677 DisplaySuggestion::Underline => l_start.line,
2678 DisplaySuggestion::Diff => {
2679 let file_lines = sm.span_to_lines(first.span.clone());
2680 file_lines
2681 .last()
2682 .map_or(l_start.line + nc, |line| line.line_index)
2683 }
2684 DisplaySuggestion::None => l_start.line + nc,
2685 DisplaySuggestion::Add => l_start.line + nc,
2686 };
2687 max_line_num = max(sugg_max_line_num, max_line_num);
2688 }
2689 } else {
2690 max_line_num = max(
2691 suggestion.line_start + newline_count(&complete),
2692 max_line_num,
2693 );
2694 }
2695
2696 elements.push(PreProcessedElement::Suggestion((
2697 suggestion,
2698 sm,
2699 (complete, patches, highlights),
2700 display_suggestion,
2701 )));
2702 }
2703 }
2704 Element::Origin(origin) => {
2705 if primary_path.is_none() {
2706 primary_path = Some(Some(&origin.path));
2707 }
2708 elements.push(PreProcessedElement::Origin(origin));
2709 }
2710 Element::Padding(padding) => {
2711 elements.push(PreProcessedElement::Padding(padding.clone()));
2712 }
2713 }
2714 }
2715 let group = PreProcessedGroup {
2716 group,
2717 elements,
2718 primary_path: primary_path.unwrap_or_default(),
2719 max_depth,
2720 };
2721 if og_primary_path.is_none() && group.primary_path.is_some() {
2722 og_primary_path = group.primary_path;
2723 }
2724 out.push(group);
2725 }
2726
2727 (max_line_num, og_primary_path, out)
2728}
2729
2730fn newline_count(body: &str) -> usize {
2731 #[cfg(feature = "simd")]
2732 {
2733 memchr::memchr_iter(b'\n', body.as_bytes()).count()
2734 }
2735 #[cfg(not(feature = "simd"))]
2736 {
2737 body.lines().count().saturating_sub(1)
2738 }
2739}
2740
2741#[cfg(test)]
2742mod test {
2743 use super::{newline_count, OUTPUT_REPLACEMENTS};
2744 use snapbox::IntoData;
2745
2746 fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2747 replacements
2748 .into_iter()
2749 .map(|r| format!(" {r:?}"))
2750 .collect::<Vec<_>>()
2751 .join("\n")
2752 }
2753
2754 #[test]
2755 fn ensure_output_replacements_is_sorted() {
2758 let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2759 expected.sort_by_key(|r| r.0);
2760 expected.dedup_by_key(|r| r.0);
2761 let expected = format_replacements(expected);
2762 let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2763 snapbox::assert_data_eq!(actual, expected.into_data().raw());
2764 }
2765
2766 #[test]
2767 fn ensure_newline_count_correct() {
2768 let source = r#"
2769 cargo-features = ["path-bases"]
2770
2771 [package]
2772 name = "foo"
2773 version = "0.5.0"
2774 authors = ["wycats@example.com"]
2775
2776 [dependencies]
2777 bar = { base = '^^not-valid^^', path = 'bar' }
2778 "#;
2779 let actual_count = newline_count(source);
2780 let expected_count = 10;
2781
2782 assert_eq!(expected_count, actual_count);
2783 }
2784}