1 | //! `display_list` module stores the output model for the snippet. |
2 | //! |
3 | //! `DisplayList` is a central structure in the crate, which contains |
4 | //! the structured list of lines to be displayed. |
5 | //! |
6 | //! It is made of two types of lines: `Source` and `Raw`. All `Source` lines |
7 | //! are structured using four columns: |
8 | //! |
9 | //! ```text |
10 | //! /------------ (1) Line number column. |
11 | //! | /--------- (2) Line number column delimiter. |
12 | //! | | /------- (3) Inline marks column. |
13 | //! | | | /--- (4) Content column with the source and annotations for slices. |
14 | //! | | | | |
15 | //! ============================================================================= |
16 | //! error[E0308]: mismatched types |
17 | //! --> src/format.rs:51:5 |
18 | //! | |
19 | //! 151 | / fn test() -> String { |
20 | //! 152 | | return "test"; |
21 | //! 153 | | } |
22 | //! | |___^ error: expected `String`, for `&str`. |
23 | //! | |
24 | //! ``` |
25 | //! |
26 | //! The first two lines of the example above are `Raw` lines, while the rest |
27 | //! are `Source` lines. |
28 | //! |
29 | //! `DisplayList` does not store column alignment information, and those are |
30 | //! only calculated by the implementation of `std::fmt::Display` using information such as |
31 | //! styling. |
32 | //! |
33 | //! The above snippet has been built out of the following structure: |
34 | use crate::snippet; |
35 | use std::cmp::{max, min, Reverse}; |
36 | use std::collections::HashMap; |
37 | use std::fmt::Display; |
38 | use std::ops::Range; |
39 | use std::{cmp, fmt}; |
40 | |
41 | use crate::renderer::styled_buffer::StyledBuffer; |
42 | use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH}; |
43 | |
44 | const ANONYMIZED_LINE_NUM: &str = "LL" ; |
45 | const ERROR_TXT: &str = "error" ; |
46 | const HELP_TXT: &str = "help" ; |
47 | const INFO_TXT: &str = "info" ; |
48 | const NOTE_TXT: &str = "note" ; |
49 | const WARNING_TXT: &str = "warning" ; |
50 | |
51 | /// List of lines to be displayed. |
52 | pub(crate) struct DisplayList<'a> { |
53 | pub(crate) body: Vec<DisplaySet<'a>>, |
54 | pub(crate) stylesheet: &'a Stylesheet, |
55 | pub(crate) anonymized_line_numbers: bool, |
56 | } |
57 | |
58 | impl PartialEq for DisplayList<'_> { |
59 | fn eq(&self, other: &Self) -> bool { |
60 | self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers |
61 | } |
62 | } |
63 | |
64 | impl fmt::Debug for DisplayList<'_> { |
65 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
66 | f&mut DebugStruct<'_, '_>.debug_struct("DisplayList" ) |
67 | .field("body" , &self.body) |
68 | .field(name:"anonymized_line_numbers" , &self.anonymized_line_numbers) |
69 | .finish() |
70 | } |
71 | } |
72 | |
73 | impl Display for DisplayList<'_> { |
74 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
75 | let lineno_width = self.body.iter().fold(0, |max, set| { |
76 | set.display_lines.iter().fold(max, |max, line| match line { |
77 | DisplayLine::Source { lineno, .. } => cmp::max(lineno.unwrap_or(0), max), |
78 | _ => max, |
79 | }) |
80 | }); |
81 | let lineno_width = if lineno_width == 0 { |
82 | lineno_width |
83 | } else if self.anonymized_line_numbers { |
84 | ANONYMIZED_LINE_NUM.len() |
85 | } else { |
86 | ((lineno_width as f64).log10().floor() as usize) + 1 |
87 | }; |
88 | |
89 | let multiline_depth = self.body.iter().fold(0, |max, set| { |
90 | set.display_lines.iter().fold(max, |max2, line| match line { |
91 | DisplayLine::Source { annotations, .. } => cmp::max( |
92 | annotations.iter().fold(max2, |max3, line| { |
93 | cmp::max( |
94 | match line.annotation_part { |
95 | DisplayAnnotationPart::Standalone => 0, |
96 | DisplayAnnotationPart::LabelContinuation => 0, |
97 | DisplayAnnotationPart::MultilineStart(depth) => depth + 1, |
98 | DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, |
99 | }, |
100 | max3, |
101 | ) |
102 | }), |
103 | max, |
104 | ), |
105 | _ => max2, |
106 | }) |
107 | }); |
108 | let mut buffer = StyledBuffer::new(); |
109 | for set in self.body.iter() { |
110 | self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; |
111 | } |
112 | write!(f, " {}" , buffer.render(self.stylesheet)?) |
113 | } |
114 | } |
115 | |
116 | impl<'a> DisplayList<'a> { |
117 | pub(crate) fn new( |
118 | message: snippet::Message<'a>, |
119 | stylesheet: &'a Stylesheet, |
120 | anonymized_line_numbers: bool, |
121 | term_width: usize, |
122 | ) -> DisplayList<'a> { |
123 | let body = format_message(message, term_width, anonymized_line_numbers, true); |
124 | |
125 | Self { |
126 | body, |
127 | stylesheet, |
128 | anonymized_line_numbers, |
129 | } |
130 | } |
131 | |
132 | fn format_set( |
133 | &self, |
134 | set: &DisplaySet<'_>, |
135 | lineno_width: usize, |
136 | multiline_depth: usize, |
137 | buffer: &mut StyledBuffer, |
138 | ) -> fmt::Result { |
139 | for line in &set.display_lines { |
140 | set.format_line( |
141 | line, |
142 | lineno_width, |
143 | multiline_depth, |
144 | self.stylesheet, |
145 | self.anonymized_line_numbers, |
146 | buffer, |
147 | )?; |
148 | } |
149 | Ok(()) |
150 | } |
151 | } |
152 | |
153 | #[derive (Debug, PartialEq)] |
154 | pub(crate) struct DisplaySet<'a> { |
155 | pub(crate) display_lines: Vec<DisplayLine<'a>>, |
156 | pub(crate) margin: Margin, |
157 | } |
158 | |
159 | impl DisplaySet<'_> { |
160 | fn format_label( |
161 | &self, |
162 | line_offset: usize, |
163 | label: &[DisplayTextFragment<'_>], |
164 | stylesheet: &Stylesheet, |
165 | buffer: &mut StyledBuffer, |
166 | ) -> fmt::Result { |
167 | for fragment in label { |
168 | let style = match fragment.style { |
169 | DisplayTextStyle::Regular => stylesheet.none(), |
170 | DisplayTextStyle::Emphasis => stylesheet.emphasis(), |
171 | }; |
172 | buffer.append(line_offset, fragment.content, *style); |
173 | } |
174 | Ok(()) |
175 | } |
176 | fn format_annotation( |
177 | &self, |
178 | line_offset: usize, |
179 | annotation: &Annotation<'_>, |
180 | continuation: bool, |
181 | stylesheet: &Stylesheet, |
182 | buffer: &mut StyledBuffer, |
183 | ) -> fmt::Result { |
184 | let color = get_annotation_style(&annotation.annotation_type, stylesheet); |
185 | let formatted_len = if let Some(id) = &annotation.id { |
186 | 2 + id.len() + annotation_type_len(&annotation.annotation_type) |
187 | } else { |
188 | annotation_type_len(&annotation.annotation_type) |
189 | }; |
190 | |
191 | if continuation { |
192 | for _ in 0..formatted_len + 2 { |
193 | buffer.append(line_offset, " " , Style::new()); |
194 | } |
195 | return self.format_label(line_offset, &annotation.label, stylesheet, buffer); |
196 | } |
197 | if formatted_len == 0 { |
198 | self.format_label(line_offset, &annotation.label, stylesheet, buffer) |
199 | } else { |
200 | let id = match &annotation.id { |
201 | Some(id) => format!("[ {id}]" ), |
202 | None => String::new(), |
203 | }; |
204 | buffer.append( |
205 | line_offset, |
206 | &format!(" {}{}" , annotation_type_str(&annotation.annotation_type), id), |
207 | *color, |
208 | ); |
209 | |
210 | if !is_annotation_empty(annotation) { |
211 | buffer.append(line_offset, ": " , stylesheet.none); |
212 | self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; |
213 | } |
214 | Ok(()) |
215 | } |
216 | } |
217 | |
218 | #[inline ] |
219 | fn format_raw_line( |
220 | &self, |
221 | line_offset: usize, |
222 | line: &DisplayRawLine<'_>, |
223 | lineno_width: usize, |
224 | stylesheet: &Stylesheet, |
225 | buffer: &mut StyledBuffer, |
226 | ) -> fmt::Result { |
227 | match line { |
228 | DisplayRawLine::Origin { |
229 | path, |
230 | pos, |
231 | header_type, |
232 | } => { |
233 | let header_sigil = match header_type { |
234 | DisplayHeaderType::Initial => "-->" , |
235 | DisplayHeaderType::Continuation => ":::" , |
236 | }; |
237 | let lineno_color = stylesheet.line_no(); |
238 | buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); |
239 | buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); |
240 | if let Some((col, row)) = pos { |
241 | buffer.append(line_offset, ":" , stylesheet.none); |
242 | buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); |
243 | buffer.append(line_offset, ":" , stylesheet.none); |
244 | buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); |
245 | } |
246 | Ok(()) |
247 | } |
248 | DisplayRawLine::Annotation { |
249 | annotation, |
250 | source_aligned, |
251 | continuation, |
252 | } => { |
253 | if *source_aligned { |
254 | if *continuation { |
255 | for _ in 0..lineno_width + 3 { |
256 | buffer.append(line_offset, " " , stylesheet.none); |
257 | } |
258 | } else { |
259 | let lineno_color = stylesheet.line_no(); |
260 | for _ in 0..lineno_width + 1 { |
261 | buffer.append(line_offset, " " , stylesheet.none); |
262 | } |
263 | buffer.append(line_offset, "=" , *lineno_color); |
264 | buffer.append(line_offset, " " , *lineno_color); |
265 | } |
266 | } |
267 | self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) |
268 | } |
269 | } |
270 | } |
271 | |
272 | // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 |
273 | #[inline ] |
274 | fn format_line( |
275 | &self, |
276 | dl: &DisplayLine<'_>, |
277 | lineno_width: usize, |
278 | multiline_depth: usize, |
279 | stylesheet: &Stylesheet, |
280 | anonymized_line_numbers: bool, |
281 | buffer: &mut StyledBuffer, |
282 | ) -> fmt::Result { |
283 | let line_offset = buffer.num_lines(); |
284 | match dl { |
285 | DisplayLine::Source { |
286 | lineno, |
287 | inline_marks, |
288 | line, |
289 | annotations, |
290 | } => { |
291 | let lineno_color = stylesheet.line_no(); |
292 | if anonymized_line_numbers && lineno.is_some() { |
293 | let num = format!(" {ANONYMIZED_LINE_NUM:>lineno_width$} |" ); |
294 | buffer.puts(line_offset, 0, &num, *lineno_color); |
295 | } else { |
296 | match lineno { |
297 | Some(n) => { |
298 | let num = format!(" {n:>lineno_width$} |" ); |
299 | buffer.puts(line_offset, 0, &num, *lineno_color); |
300 | } |
301 | None => { |
302 | buffer.putc(line_offset, lineno_width + 1, '|' , *lineno_color); |
303 | } |
304 | }; |
305 | } |
306 | if let DisplaySourceLine::Content { text, .. } = line { |
307 | // The width of the line number, a space, pipe, and a space |
308 | // `123 | ` is `lineno_width + 3`. |
309 | let width_offset = lineno_width + 3; |
310 | let code_offset = if multiline_depth == 0 { |
311 | width_offset |
312 | } else { |
313 | width_offset + multiline_depth + 1 |
314 | }; |
315 | |
316 | // Add any inline marks to the code line |
317 | if !inline_marks.is_empty() || 0 < multiline_depth { |
318 | format_inline_marks( |
319 | line_offset, |
320 | inline_marks, |
321 | lineno_width, |
322 | stylesheet, |
323 | buffer, |
324 | )?; |
325 | } |
326 | |
327 | let text = normalize_whitespace(text); |
328 | let line_len = text.as_bytes().len(); |
329 | let left = self.margin.left(line_len); |
330 | let right = self.margin.right(line_len); |
331 | |
332 | // On long lines, we strip the source line, accounting for unicode. |
333 | let mut taken = 0; |
334 | let code: String = text |
335 | .chars() |
336 | .skip(left) |
337 | .take_while(|ch| { |
338 | // Make sure that the trimming on the right will fall within the terminal width. |
339 | // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` |
340 | // is. For now, just accept that sometimes the code line will be longer than |
341 | // desired. |
342 | let next = unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); |
343 | if taken + next > right - left { |
344 | return false; |
345 | } |
346 | taken += next; |
347 | true |
348 | }) |
349 | .collect(); |
350 | buffer.puts(line_offset, code_offset, &code, Style::new()); |
351 | if self.margin.was_cut_left() { |
352 | // We have stripped some code/whitespace from the beginning, make it clear. |
353 | buffer.puts(line_offset, code_offset, "..." , *lineno_color); |
354 | } |
355 | if self.margin.was_cut_right(line_len) { |
356 | buffer.puts(line_offset, code_offset + taken - 3, "..." , *lineno_color); |
357 | } |
358 | |
359 | let left: usize = text |
360 | .chars() |
361 | .take(left) |
362 | .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) |
363 | .sum(); |
364 | |
365 | let mut annotations = annotations.clone(); |
366 | annotations.sort_by_key(|a| Reverse(a.range.0)); |
367 | |
368 | let mut annotations_positions = vec![]; |
369 | let mut line_len: usize = 0; |
370 | let mut p = 0; |
371 | for (i, annotation) in annotations.iter().enumerate() { |
372 | for (j, next) in annotations.iter().enumerate() { |
373 | // This label overlaps with another one and both take space ( |
374 | // they have text and are not multiline lines). |
375 | if overlaps(next, annotation, 0) |
376 | && annotation.has_label() |
377 | && j > i |
378 | && p == 0 |
379 | // We're currently on the first line, move the label one line down |
380 | { |
381 | // If we're overlapping with an un-labelled annotation with the same span |
382 | // we can just merge them in the output |
383 | if next.range.0 == annotation.range.0 |
384 | && next.range.1 == annotation.range.1 |
385 | && !next.has_label() |
386 | { |
387 | continue; |
388 | } |
389 | |
390 | // This annotation needs a new line in the output. |
391 | p += 1; |
392 | break; |
393 | } |
394 | } |
395 | annotations_positions.push((p, annotation)); |
396 | for (j, next) in annotations.iter().enumerate() { |
397 | if j > i { |
398 | let l = next |
399 | .annotation |
400 | .label |
401 | .iter() |
402 | .map(|label| label.content) |
403 | .collect::<Vec<_>>() |
404 | .join("" ) |
405 | .len() |
406 | + 2; |
407 | // Do not allow two labels to be in the same line if they |
408 | // overlap including padding, to avoid situations like: |
409 | // |
410 | // fn foo(x: u32) { |
411 | // -------^------ |
412 | // | | |
413 | // fn_spanx_span |
414 | // |
415 | // Both labels must have some text, otherwise they are not |
416 | // overlapping. Do not add a new line if this annotation or |
417 | // the next are vertical line placeholders. If either this |
418 | // or the next annotation is multiline start/end, move it |
419 | // to a new line so as not to overlap the horizontal lines. |
420 | if (overlaps(next, annotation, l) |
421 | && annotation.has_label() |
422 | && next.has_label()) |
423 | || (annotation.takes_space() && next.has_label()) |
424 | || (annotation.has_label() && next.takes_space()) |
425 | || (annotation.takes_space() && next.takes_space()) |
426 | || (overlaps(next, annotation, l) |
427 | && next.range.1 <= annotation.range.1 |
428 | && next.has_label() |
429 | && p == 0) |
430 | // Avoid #42595. |
431 | { |
432 | // This annotation needs a new line in the output. |
433 | p += 1; |
434 | break; |
435 | } |
436 | } |
437 | } |
438 | line_len = max(line_len, p); |
439 | } |
440 | |
441 | if line_len != 0 { |
442 | line_len += 1; |
443 | } |
444 | |
445 | if annotations_positions.iter().all(|(_, ann)| { |
446 | matches!( |
447 | ann.annotation_part, |
448 | DisplayAnnotationPart::MultilineStart(_) |
449 | ) |
450 | }) { |
451 | if let Some(max_pos) = |
452 | annotations_positions.iter().map(|(pos, _)| *pos).max() |
453 | { |
454 | // Special case the following, so that we minimize overlapping multiline spans. |
455 | // |
456 | // 3 │ X0 Y0 Z0 |
457 | // │ ┏━━━━━┛ │ │ < We are writing these lines |
458 | // │ ┃┌───────┘ │ < by reverting the "depth" of |
459 | // │ ┃│┌─────────┘ < their multilne spans. |
460 | // 4 │ ┃││ X1 Y1 Z1 |
461 | // 5 │ ┃││ X2 Y2 Z2 |
462 | // │ ┃│└────╿──│──┘ `Z` label |
463 | // │ ┃└─────│──┤ |
464 | // │ ┗━━━━━━┥ `Y` is a good letter too |
465 | // ╰╴ `X` is a good letter |
466 | for (pos, _) in &mut annotations_positions { |
467 | *pos = max_pos - *pos; |
468 | } |
469 | // We know then that we don't need an additional line for the span label, saving us |
470 | // one line of vertical space. |
471 | line_len = line_len.saturating_sub(1); |
472 | } |
473 | } |
474 | |
475 | // This is a special case where we have a multiline |
476 | // annotation that is at the start of the line disregarding |
477 | // any leading whitespace, and no other multiline |
478 | // annotations overlap it. In this case, we want to draw |
479 | // |
480 | // 2 | fn foo() { |
481 | // | _^ |
482 | // 3 | | |
483 | // 4 | | } |
484 | // | |_^ test |
485 | // |
486 | // we simplify the output to: |
487 | // |
488 | // 2 | / fn foo() { |
489 | // 3 | | |
490 | // 4 | | } |
491 | // | |_^ test |
492 | if multiline_depth == 1 |
493 | && annotations_positions.len() == 1 |
494 | && annotations_positions |
495 | .first() |
496 | .map_or(false, |(_, annotation)| { |
497 | matches!( |
498 | annotation.annotation_part, |
499 | DisplayAnnotationPart::MultilineStart(_) |
500 | ) && text |
501 | .chars() |
502 | .take(annotation.range.0) |
503 | .all(|c| c.is_whitespace()) |
504 | }) |
505 | { |
506 | let (_, ann) = annotations_positions.remove(0); |
507 | let style = get_annotation_style(&ann.annotation_type, stylesheet); |
508 | buffer.putc(line_offset, 3 + lineno_width, '/' , *style); |
509 | } |
510 | |
511 | // Draw the column separator for any extra lines that were |
512 | // created |
513 | // |
514 | // After this we will have: |
515 | // |
516 | // 2 | fn foo() { |
517 | // | |
518 | // | |
519 | // | |
520 | // 3 | |
521 | // 4 | } |
522 | // | |
523 | if !annotations_positions.is_empty() { |
524 | for pos in 0..=line_len { |
525 | buffer.putc( |
526 | line_offset + pos + 1, |
527 | lineno_width + 1, |
528 | '|' , |
529 | stylesheet.line_no, |
530 | ); |
531 | } |
532 | } |
533 | |
534 | // Write the horizontal lines for multiline annotations |
535 | // (only the first and last lines need this). |
536 | // |
537 | // After this we will have: |
538 | // |
539 | // 2 | fn foo() { |
540 | // | __________ |
541 | // | |
542 | // | |
543 | // 3 | |
544 | // 4 | } |
545 | // | _ |
546 | for &(pos, annotation) in &annotations_positions { |
547 | let style = get_annotation_style(&annotation.annotation_type, stylesheet); |
548 | let pos = pos + 1; |
549 | match annotation.annotation_part { |
550 | DisplayAnnotationPart::MultilineStart(depth) |
551 | | DisplayAnnotationPart::MultilineEnd(depth) => { |
552 | for col in width_offset + depth |
553 | ..(code_offset + annotation.range.0).saturating_sub(left) |
554 | { |
555 | buffer.putc(line_offset + pos, col + 1, '_' , *style); |
556 | } |
557 | } |
558 | _ => {} |
559 | } |
560 | } |
561 | |
562 | // Write the vertical lines for labels that are on a different line as the underline. |
563 | // |
564 | // After this we will have: |
565 | // |
566 | // 2 | fn foo() { |
567 | // | __________ |
568 | // | | | |
569 | // | | |
570 | // 3 | | |
571 | // 4 | | } |
572 | // | |_ |
573 | for &(pos, annotation) in &annotations_positions { |
574 | let style = get_annotation_style(&annotation.annotation_type, stylesheet); |
575 | let pos = pos + 1; |
576 | if pos > 1 && (annotation.has_label() || annotation.takes_space()) { |
577 | for p in line_offset + 2..=line_offset + pos { |
578 | buffer.putc( |
579 | p, |
580 | (code_offset + annotation.range.0).saturating_sub(left), |
581 | '|' , |
582 | *style, |
583 | ); |
584 | } |
585 | } |
586 | match annotation.annotation_part { |
587 | DisplayAnnotationPart::MultilineStart(depth) => { |
588 | for p in line_offset + pos + 1..line_offset + line_len + 2 { |
589 | buffer.putc(p, width_offset + depth, '|' , *style); |
590 | } |
591 | } |
592 | DisplayAnnotationPart::MultilineEnd(depth) => { |
593 | for p in line_offset..=line_offset + pos { |
594 | buffer.putc(p, width_offset + depth, '|' , *style); |
595 | } |
596 | } |
597 | _ => {} |
598 | } |
599 | } |
600 | |
601 | // Add in any inline marks for any extra lines that have |
602 | // been created. Output should look like above. |
603 | for inline_mark in inline_marks { |
604 | let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; |
605 | let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); |
606 | if annotations_positions.is_empty() { |
607 | buffer.putc(line_offset, width_offset + depth, '|' , *style); |
608 | } else { |
609 | for p in line_offset..=line_offset + line_len + 1 { |
610 | buffer.putc(p, width_offset + depth, '|' , *style); |
611 | } |
612 | } |
613 | } |
614 | |
615 | // Write the labels on the annotations that actually have a label. |
616 | // |
617 | // After this we will have: |
618 | // |
619 | // 2 | fn foo() { |
620 | // | __________ |
621 | // | | |
622 | // | something about `foo` |
623 | // 3 | |
624 | // 4 | } |
625 | // | _ test |
626 | for &(pos, annotation) in &annotations_positions { |
627 | if !is_annotation_empty(&annotation.annotation) { |
628 | let style = |
629 | get_annotation_style(&annotation.annotation_type, stylesheet); |
630 | let mut formatted_len = if let Some(id) = &annotation.annotation.id { |
631 | 2 + id.len() |
632 | + annotation_type_len(&annotation.annotation.annotation_type) |
633 | } else { |
634 | annotation_type_len(&annotation.annotation.annotation_type) |
635 | }; |
636 | let (pos, col) = if pos == 0 { |
637 | (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) |
638 | } else { |
639 | (pos + 2, annotation.range.0.saturating_sub(left)) |
640 | }; |
641 | if annotation.annotation_part |
642 | == DisplayAnnotationPart::LabelContinuation |
643 | { |
644 | formatted_len = 0; |
645 | } else if formatted_len != 0 { |
646 | formatted_len += 2; |
647 | let id = match &annotation.annotation.id { |
648 | Some(id) => format!("[ {id}]" ), |
649 | None => String::new(), |
650 | }; |
651 | buffer.puts( |
652 | line_offset + pos, |
653 | col + code_offset, |
654 | &format!( |
655 | " {}{}: " , |
656 | annotation_type_str(&annotation.annotation_type), |
657 | id |
658 | ), |
659 | *style, |
660 | ); |
661 | } else { |
662 | formatted_len = 0; |
663 | } |
664 | let mut before = 0; |
665 | for fragment in &annotation.annotation.label { |
666 | let inner_col = before + formatted_len + col + code_offset; |
667 | buffer.puts(line_offset + pos, inner_col, fragment.content, *style); |
668 | before += fragment.content.len(); |
669 | } |
670 | } |
671 | } |
672 | |
673 | // Sort from biggest span to smallest span so that smaller spans are |
674 | // represented in the output: |
675 | // |
676 | // x | fn foo() |
677 | // | ^^^---^^ |
678 | // | | | |
679 | // | | something about `foo` |
680 | // | something about `fn foo()` |
681 | annotations_positions.sort_by_key(|(_, ann)| { |
682 | // Decreasing order. When annotations share the same length, prefer `Primary`. |
683 | Reverse(ann.len()) |
684 | }); |
685 | |
686 | // Write the underlines. |
687 | // |
688 | // After this we will have: |
689 | // |
690 | // 2 | fn foo() { |
691 | // | ____-_____^ |
692 | // | | |
693 | // | something about `foo` |
694 | // 3 | |
695 | // 4 | } |
696 | // | _^ test |
697 | for &(_, annotation) in &annotations_positions { |
698 | let mark = match annotation.annotation_type { |
699 | DisplayAnnotationType::Error => '^' , |
700 | DisplayAnnotationType::Warning => '-' , |
701 | DisplayAnnotationType::Info => '-' , |
702 | DisplayAnnotationType::Note => '-' , |
703 | DisplayAnnotationType::Help => '-' , |
704 | DisplayAnnotationType::None => ' ' , |
705 | }; |
706 | let style = get_annotation_style(&annotation.annotation_type, stylesheet); |
707 | for p in annotation.range.0..annotation.range.1 { |
708 | buffer.putc( |
709 | line_offset + 1, |
710 | (code_offset + p).saturating_sub(left), |
711 | mark, |
712 | *style, |
713 | ); |
714 | } |
715 | } |
716 | } else if !inline_marks.is_empty() { |
717 | format_inline_marks( |
718 | line_offset, |
719 | inline_marks, |
720 | lineno_width, |
721 | stylesheet, |
722 | buffer, |
723 | )?; |
724 | } |
725 | Ok(()) |
726 | } |
727 | DisplayLine::Fold { inline_marks } => { |
728 | buffer.puts(line_offset, 0, "..." , *stylesheet.line_no()); |
729 | if !inline_marks.is_empty() || 0 < multiline_depth { |
730 | format_inline_marks( |
731 | line_offset, |
732 | inline_marks, |
733 | lineno_width, |
734 | stylesheet, |
735 | buffer, |
736 | )?; |
737 | } |
738 | Ok(()) |
739 | } |
740 | DisplayLine::Raw(line) => { |
741 | self.format_raw_line(line_offset, line, lineno_width, stylesheet, buffer) |
742 | } |
743 | } |
744 | } |
745 | } |
746 | |
747 | /// Inline annotation which can be used in either Raw or Source line. |
748 | #[derive (Clone, Debug, PartialEq)] |
749 | pub(crate) struct Annotation<'a> { |
750 | pub(crate) annotation_type: DisplayAnnotationType, |
751 | pub(crate) id: Option<&'a str>, |
752 | pub(crate) label: Vec<DisplayTextFragment<'a>>, |
753 | } |
754 | |
755 | /// A single line used in `DisplayList`. |
756 | #[derive (Debug, PartialEq)] |
757 | pub(crate) enum DisplayLine<'a> { |
758 | /// A line with `lineno` portion of the slice. |
759 | Source { |
760 | lineno: Option<usize>, |
761 | inline_marks: Vec<DisplayMark>, |
762 | line: DisplaySourceLine<'a>, |
763 | annotations: Vec<DisplaySourceAnnotation<'a>>, |
764 | }, |
765 | |
766 | /// A line indicating a folded part of the slice. |
767 | Fold { inline_marks: Vec<DisplayMark> }, |
768 | |
769 | /// A line which is displayed outside of slices. |
770 | Raw(DisplayRawLine<'a>), |
771 | } |
772 | |
773 | /// A source line. |
774 | #[derive (Debug, PartialEq)] |
775 | pub(crate) enum DisplaySourceLine<'a> { |
776 | /// A line with the content of the Snippet. |
777 | Content { |
778 | text: &'a str, |
779 | range: (usize, usize), // meta information for annotation placement. |
780 | end_line: EndLine, |
781 | }, |
782 | /// An empty source line. |
783 | Empty, |
784 | } |
785 | |
786 | #[derive (Clone, Debug, PartialEq)] |
787 | pub(crate) struct DisplaySourceAnnotation<'a> { |
788 | pub(crate) annotation: Annotation<'a>, |
789 | pub(crate) range: (usize, usize), |
790 | pub(crate) annotation_type: DisplayAnnotationType, |
791 | pub(crate) annotation_part: DisplayAnnotationPart, |
792 | } |
793 | |
794 | impl DisplaySourceAnnotation<'_> { |
795 | fn has_label(&self) -> bool { |
796 | !self |
797 | .annotation |
798 | .label |
799 | .iter() |
800 | .all(|label| label.content.is_empty()) |
801 | } |
802 | |
803 | // Length of this annotation as displayed in the stderr output |
804 | fn len(&self) -> usize { |
805 | // Account for usize underflows |
806 | if self.range.1 > self.range.0 { |
807 | self.range.1 - self.range.0 |
808 | } else { |
809 | self.range.0 - self.range.1 |
810 | } |
811 | } |
812 | |
813 | fn takes_space(&self) -> bool { |
814 | // Multiline annotations always have to keep vertical space. |
815 | matches!( |
816 | self.annotation_part, |
817 | DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) |
818 | ) |
819 | } |
820 | } |
821 | |
822 | /// Raw line - a line which does not have the `lineno` part and is not considered |
823 | /// a part of the snippet. |
824 | #[derive (Debug, PartialEq)] |
825 | pub(crate) enum DisplayRawLine<'a> { |
826 | /// A line which provides information about the location of the given |
827 | /// slice in the project structure. |
828 | Origin { |
829 | path: &'a str, |
830 | pos: Option<(usize, usize)>, |
831 | header_type: DisplayHeaderType, |
832 | }, |
833 | |
834 | /// An annotation line which is not part of any snippet. |
835 | Annotation { |
836 | annotation: Annotation<'a>, |
837 | |
838 | /// If set to `true`, the annotation will be aligned to the |
839 | /// lineno delimiter of the snippet. |
840 | source_aligned: bool, |
841 | /// If set to `true`, only the label of the `Annotation` will be |
842 | /// displayed. It allows for a multiline annotation to be aligned |
843 | /// without displaying the meta information (`type` and `id`) to be |
844 | /// displayed on each line. |
845 | continuation: bool, |
846 | }, |
847 | } |
848 | |
849 | /// An inline text fragment which any label is composed of. |
850 | #[derive (Clone, Debug, PartialEq)] |
851 | pub(crate) struct DisplayTextFragment<'a> { |
852 | pub(crate) content: &'a str, |
853 | pub(crate) style: DisplayTextStyle, |
854 | } |
855 | |
856 | /// A style for the `DisplayTextFragment` which can be visually formatted. |
857 | /// |
858 | /// This information may be used to emphasis parts of the label. |
859 | #[derive (Debug, Clone, Copy, PartialEq)] |
860 | pub(crate) enum DisplayTextStyle { |
861 | Regular, |
862 | Emphasis, |
863 | } |
864 | |
865 | /// An indicator of what part of the annotation a given `Annotation` is. |
866 | #[derive (Debug, Clone, PartialEq)] |
867 | pub(crate) enum DisplayAnnotationPart { |
868 | /// A standalone, single-line annotation. |
869 | Standalone, |
870 | /// A continuation of a multi-line label of an annotation. |
871 | LabelContinuation, |
872 | /// A line starting a multiline annotation. |
873 | MultilineStart(usize), |
874 | /// A line ending a multiline annotation. |
875 | MultilineEnd(usize), |
876 | } |
877 | |
878 | /// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. |
879 | #[derive (Debug, Clone, PartialEq)] |
880 | pub(crate) struct DisplayMark { |
881 | pub(crate) mark_type: DisplayMarkType, |
882 | pub(crate) annotation_type: DisplayAnnotationType, |
883 | } |
884 | |
885 | /// A type of the `DisplayMark`. |
886 | #[derive (Debug, Clone, PartialEq)] |
887 | pub(crate) enum DisplayMarkType { |
888 | /// A mark indicating a multiline annotation going through the current line. |
889 | AnnotationThrough(usize), |
890 | } |
891 | |
892 | /// A type of the `Annotation` which may impact the sigils, style or text displayed. |
893 | /// |
894 | /// There are several ways to uses this information when formatting the `DisplayList`: |
895 | /// |
896 | /// * An annotation may display the name of the type like `error` or `info`. |
897 | /// * An underline for `Error` may be `^^^` while for `Warning` it could be `---`. |
898 | /// * `ColorStylesheet` may use different colors for different annotations. |
899 | #[derive (Debug, Clone, PartialEq)] |
900 | pub(crate) enum DisplayAnnotationType { |
901 | None, |
902 | Error, |
903 | Warning, |
904 | Info, |
905 | Note, |
906 | Help, |
907 | } |
908 | |
909 | impl From<snippet::Level> for DisplayAnnotationType { |
910 | fn from(at: snippet::Level) -> Self { |
911 | match at { |
912 | snippet::Level::Error => DisplayAnnotationType::Error, |
913 | snippet::Level::Warning => DisplayAnnotationType::Warning, |
914 | snippet::Level::Info => DisplayAnnotationType::Info, |
915 | snippet::Level::Note => DisplayAnnotationType::Note, |
916 | snippet::Level::Help => DisplayAnnotationType::Help, |
917 | } |
918 | } |
919 | } |
920 | |
921 | /// Information whether the header is the initial one or a consequitive one |
922 | /// for multi-slice cases. |
923 | // TODO: private |
924 | #[derive (Debug, Clone, PartialEq)] |
925 | pub(crate) enum DisplayHeaderType { |
926 | /// Initial header is the first header in the snippet. |
927 | Initial, |
928 | |
929 | /// Continuation marks all headers of following slices in the snippet. |
930 | Continuation, |
931 | } |
932 | |
933 | struct CursorLines<'a>(&'a str); |
934 | |
935 | impl CursorLines<'_> { |
936 | fn new(src: &str) -> CursorLines<'_> { |
937 | CursorLines(src) |
938 | } |
939 | } |
940 | |
941 | #[derive (Copy, Clone, Debug, PartialEq)] |
942 | pub(crate) enum EndLine { |
943 | Eof, |
944 | Lf, |
945 | Crlf, |
946 | } |
947 | |
948 | impl EndLine { |
949 | /// The number of characters this line ending occupies in bytes. |
950 | pub(crate) fn len(self) -> usize { |
951 | match self { |
952 | EndLine::Eof => 0, |
953 | EndLine::Lf => 1, |
954 | EndLine::Crlf => 2, |
955 | } |
956 | } |
957 | } |
958 | |
959 | impl<'a> Iterator for CursorLines<'a> { |
960 | type Item = (&'a str, EndLine); |
961 | |
962 | fn next(&mut self) -> Option<Self::Item> { |
963 | if self.0.is_empty() { |
964 | None |
965 | } else { |
966 | self.0 |
967 | .find(' \n' ) |
968 | .map(|x| { |
969 | let ret = if 0 < x { |
970 | if self.0.as_bytes()[x - 1] == b' \r' { |
971 | (&self.0[..x - 1], EndLine::Crlf) |
972 | } else { |
973 | (&self.0[..x], EndLine::Lf) |
974 | } |
975 | } else { |
976 | ("" , EndLine::Lf) |
977 | }; |
978 | self.0 = &self.0[x + 1..]; |
979 | ret |
980 | }) |
981 | .or_else(|| { |
982 | let ret = Some((self.0, EndLine::Eof)); |
983 | self.0 = "" ; |
984 | ret |
985 | }) |
986 | } |
987 | } |
988 | } |
989 | |
990 | fn format_message( |
991 | message: snippet::Message<'_>, |
992 | term_width: usize, |
993 | anonymized_line_numbers: bool, |
994 | primary: bool, |
995 | ) -> Vec<DisplaySet<'_>> { |
996 | let snippet::Message { |
997 | level, |
998 | id, |
999 | title, |
1000 | footer, |
1001 | snippets, |
1002 | } = message; |
1003 | |
1004 | let mut sets = vec![]; |
1005 | let body = if !snippets.is_empty() || primary { |
1006 | vec![format_title(level, id, title)] |
1007 | } else { |
1008 | format_footer(level, id, title) |
1009 | }; |
1010 | |
1011 | for (idx, snippet) in snippets.into_iter().enumerate() { |
1012 | let snippet = fold_prefix_suffix(snippet); |
1013 | sets.push(format_snippet( |
1014 | snippet, |
1015 | idx == 0, |
1016 | !footer.is_empty(), |
1017 | term_width, |
1018 | anonymized_line_numbers, |
1019 | )); |
1020 | } |
1021 | |
1022 | if let Some(first) = sets.first_mut() { |
1023 | for line in body { |
1024 | first.display_lines.insert(0, line); |
1025 | } |
1026 | } else { |
1027 | sets.push(DisplaySet { |
1028 | display_lines: body, |
1029 | margin: Margin::new(0, 0, 0, 0, DEFAULT_TERM_WIDTH, 0), |
1030 | }); |
1031 | } |
1032 | |
1033 | for annotation in footer { |
1034 | sets.extend(format_message( |
1035 | annotation, |
1036 | term_width, |
1037 | anonymized_line_numbers, |
1038 | false, |
1039 | )); |
1040 | } |
1041 | |
1042 | sets |
1043 | } |
1044 | |
1045 | fn format_title<'a>(level: crate::Level, id: Option<&'a str>, label: &'a str) -> DisplayLine<'a> { |
1046 | DisplayLine::Raw(DisplayRawLine::Annotation { |
1047 | annotation: Annotation { |
1048 | annotation_type: DisplayAnnotationType::from(level), |
1049 | id, |
1050 | label: format_label(label:Some(label), style:Some(DisplayTextStyle::Emphasis)), |
1051 | }, |
1052 | source_aligned: false, |
1053 | continuation: false, |
1054 | }) |
1055 | } |
1056 | |
1057 | fn format_footer<'a>( |
1058 | level: crate::Level, |
1059 | id: Option<&'a str>, |
1060 | label: &'a str, |
1061 | ) -> Vec<DisplayLine<'a>> { |
1062 | let mut result: Vec> = vec![]; |
1063 | for (i: usize, line: &str) in label.lines().enumerate() { |
1064 | result.push(DisplayLine::Raw(DisplayRawLine::Annotation { |
1065 | annotation: Annotation { |
1066 | annotation_type: DisplayAnnotationType::from(level), |
1067 | id, |
1068 | label: format_label(label:Some(line), style:None), |
1069 | }, |
1070 | source_aligned: true, |
1071 | continuation: i != 0, |
1072 | })); |
1073 | } |
1074 | result |
1075 | } |
1076 | |
1077 | fn format_label( |
1078 | label: Option<&str>, |
1079 | style: Option<DisplayTextStyle>, |
1080 | ) -> Vec<DisplayTextFragment<'_>> { |
1081 | let mut result: Vec> = vec![]; |
1082 | if let Some(label: &str) = label { |
1083 | let element_style: DisplayTextStyle = style.unwrap_or(default:DisplayTextStyle::Regular); |
1084 | result.push(DisplayTextFragment { |
1085 | content: label, |
1086 | style: element_style, |
1087 | }); |
1088 | } |
1089 | result |
1090 | } |
1091 | |
1092 | fn format_snippet( |
1093 | snippet: snippet::Snippet<'_>, |
1094 | is_first: bool, |
1095 | has_footer: bool, |
1096 | term_width: usize, |
1097 | anonymized_line_numbers: bool, |
1098 | ) -> DisplaySet<'_> { |
1099 | let main_range: Option = snippet.annotations.first().map(|x: &Annotation<'_>| x.range.start); |
1100 | let origin: Option<&str> = snippet.origin; |
1101 | let need_empty_header: bool = origin.is_some() || is_first; |
1102 | let mut body: DisplaySet<'_> = format_body( |
1103 | snippet, |
1104 | need_empty_header, |
1105 | has_footer, |
1106 | term_width, |
1107 | anonymized_line_numbers, |
1108 | ); |
1109 | let header: Option> = format_header(origin, main_range, &body.display_lines, is_first); |
1110 | |
1111 | if let Some(header: DisplayLine<'_>) = header { |
1112 | body.display_lines.insert(index:0, element:header); |
1113 | } |
1114 | |
1115 | body |
1116 | } |
1117 | |
1118 | #[inline ] |
1119 | // TODO: option_zip |
1120 | fn zip_opt<A, B>(a: Option<A>, b: Option<B>) -> Option<(A, B)> { |
1121 | a.and_then(|a: A| b.map(|b: B| (a, b))) |
1122 | } |
1123 | |
1124 | fn format_header<'a>( |
1125 | origin: Option<&'a str>, |
1126 | main_range: Option<usize>, |
1127 | body: &[DisplayLine<'_>], |
1128 | is_first: bool, |
1129 | ) -> Option<DisplayLine<'a>> { |
1130 | let display_header = if is_first { |
1131 | DisplayHeaderType::Initial |
1132 | } else { |
1133 | DisplayHeaderType::Continuation |
1134 | }; |
1135 | |
1136 | if let Some((main_range, path)) = zip_opt(main_range, origin) { |
1137 | let mut col = 1; |
1138 | let mut line_offset = 1; |
1139 | |
1140 | for item in body { |
1141 | if let DisplayLine::Source { |
1142 | line: |
1143 | DisplaySourceLine::Content { |
1144 | text, |
1145 | range, |
1146 | end_line, |
1147 | }, |
1148 | lineno, |
1149 | .. |
1150 | } = item |
1151 | { |
1152 | if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) { |
1153 | let char_column = text[0..(main_range - range.0).min(text.len())] |
1154 | .chars() |
1155 | .count(); |
1156 | col = char_column + 1; |
1157 | line_offset = lineno.unwrap_or(1); |
1158 | break; |
1159 | } |
1160 | } |
1161 | } |
1162 | |
1163 | return Some(DisplayLine::Raw(DisplayRawLine::Origin { |
1164 | path, |
1165 | pos: Some((line_offset, col)), |
1166 | header_type: display_header, |
1167 | })); |
1168 | } |
1169 | |
1170 | if let Some(path) = origin { |
1171 | return Some(DisplayLine::Raw(DisplayRawLine::Origin { |
1172 | path, |
1173 | pos: None, |
1174 | header_type: display_header, |
1175 | })); |
1176 | } |
1177 | |
1178 | None |
1179 | } |
1180 | |
1181 | fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { |
1182 | if !snippet.fold { |
1183 | return snippet; |
1184 | } |
1185 | |
1186 | let ann_start = snippet |
1187 | .annotations |
1188 | .iter() |
1189 | .map(|ann| ann.range.start) |
1190 | .min() |
1191 | .unwrap_or(0); |
1192 | if let Some(before_new_start) = snippet.source[0..ann_start].rfind(' \n' ) { |
1193 | let new_start = before_new_start + 1; |
1194 | |
1195 | let line_offset = newline_count(&snippet.source[..new_start]); |
1196 | snippet.line_start += line_offset; |
1197 | |
1198 | snippet.source = &snippet.source[new_start..]; |
1199 | |
1200 | for ann in &mut snippet.annotations { |
1201 | let range_start = ann.range.start - new_start; |
1202 | let range_end = ann.range.end - new_start; |
1203 | ann.range = range_start..range_end; |
1204 | } |
1205 | } |
1206 | |
1207 | let ann_end = snippet |
1208 | .annotations |
1209 | .iter() |
1210 | .map(|ann| ann.range.end) |
1211 | .max() |
1212 | .unwrap_or(snippet.source.len()); |
1213 | if let Some(end_offset) = snippet.source[ann_end..].find(' \n' ) { |
1214 | let new_end = ann_end + end_offset; |
1215 | snippet.source = &snippet.source[..new_end]; |
1216 | } |
1217 | |
1218 | snippet |
1219 | } |
1220 | |
1221 | fn newline_count(body: &str) -> usize { |
1222 | #[cfg (feature = "simd" )] |
1223 | { |
1224 | memchr::memchr_iter(b' \n' , body.as_bytes()).count() |
1225 | } |
1226 | #[cfg (not(feature = "simd" ))] |
1227 | { |
1228 | body.lines().count() |
1229 | } |
1230 | } |
1231 | |
1232 | fn fold_body(body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> { |
1233 | const INNER_CONTEXT: usize = 1; |
1234 | const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; |
1235 | |
1236 | let mut lines = vec![]; |
1237 | let mut unhighlighed_lines = vec![]; |
1238 | for line in body { |
1239 | match &line { |
1240 | DisplayLine::Source { annotations, .. } => { |
1241 | if annotations.is_empty() { |
1242 | unhighlighed_lines.push(line); |
1243 | } else { |
1244 | if lines.is_empty() { |
1245 | // Ignore leading unhighlighed lines |
1246 | unhighlighed_lines.clear(); |
1247 | } |
1248 | match unhighlighed_lines.len() { |
1249 | 0 => {} |
1250 | n if n <= INNER_UNFOLD_SIZE => { |
1251 | // Rather than render `...`, don't fold |
1252 | lines.append(&mut unhighlighed_lines); |
1253 | } |
1254 | _ => { |
1255 | lines.extend(unhighlighed_lines.drain(..INNER_CONTEXT)); |
1256 | let inline_marks = lines |
1257 | .last() |
1258 | .and_then(|line| { |
1259 | if let DisplayLine::Source { |
1260 | ref inline_marks, .. |
1261 | } = line |
1262 | { |
1263 | let inline_marks = inline_marks.clone(); |
1264 | Some(inline_marks) |
1265 | } else { |
1266 | None |
1267 | } |
1268 | }) |
1269 | .unwrap_or_default(); |
1270 | lines.push(DisplayLine::Fold { |
1271 | inline_marks: inline_marks.clone(), |
1272 | }); |
1273 | unhighlighed_lines |
1274 | .drain(..unhighlighed_lines.len().saturating_sub(INNER_CONTEXT)); |
1275 | lines.append(&mut unhighlighed_lines); |
1276 | } |
1277 | } |
1278 | lines.push(line); |
1279 | } |
1280 | } |
1281 | _ => { |
1282 | unhighlighed_lines.push(line); |
1283 | } |
1284 | } |
1285 | } |
1286 | |
1287 | lines |
1288 | } |
1289 | |
1290 | fn format_body( |
1291 | snippet: snippet::Snippet<'_>, |
1292 | need_empty_header: bool, |
1293 | has_footer: bool, |
1294 | term_width: usize, |
1295 | anonymized_line_numbers: bool, |
1296 | ) -> DisplaySet<'_> { |
1297 | let source_len = snippet.source.len(); |
1298 | if let Some(bigger) = snippet.annotations.iter().find_map(|x| { |
1299 | // Allow highlighting one past the last character in the source. |
1300 | if source_len + 1 < x.range.end { |
1301 | Some(&x.range) |
1302 | } else { |
1303 | None |
1304 | } |
1305 | }) { |
1306 | panic!("SourceAnnotation range ` {bigger:?}` is beyond the end of buffer ` {source_len}`" ) |
1307 | } |
1308 | |
1309 | let mut body = vec![]; |
1310 | let mut current_line = snippet.line_start; |
1311 | let mut current_index = 0; |
1312 | |
1313 | let mut whitespace_margin = usize::MAX; |
1314 | let mut span_left_margin = usize::MAX; |
1315 | let mut span_right_margin = 0; |
1316 | let mut label_right_margin = 0; |
1317 | let mut max_line_len = 0; |
1318 | |
1319 | let mut depth_map: HashMap<usize, usize> = HashMap::new(); |
1320 | let mut current_depth = 0; |
1321 | let mut annotations = snippet.annotations; |
1322 | let ranges = annotations |
1323 | .iter() |
1324 | .map(|a| a.range.clone()) |
1325 | .collect::<Vec<_>>(); |
1326 | // We want to merge multiline annotations that have the same range into one |
1327 | // multiline annotation to save space. This is done by making any duplicate |
1328 | // multiline annotations into a single-line annotation pointing at the end |
1329 | // of the range. |
1330 | // |
1331 | // 3 | X0 Y0 Z0 |
1332 | // | _____^ |
1333 | // | | ____| |
1334 | // | || ___| |
1335 | // | ||| |
1336 | // 4 | ||| X1 Y1 Z1 |
1337 | // 5 | ||| X2 Y2 Z2 |
1338 | // | ||| ^ |
1339 | // | |||____| |
1340 | // | ||____`X` is a good letter |
1341 | // | |____`Y` is a good letter too |
1342 | // | `Z` label |
1343 | // Should be |
1344 | // error: foo |
1345 | // --> test.rs:3:3 |
1346 | // | |
1347 | // 3 | / X0 Y0 Z0 |
1348 | // 4 | | X1 Y1 Z1 |
1349 | // 5 | | X2 Y2 Z2 |
1350 | // | | ^ |
1351 | // | |____| |
1352 | // | `X` is a good letter |
1353 | // | `Y` is a good letter too |
1354 | // | `Z` label |
1355 | // | |
1356 | ranges.iter().enumerate().for_each(|(r_idx, range)| { |
1357 | annotations |
1358 | .iter_mut() |
1359 | .enumerate() |
1360 | .skip(r_idx + 1) |
1361 | .for_each(|(ann_idx, ann)| { |
1362 | // Skip if the annotation's index matches the range index |
1363 | if ann_idx != r_idx |
1364 | // We only want to merge multiline annotations |
1365 | && snippet.source[ann.range.clone()].lines().count() > 1 |
1366 | // We only want to merge annotations that have the same range |
1367 | && ann.range.start == range.start |
1368 | && ann.range.end == range.end |
1369 | { |
1370 | ann.range.start = ann.range.end.saturating_sub(1); |
1371 | } |
1372 | }); |
1373 | }); |
1374 | annotations.sort_by_key(|a| a.range.start); |
1375 | let mut annotations = annotations.into_iter().enumerate().collect::<Vec<_>>(); |
1376 | |
1377 | for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { |
1378 | let line_length: usize = line.len(); |
1379 | let line_range = (current_index, current_index + line_length); |
1380 | let end_line_size = end_line.len(); |
1381 | body.push(DisplayLine::Source { |
1382 | lineno: Some(current_line), |
1383 | inline_marks: vec![], |
1384 | line: DisplaySourceLine::Content { |
1385 | text: line, |
1386 | range: line_range, |
1387 | end_line, |
1388 | }, |
1389 | annotations: vec![], |
1390 | }); |
1391 | |
1392 | let leading_whitespace = line |
1393 | .chars() |
1394 | .take_while(|c| c.is_whitespace()) |
1395 | .map(|c| { |
1396 | match c { |
1397 | // Tabs are displayed as 4 spaces |
1398 | ' \t' => 4, |
1399 | _ => 1, |
1400 | } |
1401 | }) |
1402 | .sum(); |
1403 | if line.chars().any(|c| !c.is_whitespace()) { |
1404 | whitespace_margin = min(whitespace_margin, leading_whitespace); |
1405 | } |
1406 | max_line_len = max(max_line_len, line_length); |
1407 | |
1408 | let line_start_index = line_range.0; |
1409 | let line_end_index = line_range.1; |
1410 | current_line += 1; |
1411 | current_index += line_length + end_line_size; |
1412 | |
1413 | // It would be nice to use filter_drain here once it's stable. |
1414 | annotations.retain(|(key, annotation)| { |
1415 | let body_idx = idx; |
1416 | let annotation_type = match annotation.level { |
1417 | snippet::Level::Error => DisplayAnnotationType::None, |
1418 | snippet::Level::Warning => DisplayAnnotationType::None, |
1419 | _ => DisplayAnnotationType::from(annotation.level), |
1420 | }; |
1421 | let label_right = annotation.label.map_or(0, |label| label.len() + 1); |
1422 | match annotation.range { |
1423 | // This handles if the annotation is on the next line. We add |
1424 | // the `end_line_size` to account for annotating the line end. |
1425 | Range { start, .. } if start > line_end_index + end_line_size => true, |
1426 | // This handles the case where an annotation is contained |
1427 | // within the current line including any line-end characters. |
1428 | Range { start, end } |
1429 | if start >= line_start_index |
1430 | // We add at least one to `line_end_index` to allow |
1431 | // highlighting the end of a file |
1432 | && end <= line_end_index + max(end_line_size, 1) => |
1433 | { |
1434 | if let DisplayLine::Source { |
1435 | ref mut annotations, |
1436 | .. |
1437 | } = body[body_idx] |
1438 | { |
1439 | let annotation_start_col = line |
1440 | [0..(start - line_start_index).min(line_length)] |
1441 | .chars() |
1442 | .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) |
1443 | .sum::<usize>(); |
1444 | let mut annotation_end_col = line |
1445 | [0..(end - line_start_index).min(line_length)] |
1446 | .chars() |
1447 | .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) |
1448 | .sum::<usize>(); |
1449 | if annotation_start_col == annotation_end_col { |
1450 | // At least highlight something |
1451 | annotation_end_col += 1; |
1452 | } |
1453 | |
1454 | span_left_margin = min(span_left_margin, annotation_start_col); |
1455 | span_right_margin = max(span_right_margin, annotation_end_col); |
1456 | label_right_margin = |
1457 | max(label_right_margin, annotation_end_col + label_right); |
1458 | |
1459 | let range = (annotation_start_col, annotation_end_col); |
1460 | annotations.push(DisplaySourceAnnotation { |
1461 | annotation: Annotation { |
1462 | annotation_type, |
1463 | id: None, |
1464 | label: format_label(annotation.label, None), |
1465 | }, |
1466 | range, |
1467 | annotation_type: DisplayAnnotationType::from(annotation.level), |
1468 | annotation_part: DisplayAnnotationPart::Standalone, |
1469 | }); |
1470 | } |
1471 | false |
1472 | } |
1473 | // This handles the case where a multiline annotation starts |
1474 | // somewhere on the current line, including any line-end chars |
1475 | Range { start, end } |
1476 | if start >= line_start_index |
1477 | // The annotation can start on a line ending |
1478 | && start <= line_end_index + end_line_size.saturating_sub(1) |
1479 | && end > line_end_index => |
1480 | { |
1481 | if let DisplayLine::Source { |
1482 | ref mut annotations, |
1483 | .. |
1484 | } = body[body_idx] |
1485 | { |
1486 | let annotation_start_col = line |
1487 | [0..(start - line_start_index).min(line_length)] |
1488 | .chars() |
1489 | .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) |
1490 | .sum::<usize>(); |
1491 | let annotation_end_col = annotation_start_col + 1; |
1492 | |
1493 | span_left_margin = min(span_left_margin, annotation_start_col); |
1494 | span_right_margin = max(span_right_margin, annotation_end_col); |
1495 | label_right_margin = |
1496 | max(label_right_margin, annotation_end_col + label_right); |
1497 | |
1498 | let range = (annotation_start_col, annotation_end_col); |
1499 | annotations.push(DisplaySourceAnnotation { |
1500 | annotation: Annotation { |
1501 | annotation_type, |
1502 | id: None, |
1503 | label: vec![], |
1504 | }, |
1505 | range, |
1506 | annotation_type: DisplayAnnotationType::from(annotation.level), |
1507 | annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), |
1508 | }); |
1509 | depth_map.insert(*key, current_depth); |
1510 | current_depth += 1; |
1511 | } |
1512 | true |
1513 | } |
1514 | // This handles the case where a multiline annotation starts |
1515 | // somewhere before this line and ends after it as well |
1516 | Range { start, end } |
1517 | if start < line_start_index && end > line_end_index + max(end_line_size, 1) => |
1518 | { |
1519 | if let DisplayLine::Source { |
1520 | ref mut inline_marks, |
1521 | .. |
1522 | } = body[body_idx] |
1523 | { |
1524 | let depth = depth_map.get(key).cloned().unwrap_or_default(); |
1525 | inline_marks.push(DisplayMark { |
1526 | mark_type: DisplayMarkType::AnnotationThrough(depth), |
1527 | annotation_type: DisplayAnnotationType::from(annotation.level), |
1528 | }); |
1529 | } |
1530 | true |
1531 | } |
1532 | // This handles the case where a multiline annotation ends |
1533 | // somewhere on the current line, including any line-end chars |
1534 | Range { start, end } |
1535 | if start < line_start_index |
1536 | && end >= line_start_index |
1537 | // We add at least one to `line_end_index` to allow |
1538 | // highlighting the end of a file |
1539 | && end <= line_end_index + max(end_line_size, 1) => |
1540 | { |
1541 | if let DisplayLine::Source { |
1542 | ref mut annotations, |
1543 | .. |
1544 | } = body[body_idx] |
1545 | { |
1546 | let end_mark = line[0..(end - line_start_index).min(line_length)] |
1547 | .chars() |
1548 | .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) |
1549 | .sum::<usize>() |
1550 | .saturating_sub(1); |
1551 | // If the annotation ends on a line-end character, we |
1552 | // need to annotate one past the end of the line |
1553 | let (end_mark, end_plus_one) = if end > line_end_index |
1554 | // Special case for highlighting the end of a file |
1555 | || (end == line_end_index + 1 && end_line_size == 0) |
1556 | { |
1557 | (end_mark + 1, end_mark + 2) |
1558 | } else { |
1559 | (end_mark, end_mark + 1) |
1560 | }; |
1561 | |
1562 | span_left_margin = min(span_left_margin, end_mark); |
1563 | span_right_margin = max(span_right_margin, end_plus_one); |
1564 | label_right_margin = max(label_right_margin, end_plus_one + label_right); |
1565 | |
1566 | let range = (end_mark, end_plus_one); |
1567 | let depth = depth_map.remove(key).unwrap_or(0); |
1568 | annotations.push(DisplaySourceAnnotation { |
1569 | annotation: Annotation { |
1570 | annotation_type, |
1571 | id: None, |
1572 | label: format_label(annotation.label, None), |
1573 | }, |
1574 | range, |
1575 | annotation_type: DisplayAnnotationType::from(annotation.level), |
1576 | annotation_part: DisplayAnnotationPart::MultilineEnd(depth), |
1577 | }); |
1578 | } |
1579 | false |
1580 | } |
1581 | _ => true, |
1582 | } |
1583 | }); |
1584 | // Reset the depth counter, but only after we've processed all |
1585 | // annotations for a given line. |
1586 | let max = depth_map.len(); |
1587 | if current_depth > max { |
1588 | current_depth = max; |
1589 | } |
1590 | } |
1591 | |
1592 | if snippet.fold { |
1593 | body = fold_body(body); |
1594 | } |
1595 | |
1596 | if need_empty_header { |
1597 | body.insert( |
1598 | 0, |
1599 | DisplayLine::Source { |
1600 | lineno: None, |
1601 | inline_marks: vec![], |
1602 | line: DisplaySourceLine::Empty, |
1603 | annotations: vec![], |
1604 | }, |
1605 | ); |
1606 | } |
1607 | |
1608 | if has_footer { |
1609 | body.push(DisplayLine::Source { |
1610 | lineno: None, |
1611 | inline_marks: vec![], |
1612 | line: DisplaySourceLine::Empty, |
1613 | annotations: vec![], |
1614 | }); |
1615 | } else if let Some(DisplayLine::Source { .. }) = body.last() { |
1616 | body.push(DisplayLine::Source { |
1617 | lineno: None, |
1618 | inline_marks: vec![], |
1619 | line: DisplaySourceLine::Empty, |
1620 | annotations: vec![], |
1621 | }); |
1622 | } |
1623 | let max_line_num_len = if anonymized_line_numbers { |
1624 | ANONYMIZED_LINE_NUM.len() |
1625 | } else { |
1626 | current_line.to_string().len() |
1627 | }; |
1628 | |
1629 | let width_offset = 3 + max_line_num_len; |
1630 | |
1631 | if span_left_margin == usize::MAX { |
1632 | span_left_margin = 0; |
1633 | } |
1634 | |
1635 | let margin = Margin::new( |
1636 | whitespace_margin, |
1637 | span_left_margin, |
1638 | span_right_margin, |
1639 | label_right_margin, |
1640 | term_width.saturating_sub(width_offset), |
1641 | max_line_len, |
1642 | ); |
1643 | |
1644 | DisplaySet { |
1645 | display_lines: body, |
1646 | margin, |
1647 | } |
1648 | } |
1649 | |
1650 | #[inline ] |
1651 | fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { |
1652 | match annotation_type { |
1653 | DisplayAnnotationType::Error => ERROR_TXT, |
1654 | DisplayAnnotationType::Help => HELP_TXT, |
1655 | DisplayAnnotationType::Info => INFO_TXT, |
1656 | DisplayAnnotationType::Note => NOTE_TXT, |
1657 | DisplayAnnotationType::Warning => WARNING_TXT, |
1658 | DisplayAnnotationType::None => "" , |
1659 | } |
1660 | } |
1661 | |
1662 | fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { |
1663 | match annotation_type { |
1664 | DisplayAnnotationType::Error => ERROR_TXT.len(), |
1665 | DisplayAnnotationType::Help => HELP_TXT.len(), |
1666 | DisplayAnnotationType::Info => INFO_TXT.len(), |
1667 | DisplayAnnotationType::Note => NOTE_TXT.len(), |
1668 | DisplayAnnotationType::Warning => WARNING_TXT.len(), |
1669 | DisplayAnnotationType::None => 0, |
1670 | } |
1671 | } |
1672 | |
1673 | fn get_annotation_style<'a>( |
1674 | annotation_type: &DisplayAnnotationType, |
1675 | stylesheet: &'a Stylesheet, |
1676 | ) -> &'a Style { |
1677 | match annotation_type { |
1678 | DisplayAnnotationType::Error => stylesheet.error(), |
1679 | DisplayAnnotationType::Warning => stylesheet.warning(), |
1680 | DisplayAnnotationType::Info => stylesheet.info(), |
1681 | DisplayAnnotationType::Note => stylesheet.note(), |
1682 | DisplayAnnotationType::Help => stylesheet.help(), |
1683 | DisplayAnnotationType::None => stylesheet.none(), |
1684 | } |
1685 | } |
1686 | |
1687 | #[inline ] |
1688 | fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { |
1689 | annotationIter<'_, DisplayTextFragment<'_>> |
1690 | .label |
1691 | .iter() |
1692 | .all(|fragment: &DisplayTextFragment<'_>| fragment.content.is_empty()) |
1693 | } |
1694 | |
1695 | // We replace some characters so the CLI output is always consistent and underlines aligned. |
1696 | const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ |
1697 | (' \t' , " " ), // We do our own tab replacement |
1698 | (' \u{200D}' , "" ), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. |
1699 | (' \u{202A}' , "" ), // The following unicode text flow control characters are inconsistently |
1700 | (' \u{202B}' , "" ), // supported across CLIs and can cause confusion due to the bytes on disk |
1701 | (' \u{202D}' , "" ), // not corresponding to the visible source code, so we replace them always. |
1702 | (' \u{202E}' , "" ), |
1703 | (' \u{2066}' , "" ), |
1704 | (' \u{2067}' , "" ), |
1705 | (' \u{2068}' , "" ), |
1706 | (' \u{202C}' , "" ), |
1707 | (' \u{2069}' , "" ), |
1708 | ]; |
1709 | |
1710 | fn normalize_whitespace(str: &str) -> String { |
1711 | let mut s: String = str.to_owned(); |
1712 | for (c: &char, replacement: &&str) in OUTPUT_REPLACEMENTS { |
1713 | s = s.replace(*c, to:replacement); |
1714 | } |
1715 | s |
1716 | } |
1717 | |
1718 | fn overlaps( |
1719 | a1: &DisplaySourceAnnotation<'_>, |
1720 | a2: &DisplaySourceAnnotation<'_>, |
1721 | padding: usize, |
1722 | ) -> bool { |
1723 | (a2.range.0..a2.range.1).contains(&a1.range.0) |
1724 | || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) |
1725 | } |
1726 | |
1727 | fn format_inline_marks( |
1728 | line: usize, |
1729 | inline_marks: &[DisplayMark], |
1730 | lineno_width: usize, |
1731 | stylesheet: &Stylesheet, |
1732 | buf: &mut StyledBuffer, |
1733 | ) -> fmt::Result { |
1734 | for mark: &DisplayMark in inline_marks.iter() { |
1735 | let annotation_style: &Style = get_annotation_style(&mark.annotation_type, stylesheet); |
1736 | match mark.mark_type { |
1737 | DisplayMarkType::AnnotationThrough(depth: usize) => { |
1738 | buf.putc(line, col:3 + lineno_width + depth, chr:'|' , *annotation_style); |
1739 | } |
1740 | }; |
1741 | } |
1742 | Ok(()) |
1743 | } |
1744 | |