1use std::borrow::Cow;
4use std::cmp::{max, min, Ordering, Reverse};
5use std::collections::{HashMap, VecDeque};
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, SubstitutionHighlight,
17};
18use crate::renderer::styled_buffer::StyledBuffer;
19use crate::snippet::Id;
20use crate::{
21 Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Report, Snippet, Title,
22};
23
24const ANONYMIZED_LINE_NUM: &str = "LL";
25
26pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String {
27 if renderer.short_message {
28 render_short_message(renderer, groups).unwrap()
29 } else {
30 let max_line_num_len = if renderer.anonymized_line_numbers {
31 ANONYMIZED_LINE_NUM.len()
32 } else {
33 num_decimal_digits(max_line_number(groups))
34 };
35 let mut out_string = String::new();
36 let group_len = groups.len();
37 let mut og_primary_path = None;
38 for (g, group) in groups.iter().enumerate() {
39 let mut buffer = StyledBuffer::new();
40 let primary_path = group
41 .elements
42 .iter()
43 .find_map(|s| match &s {
44 Element::Cause(cause) => Some(cause.path.as_ref()),
45 Element::Origin(origin) => Some(Some(&origin.path)),
46 _ => None,
47 })
48 .unwrap_or_default();
49 if og_primary_path.is_none() && primary_path.is_some() {
50 og_primary_path = primary_path;
51 }
52 let level = group.primary_level.clone();
53 let mut source_map_annotated_lines = VecDeque::new();
54 let mut max_depth = 0;
55 for e in &group.elements {
56 if let Element::Cause(cause) = e {
57 let source_map = SourceMap::new(&cause.source, cause.line_start);
58 let (depth, annotated_lines) =
59 source_map.annotated_lines(cause.markers.clone(), cause.fold);
60 max_depth = max(max_depth, depth);
61 source_map_annotated_lines.push_back((source_map, annotated_lines));
62 }
63 }
64 let mut message_iter = group.elements.iter().enumerate().peekable();
65 if let Some(title) = &group.title {
66 let peek = message_iter.peek().map(|(_, s)| s).copied();
67 let title_style = if title.allows_styling {
68 TitleStyle::Header
69 } else {
70 TitleStyle::MainHeader
71 };
72 let buffer_msg_line_offset = buffer.num_lines();
73 render_title(
74 renderer,
75 &mut buffer,
76 title,
77 max_line_num_len,
78 title_style,
79 matches!(peek, Some(Element::Message(_))),
80 buffer_msg_line_offset,
81 );
82 let buffer_msg_line_offset = buffer.num_lines();
83
84 if matches!(peek, Some(Element::Message(_))) {
85 draw_col_separator_no_space(
86 renderer,
87 &mut buffer,
88 buffer_msg_line_offset,
89 max_line_num_len + 1,
90 );
91 }
92 if peek.is_none()
93 && title_style == TitleStyle::MainHeader
94 && g == 0
95 && group_len > 1
96 {
97 draw_col_separator_end(
98 renderer,
99 &mut buffer,
100 buffer_msg_line_offset,
101 max_line_num_len + 1,
102 );
103 }
104 }
105 let mut seen_primary = false;
106 let mut last_suggestion_path = None;
107 while let Some((i, section)) = message_iter.next() {
108 let peek = message_iter.peek().map(|(_, s)| s).copied();
109 let is_first = i == 0;
110 match §ion {
111 Element::Message(title) => {
112 let title_style = TitleStyle::Secondary;
113 let buffer_msg_line_offset = buffer.num_lines();
114 render_title(
115 renderer,
116 &mut buffer,
117 title,
118 max_line_num_len,
119 title_style,
120 peek.is_some(),
121 buffer_msg_line_offset,
122 );
123 }
124 Element::Cause(cause) => {
125 if let Some((source_map, annotated_lines)) =
126 source_map_annotated_lines.pop_front()
127 {
128 let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
129 seen_primary |= is_primary;
130 render_snippet_annotations(
131 renderer,
132 &mut buffer,
133 max_line_num_len,
134 cause,
135 is_primary,
136 &source_map,
137 &annotated_lines,
138 max_depth,
139 peek.is_some() || (g == 0 && group_len > 1),
140 is_first,
141 );
142
143 if g == 0 {
144 let current_line = buffer.num_lines();
145 match peek {
146 Some(Element::Message(_)) => {
147 draw_col_separator_no_space(
148 renderer,
149 &mut buffer,
150 current_line,
151 max_line_num_len + 1,
152 );
153 }
154 None if group_len > 1 => draw_col_separator_end(
155 renderer,
156 &mut buffer,
157 current_line,
158 max_line_num_len + 1,
159 ),
160 _ => {}
161 }
162 }
163 }
164 }
165 Element::Suggestion(suggestion) => {
166 let source_map = SourceMap::new(&suggestion.source, suggestion.line_start);
167 let matches_previous_suggestion =
168 last_suggestion_path == Some(suggestion.path.as_ref());
169 emit_suggestion_default(
170 renderer,
171 &mut buffer,
172 suggestion,
173 max_line_num_len,
174 &source_map,
175 primary_path.or(og_primary_path),
176 matches_previous_suggestion,
177 is_first,
178 peek.is_some(),
180 );
181
182 if matches!(peek, Some(Element::Suggestion(_))) {
183 last_suggestion_path = Some(suggestion.path.as_ref());
184 } else {
185 last_suggestion_path = None;
186 }
187 }
188
189 Element::Origin(origin) => {
190 let buffer_msg_line_offset = buffer.num_lines();
191 let is_primary = primary_path == Some(&origin.path) && !seen_primary;
192 seen_primary |= is_primary;
193 render_origin(
194 renderer,
195 &mut buffer,
196 max_line_num_len,
197 origin,
198 is_primary,
199 is_first,
200 buffer_msg_line_offset,
201 );
202 let current_line = buffer.num_lines();
203 if g == 0 && peek.is_none() && group_len > 1 {
204 draw_col_separator_end(
205 renderer,
206 &mut buffer,
207 current_line,
208 max_line_num_len + 1,
209 );
210 }
211 }
212 Element::Padding(_) => {
213 let current_line = buffer.num_lines();
214 if peek.is_none() {
215 draw_col_separator_end(
216 renderer,
217 &mut buffer,
218 current_line,
219 max_line_num_len + 1,
220 );
221 } else {
222 draw_col_separator_no_space(
223 renderer,
224 &mut buffer,
225 current_line,
226 max_line_num_len + 1,
227 );
228 }
229 }
230 }
231 }
232 buffer
233 .render(&level, &renderer.stylesheet, &mut out_string)
234 .unwrap();
235 if g != group_len - 1 {
236 use std::fmt::Write;
237
238 writeln!(out_string).unwrap();
239 }
240 }
241 out_string
242 }
243}
244
245fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
246 let mut buffer = StyledBuffer::new();
247 let mut labels = None;
248 let group = groups.first().expect("Expected at least one group");
249
250 let Some(title) = &group.title else {
251 panic!("Expected a Title");
252 };
253
254 if let Some(Element::Cause(cause)) = group
255 .elements
256 .iter()
257 .find(|e| matches!(e, Element::Cause(_)))
258 {
259 let labels_inner = cause
260 .markers
261 .iter()
262 .filter_map(|ann| match &ann.label {
263 Some(msg) if ann.kind.is_primary() => {
264 if !msg.trim().is_empty() {
265 Some(msg.to_string())
266 } else {
267 None
268 }
269 }
270 _ => None,
271 })
272 .collect::<Vec<_>>()
273 .join(", ");
274 if !labels_inner.is_empty() {
275 labels = Some(labels_inner);
276 }
277
278 if let Some(path) = &cause.path {
279 let mut origin = Origin::path(path.as_ref());
280
281 let source_map = SourceMap::new(&cause.source, cause.line_start);
282 let (_depth, annotated_lines) =
283 source_map.annotated_lines(cause.markers.clone(), cause.fold);
284
285 if let Some(primary_line) = annotated_lines
286 .iter()
287 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
288 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
289 {
290 origin.line = Some(primary_line.line_index);
291 if let Some(first_annotation) = primary_line
292 .annotations
293 .iter()
294 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
295 {
296 origin.char_column = Some(first_annotation.start.char + 1);
297 }
298 }
299
300 render_origin(renderer, &mut buffer, 0, &origin, true, true, 0);
301 buffer.append(0, ": ", ElementStyle::LineAndColumn);
302 }
303 }
304
305 render_title(
306 renderer,
307 &mut buffer,
308 title,
309 0, TitleStyle::MainHeader,
311 false,
312 0,
313 );
314
315 if let Some(labels) = labels {
316 buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
317 }
318
319 let mut out_string = String::new();
320 buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?;
321
322 Ok(out_string)
323}
324
325#[allow(clippy::too_many_arguments)]
326fn render_title(
327 renderer: &Renderer,
328 buffer: &mut StyledBuffer,
329 title: &dyn MessageOrTitle,
330 max_line_num_len: usize,
331 title_style: TitleStyle,
332 is_cont: bool,
333 buffer_msg_line_offset: usize,
334) {
335 let (label_style, title_element_style) = match title_style {
336 TitleStyle::MainHeader => (
337 ElementStyle::Level(title.level().level),
338 if renderer.short_message {
339 ElementStyle::NoStyle
340 } else {
341 ElementStyle::MainHeaderMsg
342 },
343 ),
344 TitleStyle::Header => (
345 ElementStyle::Level(title.level().level),
346 ElementStyle::HeaderMsg,
347 ),
348 TitleStyle::Secondary => {
349 for _ in 0..max_line_num_len {
350 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
351 }
352
353 draw_note_separator(
354 renderer,
355 buffer,
356 buffer_msg_line_offset,
357 max_line_num_len + 1,
358 is_cont,
359 );
360 (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
361 }
362 };
363 let mut label_width = 0;
364
365 if title.level().name != Some(None) {
366 buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
367 label_width += title.level().as_str().len();
368 if let Some(Id { id: Some(id), url }) = &title.id() {
369 buffer.append(buffer_msg_line_offset, "[", label_style);
370 if let Some(url) = url.as_ref() {
371 buffer.append(
372 buffer_msg_line_offset,
373 &format!("\x1B]8;;{url}\x1B\\"),
374 label_style,
375 );
376 }
377 buffer.append(buffer_msg_line_offset, id, label_style);
378 if url.is_some() {
379 buffer.append(buffer_msg_line_offset, "\x1B]8;;\x1B\\", label_style);
380 }
381 buffer.append(buffer_msg_line_offset, "]", label_style);
382 label_width += 2 + id.len();
383 }
384 buffer.append(buffer_msg_line_offset, ": ", title_element_style);
385 label_width += 2;
386 }
387
388 let padding = " ".repeat(if title_style == TitleStyle::Secondary {
389 max_line_num_len + 3 + label_width
407 } else {
408 label_width
409 });
410
411 let (title_str, style) = if title.allows_styling() {
412 (title.text().to_owned(), ElementStyle::NoStyle)
413 } else {
414 (normalize_whitespace(title.text()), title_element_style)
415 };
416 for (i, text) in title_str.split('\n').enumerate() {
417 if i != 0 {
418 buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
419 if title_style == TitleStyle::Secondary
420 && is_cont
421 && matches!(renderer.decor_style, DecorStyle::Unicode)
422 {
423 draw_col_separator_no_space(
435 renderer,
436 buffer,
437 buffer_msg_line_offset + i,
438 max_line_num_len + 1,
439 );
440 }
441 }
442 buffer.append(buffer_msg_line_offset + i, text, style);
443 }
444}
445
446fn render_origin(
447 renderer: &Renderer,
448 buffer: &mut StyledBuffer,
449 max_line_num_len: usize,
450 origin: &Origin<'_>,
451 is_primary: bool,
452 is_first: bool,
453 buffer_msg_line_offset: usize,
454) {
455 if is_primary && !renderer.short_message {
456 buffer.prepend(
457 buffer_msg_line_offset,
458 renderer.decor_style.file_start(is_first),
459 ElementStyle::LineNumber,
460 );
461 } else if !renderer.short_message {
462 buffer.prepend(
483 buffer_msg_line_offset,
484 renderer.decor_style.secondary_file_start(),
485 ElementStyle::LineNumber,
486 );
487 }
488
489 let str = match (&origin.line, &origin.char_column) {
490 (Some(line), Some(col)) => {
491 format!("{}:{}:{}", origin.path, line, col)
492 }
493 (Some(line), None) => format!("{}:{}", origin.path, line),
494 _ => origin.path.to_string(),
495 };
496
497 buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
498 if !renderer.short_message {
499 for _ in 0..max_line_num_len {
500 buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
501 }
502 }
503}
504
505#[allow(clippy::too_many_arguments)]
506fn render_snippet_annotations(
507 renderer: &Renderer,
508 buffer: &mut StyledBuffer,
509 max_line_num_len: usize,
510 snippet: &Snippet<'_, Annotation<'_>>,
511 is_primary: bool,
512 sm: &SourceMap<'_>,
513 annotated_lines: &[AnnotatedLineInfo<'_>],
514 multiline_depth: usize,
515 is_cont: bool,
516 is_first: bool,
517) {
518 if let Some(path) = &snippet.path {
519 let mut origin = Origin::path(path.as_ref());
520 if is_primary {
525 if let Some(primary_line) = annotated_lines
526 .iter()
527 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
528 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
529 {
530 origin.line = Some(primary_line.line_index);
531 if let Some(first_annotation) = primary_line
532 .annotations
533 .iter()
534 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
535 {
536 origin.char_column = Some(first_annotation.start.char + 1);
537 }
538 }
539 } else {
540 let buffer_msg_line_offset = buffer.num_lines();
541 draw_col_separator_no_space(
552 renderer,
553 buffer,
554 buffer_msg_line_offset,
555 max_line_num_len + 1,
556 );
557 if let Some(first_line) = annotated_lines.first() {
558 origin.line = Some(first_line.line_index);
559 if let Some(first_annotation) = first_line.annotations.first() {
560 origin.char_column = Some(first_annotation.start.char + 1);
561 }
562 }
563 }
564 let buffer_msg_line_offset = buffer.num_lines();
565 render_origin(
566 renderer,
567 buffer,
568 max_line_num_len,
569 &origin,
570 is_primary,
571 is_first,
572 buffer_msg_line_offset,
573 );
574 draw_col_separator_no_space(
576 renderer,
577 buffer,
578 buffer_msg_line_offset + 1,
579 max_line_num_len + 1,
580 );
581 } else {
582 let buffer_msg_line_offset = buffer.num_lines();
583 if is_primary {
584 if renderer.decor_style == DecorStyle::Unicode {
585 buffer.puts(
586 buffer_msg_line_offset,
587 max_line_num_len,
588 renderer.decor_style.file_start(is_first),
589 ElementStyle::LineNumber,
590 );
591 } else {
592 draw_col_separator_no_space(
593 renderer,
594 buffer,
595 buffer_msg_line_offset,
596 max_line_num_len + 1,
597 );
598 }
599 } else {
600 draw_col_separator_no_space(
611 renderer,
612 buffer,
613 buffer_msg_line_offset,
614 max_line_num_len + 1,
615 );
616
617 buffer.puts(
618 buffer_msg_line_offset + 1,
619 max_line_num_len,
620 renderer.decor_style.secondary_file_start(),
621 ElementStyle::LineNumber,
622 );
623 }
624 }
625
626 let mut multilines = Vec::new();
628
629 let mut whitespace_margin = usize::MAX;
631 for line_info in annotated_lines {
632 let leading_whitespace = line_info
638 .line
639 .chars()
640 .take_while(|c| c.is_whitespace())
641 .map(|c| {
642 match c {
643 '\t' => 4,
645 _ => 1,
646 }
647 })
648 .sum();
649 if line_info.line.chars().any(|c| !c.is_whitespace()) {
650 whitespace_margin = min(whitespace_margin, leading_whitespace);
651 }
652 }
653 if whitespace_margin == usize::MAX {
654 whitespace_margin = 0;
655 }
656
657 let mut span_left_margin = usize::MAX;
659 for line_info in annotated_lines {
660 for ann in &line_info.annotations {
661 span_left_margin = min(span_left_margin, ann.start.display);
662 span_left_margin = min(span_left_margin, ann.end.display);
663 }
664 }
665 if span_left_margin == usize::MAX {
666 span_left_margin = 0;
667 }
668
669 let mut span_right_margin = 0;
671 let mut label_right_margin = 0;
672 let mut max_line_len = 0;
673 for line_info in annotated_lines {
674 max_line_len = max(max_line_len, line_info.line.len());
675 for ann in &line_info.annotations {
676 span_right_margin = max(span_right_margin, ann.start.display);
677 span_right_margin = max(span_right_margin, ann.end.display);
678 let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1);
680 label_right_margin = max(label_right_margin, ann.end.display + label_right);
681 }
682 }
683 let width_offset = 3 + max_line_num_len;
684 let code_offset = if multiline_depth == 0 {
685 width_offset
686 } else {
687 width_offset + multiline_depth + 1
688 };
689
690 let column_width = renderer.term_width.saturating_sub(code_offset);
691
692 let margin = Margin::new(
693 whitespace_margin,
694 span_left_margin,
695 span_right_margin,
696 label_right_margin,
697 column_width,
698 max_line_len,
699 );
700
701 for annotated_line_idx in 0..annotated_lines.len() {
703 let previous_buffer_line = buffer.num_lines();
704
705 let depths = render_source_line(
706 renderer,
707 &annotated_lines[annotated_line_idx],
708 buffer,
709 width_offset,
710 code_offset,
711 max_line_num_len,
712 margin,
713 !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
714 );
715
716 let mut to_add = HashMap::new();
717
718 for (depth, style) in depths {
719 if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
720 multilines.swap_remove(index);
721 } else {
722 to_add.insert(depth, style);
723 }
724 }
725
726 for (depth, style) in &multilines {
729 for line in previous_buffer_line..buffer.num_lines() {
730 draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style);
731 }
732 }
733 if annotated_line_idx < (annotated_lines.len() - 1) {
736 let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
737 - annotated_lines[annotated_line_idx].line_index;
738 match line_idx_delta.cmp(&2) {
739 Ordering::Greater => {
740 let last_buffer_line_num = buffer.num_lines();
741
742 draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
743
744 for (depth, style) in &multilines {
746 draw_multiline_line(
747 renderer,
748 buffer,
749 last_buffer_line_num,
750 width_offset,
751 *depth,
752 *style,
753 );
754 }
755 if let Some(line) = annotated_lines.get(annotated_line_idx) {
756 for ann in &line.annotations {
757 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
758 draw_multiline_line(
762 renderer,
763 buffer,
764 last_buffer_line_num,
765 width_offset,
766 pos,
767 if ann.is_primary() {
768 ElementStyle::UnderlinePrimary
769 } else {
770 ElementStyle::UnderlineSecondary
771 },
772 );
773 }
774 }
775 }
776 }
777
778 Ordering::Equal => {
779 let unannotated_line = sm
780 .get_line(annotated_lines[annotated_line_idx].line_index + 1)
781 .unwrap_or("");
782
783 let last_buffer_line_num = buffer.num_lines();
784
785 draw_line(
786 renderer,
787 buffer,
788 &normalize_whitespace(unannotated_line),
789 annotated_lines[annotated_line_idx + 1].line_index - 1,
790 last_buffer_line_num,
791 width_offset,
792 code_offset,
793 max_line_num_len,
794 margin,
795 );
796
797 for (depth, style) in &multilines {
798 draw_multiline_line(
799 renderer,
800 buffer,
801 last_buffer_line_num,
802 width_offset,
803 *depth,
804 *style,
805 );
806 }
807 if let Some(line) = annotated_lines.get(annotated_line_idx) {
808 for ann in &line.annotations {
809 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
810 draw_multiline_line(
811 renderer,
812 buffer,
813 last_buffer_line_num,
814 width_offset,
815 pos,
816 if ann.is_primary() {
817 ElementStyle::UnderlinePrimary
818 } else {
819 ElementStyle::UnderlineSecondary
820 },
821 );
822 }
823 }
824 }
825 }
826 Ordering::Less => {}
827 }
828 }
829
830 multilines.extend(to_add);
831 }
832}
833
834#[allow(clippy::too_many_arguments)]
835fn render_source_line(
836 renderer: &Renderer,
837 line_info: &AnnotatedLineInfo<'_>,
838 buffer: &mut StyledBuffer,
839 width_offset: usize,
840 code_offset: usize,
841 max_line_num_len: usize,
842 margin: Margin,
843 close_window: bool,
844) -> Vec<(usize, ElementStyle)> {
845 let source_string = normalize_whitespace(line_info.line);
860
861 let line_offset = buffer.num_lines();
862
863 let left = draw_line(
864 renderer,
865 buffer,
866 &source_string,
867 line_info.line_index,
868 line_offset,
869 width_offset,
870 code_offset,
871 max_line_num_len,
872 margin,
873 );
874
875 if line_info.annotations.is_empty() {
877 if close_window {
880 draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
881 }
882 return vec![];
883 }
884
885 let mut buffer_ops = vec![];
902 let mut annotations = vec![];
903 let mut short_start = true;
904 for ann in &line_info.annotations {
905 if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
906 if source_string
907 .chars()
908 .take(ann.start.display)
909 .all(char::is_whitespace)
910 {
911 let uline = renderer.decor_style.underline(ann.is_primary());
912 let chr = uline.multiline_whole_line;
913 annotations.push((depth, uline.style));
914 buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
915 } else {
916 short_start = false;
917 break;
918 }
919 } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
920 } else {
921 short_start = false;
922 break;
923 }
924 }
925 if short_start {
926 for (y, x, c, s) in buffer_ops {
927 buffer.putc(y, x, c, s);
928 }
929 return annotations;
930 }
931
932 let mut annotations = line_info.annotations.clone();
965 annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
966
967 let mut overlap = vec![false; annotations.len()];
1030 let mut annotations_position = vec![];
1031 let mut line_len: usize = 0;
1032 let mut p = 0;
1033 for (i, annotation) in annotations.iter().enumerate() {
1034 for (j, next) in annotations.iter().enumerate() {
1035 if overlaps(next, annotation, 0) && j > 1 {
1036 overlap[i] = true;
1037 overlap[j] = true;
1038 }
1039 if overlaps(next, annotation, 0) && annotation.has_label() && j > i && p == 0
1043 {
1045 if next.start.display == annotation.start.display
1048 && next.start.char == annotation.start.char
1049 && next.end.display == annotation.end.display
1050 && next.end.char == annotation.end.char
1051 && !next.has_label()
1052 {
1053 continue;
1054 }
1055
1056 p += 1;
1058 break;
1059 }
1060 }
1061 annotations_position.push((p, annotation));
1062 for (j, next) in annotations.iter().enumerate() {
1063 if j > i {
1064 let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
1065 if (overlaps(next, annotation, l) && annotation.has_label() && next.has_label()) || (annotation.takes_space() && next.has_label()) || (annotation.has_label() && next.takes_space())
1082 || (annotation.takes_space() && next.takes_space())
1083 || (overlaps(next, annotation, l)
1084 && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1085 && next.has_label()
1086 && p == 0)
1087 {
1089 p += 1;
1091 break;
1092 }
1093 }
1094 }
1095 line_len = max(line_len, p);
1096 }
1097
1098 if line_len != 0 {
1099 line_len += 1;
1100 }
1101
1102 if line_info.annotations.iter().all(LineAnnotation::is_line) {
1105 return vec![];
1106 }
1107
1108 if annotations_position
1109 .iter()
1110 .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
1111 {
1112 if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() {
1113 for (pos, _) in &mut annotations_position {
1126 *pos = max_pos - *pos;
1127 }
1128 line_len = line_len.saturating_sub(1);
1131 }
1132 }
1133
1134 for pos in 0..=line_len {
1146 draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
1147 }
1148 if close_window {
1149 draw_col_separator_end(
1150 renderer,
1151 buffer,
1152 line_offset + line_len + 1,
1153 width_offset - 2,
1154 );
1155 }
1156 for &(pos, annotation) in &annotations_position {
1169 let underline = renderer.decor_style.underline(annotation.is_primary());
1170 let pos = pos + 1;
1171 match annotation.annotation_type {
1172 LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
1173 draw_range(
1174 buffer,
1175 underline.multiline_horizontal,
1176 line_offset + pos,
1177 width_offset + depth,
1178 (code_offset + annotation.start.display).saturating_sub(left),
1179 underline.style,
1180 );
1181 }
1182 _ if annotation.highlight_source => {
1183 buffer.set_style_range(
1184 line_offset,
1185 (code_offset + annotation.start.display).saturating_sub(left),
1186 (code_offset + annotation.end.display).saturating_sub(left),
1187 underline.style,
1188 annotation.is_primary(),
1189 );
1190 }
1191 _ => {}
1192 }
1193 }
1194
1195 for &(pos, annotation) in &annotations_position {
1207 let underline = renderer.decor_style.underline(annotation.is_primary());
1208 let pos = pos + 1;
1209
1210 if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
1211 for p in line_offset + 1..=line_offset + pos {
1212 buffer.putc(
1213 p,
1214 (code_offset + annotation.start.display).saturating_sub(left),
1215 match annotation.annotation_type {
1216 LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
1217 _ => underline.vertical_text_line,
1218 },
1219 underline.style,
1220 );
1221 }
1222 if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
1223 buffer.putc(
1224 line_offset + pos,
1225 (code_offset + annotation.start.display).saturating_sub(left),
1226 underline.bottom_right,
1227 underline.style,
1228 );
1229 }
1230 if matches!(
1231 annotation.annotation_type,
1232 LineAnnotationType::MultilineEnd(_)
1233 ) && annotation.has_label()
1234 {
1235 buffer.putc(
1236 line_offset + pos,
1237 (code_offset + annotation.start.display).saturating_sub(left),
1238 underline.multiline_bottom_right_with_text,
1239 underline.style,
1240 );
1241 }
1242 }
1243 match annotation.annotation_type {
1244 LineAnnotationType::MultilineStart(depth) => {
1245 buffer.putc(
1246 line_offset + pos,
1247 width_offset + depth - 1,
1248 underline.top_left,
1249 underline.style,
1250 );
1251 for p in line_offset + pos + 1..line_offset + line_len + 2 {
1252 buffer.putc(
1253 p,
1254 width_offset + depth - 1,
1255 underline.multiline_vertical,
1256 underline.style,
1257 );
1258 }
1259 }
1260 LineAnnotationType::MultilineEnd(depth) => {
1261 for p in line_offset..line_offset + pos {
1262 buffer.putc(
1263 p,
1264 width_offset + depth - 1,
1265 underline.multiline_vertical,
1266 underline.style,
1267 );
1268 }
1269 buffer.putc(
1270 line_offset + pos,
1271 width_offset + depth - 1,
1272 underline.bottom_left,
1273 underline.style,
1274 );
1275 }
1276 _ => (),
1277 }
1278 }
1279
1280 for &(pos, annotation) in &annotations_position {
1292 let style = if annotation.is_primary() {
1293 ElementStyle::LabelPrimary
1294 } else {
1295 ElementStyle::LabelSecondary
1296 };
1297 let (pos, col) = if pos == 0 {
1298 if annotation.end.display == 0 {
1299 (pos + 1, (annotation.end.display + 2).saturating_sub(left))
1300 } else {
1301 (pos + 1, (annotation.end.display + 1).saturating_sub(left))
1302 }
1303 } else {
1304 (pos + 2, annotation.start.display.saturating_sub(left))
1305 };
1306 if let Some(label) = &annotation.label {
1307 buffer.puts(line_offset + pos, code_offset + col, label, style);
1308 }
1309 }
1310
1311 annotations_position.sort_by_key(|(_, ann)| {
1320 (Reverse(ann.len()), ann.is_primary())
1322 });
1323
1324 for &(pos, annotation) in &annotations_position {
1336 let uline = renderer.decor_style.underline(annotation.is_primary());
1337 for p in annotation.start.display..annotation.end.display {
1338 buffer.putc(
1340 line_offset + 1,
1341 (code_offset + p).saturating_sub(left),
1342 uline.underline,
1343 uline.style,
1344 );
1345 }
1346
1347 if pos == 0
1348 && matches!(
1349 annotation.annotation_type,
1350 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1351 )
1352 {
1353 buffer.putc(
1355 line_offset + 1,
1356 (code_offset + annotation.start.display).saturating_sub(left),
1357 match annotation.annotation_type {
1358 LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
1359 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
1360 _ => panic!("unexpected annotation type: {annotation:?}"),
1361 },
1362 uline.style,
1363 );
1364 } else if pos != 0
1365 && matches!(
1366 annotation.annotation_type,
1367 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1368 )
1369 {
1370 buffer.putc(
1373 line_offset + 1,
1374 (code_offset + annotation.start.display).saturating_sub(left),
1375 match annotation.annotation_type {
1376 LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
1377 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
1378 _ => panic!("unexpected annotation type: {annotation:?}"),
1379 },
1380 uline.style,
1381 );
1382 } else if pos != 0 && annotation.has_label() {
1383 buffer.putc(
1385 line_offset + 1,
1386 (code_offset + annotation.start.display).saturating_sub(left),
1387 uline.label_start,
1388 uline.style,
1389 );
1390 }
1391 }
1392
1393 for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
1397 if overlap[i] {
1399 continue;
1400 };
1401 let LineAnnotationType::Singleline = annotation.annotation_type else {
1402 continue;
1403 };
1404 let width = annotation.end.display - annotation.start.display;
1405 if width > margin.term_width * 2 && width > 10 {
1406 let pad = max(margin.term_width / 3, 5);
1409 buffer.replace(
1411 line_offset,
1412 annotation.start.display + pad,
1413 annotation.end.display - pad,
1414 renderer.decor_style.margin(),
1415 );
1416 buffer.replace(
1418 line_offset + 1,
1419 annotation.start.display + pad,
1420 annotation.end.display - pad,
1421 renderer.decor_style.margin(),
1422 );
1423 }
1424 }
1425 annotations_position
1426 .iter()
1427 .filter_map(|&(_, annotation)| match annotation.annotation_type {
1428 LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
1429 let style = if annotation.is_primary() {
1430 ElementStyle::LabelPrimary
1431 } else {
1432 ElementStyle::LabelSecondary
1433 };
1434 Some((p, style))
1435 }
1436 _ => None,
1437 })
1438 .collect::<Vec<_>>()
1439}
1440
1441#[allow(clippy::too_many_arguments)]
1442fn emit_suggestion_default(
1443 renderer: &Renderer,
1444 buffer: &mut StyledBuffer,
1445 suggestion: &Snippet<'_, Patch<'_>>,
1446 max_line_num_len: usize,
1447 sm: &SourceMap<'_>,
1448 primary_path: Option<&Cow<'_, str>>,
1449 matches_previous_suggestion: bool,
1450 is_first: bool,
1451 is_cont: bool,
1452) {
1453 let suggestions = sm.splice_lines(suggestion.markers.clone());
1454
1455 let buffer_offset = buffer.num_lines();
1456 let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
1457 for (complete, parts, highlights) in &suggestions {
1458 let has_deletion = parts
1459 .iter()
1460 .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm));
1461 let is_multiline = complete.lines().count() > 1;
1462
1463 if matches_previous_suggestion {
1464 buffer.puts(
1465 row_num - 1,
1466 max_line_num_len + 1,
1467 renderer.decor_style.multi_suggestion_separator(),
1468 ElementStyle::LineNumber,
1469 );
1470 } else {
1471 draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
1472 }
1473 if suggestion.path.as_ref() != primary_path {
1474 if let Some(path) = suggestion.path.as_ref() {
1475 if !matches_previous_suggestion {
1476 let (loc, _) = sm.span_to_locations(parts[0].span.clone());
1477 let arrow = renderer.decor_style.file_start(is_first);
1480 buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber);
1481 let message = format!("{}:{}:{}", path, loc.line, loc.char + 1);
1482 let col = usize::max(max_line_num_len + 1, arrow.len());
1483 buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn);
1484 for _ in 0..max_line_num_len {
1485 buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle);
1486 }
1487 draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
1488 row_num += 1;
1489 }
1490 }
1491 }
1492 let show_code_change = if has_deletion && !is_multiline {
1493 DisplaySuggestion::Diff
1494 } else if parts.len() == 1
1495 && parts.first().map_or(false, |p| {
1496 p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim()
1497 })
1498 {
1499 DisplaySuggestion::Add
1501 } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim())
1502 && !is_multiline
1503 {
1504 DisplaySuggestion::Underline
1505 } else {
1506 DisplaySuggestion::None
1507 };
1508
1509 if let DisplaySuggestion::Diff = show_code_change {
1510 row_num += 1;
1511 }
1512
1513 let file_lines = sm.span_to_lines(parts[0].span.clone());
1514 let (line_start, line_end) = sm.span_to_locations(parts[0].span.clone());
1515 let mut lines = complete.lines();
1516 if lines.clone().next().is_none() {
1517 for line in line_start.line..=line_end.line {
1519 buffer.puts(
1520 row_num - 1 + line - line_start.line,
1521 0,
1522 &maybe_anonymized(renderer, line, max_line_num_len),
1523 ElementStyle::LineNumber,
1524 );
1525 buffer.puts(
1526 row_num - 1 + line - line_start.line,
1527 max_line_num_len + 1,
1528 "- ",
1529 ElementStyle::Removal,
1530 );
1531 buffer.puts(
1532 row_num - 1 + line - line_start.line,
1533 max_line_num_len + 3,
1534 &normalize_whitespace(sm.get_line(line).unwrap()),
1535 ElementStyle::Removal,
1536 );
1537 }
1538 row_num += line_end.line - line_start.line;
1539 }
1540 let mut last_pos = 0;
1541 let mut is_item_attribute = false;
1542 let mut unhighlighted_lines = Vec::new();
1543 for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() {
1544 last_pos = line_pos;
1545
1546 if highlight_parts.is_empty() {
1548 unhighlighted_lines.push((line_pos, line));
1549 continue;
1550 }
1551 if highlight_parts.len() == 1
1552 && line.trim().starts_with("#[")
1553 && line.trim().ends_with(']')
1554 {
1555 is_item_attribute = true;
1556 }
1557
1558 match unhighlighted_lines.len() {
1559 0 => (),
1560 n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
1565 draw_code_line(
1566 renderer,
1567 buffer,
1568 &mut row_num,
1569 &[],
1570 p + line_start.line,
1571 l,
1572 show_code_change,
1573 max_line_num_len,
1574 &file_lines,
1575 is_multiline,
1576 );
1577 }),
1578 _ => {
1586 let last_line = unhighlighted_lines.pop();
1587 let first_line = unhighlighted_lines.drain(..).next();
1588
1589 if let Some((p, l)) = first_line {
1590 draw_code_line(
1591 renderer,
1592 buffer,
1593 &mut row_num,
1594 &[],
1595 p + line_start.line,
1596 l,
1597 show_code_change,
1598 max_line_num_len,
1599 &file_lines,
1600 is_multiline,
1601 );
1602 }
1603
1604 let placeholder = renderer.decor_style.margin();
1605 let padding = str_width(placeholder);
1606 buffer.puts(
1607 row_num,
1608 max_line_num_len.saturating_sub(padding),
1609 placeholder,
1610 ElementStyle::LineNumber,
1611 );
1612 row_num += 1;
1613
1614 if let Some((p, l)) = last_line {
1615 draw_code_line(
1616 renderer,
1617 buffer,
1618 &mut row_num,
1619 &[],
1620 p + line_start.line,
1621 l,
1622 show_code_change,
1623 max_line_num_len,
1624 &file_lines,
1625 is_multiline,
1626 );
1627 }
1628 }
1629 }
1630 draw_code_line(
1631 renderer,
1632 buffer,
1633 &mut row_num,
1634 highlight_parts,
1635 line_pos + line_start.line,
1636 line,
1637 show_code_change,
1638 max_line_num_len,
1639 &file_lines,
1640 is_multiline,
1641 );
1642 }
1643
1644 if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute {
1645 let file_lines = sm.span_to_lines(parts[0].span.end..parts[0].span.end);
1652 let (lo, _) = sm.span_to_locations(parts[0].span.clone());
1653 let line_num = lo.line;
1654 if let Some(line) = sm.get_line(line_num) {
1655 let line = normalize_whitespace(line);
1656 draw_code_line(
1657 renderer,
1658 buffer,
1659 &mut row_num,
1660 &[],
1661 line_num + last_pos + 1,
1662 &line,
1663 DisplaySuggestion::None,
1664 max_line_num_len,
1665 &file_lines,
1666 is_multiline,
1667 );
1668 }
1669 }
1670 let mut offsets: Vec<(usize, isize)> = Vec::new();
1673 if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
1676 show_code_change
1677 {
1678 for part in parts {
1679 let snippet = sm.span_to_snippet(part.span.clone()).unwrap_or_default();
1680 let (span_start, span_end) = sm.span_to_locations(part.span.clone());
1681 let span_start_pos = span_start.display;
1682 let span_end_pos = span_end.display;
1683
1684 let is_whitespace_addition = part.replacement.trim().is_empty();
1687
1688 let start = if is_whitespace_addition {
1690 0
1691 } else {
1692 part.replacement
1693 .len()
1694 .saturating_sub(part.replacement.trim_start().len())
1695 };
1696 let sub_len: usize = str_width(if is_whitespace_addition {
1699 &part.replacement
1700 } else {
1701 part.replacement.trim()
1702 });
1703
1704 let offset: isize = offsets
1705 .iter()
1706 .filter_map(|(start, v)| {
1707 if span_start_pos < *start {
1708 None
1709 } else {
1710 Some(v)
1711 }
1712 })
1713 .sum();
1714 let underline_start = (span_start_pos + start) as isize + offset;
1715 let underline_end = (span_start_pos + start + sub_len) as isize + offset;
1716 assert!(underline_start >= 0 && underline_end >= 0);
1717 let padding: usize = max_line_num_len + 3;
1718 for p in underline_start..underline_end {
1719 if matches!(show_code_change, DisplaySuggestion::Underline) {
1720 buffer.putc(
1723 row_num,
1724 (padding as isize + p) as usize,
1725 if part.is_addition(sm) {
1726 '+'
1727 } else {
1728 renderer.decor_style.diff()
1729 },
1730 ElementStyle::Addition,
1731 );
1732 }
1733 }
1734 if let DisplaySuggestion::Diff = show_code_change {
1735 let newlines = snippet.lines().count();
1766 if newlines > 0 && row_num > newlines {
1767 for (i, line) in snippet.lines().enumerate() {
1776 let line = normalize_whitespace(line);
1777 let min_row = buffer_offset + usize::from(!matches_previous_suggestion);
1780 let row = (row_num - 2 - (newlines - i - 1)).max(min_row);
1781 let start = if i == 0 {
1787 (padding as isize + span_start_pos as isize) as usize
1788 } else {
1789 padding
1790 };
1791 let end = if i == 0 {
1792 (padding as isize + span_start_pos as isize + line.len() as isize)
1793 as usize
1794 } else if i == newlines - 1 {
1795 (padding as isize + span_end_pos as isize) as usize
1796 } else {
1797 (padding as isize + line.len() as isize) as usize
1798 };
1799 buffer.set_style_range(row, start, end, ElementStyle::Removal, true);
1800 }
1801 } else {
1802 buffer.set_style_range(
1804 row_num - 2,
1805 (padding as isize + span_start_pos as isize) as usize,
1806 (padding as isize + span_end_pos as isize) as usize,
1807 ElementStyle::Removal,
1808 true,
1809 );
1810 }
1811 }
1812
1813 let full_sub_len = str_width(&part.replacement) as isize;
1815
1816 let snippet_len = span_end_pos as isize - span_start_pos as isize;
1818 offsets.push((span_end_pos, full_sub_len - snippet_len));
1822 }
1823 row_num += 1;
1824 }
1825
1826 if lines.next().is_some() {
1828 let placeholder = renderer.decor_style.margin();
1829 let padding = str_width(placeholder);
1830 buffer.puts(
1831 row_num,
1832 max_line_num_len.saturating_sub(padding),
1833 placeholder,
1834 ElementStyle::LineNumber,
1835 );
1836 } else {
1837 let row = match show_code_change {
1838 DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1839 row_num - 1
1840 }
1841 DisplaySuggestion::None => row_num,
1842 };
1843 if is_cont {
1844 draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
1845 } else {
1846 draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
1847 }
1848 row_num = row + 1;
1849 }
1850 }
1851}
1852
1853#[allow(clippy::too_many_arguments)]
1854fn draw_code_line(
1855 renderer: &Renderer,
1856 buffer: &mut StyledBuffer,
1857 row_num: &mut usize,
1858 highlight_parts: &[SubstitutionHighlight],
1859 line_num: usize,
1860 line_to_add: &str,
1861 show_code_change: DisplaySuggestion,
1862 max_line_num_len: usize,
1863 file_lines: &[&LineInfo<'_>],
1864 is_multiline: bool,
1865) {
1866 if let DisplaySuggestion::Diff = show_code_change {
1867 let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
1870 for (index, line_to_remove) in lines_to_remove.enumerate() {
1871 buffer.puts(
1872 *row_num - 1,
1873 0,
1874 &maybe_anonymized(renderer, line_num + index, max_line_num_len),
1875 ElementStyle::LineNumber,
1876 );
1877 buffer.puts(
1878 *row_num - 1,
1879 max_line_num_len + 1,
1880 "- ",
1881 ElementStyle::Removal,
1882 );
1883 let line = normalize_whitespace(line_to_remove.line);
1884 buffer.puts(
1885 *row_num - 1,
1886 max_line_num_len + 3,
1887 &line,
1888 ElementStyle::NoStyle,
1889 );
1890 *row_num += 1;
1891 }
1892 let last_line = &file_lines.last().unwrap();
1899 if last_line.line == line_to_add {
1900 *row_num -= 2;
1901 } else {
1902 buffer.puts(
1903 *row_num - 1,
1904 0,
1905 &maybe_anonymized(renderer, line_num + file_lines.len() - 1, max_line_num_len),
1906 ElementStyle::LineNumber,
1907 );
1908 buffer.puts(
1909 *row_num - 1,
1910 max_line_num_len + 1,
1911 "- ",
1912 ElementStyle::Removal,
1913 );
1914 buffer.puts(
1915 *row_num - 1,
1916 max_line_num_len + 3,
1917 &normalize_whitespace(last_line.line),
1918 ElementStyle::NoStyle,
1919 );
1920 if line_to_add.trim().is_empty() {
1921 *row_num -= 1;
1922 } else {
1923 buffer.puts(
1937 *row_num,
1938 0,
1939 &maybe_anonymized(renderer, line_num, max_line_num_len),
1940 ElementStyle::LineNumber,
1941 );
1942 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1943 buffer.append(
1944 *row_num,
1945 &normalize_whitespace(line_to_add),
1946 ElementStyle::NoStyle,
1947 );
1948 }
1949 }
1950 } else if is_multiline {
1951 buffer.puts(
1952 *row_num,
1953 0,
1954 &maybe_anonymized(renderer, line_num, max_line_num_len),
1955 ElementStyle::LineNumber,
1956 );
1957 match &highlight_parts {
1958 [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
1959 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1960 }
1961 [] => {
1962 draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1);
1964 }
1965 _ => {
1966 let diff = renderer.decor_style.diff();
1967 buffer.puts(
1968 *row_num,
1969 max_line_num_len + 1,
1970 &format!("{diff} "),
1971 ElementStyle::Addition,
1972 );
1973 }
1974 }
1975 buffer.puts(
1981 *row_num,
1982 max_line_num_len + 3,
1983 &normalize_whitespace(line_to_add),
1984 ElementStyle::NoStyle,
1985 );
1986 } else if let DisplaySuggestion::Add = show_code_change {
1987 buffer.puts(
1988 *row_num,
1989 0,
1990 &maybe_anonymized(renderer, line_num, max_line_num_len),
1991 ElementStyle::LineNumber,
1992 );
1993 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1994 buffer.append(
1995 *row_num,
1996 &normalize_whitespace(line_to_add),
1997 ElementStyle::NoStyle,
1998 );
1999 } else {
2000 buffer.puts(
2001 *row_num,
2002 0,
2003 &maybe_anonymized(renderer, line_num, max_line_num_len),
2004 ElementStyle::LineNumber,
2005 );
2006 draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1);
2007 buffer.append(
2008 *row_num,
2009 &normalize_whitespace(line_to_add),
2010 ElementStyle::NoStyle,
2011 );
2012 }
2013
2014 for &SubstitutionHighlight { start, end } in highlight_parts {
2016 if start != end {
2018 let tabs: usize = line_to_add
2020 .chars()
2021 .take(start)
2022 .map(|ch| match ch {
2023 '\t' => 3,
2024 _ => 0,
2025 })
2026 .sum();
2027 buffer.set_style_range(
2028 *row_num,
2029 max_line_num_len + 3 + start + tabs,
2030 max_line_num_len + 3 + end + tabs,
2031 ElementStyle::Addition,
2032 true,
2033 );
2034 }
2035 }
2036 *row_num += 1;
2037}
2038
2039#[allow(clippy::too_many_arguments)]
2040fn draw_line(
2041 renderer: &Renderer,
2042 buffer: &mut StyledBuffer,
2043 source_string: &str,
2044 line_index: usize,
2045 line_offset: usize,
2046 width_offset: usize,
2047 code_offset: usize,
2048 max_line_num_len: usize,
2049 margin: Margin,
2050) -> usize {
2051 debug_assert!(!source_string.contains('\t'));
2053 let line_len = str_width(source_string);
2054 let mut left = margin.left(line_len);
2056 let right = margin.right(line_len);
2057 let mut taken = 0;
2060 let mut skipped = 0;
2061 let code: String = source_string
2062 .chars()
2063 .skip_while(|ch| {
2064 skipped += char_width(*ch);
2065 skipped <= left
2066 })
2067 .take_while(|ch| {
2068 taken += char_width(*ch);
2070 taken <= (right - left)
2071 })
2072 .collect();
2073
2074 let placeholder = renderer.decor_style.margin();
2075 let padding = str_width(placeholder);
2076 let (width_taken, bytes_taken) = if margin.was_cut_left() {
2077 let mut bytes_taken = 0;
2079 let mut width_taken = 0;
2080 for ch in code.chars() {
2081 width_taken += char_width(ch);
2082 bytes_taken += ch.len_utf8();
2083
2084 if width_taken >= padding {
2085 break;
2086 }
2087 }
2088
2089 if width_taken > padding {
2090 left -= width_taken - padding;
2091 }
2092
2093 buffer.puts(
2094 line_offset,
2095 code_offset,
2096 placeholder,
2097 ElementStyle::LineNumber,
2098 );
2099 (width_taken, bytes_taken)
2100 } else {
2101 (0, 0)
2102 };
2103
2104 buffer.puts(
2105 line_offset,
2106 code_offset + width_taken,
2107 &code[bytes_taken..],
2108 ElementStyle::Quotation,
2109 );
2110
2111 if line_len > right {
2112 let mut char_taken = 0;
2114 let mut width_taken_inner = 0;
2115 for ch in code.chars().rev() {
2116 width_taken_inner += char_width(ch);
2117 char_taken += 1;
2118
2119 if width_taken_inner >= padding {
2120 break;
2121 }
2122 }
2123
2124 buffer.puts(
2125 line_offset,
2126 code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
2127 placeholder,
2128 ElementStyle::LineNumber,
2129 );
2130 }
2131
2132 buffer.puts(
2133 line_offset,
2134 0,
2135 &maybe_anonymized(renderer, line_index, max_line_num_len),
2136 ElementStyle::LineNumber,
2137 );
2138
2139 draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
2140
2141 left
2142}
2143
2144fn draw_range(
2145 buffer: &mut StyledBuffer,
2146 symbol: char,
2147 line: usize,
2148 col_from: usize,
2149 col_to: usize,
2150 style: ElementStyle,
2151) {
2152 for col in col_from..col_to {
2153 buffer.putc(line, col, symbol, style);
2154 }
2155}
2156
2157fn draw_multiline_line(
2158 renderer: &Renderer,
2159 buffer: &mut StyledBuffer,
2160 line: usize,
2161 offset: usize,
2162 depth: usize,
2163 style: ElementStyle,
2164) {
2165 let chr = match (style, renderer.decor_style) {
2166 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
2167 (_, DecorStyle::Ascii) => '|',
2168 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => '┃',
2169 (_, DecorStyle::Unicode) => '│',
2170 };
2171 buffer.putc(line, offset + depth - 1, chr, style);
2172}
2173
2174fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2175 let chr = renderer.decor_style.col_separator();
2176 buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2177}
2178
2179fn draw_col_separator_no_space(
2180 renderer: &Renderer,
2181 buffer: &mut StyledBuffer,
2182 line: usize,
2183 col: usize,
2184) {
2185 let chr = renderer.decor_style.col_separator();
2186 draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
2187}
2188
2189fn draw_col_separator_start(
2190 renderer: &Renderer,
2191 buffer: &mut StyledBuffer,
2192 line: usize,
2193 col: usize,
2194) {
2195 match renderer.decor_style {
2196 DecorStyle::Ascii => {
2197 draw_col_separator_no_space_with_style(
2198 buffer,
2199 '|',
2200 line,
2201 col,
2202 ElementStyle::LineNumber,
2203 );
2204 }
2205 DecorStyle::Unicode => {
2206 draw_col_separator_no_space_with_style(
2207 buffer,
2208 '╭',
2209 line,
2210 col,
2211 ElementStyle::LineNumber,
2212 );
2213 draw_col_separator_no_space_with_style(
2214 buffer,
2215 '╴',
2216 line,
2217 col + 1,
2218 ElementStyle::LineNumber,
2219 );
2220 }
2221 }
2222}
2223
2224fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2225 match renderer.decor_style {
2226 DecorStyle::Ascii => {
2227 draw_col_separator_no_space_with_style(
2228 buffer,
2229 '|',
2230 line,
2231 col,
2232 ElementStyle::LineNumber,
2233 );
2234 }
2235 DecorStyle::Unicode => {
2236 draw_col_separator_no_space_with_style(
2237 buffer,
2238 '╰',
2239 line,
2240 col,
2241 ElementStyle::LineNumber,
2242 );
2243 draw_col_separator_no_space_with_style(
2244 buffer,
2245 '╴',
2246 line,
2247 col + 1,
2248 ElementStyle::LineNumber,
2249 );
2250 }
2251 }
2252}
2253
2254fn draw_col_separator_no_space_with_style(
2255 buffer: &mut StyledBuffer,
2256 chr: char,
2257 line: usize,
2258 col: usize,
2259 style: ElementStyle,
2260) {
2261 buffer.putc(line, col, chr, style);
2262}
2263
2264fn maybe_anonymized(renderer: &Renderer, line_num: usize, max_line_num_len: usize) -> String {
2265 format!(
2266 "{:>max_line_num_len$}",
2267 if renderer.anonymized_line_numbers {
2268 Cow::Borrowed(ANONYMIZED_LINE_NUM)
2269 } else {
2270 Cow::Owned(line_num.to_string())
2271 }
2272 )
2273}
2274
2275fn draw_note_separator(
2276 renderer: &Renderer,
2277 buffer: &mut StyledBuffer,
2278 line: usize,
2279 col: usize,
2280 is_cont: bool,
2281) {
2282 let chr = renderer.decor_style.note_separator(is_cont);
2283 buffer.puts(line, col, chr, ElementStyle::LineNumber);
2284}
2285
2286fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2287 let (column, dots) = match renderer.decor_style {
2288 DecorStyle::Ascii => (0, "..."),
2289 DecorStyle::Unicode => (col - 2, "‡"),
2290 };
2291 buffer.puts(line, column, dots, ElementStyle::LineNumber);
2292}
2293
2294trait MessageOrTitle {
2295 fn level(&self) -> &Level<'_>;
2296 fn id(&self) -> Option<&Id<'_>>;
2297 fn text(&self) -> &str;
2298 fn allows_styling(&self) -> bool;
2299}
2300
2301impl MessageOrTitle for Title<'_> {
2302 fn level(&self) -> &Level<'_> {
2303 &self.level
2304 }
2305 fn id(&self) -> Option<&Id<'_>> {
2306 self.id.as_ref()
2307 }
2308 fn text(&self) -> &str {
2309 self.text.as_ref()
2310 }
2311 fn allows_styling(&self) -> bool {
2312 self.allows_styling
2313 }
2314}
2315
2316impl MessageOrTitle for Message<'_> {
2317 fn level(&self) -> &Level<'_> {
2318 &self.level
2319 }
2320 fn id(&self) -> Option<&Id<'_>> {
2321 None
2322 }
2323 fn text(&self) -> &str {
2324 self.text.as_ref()
2325 }
2326 fn allows_styling(&self) -> bool {
2327 true
2328 }
2329}
2330
2331fn num_decimal_digits(num: usize) -> usize {
2336 #[cfg(target_pointer_width = "64")]
2337 const MAX_DIGITS: usize = 20;
2338
2339 #[cfg(target_pointer_width = "32")]
2340 const MAX_DIGITS: usize = 10;
2341
2342 #[cfg(target_pointer_width = "16")]
2343 const MAX_DIGITS: usize = 5;
2344
2345 let mut lim = 10;
2346 for num_digits in 1..MAX_DIGITS {
2347 if num < lim {
2348 return num_digits;
2349 }
2350 lim = lim.wrapping_mul(10);
2351 }
2352 MAX_DIGITS
2353}
2354
2355fn str_width(s: &str) -> usize {
2356 s.chars().map(char_width).sum()
2357}
2358
2359pub(crate) fn char_width(ch: char) -> usize {
2360 match ch {
2363 '\t' => 4,
2364 '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
2368 | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2369 | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2370 | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2371 | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2372 | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2373 | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2374 _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
2375 }
2376}
2377
2378pub(crate) fn num_overlap(
2379 a_start: usize,
2380 a_end: usize,
2381 b_start: usize,
2382 b_end: usize,
2383 inclusive: bool,
2384) -> bool {
2385 let extra = usize::from(inclusive);
2386 (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
2387}
2388
2389fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
2390 num_overlap(
2391 a1.start.display,
2392 a1.end.display + padding,
2393 a2.start.display,
2394 a2.end.display,
2395 false,
2396 )
2397}
2398
2399#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2400pub(crate) enum LineAnnotationType {
2401 Singleline,
2403
2404 MultilineStart(usize),
2416 MultilineEnd(usize),
2418 MultilineLine(usize),
2423}
2424
2425#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2426pub(crate) struct LineAnnotation<'a> {
2427 pub start: Loc,
2432
2433 pub end: Loc,
2435
2436 pub kind: AnnotationKind,
2438
2439 pub label: Option<Cow<'a, str>>,
2441
2442 pub annotation_type: LineAnnotationType,
2445
2446 pub highlight_source: bool,
2448}
2449
2450impl LineAnnotation<'_> {
2451 pub(crate) fn is_primary(&self) -> bool {
2452 self.kind == AnnotationKind::Primary
2453 }
2454
2455 pub(crate) fn is_line(&self) -> bool {
2457 matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
2458 }
2459
2460 pub(crate) fn len(&self) -> usize {
2462 self.end.display.abs_diff(self.start.display)
2464 }
2465
2466 pub(crate) fn has_label(&self) -> bool {
2467 if let Some(label) = &self.label {
2468 !label.is_empty()
2479 } else {
2480 false
2481 }
2482 }
2483
2484 pub(crate) fn takes_space(&self) -> bool {
2485 matches!(
2487 self.annotation_type,
2488 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2489 )
2490 }
2491}
2492
2493#[derive(Clone, Copy, Debug)]
2494pub(crate) enum DisplaySuggestion {
2495 Underline,
2496 Diff,
2497 None,
2498 Add,
2499}
2500
2501const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2504 ('\0', "␀"),
2508 ('\u{0001}', "␁"),
2509 ('\u{0002}', "␂"),
2510 ('\u{0003}', "␃"),
2511 ('\u{0004}', "␄"),
2512 ('\u{0005}', "␅"),
2513 ('\u{0006}', "␆"),
2514 ('\u{0007}', "␇"),
2515 ('\u{0008}', "␈"),
2516 ('\t', " "), ('\u{000b}', "␋"),
2518 ('\u{000c}', "␌"),
2519 ('\u{000d}', "␍"),
2520 ('\u{000e}', "␎"),
2521 ('\u{000f}', "␏"),
2522 ('\u{0010}', "␐"),
2523 ('\u{0011}', "␑"),
2524 ('\u{0012}', "␒"),
2525 ('\u{0013}', "␓"),
2526 ('\u{0014}', "␔"),
2527 ('\u{0015}', "␕"),
2528 ('\u{0016}', "␖"),
2529 ('\u{0017}', "␗"),
2530 ('\u{0018}', "␘"),
2531 ('\u{0019}', "␙"),
2532 ('\u{001a}', "␚"),
2533 ('\u{001b}', "␛"),
2534 ('\u{001c}', "␜"),
2535 ('\u{001d}', "␝"),
2536 ('\u{001e}', "␞"),
2537 ('\u{001f}', "␟"),
2538 ('\u{007f}', "␡"),
2539 ('\u{200d}', ""), ('\u{202a}', "�"), ('\u{202b}', "�"), ('\u{202c}', "�"), ('\u{202d}', "�"),
2544 ('\u{202e}', "�"),
2545 ('\u{2066}', "�"),
2546 ('\u{2067}', "�"),
2547 ('\u{2068}', "�"),
2548 ('\u{2069}', "�"),
2549];
2550
2551pub(crate) fn normalize_whitespace(s: &str) -> String {
2552 s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
2556 match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
2557 Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2558 _ => s.push(c),
2559 }
2560 s
2561 })
2562}
2563
2564#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2565pub(crate) enum ElementStyle {
2566 MainHeaderMsg,
2567 HeaderMsg,
2568 LineAndColumn,
2569 LineNumber,
2570 Quotation,
2571 UnderlinePrimary,
2572 UnderlineSecondary,
2573 LabelPrimary,
2574 LabelSecondary,
2575 NoStyle,
2576 Level(LevelInner),
2577 Addition,
2578 Removal,
2579}
2580
2581impl ElementStyle {
2582 pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
2583 match self {
2584 ElementStyle::Addition => stylesheet.addition,
2585 ElementStyle::Removal => stylesheet.removal,
2586 ElementStyle::LineAndColumn => stylesheet.none,
2587 ElementStyle::LineNumber => stylesheet.line_num,
2588 ElementStyle::Quotation => stylesheet.none,
2589 ElementStyle::MainHeaderMsg => stylesheet.emphasis,
2590 ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet),
2591 ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context,
2592 ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none,
2593 ElementStyle::Level(lvl) => lvl.style(stylesheet),
2594 }
2595 }
2596}
2597
2598#[derive(Debug, Clone, Copy)]
2599pub(crate) struct UnderlineParts {
2600 pub(crate) style: ElementStyle,
2601 pub(crate) underline: char,
2602 pub(crate) label_start: char,
2603 pub(crate) vertical_text_line: char,
2604 pub(crate) multiline_vertical: char,
2605 pub(crate) multiline_horizontal: char,
2606 pub(crate) multiline_whole_line: char,
2607 pub(crate) multiline_start_down: char,
2608 pub(crate) bottom_right: char,
2609 pub(crate) top_left: char,
2610 pub(crate) top_right_flat: char,
2611 pub(crate) bottom_left: char,
2612 pub(crate) multiline_end_up: char,
2613 pub(crate) multiline_end_same_line: char,
2614 pub(crate) multiline_bottom_right_with_text: char,
2615}
2616
2617#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2618enum TitleStyle {
2619 MainHeader,
2620 Header,
2621 Secondary,
2622}
2623
2624fn max_line_number(groups: &[Group<'_>]) -> usize {
2625 groups
2626 .iter()
2627 .map(|v| {
2628 v.elements
2629 .iter()
2630 .map(|s| match s {
2631 Element::Message(_) | Element::Origin(_) | Element::Padding(_) => 0,
2632 Element::Cause(cause) => {
2633 if cause.fold {
2634 let end = cause
2635 .markers
2636 .iter()
2637 .map(|a| a.span.end)
2638 .max()
2639 .unwrap_or(cause.source.len())
2640 .min(cause.source.len());
2641
2642 cause.line_start + newline_count(&cause.source[..end])
2643 } else {
2644 cause.line_start + newline_count(&cause.source)
2645 }
2646 }
2647 Element::Suggestion(suggestion) => {
2648 if suggestion.fold {
2649 let end = suggestion
2650 .markers
2651 .iter()
2652 .map(|a| a.span.end)
2653 .max()
2654 .unwrap_or(suggestion.source.len())
2655 .min(suggestion.source.len());
2656
2657 suggestion.line_start + newline_count(&suggestion.source[..end])
2658 } else {
2659 suggestion.line_start + newline_count(&suggestion.source)
2660 }
2661 }
2662 })
2663 .max()
2664 .unwrap_or(1)
2665 })
2666 .max()
2667 .unwrap_or(1)
2668}
2669
2670fn newline_count(body: &str) -> usize {
2671 #[cfg(feature = "simd")]
2672 {
2673 memchr::memchr_iter(b'\n', body.as_bytes()).count()
2674 }
2675 #[cfg(not(feature = "simd"))]
2676 {
2677 body.lines().count().saturating_sub(1)
2678 }
2679}
2680
2681#[cfg(test)]
2682mod test {
2683 use super::{newline_count, OUTPUT_REPLACEMENTS};
2684 use snapbox::IntoData;
2685
2686 fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2687 replacements
2688 .into_iter()
2689 .map(|r| format!(" {r:?}"))
2690 .collect::<Vec<_>>()
2691 .join("\n")
2692 }
2693
2694 #[test]
2695 fn ensure_output_replacements_is_sorted() {
2698 let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2699 expected.sort_by_key(|r| r.0);
2700 expected.dedup_by_key(|r| r.0);
2701 let expected = format_replacements(expected);
2702 let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2703 snapbox::assert_data_eq!(actual, expected.into_data().raw());
2704 }
2705
2706 #[test]
2707 fn ensure_newline_count_correct() {
2708 let source = r#"
2709 cargo-features = ["path-bases"]
2710
2711 [package]
2712 name = "foo"
2713 version = "0.5.0"
2714 authors = ["wycats@example.com"]
2715
2716 [dependencies]
2717 bar = { base = '^^not-valid^^', path = 'bar' }
2718 "#;
2719 let actual_count = newline_count(source);
2720 let expected_count = 10;
2721
2722 assert_eq!(expected_count, actual_count);
2723 }
2724}