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