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