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