1use alloc::borrow::Cow;
4use alloc::borrow::ToOwned;
5use alloc::collections::BTreeMap;
6use alloc::string::{String, ToString};
7use alloc::{format, vec, vec::Vec};
8use core::cmp::{Ordering, Reverse, max, min};
9use core::fmt;
10
11use anstyle::Style;
12
13use super::DecorStyle;
14use super::Renderer;
15use super::margin::Margin;
16use super::preprocess::{DisplaySuggestion, Preprocessed, PreprocessedElement, PreprocessedGroup};
17use super::stylesheet::Stylesheet;
18use crate::level::{Level, LevelInner};
19use crate::renderer::source_map::{
20 AnnotatedLineInfo, LineInfo, Loc, SourceMap, SplicedLines, SubstitutionHighlight,
21};
22use crate::renderer::styled_buffer::StyledBuffer;
23use crate::snippet::Id;
24use crate::{
25 Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Report, Snippet, Title,
26};
27
28pub(crate) const ANONYMIZED_LINE_NUM: &str = "LL";
29
30pub(crate) fn render(renderer: &Renderer, groups: Report<'_>) -> String {
31 if renderer.short_message {
32 render_short_message(renderer, groups).unwrap()
33 } else {
34 render_full_message(renderer, groups)
35 }
36}
37
38pub(crate) fn render_full_message(renderer: &Renderer, groups: Report<'_>) -> String {
39 let Preprocessed {
40 max_line_num,
41 report_primary_path,
42 groups,
43 } = Preprocessed::preprocess(groups);
44 let max_line_num_len = if renderer.anonymized_snippet_line_numbers {
45 ANONYMIZED_LINE_NUM.len()
46 } else {
47 num_decimal_digits(max_line_num)
48 };
49 let mut out_string = String::new();
50 let group_len = groups.len();
51 for (
52 g,
53 PreprocessedGroup {
54 group,
55 elements,
56 primary_path,
57 max_depth,
58 },
59 ) in groups.into_iter().enumerate()
60 {
61 let mut buffer = StyledBuffer::new();
62 let level = group.primary_level.clone();
63 let mut message_iter = elements.into_iter().enumerate().peekable();
64 if let Some(title) = &group.title {
65 let peek = message_iter.peek().map(|(_, s)| s);
66 let title_style = if title.allows_styling {
67 TitleStyle::Secondary
68 } else {
69 TitleStyle::Primary
70 };
71 let buffer_msg_line_offset = buffer.num_lines();
72 render_title(
73 renderer,
74 &mut buffer,
75 title,
76 max_line_num_len,
77 title_style,
78 matches!(peek, Some(PreprocessedElement::Message(_))),
79 buffer_msg_line_offset,
80 );
81 let buffer_msg_line_offset = buffer.num_lines();
82
83 if matches!(peek, Some(PreprocessedElement::Message(_))) {
84 draw_col_separator_no_space(
85 renderer,
86 &mut buffer,
87 buffer_msg_line_offset,
88 max_line_num_len + 1,
89 );
90 }
91 if peek.is_none() && title_style == TitleStyle::Primary && g == 0 && group_len > 1 {
92 draw_col_separator_end(
93 renderer,
94 &mut buffer,
95 buffer_msg_line_offset,
96 max_line_num_len + 1,
97 );
98 }
99 }
100 let mut seen_primary = false;
101 let mut last_suggestion_path = None;
102 while let Some((i, section)) = message_iter.next() {
103 let peek = message_iter.peek().map(|(_, s)| s);
104 let is_first = i == 0;
105 match section {
106 PreprocessedElement::Message(title) => {
107 let title_style = TitleStyle::Message;
108 let buffer_msg_line_offset = buffer.num_lines();
109 render_title(
110 renderer,
111 &mut buffer,
112 title,
113 max_line_num_len,
114 title_style,
115 peek.is_some(),
116 buffer_msg_line_offset,
117 );
118 }
119 PreprocessedElement::Cause((cause, source_map, annotated_lines)) => {
120 let is_primary = primary_path == cause.path.as_ref() && !seen_primary;
121 seen_primary |= is_primary;
122 render_snippet_annotations(
123 renderer,
124 &mut buffer,
125 max_line_num_len,
126 cause,
127 is_primary,
128 &source_map,
129 &annotated_lines,
130 max_depth,
131 peek.is_some() || (g == 0 && group_len > 1),
132 is_first,
133 );
134
135 if g == 0 {
136 let current_line = buffer.num_lines();
137 match peek {
138 Some(PreprocessedElement::Message(_)) => {
139 draw_col_separator_no_space(
140 renderer,
141 &mut buffer,
142 current_line,
143 max_line_num_len + 1,
144 );
145 }
146 None if group_len > 1 => draw_col_separator_end(
147 renderer,
148 &mut buffer,
149 current_line,
150 max_line_num_len + 1,
151 ),
152 _ => {}
153 }
154 }
155 }
156 PreprocessedElement::Suggestion((
157 suggestion,
158 source_map,
159 spliced_lines,
160 display_suggestion,
161 )) => {
162 let matches_previous_suggestion =
163 last_suggestion_path == Some(suggestion.path.as_ref());
164 emit_suggestion_default(
165 renderer,
166 &mut buffer,
167 suggestion,
168 spliced_lines,
169 display_suggestion,
170 max_line_num_len,
171 &source_map,
172 primary_path.or(report_primary_path),
173 matches_previous_suggestion,
174 is_first,
175 peek.is_some(),
177 );
178
179 if matches!(peek, Some(PreprocessedElement::Suggestion(_))) {
180 last_suggestion_path = Some(suggestion.path.as_ref());
181 } else {
182 last_suggestion_path = None;
183 }
184 }
185
186 PreprocessedElement::Origin(origin) => {
187 let buffer_msg_line_offset = buffer.num_lines();
188 let is_primary = primary_path == Some(&origin.path) && !seen_primary;
189 seen_primary |= is_primary;
190 render_origin(
191 renderer,
192 &mut buffer,
193 max_line_num_len,
194 origin,
195 is_primary,
196 is_first,
197 peek.is_none(),
198 buffer_msg_line_offset,
199 );
200 let current_line = buffer.num_lines();
201 if g == 0 && peek.is_none() && group_len > 1 {
202 draw_col_separator_end(
203 renderer,
204 &mut buffer,
205 current_line,
206 max_line_num_len + 1,
207 );
208 }
209 }
210 PreprocessedElement::Padding(_) => {
211 let current_line = buffer.num_lines();
212 if peek.is_none() {
213 draw_col_separator_end(
214 renderer,
215 &mut buffer,
216 current_line,
217 max_line_num_len + 1,
218 );
219 } else {
220 draw_col_separator_no_space(
221 renderer,
222 &mut buffer,
223 current_line,
224 max_line_num_len + 1,
225 );
226 }
227 }
228 }
229 }
230 buffer
231 .render(&level, &renderer.stylesheet, &mut out_string)
232 .unwrap();
233 if g != group_len - 1 {
234 out_string.push('\n');
235 }
236 }
237 out_string
238}
239
240fn render_short_message(renderer: &Renderer, groups: &[Group<'_>]) -> Result<String, fmt::Error> {
241 let mut buffer = StyledBuffer::new();
242 let mut labels = None;
243 let group = groups.first().expect("Expected at least one group");
244
245 let Some(title) = &group.title else {
246 panic!("Expected a Title");
247 };
248
249 if let Some(Element::Cause(cause)) = group
250 .elements
251 .iter()
252 .find(|e| matches!(e, Element::Cause(_)))
253 {
254 let labels_inner = cause
255 .markers
256 .iter()
257 .filter_map(|ann| match &ann.label {
258 Some(msg) if ann.kind.is_primary() => {
259 if !msg.trim().is_empty() {
260 Some(msg.to_string())
261 } else {
262 None
263 }
264 }
265 _ => None,
266 })
267 .collect::<Vec<_>>()
268 .join(", ");
269 if !labels_inner.is_empty() {
270 labels = Some(labels_inner);
271 }
272
273 if let Some(path) = &cause.path {
274 let mut origin = Origin::path(path.as_ref());
275
276 let source_map = SourceMap::new(&cause.source, cause.line_start);
277 let (_depth, annotated_lines) =
278 source_map.annotated_lines(cause.markers.clone(), cause.fold);
279
280 if let Some(primary_line) = annotated_lines
281 .iter()
282 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
283 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
284 {
285 origin.line = Some(primary_line.line_index);
286 if let Some(first_annotation) = primary_line
287 .annotations
288 .iter()
289 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
290 {
291 origin.char_column = Some(first_annotation.start.char + 1);
292 }
293 }
294
295 let str = format_origin(&origin, renderer.anonymized_origin_line_numbers);
296 buffer.append(0, &str, ElementStyle::LineAndColumn);
297 buffer.append(0, ": ", ElementStyle::LineAndColumn);
298 }
299 }
300
301 render_title(
302 renderer,
303 &mut buffer,
304 title,
305 0, TitleStyle::Primary,
307 false,
308 0,
309 );
310
311 if let Some(labels) = labels {
312 buffer.append(0, &format!(": {labels}"), ElementStyle::NoStyle);
313 }
314
315 let mut out_string = String::new();
316 buffer.render(&title.level, &renderer.stylesheet, &mut out_string)?;
317
318 Ok(out_string)
319}
320
321#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
322fn render_title(
323 renderer: &Renderer,
324 buffer: &mut StyledBuffer,
325 title: &dyn MessageOrTitle,
326 max_line_num_len: usize,
327 title_style: TitleStyle,
328 is_cont: bool,
329 buffer_msg_line_offset: usize,
330) {
331 let (label_style, title_element_style) = match title_style {
332 TitleStyle::Primary => (
333 ElementStyle::Level(title.level().level),
334 if renderer.short_message {
335 ElementStyle::NoStyle
336 } else {
337 ElementStyle::MainHeaderMsg
338 },
339 ),
340 TitleStyle::Secondary => (
341 ElementStyle::Level(title.level().level),
342 ElementStyle::HeaderMsg,
343 ),
344 TitleStyle::Message => {
345 for _ in 0..max_line_num_len {
346 buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
347 }
348
349 draw_note_separator(
350 renderer,
351 buffer,
352 buffer_msg_line_offset,
353 max_line_num_len + 1,
354 is_cont,
355 );
356 (ElementStyle::MainHeaderMsg, ElementStyle::NoStyle)
357 }
358 };
359
360 let mut label_width = 0;
361 let level_is_visible = title.level().name != Some(None);
362 if level_is_visible || title.id().is_some() {
363 if level_is_visible {
364 buffer.append(buffer_msg_line_offset, title.level().as_str(), label_style);
365 label_width += str_width(title.level().as_str());
366 }
367
368 if let Some(Id { id: Some(id), url }) = &title.id() {
369 let url = url
370 .as_deref()
371 .filter(|_| renderer.hyperlink)
372 .map(Hyperlink::with_url)
373 .unwrap_or_default();
374
375 if level_is_visible {
376 buffer.append(buffer_msg_line_offset, "[", label_style);
377 label_width += 1;
378 }
379
380 buffer.append(buffer_msg_line_offset, &format!("{url}"), label_style);
381 buffer.append(buffer_msg_line_offset, id, label_style);
382 buffer.append(buffer_msg_line_offset, &format!("{url:#}"), label_style);
383 label_width += str_width(id);
384
385 if level_is_visible {
386 buffer.append(buffer_msg_line_offset, "]", label_style);
387 label_width += 1;
388 }
389 }
390
391 buffer.append(buffer_msg_line_offset, ": ", title_element_style);
392 label_width += 2;
393 }
394
395 let padding = " ".repeat(if title_style == TitleStyle::Message {
396 max_line_num_len + 3 + label_width
414 } else {
415 label_width
416 });
417
418 let (title_str, style) = if title.allows_styling() {
419 (Cow::Borrowed(title.text()), ElementStyle::NoStyle)
420 } else {
421 (normalize_whitespace(title.text()), title_element_style)
422 };
423 for (i, text) in title_str.split('\n').enumerate() {
424 if i != 0 {
425 buffer.append(buffer_msg_line_offset + i, &padding, ElementStyle::NoStyle);
426 if title_style == TitleStyle::Message
427 && is_cont
428 && matches!(renderer.decor_style, DecorStyle::Unicode)
429 {
430 draw_col_separator_no_space(
442 renderer,
443 buffer,
444 buffer_msg_line_offset + i,
445 max_line_num_len + 1,
446 );
447 }
448 }
449 buffer.append(buffer_msg_line_offset + i, text, style);
450 }
451}
452
453#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
454fn render_origin(
455 renderer: &Renderer,
456 buffer: &mut StyledBuffer,
457 max_line_num_len: usize,
458 origin: &Origin<'_>,
459 is_primary: bool,
460 is_first: bool,
461 alone: bool,
462 buffer_msg_line_offset: usize,
463) {
464 if !renderer.short_message {
465 for _ in 0..max_line_num_len {
466 buffer.append(buffer_msg_line_offset, " ", ElementStyle::NoStyle);
467 }
468
469 if is_primary {
470 buffer.append(
471 buffer_msg_line_offset,
472 renderer.decor_style.file_start(is_first, alone),
473 ElementStyle::LineNumber,
474 );
475 } else {
476 buffer.append(
497 buffer_msg_line_offset,
498 renderer.decor_style.secondary_file_start(),
499 ElementStyle::LineNumber,
500 );
501 }
502 }
503
504 let str = format_origin(origin, renderer.anonymized_origin_line_numbers);
505 buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn);
506}
507
508fn format_origin(origin: &Origin<'_>, anonymized_origin_line_numbers: bool) -> String {
509 use core::fmt::Write as _;
510
511 let mut buffer = origin.path.as_ref().to_owned();
512 if let Some(line) = origin.line {
513 if anonymized_origin_line_numbers {
514 let line = ANONYMIZED_LINE_NUM;
515 write!(&mut buffer, ":{line}").unwrap();
516 } else {
517 write!(&mut buffer, ":{line}").unwrap();
518 }
519 if let Some(col) = origin.char_column {
520 write!(&mut buffer, ":{col}").unwrap();
521 }
522 }
523 buffer
524}
525
526#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
527fn render_snippet_annotations(
528 renderer: &Renderer,
529 buffer: &mut StyledBuffer,
530 max_line_num_len: usize,
531 snippet: &Snippet<'_, Annotation<'_>>,
532 is_primary: bool,
533 sm: &SourceMap<'_>,
534 annotated_lines: &[AnnotatedLineInfo<'_>],
535 multiline_depth: usize,
536 is_cont: bool,
537 is_first: bool,
538) {
539 if let Some(path) = &snippet.path {
540 let mut origin = Origin::path(path.as_ref());
541 if is_primary {
546 if let Some(primary_line) = annotated_lines
547 .iter()
548 .find(|l| l.annotations.iter().any(LineAnnotation::is_primary))
549 .or(annotated_lines.iter().find(|l| !l.annotations.is_empty()))
550 {
551 origin.line = Some(primary_line.line_index);
552 if let Some(first_annotation) = primary_line
553 .annotations
554 .iter()
555 .min_by_key(|a| (Reverse(a.is_primary()), a.start.char))
556 {
557 origin.char_column = Some(first_annotation.start.char + 1);
558 }
559 }
560 } else {
561 let buffer_msg_line_offset = buffer.num_lines();
562 draw_col_separator_no_space(
573 renderer,
574 buffer,
575 buffer_msg_line_offset,
576 max_line_num_len + 1,
577 );
578 if let Some(first_line) = annotated_lines
579 .iter()
580 .find(|l| !l.annotations.is_empty())
581 .or(annotated_lines.first())
582 {
583 origin.line = Some(first_line.line_index);
584 if let Some(first_annotation) = first_line.annotations.first() {
585 origin.char_column = Some(first_annotation.start.char + 1);
586 }
587 }
588 }
589 let buffer_msg_line_offset = buffer.num_lines();
590 render_origin(
591 renderer,
592 buffer,
593 max_line_num_len,
594 &origin,
595 is_primary,
596 is_first,
597 false,
598 buffer_msg_line_offset,
599 );
600 draw_col_separator_no_space(
602 renderer,
603 buffer,
604 buffer_msg_line_offset + 1,
605 max_line_num_len + 1,
606 );
607 } else {
608 let buffer_msg_line_offset = buffer.num_lines();
609 if is_primary {
610 if renderer.decor_style == DecorStyle::Unicode {
611 buffer.puts(
612 buffer_msg_line_offset,
613 max_line_num_len,
614 renderer.decor_style.file_start(is_first, false),
615 ElementStyle::LineNumber,
616 );
617 } else {
618 draw_col_separator_no_space(
619 renderer,
620 buffer,
621 buffer_msg_line_offset,
622 max_line_num_len + 1,
623 );
624 }
625 } else {
626 draw_col_separator_no_space(
637 renderer,
638 buffer,
639 buffer_msg_line_offset,
640 max_line_num_len + 1,
641 );
642
643 buffer.puts(
644 buffer_msg_line_offset + 1,
645 max_line_num_len,
646 renderer.decor_style.secondary_file_start(),
647 ElementStyle::LineNumber,
648 );
649 }
650 }
651
652 let mut multilines = Vec::new();
654
655 let mut whitespace_margin = usize::MAX;
657 for line_info in annotated_lines {
658 let leading_whitespace = line_info
659 .line
660 .chars()
661 .take_while(|c| c.is_whitespace())
662 .map(|c| {
663 match c {
664 '\t' => 4,
666 _ => 1,
667 }
668 })
669 .sum();
670 if line_info.line.chars().any(|c| !c.is_whitespace()) {
671 whitespace_margin = min(whitespace_margin, leading_whitespace);
672 }
673 }
674 if whitespace_margin == usize::MAX {
675 whitespace_margin = 0;
676 }
677
678 let mut span_left_margin = usize::MAX;
680 for line_info in annotated_lines {
681 for ann in &line_info.annotations {
682 if matches!(ann.annotation_type, LineAnnotationType::MultilineLine(_)) {
686 continue;
687 }
688 span_left_margin = min(span_left_margin, ann.start.display);
689 span_left_margin = min(span_left_margin, ann.end.display);
690 }
691 }
692 if span_left_margin == usize::MAX {
693 span_left_margin = 0;
694 }
695
696 let mut span_right_margin = 0;
698 let mut label_right_margin = 0;
699 let mut max_line_len = 0;
700 for line_info in annotated_lines {
701 max_line_len = max(max_line_len, str_width(line_info.line));
702 for ann in &line_info.annotations {
703 span_right_margin = max(span_right_margin, ann.start.display);
704 span_right_margin = max(span_right_margin, ann.end.display);
705 let label_right = ann.label.as_ref().map_or(0, |l| str_width(l) + 1);
707 label_right_margin = max(label_right_margin, ann.end.display + label_right);
708 }
709 }
710 let width_offset = 3 + max_line_num_len;
711 let code_offset = if multiline_depth == 0 {
712 width_offset
713 } else {
714 width_offset + multiline_depth + 1
715 };
716
717 let column_width = renderer.term_width.saturating_sub(code_offset);
718
719 let margin = Margin::new(
720 whitespace_margin,
721 span_left_margin,
722 span_right_margin,
723 label_right_margin,
724 column_width,
725 max_line_len,
726 );
727
728 for annotated_line_idx in 0..annotated_lines.len() {
730 let previous_buffer_line = buffer.num_lines();
731
732 let depths = render_source_line(
733 renderer,
734 &annotated_lines[annotated_line_idx],
735 buffer,
736 width_offset,
737 code_offset,
738 max_line_num_len,
739 margin,
740 !is_cont && annotated_line_idx + 1 == annotated_lines.len(),
741 snippet.line_numbering,
742 );
743
744 let mut to_add = BTreeMap::new();
745
746 for (depth, style) in depths {
747 if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) {
748 multilines.swap_remove(index);
749 } else {
750 to_add.insert(depth, style);
751 }
752 }
753
754 for (depth, style) in &multilines {
757 for line in previous_buffer_line..buffer.num_lines() {
758 draw_multiline_line(renderer, buffer, line, width_offset, *depth, *style, false);
759 }
760 }
761 if annotated_line_idx < (annotated_lines.len() - 1) {
764 let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index
765 - annotated_lines[annotated_line_idx].line_index;
766 match line_idx_delta.cmp(&2) {
767 Ordering::Greater => {
768 let last_buffer_line_num = buffer.num_lines();
769
770 draw_line_separator(renderer, buffer, last_buffer_line_num, width_offset);
771
772 for (depth, style) in &multilines {
774 draw_multiline_line(
775 renderer,
776 buffer,
777 last_buffer_line_num,
778 width_offset,
779 *depth,
780 *style,
781 true,
782 );
783 }
784 if let Some(line) = annotated_lines.get(annotated_line_idx) {
785 for ann in &line.annotations {
786 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
787 draw_multiline_line(
791 renderer,
792 buffer,
793 last_buffer_line_num,
794 width_offset,
795 pos,
796 if ann.is_primary() {
797 ElementStyle::UnderlinePrimary
798 } else {
799 ElementStyle::UnderlineSecondary
800 },
801 true,
802 );
803 }
804 }
805 }
806 }
807
808 Ordering::Equal => {
809 let unannotated_line = sm
810 .get_line(annotated_lines[annotated_line_idx].line_index + 1)
811 .unwrap_or("");
812
813 let last_buffer_line_num = buffer.num_lines();
814
815 draw_line(
816 renderer,
817 buffer,
818 &normalize_whitespace(unannotated_line),
819 annotated_lines[annotated_line_idx + 1].line_index - 1,
820 last_buffer_line_num,
821 width_offset,
822 code_offset,
823 max_line_num_len,
824 margin,
825 snippet.line_numbering,
826 );
827
828 for (depth, style) in &multilines {
829 draw_multiline_line(
830 renderer,
831 buffer,
832 last_buffer_line_num,
833 width_offset,
834 *depth,
835 *style,
836 false,
837 );
838 }
839 if let Some(line) = annotated_lines.get(annotated_line_idx) {
840 for ann in &line.annotations {
841 if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type {
842 draw_multiline_line(
843 renderer,
844 buffer,
845 last_buffer_line_num,
846 width_offset,
847 pos,
848 if ann.is_primary() {
849 ElementStyle::UnderlinePrimary
850 } else {
851 ElementStyle::UnderlineSecondary
852 },
853 false,
854 );
855 }
856 }
857 }
858 }
859 Ordering::Less => {}
860 }
861 }
862
863 multilines.extend(to_add);
864 }
865}
866
867#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
868fn render_source_line(
869 renderer: &Renderer,
870 line_info: &AnnotatedLineInfo<'_>,
871 buffer: &mut StyledBuffer,
872 width_offset: usize,
873 code_offset: usize,
874 max_line_num_len: usize,
875 margin: Margin,
876 close_window: bool,
877 line_numbering: bool,
878) -> Vec<(usize, ElementStyle)> {
879 let source_string = normalize_whitespace(line_info.line);
894
895 let line_offset = buffer.num_lines();
896
897 let left = draw_line(
898 renderer,
899 buffer,
900 &source_string,
901 line_info.line_index,
902 line_offset,
903 width_offset,
904 code_offset,
905 max_line_num_len,
906 margin,
907 line_numbering,
908 );
909
910 if line_info.annotations.is_empty() {
912 if close_window {
915 draw_col_separator_end(renderer, buffer, line_offset + 1, width_offset - 2);
916 }
917 return vec![];
918 }
919
920 let mut buffer_ops = vec![];
937 let mut annotations = vec![];
938 let mut short_start = true;
939 for ann in &line_info.annotations {
940 if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type {
941 if source_string
942 .chars()
943 .take(ann.start.display)
944 .all(char::is_whitespace)
945 {
946 let uline = renderer.decor_style.underline(ann.is_primary());
947 let chr = uline.multiline_whole_line;
948 annotations.push((depth, uline.style));
949 buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style));
950 } else {
951 short_start = false;
952 break;
953 }
954 } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type {
955 } else {
956 short_start = false;
957 break;
958 }
959 }
960 if short_start {
961 for (y, x, c, s) in buffer_ops {
962 buffer.putc(y, x, c, s);
963 }
964 return annotations;
965 }
966
967 let mut annotations = line_info.annotations.clone();
1000 annotations.sort_by_key(|a| Reverse((a.start.display, a.start.char)));
1001
1002 let mut overlap = vec![false; annotations.len()];
1065 let mut annotations_position = vec![];
1066 let mut line_len: usize = 0;
1067 let mut p = 0;
1068 for (i, annotation) in annotations.iter().enumerate() {
1069 for (j, next) in annotations.iter().enumerate() {
1070 if overlaps(next, annotation, 0) && j > 1 {
1071 overlap[i] = true;
1072 overlap[j] = true;
1073 }
1074 if overlaps(next, annotation, 0) && annotation.has_label() && j > i && p == 0
1078 {
1080 if next.start.display == annotation.start.display
1083 && next.start.char == annotation.start.char
1084 && next.end.display == annotation.end.display
1085 && next.end.char == annotation.end.char
1086 && !next.has_label()
1087 {
1088 continue;
1089 }
1090
1091 p += 1;
1093 break;
1094 }
1095 }
1096 annotations_position.push((p, annotation));
1097 for (j, next) in annotations.iter().enumerate() {
1098 if j > i {
1099 let l = next.label.as_ref().map_or(0, |label| label.len() + 2);
1100 if (overlaps(next, annotation, l) && annotation.has_label() && next.has_label()) || (annotation.takes_space() && next.has_label()) || (annotation.has_label() && next.takes_space())
1117 || (annotation.takes_space() && next.takes_space())
1118 || (overlaps(next, annotation, l)
1119 && (next.end.display, next.end.char) <= (annotation.end.display, annotation.end.char)
1120 && next.has_label()
1121 && p == 0)
1122 {
1124 p += 1;
1126 break;
1127 }
1128 }
1129 }
1130 line_len = max(line_len, p);
1131 }
1132
1133 if line_len != 0 {
1134 line_len += 1;
1135 }
1136
1137 if line_info.annotations.iter().all(LineAnnotation::is_line) {
1140 return vec![];
1141 }
1142
1143 if annotations_position
1144 .iter()
1145 .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_)))
1146 && let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max()
1147 {
1148 for (pos, _) in &mut annotations_position {
1161 *pos = max_pos - *pos;
1162 }
1163 line_len = line_len.saturating_sub(1);
1166 }
1167
1168 for pos in 0..=line_len {
1180 draw_col_separator_no_space(renderer, buffer, line_offset + pos + 1, width_offset - 2);
1181 }
1182 if close_window {
1183 draw_col_separator_end(
1184 renderer,
1185 buffer,
1186 line_offset + line_len + 1,
1187 width_offset - 2,
1188 );
1189 }
1190 for &(pos, annotation) in &annotations_position {
1203 let underline = renderer.decor_style.underline(annotation.is_primary());
1204 let pos = pos + 1;
1205 match annotation.annotation_type {
1206 LineAnnotationType::MultilineStart(depth) | LineAnnotationType::MultilineEnd(depth) => {
1207 draw_range(
1208 buffer,
1209 underline.multiline_horizontal,
1210 line_offset + pos,
1211 width_offset + depth,
1212 (code_offset + annotation.start.display).saturating_sub(left),
1213 underline.style,
1214 );
1215 }
1216 _ if annotation.highlight_source => {
1217 buffer.set_style_range(
1218 line_offset,
1219 (code_offset + annotation.start.char).saturating_sub(left),
1220 (code_offset + annotation.end.char).saturating_sub(left),
1221 underline.style,
1222 annotation.is_primary(),
1223 );
1224 }
1225 _ => {}
1226 }
1227 }
1228
1229 for &(pos, annotation) in &annotations_position {
1241 let underline = renderer.decor_style.underline(annotation.is_primary());
1242 let pos = pos + 1;
1243
1244 if pos > 1 && (annotation.has_label() || annotation.takes_space()) {
1245 for p in line_offset + 1..=line_offset + pos {
1246 buffer.putc(
1247 p,
1248 (code_offset + annotation.start.display).saturating_sub(left),
1249 match annotation.annotation_type {
1250 LineAnnotationType::MultilineLine(_) => underline.multiline_vertical,
1251 _ => underline.vertical_text_line,
1252 },
1253 underline.style,
1254 );
1255 }
1256 if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type {
1257 buffer.putc(
1258 line_offset + pos,
1259 (code_offset + annotation.start.display).saturating_sub(left),
1260 underline.bottom_right,
1261 underline.style,
1262 );
1263 }
1264 if matches!(
1265 annotation.annotation_type,
1266 LineAnnotationType::MultilineEnd(_)
1267 ) && annotation.has_label()
1268 {
1269 buffer.putc(
1270 line_offset + pos,
1271 (code_offset + annotation.start.display).saturating_sub(left),
1272 underline.multiline_bottom_right_with_text,
1273 underline.style,
1274 );
1275 }
1276 }
1277 match annotation.annotation_type {
1278 LineAnnotationType::MultilineStart(depth) => {
1279 buffer.putc(
1280 line_offset + pos,
1281 width_offset + depth - 1,
1282 underline.top_left,
1283 underline.style,
1284 );
1285 for p in line_offset + pos + 1..line_offset + line_len + 2 {
1286 buffer.putc(
1287 p,
1288 width_offset + depth - 1,
1289 underline.multiline_vertical,
1290 underline.style,
1291 );
1292 }
1293 }
1294 LineAnnotationType::MultilineEnd(depth) => {
1295 for p in line_offset..line_offset + pos {
1296 buffer.putc(
1297 p,
1298 width_offset + depth - 1,
1299 underline.multiline_vertical,
1300 underline.style,
1301 );
1302 }
1303 buffer.putc(
1304 line_offset + pos,
1305 width_offset + depth - 1,
1306 underline.bottom_left,
1307 underline.style,
1308 );
1309 }
1310 _ => (),
1311 }
1312 }
1313
1314 for &(pos, annotation) in &annotations_position {
1326 let style = if annotation.is_primary() {
1327 ElementStyle::LabelPrimary
1328 } else {
1329 ElementStyle::LabelSecondary
1330 };
1331 let (pos, col) = if pos == 0 {
1332 if annotation.end.display == 0 {
1333 (pos + 1, (annotation.end.display + 2).saturating_sub(left))
1334 } else {
1335 (pos + 1, (annotation.end.display + 1).saturating_sub(left))
1336 }
1337 } else {
1338 (pos + 2, annotation.start.display.saturating_sub(left))
1339 };
1340 if let Some(label) = &annotation.label {
1341 buffer.puts(line_offset + pos, code_offset + col, label, style);
1342 }
1343 }
1344
1345 annotations_position.sort_by_key(|(_, ann)| {
1354 (Reverse(ann.len()), ann.is_primary())
1356 });
1357
1358 for &(pos, annotation) in &annotations_position {
1370 let uline = renderer.decor_style.underline(annotation.is_primary());
1371 for p in annotation.start.display..annotation.end.display {
1372 buffer.putc(
1374 line_offset + 1,
1375 (code_offset + p).saturating_sub(left),
1376 uline.underline,
1377 uline.style,
1378 );
1379 }
1380
1381 if pos == 0
1382 && matches!(
1383 annotation.annotation_type,
1384 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1385 )
1386 {
1387 buffer.putc(
1389 line_offset + 1,
1390 (code_offset + annotation.start.display).saturating_sub(left),
1391 match annotation.annotation_type {
1392 LineAnnotationType::MultilineStart(_) => uline.top_right_flat,
1393 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line,
1394 _ => panic!("unexpected annotation type: {annotation:?}"),
1395 },
1396 uline.style,
1397 );
1398 } else if pos != 0
1399 && matches!(
1400 annotation.annotation_type,
1401 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
1402 )
1403 {
1404 buffer.putc(
1407 line_offset + 1,
1408 (code_offset + annotation.start.display).saturating_sub(left),
1409 match annotation.annotation_type {
1410 LineAnnotationType::MultilineStart(_) => uline.multiline_start_down,
1411 LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up,
1412 _ => panic!("unexpected annotation type: {annotation:?}"),
1413 },
1414 uline.style,
1415 );
1416 } else if pos != 0 && annotation.has_label() {
1417 buffer.putc(
1419 line_offset + 1,
1420 (code_offset + annotation.start.display).saturating_sub(left),
1421 uline.label_start,
1422 uline.style,
1423 );
1424 }
1425 }
1426
1427 for (i, (_pos, annotation)) in annotations_position.iter().enumerate() {
1431 if overlap[i] {
1433 continue;
1434 };
1435 let LineAnnotationType::Singleline = annotation.annotation_type else {
1436 continue;
1437 };
1438 let width = annotation.end.display - annotation.start.display;
1439
1440 static MIN_PAD: usize = 5;
1441 let margin_width = str_width(renderer.decor_style.margin());
1442 if width > margin.term_width * 2 && width > (MIN_PAD * 2 + margin_width) {
1443 let pad = max(margin.term_width / 3, MIN_PAD);
1446 buffer.replace(
1448 line_offset,
1449 code_offset + (annotation.start.display + pad).saturating_sub(left),
1450 code_offset + (annotation.end.display - pad).saturating_sub(left),
1451 renderer.decor_style.margin(),
1452 );
1453 buffer.replace(
1455 line_offset + 1,
1456 code_offset + (annotation.start.display + pad).saturating_sub(left),
1457 code_offset + (annotation.end.display - pad).saturating_sub(left),
1458 renderer.decor_style.margin(),
1459 );
1460 }
1461 }
1462 annotations_position
1463 .iter()
1464 .filter_map(|&(_, annotation)| match annotation.annotation_type {
1465 LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => {
1466 let style = if annotation.is_primary() {
1467 ElementStyle::LabelPrimary
1468 } else {
1469 ElementStyle::LabelSecondary
1470 };
1471 Some((p, style))
1472 }
1473 _ => None,
1474 })
1475 .collect::<Vec<_>>()
1476}
1477
1478#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
1479fn emit_suggestion_default(
1480 renderer: &Renderer,
1481 buffer: &mut StyledBuffer,
1482 suggestion: &Snippet<'_, Patch<'_>>,
1483 spliced_lines: SplicedLines<'_>,
1484 show_code_change: DisplaySuggestion,
1485 max_line_num_len: usize,
1486 sm: &SourceMap<'_>,
1487 primary_path: Option<&Cow<'_, str>>,
1488 matches_previous_suggestion: bool,
1489 is_first: bool,
1490 is_cont: bool,
1491) {
1492 let buffer_offset = buffer.num_lines();
1493 let mut row_num = buffer_offset + usize::from(!matches_previous_suggestion);
1494 let is_multiline = spliced_lines.complete.lines().count() > 1;
1495
1496 if suggestion.path.as_ref() != primary_path
1497 && let Some(path) = suggestion.path.as_ref()
1498 && !matches_previous_suggestion
1499 {
1500 let (loc, _) = sm.span_to_locations(spliced_lines.patches[0].span.clone());
1501 for _ in 0..max_line_num_len {
1504 buffer.append(row_num - 1, " ", ElementStyle::NoStyle);
1505 }
1506 let arrow = renderer.decor_style.file_start(is_first, false);
1507 buffer.append(row_num - 1, arrow, ElementStyle::LineNumber);
1508 let mut origin = Origin::path(path.as_ref());
1509 origin.line = Some(loc.line);
1510 origin.char_column = Some(loc.char + 1);
1511 let message = format_origin(&origin, renderer.anonymized_origin_line_numbers);
1512 buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn);
1513
1514 draw_col_separator_no_space(renderer, buffer, row_num, max_line_num_len + 1);
1515 row_num += 1;
1516 } else if matches_previous_suggestion {
1517 buffer.puts(
1518 row_num - 1,
1519 max_line_num_len + 1,
1520 renderer.decor_style.multi_suggestion_separator(),
1521 ElementStyle::LineNumber,
1522 );
1523 } else {
1524 draw_col_separator_start(renderer, buffer, row_num - 1, max_line_num_len + 1);
1525 }
1526
1527 if let DisplaySuggestion::Diff = show_code_change {
1528 row_num += 1;
1529 }
1530
1531 let lo = spliced_lines
1532 .patches
1533 .iter()
1534 .map(|p| p.span.start)
1535 .min()
1536 .unwrap();
1537 let hi = spliced_lines
1538 .patches
1539 .iter()
1540 .map(|p| p.span.end)
1541 .max()
1542 .unwrap();
1543
1544 let file_lines = sm.span_to_lines(lo..hi);
1545 let (line_start, line_end) = if suggestion.fold {
1546 sm.span_to_locations(spliced_lines.patches[0].original_span.clone())
1548 } else {
1549 sm.span_to_locations(0..sm.source.len())
1550 };
1551 let mut lines = spliced_lines.complete.lines();
1552 if lines.clone().next().is_none() {
1553 for line in line_start.line..=line_end.line {
1555 buffer.puts(
1556 row_num - 1 + line - line_start.line,
1557 0,
1558 &maybe_anonymized(renderer, line, max_line_num_len, suggestion.line_numbering),
1559 ElementStyle::LineNumber,
1560 );
1561 buffer.puts(
1562 row_num - 1 + line - line_start.line,
1563 max_line_num_len + 1,
1564 "- ",
1565 ElementStyle::Removal,
1566 );
1567 buffer.puts(
1568 row_num - 1 + line - line_start.line,
1569 max_line_num_len + 3,
1570 &normalize_whitespace(sm.get_line(line).unwrap()),
1571 ElementStyle::Removal,
1572 );
1573 }
1574 row_num += line_end.line - line_start.line;
1575 }
1576 let mut unhighlighted_lines = Vec::new();
1577 for (line_pos, (line, highlight_parts)) in
1578 lines.by_ref().zip(spliced_lines.highlights).enumerate()
1579 {
1580 if highlight_parts.is_empty() && suggestion.fold {
1582 unhighlighted_lines.push((line_pos, line));
1583 continue;
1584 }
1585
1586 match unhighlighted_lines.len() {
1587 0 => (),
1588 n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| {
1593 draw_code_line(
1594 renderer,
1595 buffer,
1596 &mut row_num,
1597 &[],
1598 &[],
1599 p + line_start.line,
1600 l,
1601 show_code_change,
1602 max_line_num_len,
1603 &file_lines,
1604 is_multiline,
1605 suggestion.line_numbering,
1606 );
1607 }),
1608 _ => {
1616 let last_line = unhighlighted_lines.pop();
1617 let first_line = unhighlighted_lines.drain(..).next();
1618
1619 if let Some((p, l)) = first_line {
1620 draw_code_line(
1621 renderer,
1622 buffer,
1623 &mut row_num,
1624 &[],
1625 &[],
1626 p + line_start.line,
1627 l,
1628 show_code_change,
1629 max_line_num_len,
1630 &file_lines,
1631 is_multiline,
1632 suggestion.line_numbering,
1633 );
1634 }
1635
1636 let placeholder = renderer.decor_style.margin();
1637 let padding = str_width(placeholder);
1638 buffer.puts(
1639 row_num,
1640 max_line_num_len.saturating_sub(padding),
1641 placeholder,
1642 ElementStyle::LineNumber,
1643 );
1644 row_num += 1;
1645
1646 if let Some((p, l)) = last_line {
1647 draw_code_line(
1648 renderer,
1649 buffer,
1650 &mut row_num,
1651 &[],
1652 &[],
1653 p + line_start.line,
1654 l,
1655 show_code_change,
1656 max_line_num_len,
1657 &file_lines,
1658 is_multiline,
1659 suggestion.line_numbering,
1660 );
1661 }
1662 }
1663 }
1664 draw_code_line(
1665 renderer,
1666 buffer,
1667 &mut row_num,
1668 &highlight_parts,
1669 &spliced_lines.replaced_highlights,
1670 line_pos + line_start.line,
1671 line,
1672 show_code_change,
1673 max_line_num_len,
1674 &file_lines,
1675 is_multiline,
1676 suggestion.line_numbering,
1677 );
1678 }
1679
1680 let mut offsets: Vec<(usize, isize)> = Vec::new();
1683 if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add =
1686 show_code_change
1687 {
1688 for part in spliced_lines.patches {
1689 let (span_start, span_end) = sm.span_to_locations(part.span.clone());
1690 let span_start_pos = span_start.display;
1691 let span_end_pos = span_end.display;
1692
1693 let is_whitespace_addition = part.replacement.trim().is_empty();
1696
1697 let start = if is_whitespace_addition {
1699 0
1700 } else {
1701 part.replacement
1702 .len()
1703 .saturating_sub(part.replacement.trim_start().len())
1704 };
1705 let sub_len: usize = str_width(if is_whitespace_addition {
1708 &part.replacement
1709 } else {
1710 part.replacement.trim()
1711 });
1712
1713 let offset: isize = offsets
1714 .iter()
1715 .filter_map(|(start, v)| {
1716 if span_start_pos < *start {
1717 None
1718 } else {
1719 Some(v)
1720 }
1721 })
1722 .sum();
1723 let underline_start = (span_start_pos + start).cast_signed() + offset;
1724 let underline_end = (span_start_pos + start + sub_len).cast_signed() + offset;
1725 assert!(underline_start >= 0 && underline_end >= 0);
1726 let padding: usize = max_line_num_len + 3;
1727 for p in underline_start..underline_end {
1728 if matches!(show_code_change, DisplaySuggestion::Underline) {
1729 buffer.putc(
1732 row_num,
1733 (padding.cast_signed() + p).cast_unsigned(),
1734 if part.is_addition(sm) {
1735 '+'
1736 } else {
1737 renderer.decor_style.diff()
1738 },
1739 ElementStyle::Addition,
1740 );
1741 }
1742 }
1743
1744 let full_sub_len = str_width(&part.replacement).cast_signed();
1746
1747 let snippet_len = span_end_pos.cast_signed() - span_start_pos.cast_signed();
1749 offsets.push((span_end_pos, full_sub_len - snippet_len));
1753 }
1754 row_num += 1;
1755 }
1756
1757 if lines.next().is_some() {
1759 let placeholder = renderer.decor_style.margin();
1760 let padding = str_width(placeholder);
1761 buffer.puts(
1762 row_num,
1763 max_line_num_len.saturating_sub(padding),
1764 placeholder,
1765 ElementStyle::LineNumber,
1766 );
1767 } else {
1768 let row = match show_code_change {
1769 DisplaySuggestion::Diff | DisplaySuggestion::Add | DisplaySuggestion::Underline => {
1770 row_num - 1
1771 }
1772 DisplaySuggestion::None => row_num,
1773 };
1774 if is_cont {
1775 draw_col_separator_no_space(renderer, buffer, row, max_line_num_len + 1);
1776 } else {
1777 draw_col_separator_end(renderer, buffer, row, max_line_num_len + 1);
1778 }
1779 }
1780}
1781
1782#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
1783fn draw_code_line(
1784 renderer: &Renderer,
1785 buffer: &mut StyledBuffer,
1786 row_num: &mut usize,
1787 highlight_parts: &[SubstitutionHighlight],
1788 replaced_parts: &[Vec<SubstitutionHighlight>],
1789 line_num: usize,
1790 line_to_add: &str,
1791 show_code_change: DisplaySuggestion,
1792 max_line_num_len: usize,
1793 file_lines: &[&LineInfo<'_>],
1794 is_multiline: bool,
1795 line_numbering: bool,
1796) {
1797 if let DisplaySuggestion::Diff = show_code_change {
1798 let lines_to_remove = file_lines.iter().take(file_lines.len() - 1);
1801 for (index, (line_to_remove, parts)) in lines_to_remove.zip(replaced_parts).enumerate() {
1802 buffer.puts(
1803 *row_num - 1,
1804 0,
1805 &maybe_anonymized(renderer, line_num + index, max_line_num_len, line_numbering),
1806 ElementStyle::LineNumber,
1807 );
1808 buffer.puts(
1809 *row_num - 1,
1810 max_line_num_len + 1,
1811 "- ",
1812 ElementStyle::Removal,
1813 );
1814 let line = normalize_whitespace(line_to_remove.line);
1815 buffer.puts(
1816 *row_num - 1,
1817 max_line_num_len + 3,
1818 &line,
1819 ElementStyle::NoStyle,
1820 );
1821 style_substitution_highlights(
1822 parts,
1823 ElementStyle::Removal,
1824 *row_num - 1,
1825 line_to_remove.line,
1826 max_line_num_len,
1827 buffer,
1828 );
1829 *row_num += 1;
1830 }
1831 let last_line = &file_lines.last().unwrap();
1838 if last_line.line == line_to_add {
1839 *row_num -= 2;
1840 style_substitution_highlights(
1843 replaced_parts.last().unwrap(),
1844 ElementStyle::Removal,
1845 *row_num,
1846 last_line.line,
1847 max_line_num_len,
1848 buffer,
1849 );
1850 } else {
1851 buffer.puts(
1852 *row_num - 1,
1853 0,
1854 &maybe_anonymized(
1855 renderer,
1856 line_num + file_lines.len() - 1,
1857 max_line_num_len,
1858 line_numbering,
1859 ),
1860 ElementStyle::LineNumber,
1861 );
1862 buffer.puts(
1863 *row_num - 1,
1864 max_line_num_len + 1,
1865 "- ",
1866 ElementStyle::Removal,
1867 );
1868 buffer.puts(
1869 *row_num - 1,
1870 max_line_num_len + 3,
1871 &normalize_whitespace(last_line.line),
1872 ElementStyle::NoStyle,
1873 );
1874 style_substitution_highlights(
1875 replaced_parts.last().unwrap(),
1876 ElementStyle::Removal,
1877 *row_num - 1,
1878 last_line.line,
1879 max_line_num_len,
1880 buffer,
1881 );
1882
1883 if line_to_add.trim().is_empty() {
1884 *row_num -= 1;
1885 } else {
1886 buffer.puts(
1900 *row_num,
1901 0,
1902 &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1903 ElementStyle::LineNumber,
1904 );
1905 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1906 buffer.append(
1907 *row_num,
1908 &normalize_whitespace(line_to_add),
1909 ElementStyle::NoStyle,
1910 );
1911 }
1912 }
1913 } else if is_multiline {
1914 buffer.puts(
1915 *row_num,
1916 0,
1917 &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1918 ElementStyle::LineNumber,
1919 );
1920 match &highlight_parts {
1921 [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => {
1922 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1923 }
1924 [] | [SubstitutionHighlight { start: 0, end: 0 }] => {
1925 draw_col_separator_no_space(renderer, buffer, *row_num, max_line_num_len + 1);
1927 }
1928 _ => {
1929 let diff = renderer.decor_style.diff();
1930 buffer.puts(
1931 *row_num,
1932 max_line_num_len + 1,
1933 &format!("{diff} "),
1934 ElementStyle::Addition,
1935 );
1936 }
1937 }
1938 buffer.puts(
1944 *row_num,
1945 max_line_num_len + 3,
1946 &normalize_whitespace(line_to_add),
1947 ElementStyle::NoStyle,
1948 );
1949 } else if let DisplaySuggestion::Add = show_code_change {
1950 buffer.puts(
1951 *row_num,
1952 0,
1953 &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1954 ElementStyle::LineNumber,
1955 );
1956 buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition);
1957 buffer.append(
1958 *row_num,
1959 &normalize_whitespace(line_to_add),
1960 ElementStyle::NoStyle,
1961 );
1962 } else {
1963 buffer.puts(
1964 *row_num,
1965 0,
1966 &maybe_anonymized(renderer, line_num, max_line_num_len, line_numbering),
1967 ElementStyle::LineNumber,
1968 );
1969 draw_col_separator(renderer, buffer, *row_num, max_line_num_len + 1);
1970 buffer.append(
1971 *row_num,
1972 &normalize_whitespace(line_to_add),
1973 ElementStyle::NoStyle,
1974 );
1975 }
1976
1977 style_substitution_highlights(
1978 highlight_parts,
1979 ElementStyle::Addition,
1980 *row_num,
1981 line_to_add,
1982 max_line_num_len,
1983 buffer,
1984 );
1985
1986 *row_num += 1;
1987}
1988
1989fn style_substitution_highlights(
1990 highlight_parts: &[SubstitutionHighlight],
1991 style: ElementStyle,
1992 row_num: usize,
1993 unnormalized_line: &str,
1994 max_line_num_len: usize,
1995 buffer: &mut StyledBuffer,
1996) {
1997 for &SubstitutionHighlight { start, end } in highlight_parts {
1998 if start != end {
2000 let extra_width_start: usize = extra_width_from_tabs(unnormalized_line, start);
2003 let extra_width_end: usize = extra_width_from_tabs(unnormalized_line, end);
2004 buffer.set_style_range(
2005 row_num,
2006 max_line_num_len + 3 + start + extra_width_start,
2007 max_line_num_len + 3 + end + extra_width_end,
2008 style,
2009 true,
2010 );
2011 }
2012 }
2013}
2014
2015#[allow(clippy::too_many_arguments, reason = "All arguments are necessary")]
2016fn draw_line(
2017 renderer: &Renderer,
2018 buffer: &mut StyledBuffer,
2019 source_string: &str,
2020 line_index: usize,
2021 line_offset: usize,
2022 width_offset: usize,
2023 code_offset: usize,
2024 max_line_num_len: usize,
2025 margin: Margin,
2026 line_numbering: bool,
2027) -> usize {
2028 debug_assert!(!source_string.contains('\t'));
2030 let line_len = str_width(source_string);
2031 let mut left = margin.left(line_len);
2033 let right = margin.right(line_len);
2034
2035 let mut taken = 0;
2036 let mut skipped = 0;
2037 let code: String = source_string
2038 .chars()
2039 .skip_while(|ch| {
2040 let w = char_width(*ch);
2041 if skipped < left {
2046 skipped += w;
2047 true
2048 } else {
2049 false
2050 }
2051 })
2052 .take_while(|ch| {
2053 taken += char_width(*ch);
2055 taken <= (right - left)
2056 })
2057 .collect();
2058 if skipped > left {
2060 left += skipped - left;
2061 }
2062 let placeholder = renderer.decor_style.margin();
2063 let padding = str_width(placeholder);
2064 let (width_taken, bytes_taken) = if margin.was_cut_left() {
2065 let mut bytes_taken = 0;
2067 let mut width_taken = 0;
2068 for ch in code.chars() {
2069 width_taken += char_width(ch);
2070 bytes_taken += ch.len_utf8();
2071
2072 if width_taken >= padding {
2073 break;
2074 }
2075 }
2076
2077 buffer.puts(
2078 line_offset,
2079 code_offset,
2080 placeholder,
2081 ElementStyle::LineNumber,
2082 );
2083 (width_taken, bytes_taken)
2084 } else {
2085 (0, 0)
2086 };
2087
2088 buffer.puts(
2089 line_offset,
2090 code_offset + width_taken,
2091 &code[bytes_taken..],
2092 ElementStyle::Quotation,
2093 );
2094
2095 if line_len > right {
2096 let mut char_taken = 0;
2098 let mut width_taken_inner = 0;
2099 for ch in code.chars().rev() {
2100 width_taken_inner += char_width(ch);
2101 char_taken += 1;
2102
2103 if width_taken_inner >= padding {
2104 break;
2105 }
2106 }
2107
2108 buffer.puts(
2109 line_offset,
2110 code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken,
2111 placeholder,
2112 ElementStyle::LineNumber,
2113 );
2114 }
2115
2116 buffer.puts(
2117 line_offset,
2118 0,
2119 &maybe_anonymized(renderer, line_index, max_line_num_len, line_numbering),
2120 ElementStyle::LineNumber,
2121 );
2122
2123 draw_col_separator_no_space(renderer, buffer, line_offset, width_offset - 2);
2124
2125 left
2126}
2127
2128fn draw_range(
2129 buffer: &mut StyledBuffer,
2130 symbol: char,
2131 line: usize,
2132 col_from: usize,
2133 col_to: usize,
2134 style: ElementStyle,
2135) {
2136 for col in col_from..col_to {
2137 buffer.putc(line, col, symbol, style);
2138 }
2139}
2140
2141fn draw_multiline_line(
2142 renderer: &Renderer,
2143 buffer: &mut StyledBuffer,
2144 line: usize,
2145 offset: usize,
2146 depth: usize,
2147 style: ElementStyle,
2148 elided: bool,
2149) {
2150 let chr = match (style, renderer.decor_style) {
2151 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Ascii) => '|',
2152 (_, DecorStyle::Ascii) => '|',
2153 (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, DecorStyle::Unicode) => {
2154 if elided {
2155 '┇'
2156 } else {
2157 '┃'
2158 }
2159 }
2160 (_, DecorStyle::Unicode) => {
2161 if elided {
2162 '┆'
2163 } else {
2164 '│'
2165 }
2166 }
2167 };
2168 buffer.putc(line, offset + depth - 1, chr, style);
2169}
2170
2171fn draw_col_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2172 let chr = renderer.decor_style.col_separator();
2173 buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber);
2174}
2175
2176fn draw_col_separator_no_space(
2177 renderer: &Renderer,
2178 buffer: &mut StyledBuffer,
2179 line: usize,
2180 col: usize,
2181) {
2182 let chr = renderer.decor_style.col_separator();
2183 draw_col_separator_no_space_with_style(buffer, chr, line, col, ElementStyle::LineNumber);
2184}
2185
2186fn draw_col_separator_start(
2187 renderer: &Renderer,
2188 buffer: &mut StyledBuffer,
2189 line: usize,
2190 col: usize,
2191) {
2192 match renderer.decor_style {
2193 DecorStyle::Ascii => {
2194 draw_col_separator_no_space_with_style(
2195 buffer,
2196 '|',
2197 line,
2198 col,
2199 ElementStyle::LineNumber,
2200 );
2201 }
2202 DecorStyle::Unicode => {
2203 draw_col_separator_no_space_with_style(
2204 buffer,
2205 '╭',
2206 line,
2207 col,
2208 ElementStyle::LineNumber,
2209 );
2210 draw_col_separator_no_space_with_style(
2211 buffer,
2212 '╴',
2213 line,
2214 col + 1,
2215 ElementStyle::LineNumber,
2216 );
2217 }
2218 }
2219}
2220
2221fn draw_col_separator_end(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2222 match renderer.decor_style {
2223 DecorStyle::Ascii => {
2224 draw_col_separator_no_space_with_style(
2225 buffer,
2226 '|',
2227 line,
2228 col,
2229 ElementStyle::LineNumber,
2230 );
2231 }
2232 DecorStyle::Unicode => {
2233 draw_col_separator_no_space_with_style(
2234 buffer,
2235 '╰',
2236 line,
2237 col,
2238 ElementStyle::LineNumber,
2239 );
2240 draw_col_separator_no_space_with_style(
2241 buffer,
2242 '╴',
2243 line,
2244 col + 1,
2245 ElementStyle::LineNumber,
2246 );
2247 }
2248 }
2249}
2250
2251fn draw_col_separator_no_space_with_style(
2252 buffer: &mut StyledBuffer,
2253 chr: char,
2254 line: usize,
2255 col: usize,
2256 style: ElementStyle,
2257) {
2258 buffer.putc(line, col, chr, style);
2259}
2260
2261fn maybe_anonymized(
2262 renderer: &Renderer,
2263 line_num: usize,
2264 max_line_num_len: usize,
2265 line_numbering: bool,
2266) -> String {
2267 format!(
2268 "{:>max_line_num_len$}",
2269 if !line_numbering {
2270 Cow::Borrowed("")
2271 } else if renderer.anonymized_snippet_line_numbers {
2272 Cow::Borrowed(ANONYMIZED_LINE_NUM)
2273 } else {
2274 Cow::Owned(line_num.to_string())
2275 }
2276 )
2277}
2278
2279fn draw_note_separator(
2280 renderer: &Renderer,
2281 buffer: &mut StyledBuffer,
2282 line: usize,
2283 col: usize,
2284 is_cont: bool,
2285) {
2286 let chr = renderer.decor_style.note_separator(is_cont);
2287 buffer.puts(line, col, chr, ElementStyle::LineNumber);
2288}
2289
2290fn draw_line_separator(renderer: &Renderer, buffer: &mut StyledBuffer, line: usize, col: usize) {
2291 let (column, dots) = match renderer.decor_style {
2292 DecorStyle::Ascii => (0, "..."),
2293 DecorStyle::Unicode => (col - 2, "┆"),
2294 };
2295 buffer.puts(line, column, dots, ElementStyle::LineNumber);
2296}
2297
2298pub(crate) trait MessageOrTitle {
2299 fn level(&self) -> &Level<'_>;
2300 fn id(&self) -> Option<&Id<'_>>;
2301 fn text(&self) -> &str;
2302 fn allows_styling(&self) -> bool;
2303}
2304
2305impl MessageOrTitle for Title<'_> {
2306 fn level(&self) -> &Level<'_> {
2307 &self.level
2308 }
2309 fn id(&self) -> Option<&Id<'_>> {
2310 self.id.as_ref()
2311 }
2312 fn text(&self) -> &str {
2313 self.text.as_ref()
2314 }
2315 fn allows_styling(&self) -> bool {
2316 self.allows_styling
2317 }
2318}
2319
2320impl MessageOrTitle for Message<'_> {
2321 fn level(&self) -> &Level<'_> {
2322 &self.level
2323 }
2324 fn id(&self) -> Option<&Id<'_>> {
2325 None
2326 }
2327 fn text(&self) -> &str {
2328 self.text.as_ref()
2329 }
2330 fn allows_styling(&self) -> bool {
2331 true
2332 }
2333}
2334
2335fn extra_width_from_tabs(s: &str, n: usize) -> usize {
2338 s.chars().take(n).filter(|&ch| ch == '\t').count() * 3
2339}
2340
2341fn num_decimal_digits(num: Option<usize>) -> usize {
2346 #[cfg(target_pointer_width = "64")]
2347 const MAX_DIGITS: usize = 20;
2348
2349 #[cfg(target_pointer_width = "32")]
2350 const MAX_DIGITS: usize = 10;
2351
2352 #[cfg(target_pointer_width = "16")]
2353 const MAX_DIGITS: usize = 5;
2354
2355 let Some(num) = num else {
2356 return 0;
2357 };
2358
2359 let mut lim = 10;
2360 for num_digits in 1..MAX_DIGITS {
2361 if num < lim {
2362 return num_digits;
2363 }
2364 lim = lim.wrapping_mul(10);
2365 }
2366 MAX_DIGITS
2367}
2368
2369pub(crate) fn str_width(s: &str) -> usize {
2370 s.chars().map(char_width).sum()
2371}
2372
2373pub(crate) fn char_width(ch: char) -> usize {
2374 match ch {
2377 '\t' => 4,
2378 '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}'
2382 | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}'
2383 | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}'
2384 | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}'
2385 | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}'
2386 | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}'
2387 | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1,
2388 _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1),
2389 }
2390}
2391
2392pub(crate) fn num_overlap(
2393 a_start: usize,
2394 a_end: usize,
2395 b_start: usize,
2396 b_end: usize,
2397 inclusive: bool,
2398) -> bool {
2399 let extra = usize::from(inclusive);
2400 (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start)
2401}
2402
2403fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool {
2404 num_overlap(
2405 a1.start.display,
2406 a1.end.display + padding,
2407 a2.start.display,
2408 a2.end.display,
2409 false,
2410 )
2411}
2412
2413#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2414pub(crate) enum LineAnnotationType {
2415 Singleline,
2417
2418 MultilineStart(usize),
2430 MultilineEnd(usize),
2432 MultilineLine(usize),
2437}
2438
2439#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
2440pub(crate) struct LineAnnotation<'a> {
2441 pub start: Loc,
2446
2447 pub end: Loc,
2449
2450 pub kind: AnnotationKind,
2452
2453 pub label: Option<Cow<'a, str>>,
2455
2456 pub annotation_type: LineAnnotationType,
2459
2460 pub highlight_source: bool,
2462}
2463
2464impl LineAnnotation<'_> {
2465 pub(crate) fn is_primary(&self) -> bool {
2466 self.kind == AnnotationKind::Primary
2467 }
2468
2469 pub(crate) fn is_line(&self) -> bool {
2471 matches!(self.annotation_type, LineAnnotationType::MultilineLine(_))
2472 }
2473
2474 pub(crate) fn len(&self) -> usize {
2476 self.end.display.abs_diff(self.start.display)
2478 }
2479
2480 pub(crate) fn has_label(&self) -> bool {
2481 if let Some(label) = &self.label {
2482 !label.is_empty()
2493 } else {
2494 false
2495 }
2496 }
2497
2498 pub(crate) fn takes_space(&self) -> bool {
2499 matches!(
2501 self.annotation_type,
2502 LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_)
2503 )
2504 }
2505}
2506
2507const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[
2510 ('\0', "␀"),
2514 ('\u{0001}', "␁"),
2515 ('\u{0002}', "␂"),
2516 ('\u{0003}', "␃"),
2517 ('\u{0004}', "␄"),
2518 ('\u{0005}', "␅"),
2519 ('\u{0006}', "␆"),
2520 ('\u{0007}', "␇"),
2521 ('\u{0008}', "␈"),
2522 ('\t', " "), ('\u{000b}', "␋"),
2524 ('\u{000c}', "␌"),
2525 ('\u{000d}', "␍"),
2526 ('\u{000e}', "␎"),
2527 ('\u{000f}', "␏"),
2528 ('\u{0010}', "␐"),
2529 ('\u{0011}', "␑"),
2530 ('\u{0012}', "␒"),
2531 ('\u{0013}', "␓"),
2532 ('\u{0014}', "␔"),
2533 ('\u{0015}', "␕"),
2534 ('\u{0016}', "␖"),
2535 ('\u{0017}', "␗"),
2536 ('\u{0018}', "␘"),
2537 ('\u{0019}', "␙"),
2538 ('\u{001a}', "␚"),
2539 ('\u{001b}', "␛"),
2540 ('\u{001c}', "␜"),
2541 ('\u{001d}', "␝"),
2542 ('\u{001e}', "␞"),
2543 ('\u{001f}', "␟"),
2544 ('\u{007f}', "␡"),
2545 ('\u{200d}', ""), ('\u{202a}', "�"), ('\u{202b}', "�"), ('\u{202c}', "�"), ('\u{202d}', "�"),
2550 ('\u{202e}', "�"),
2551 ('\u{2066}', "�"),
2552 ('\u{2067}', "�"),
2553 ('\u{2068}', "�"),
2554 ('\u{2069}', "�"),
2555];
2556
2557pub(crate) fn normalize_whitespace(s: &str) -> Cow<'_, str> {
2558 if !s
2559 .chars()
2560 .any(|user| OUTPUT_REPLACEMENTS.iter().any(|(bad, _)| user == *bad))
2561 {
2562 return Cow::Borrowed(s);
2563 }
2564
2565 let normalized = s.chars().fold(String::with_capacity(s.len()), |mut s, c| {
2569 match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) {
2570 Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1),
2571 _ => s.push(c),
2572 }
2573 s
2574 });
2575 Cow::Owned(normalized)
2576}
2577
2578#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)]
2579pub(crate) enum ElementStyle {
2580 MainHeaderMsg,
2581 HeaderMsg,
2582 LineAndColumn,
2583 LineNumber,
2584 Quotation,
2585 UnderlinePrimary,
2586 UnderlineSecondary,
2587 LabelPrimary,
2588 LabelSecondary,
2589 NoStyle,
2590 Level(LevelInner),
2591 Addition,
2592 Removal,
2593}
2594
2595impl ElementStyle {
2596 pub(crate) fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style {
2597 match self {
2598 Self::Addition => stylesheet.addition,
2599 Self::Removal => stylesheet.removal,
2600 Self::LineAndColumn => stylesheet.none,
2601 Self::LineNumber => stylesheet.line_num,
2602 Self::Quotation => stylesheet.none,
2603 Self::MainHeaderMsg => stylesheet.emphasis,
2604 Self::UnderlinePrimary | Self::LabelPrimary => level.style(stylesheet),
2605 Self::UnderlineSecondary | Self::LabelSecondary => stylesheet.context,
2606 Self::HeaderMsg | Self::NoStyle => stylesheet.none,
2607 Self::Level(lvl) => lvl.style(stylesheet),
2608 }
2609 }
2610}
2611
2612#[derive(Debug, Clone, Copy)]
2613pub(crate) struct UnderlineParts {
2614 pub(crate) style: ElementStyle,
2615 pub(crate) underline: char,
2616 pub(crate) label_start: char,
2617 pub(crate) vertical_text_line: char,
2618 pub(crate) multiline_vertical: char,
2619 pub(crate) multiline_horizontal: char,
2620 pub(crate) multiline_whole_line: char,
2621 pub(crate) multiline_start_down: char,
2622 pub(crate) bottom_right: char,
2623 pub(crate) top_left: char,
2624 pub(crate) top_right_flat: char,
2625 pub(crate) bottom_left: char,
2626 pub(crate) multiline_end_up: char,
2627 pub(crate) multiline_end_same_line: char,
2628 pub(crate) multiline_bottom_right_with_text: char,
2629}
2630
2631#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2632pub(crate) enum TitleStyle {
2633 Primary,
2634 Secondary,
2635 Message,
2636}
2637
2638pub(crate) struct Hyperlink<D: fmt::Display> {
2639 url: Option<D>,
2640}
2641
2642impl<D: fmt::Display> Hyperlink<D> {
2643 pub(crate) fn with_url(url: D) -> Self {
2644 Self { url: Some(url) }
2645 }
2646}
2647
2648impl<D: fmt::Display> Default for Hyperlink<D> {
2649 fn default() -> Self {
2650 Self { url: None }
2651 }
2652}
2653
2654impl<D: fmt::Display> fmt::Display for Hyperlink<D> {
2655 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2656 let Some(url) = self.url.as_ref() else {
2657 return Ok(());
2658 };
2659 if f.alternate() {
2660 write!(f, "\x1B]8;;\x1B\\")
2661 } else {
2662 write!(f, "\x1B]8;;{url}\x1B\\")
2663 }
2664 }
2665}
2666
2667#[cfg(test)]
2668mod test {
2669 use super::OUTPUT_REPLACEMENTS;
2670 use snapbox::IntoData;
2671
2672 fn format_replacements(replacements: Vec<(char, &str)>) -> String {
2673 replacements
2674 .into_iter()
2675 .map(|r| format!(" {r:?}"))
2676 .collect::<Vec<_>>()
2677 .join("\n")
2678 }
2679
2680 #[test]
2681 fn ensure_output_replacements_is_sorted() {
2684 let mut expected = OUTPUT_REPLACEMENTS.to_owned();
2685 expected.sort_by_key(|r| r.0);
2686 expected.dedup_by_key(|r| r.0);
2687 let expected = format_replacements(expected);
2688 let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned());
2689 snapbox::assert_data_eq!(actual, expected.into_data().raw());
2690 }
2691}