diff --git a/Cargo.lock b/Cargo.lock index a3b2f068..8070b560 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -334,9 +334,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.6.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown", diff --git a/benches/bench.rs b/benches/bench.rs index 9747954b..d50cf435 100644 --- a/benches/bench.rs +++ b/benches/bench.rs @@ -1,4 +1,4 @@ -use annotate_snippets::{Level, Renderer, Snippet}; +use annotate_snippets::{level::Level, AnnotationKind, Group, Renderer, Snippet}; #[divan::bench] fn simple() -> String { @@ -24,24 +24,26 @@ fn simple() -> String { _ => continue, } }"#; - let message = Level::Error.title("mismatched types").id("E0308").snippet( - Snippet::source(source) - .line_start(51) - .origin("src/format.rs") - .annotation( - Level::Warning - .span(5..19) - .label("expected `Option` because of return type"), - ) - .annotation( - Level::Error - .span(26..724) - .label("expected enum `std::option::Option`"), - ), + let message = Level::ERROR.message("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(source) + .line_start(51) + .origin("src/format.rs") + .annotation( + AnnotationKind::Context + .span(5..19) + .label("expected `Option` because of return type"), + ) + .annotation( + AnnotationKind::Primary + .span(26..724) + .label("expected enum `std::option::Option`"), + ), + ), ); let renderer = Renderer::plain(); - let rendered = renderer.render(message).to_string(); + let rendered = renderer.render(message); rendered } @@ -67,19 +69,21 @@ fn fold(bencher: divan::Bencher<'_, '_>, context: usize) { (input, span) }) .bench_values(|(input, span)| { - let message = Level::Error.title("mismatched types").id("E0308").snippet( - Snippet::source(&input) - .fold(true) - .origin("src/format.rs") - .annotation( - Level::Warning - .span(span) - .label("expected `Option` because of return type"), - ), + let message = Level::ERROR.message("mismatched types").id("E0308").group( + Group::new().element( + Snippet::source(&input) + .fold(true) + .origin("src/format.rs") + .annotation( + AnnotationKind::Context + .span(span) + .label("expected `Option` because of return type"), + ), + ), ); let renderer = Renderer::plain(); - let rendered = renderer.render(message).to_string(); + let rendered = renderer.render(message); rendered }); } diff --git a/examples/custom_error.rs b/examples/custom_error.rs new file mode 100644 index 00000000..4050d400 --- /dev/null +++ b/examples/custom_error.rs @@ -0,0 +1,37 @@ +use annotate_snippets::renderer::OutputTheme; +use annotate_snippets::{level::Level, AnnotationKind, Group, Renderer, Snippet}; + +fn main() { + let source = r#"//@ compile-flags: -Ztreat-err-as-bug +//@ failure-status: 101 +//@ error-pattern: aborting due to `-Z treat-err-as-bug=1` +//@ error-pattern: [eval_static_initializer] evaluating initializer of static `C` +//@ normalize-stderr: "note: .*\n\n" -> "" +//@ normalize-stderr: "thread 'rustc' panicked.*:\n.*\n" -> "" +//@ rustc-env:RUST_BACKTRACE=0 + +#![crate_type = "rlib"] + +pub static C: u32 = 0 - 1; +//~^ ERROR could not evaluate static initializer +"#; + let message = Level::ERROR + .text(Some("error: internal compiler error")) + .message("could not evaluate static initializer") + .id("E0080") + .group( + Group::new().element( + Snippet::source(source) + .origin("$DIR/err.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(386..391) + .label("attempt to compute `0_u32 - 1_u32`, which would overflow"), + ), + ), + ); + + let renderer = Renderer::styled().theme(OutputTheme::Unicode); + anstream::println!("{}", renderer.render(message)); +} diff --git a/examples/custom_error.svg b/examples/custom_error.svg new file mode 100644 index 00000000..af3611a9 --- /dev/null +++ b/examples/custom_error.svg @@ -0,0 +1,36 @@ + + + + + + + error: internal compiler error[E0080]: could not evaluate static initializer + + ╭▸ $DIR/err.rs:11:21 + + + + 11 pub static C: u32 = 0 - 1; + + ╰╴ ━━━━━ attempt to compute `0_u32 - 1_u32`, which would overflow + + + + + + diff --git a/examples/custom_level.rs b/examples/custom_level.rs new file mode 100644 index 00000000..804f7741 --- /dev/null +++ b/examples/custom_level.rs @@ -0,0 +1,71 @@ +use annotate_snippets::renderer::OutputTheme; +use annotate_snippets::{level::Level, AnnotationKind, Group, Patch, Renderer, Snippet}; + +fn main() { + let source = r#"// Regression test for issue #114529 +// Tests that we do not ICE during const eval for a +// break-with-value in contexts where it is illegal + +#[allow(while_true)] +fn main() { + [(); { + while true { + break 9; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + [(); { + while let Some(v) = Some(9) { + break v; //~ ERROR `break` with value from a `while` loop + }; + 51 + }]; + + while true { + break (|| { //~ ERROR `break` with value from a `while` loop + let local = 9; + }); + } +} +"#; + let message = Level::ERROR + .message("`break` with value from a `while` loop") + .id("E0571") + .group( + Group::new().element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .annotation( + AnnotationKind::Primary + .span(483..581) + .label("can only break with a value inside `loop` or breakable block"), + ) + .annotation( + AnnotationKind::Context + .span(462..472) + .label("you can't `break` with a value in a `while` loop"), + ), + ), + ) + .group( + Group::new() + .element( + Level::HELP + .text(Some("suggestion")) + .title("use `break` on its own without a value inside this `while` loop"), + ) + .element( + Snippet::source(source) + .line_start(1) + .origin("$DIR/issue-114529-illegal-break-with-value.rs") + .fold(true) + .patch(Patch::new(483..581, "break")), + ), + ); + + let renderer = Renderer::styled().theme(OutputTheme::Unicode); + anstream::println!("{}", renderer.render(message)); +} diff --git a/examples/custom_level.svg b/examples/custom_level.svg new file mode 100644 index 00000000..eebff280 --- /dev/null +++ b/examples/custom_level.svg @@ -0,0 +1,62 @@ + + + + + + + error[E0571]: `break` with value from a `while` loop + + ╭▸ $DIR/issue-114529-illegal-break-with-value.rs:22:9 + + + + 21 while true { + + ────────── you can't `break` with a value in a `while` loop + + 22 break (|| { //~ ERROR `break` with value from a `while` loop + + 23 let local = 9; + + 24 }); + + ┗━━━━━━━━━━┛ can only break with a value inside `loop` or breakable block + + ╰╴ + + suggestion: use `break` on its own without a value inside this `while` loop + + ╭╴ + + 22 - break (|| { //~ ERROR `break` with value from a `while` loop + + 23 - let local = 9; + + 24 - }); + + 22 + break; + + ╰╴ + + + + + + diff --git a/examples/expected_type.rs b/examples/expected_type.rs index 0184deeb..9a51dce5 100644 --- a/examples/expected_type.rs +++ b/examples/expected_type.rs @@ -1,22 +1,27 @@ -use annotate_snippets::{Level, Renderer, Snippet}; +use annotate_snippets::{level::Level, AnnotationKind, Group, Renderer, Snippet}; fn main() { let source = r#" annotations: vec![SourceAnnotation { label: "expected struct `annotate_snippets::snippet::Slice`, found reference" , range: <22, 25>,"#; - let message = Level::Error.title("expected type, found `22`").snippet( - Snippet::source(source) - .line_start(26) - .origin("examples/footer.rs") - .fold(true) - .annotation( - Level::Error - .span(193..195) - .label("expected struct `annotate_snippets::snippet::Slice`, found reference"), - ) - .annotation(Level::Info.span(34..50).label("while parsing this struct")), - ); + let message = + Level::ERROR.message("expected type, found `22`").group( + Group::new().element( + Snippet::source(source) + .line_start(26) + .origin("examples/footer.rs") + .fold(true) + .annotation(AnnotationKind::Primary.span(193..195).label( + "expected struct `annotate_snippets::snippet::Slice`, found reference", + )) + .annotation( + AnnotationKind::Context + .span(34..50) + .label("while parsing this struct"), + ), + ), + ); let renderer = Renderer::styled(); anstream::println!("{}", renderer.render(message)); diff --git a/examples/expected_type.svg b/examples/expected_type.svg index d5de44fc..7c1b073d 100644 --- a/examples/expected_type.svg +++ b/examples/expected_type.svg @@ -1,4 +1,4 @@ - + ) -> Option<(A, B)> { - a.and_then(|a| b.map(|b| (a, b))) -} - -fn format_header<'a>( - origin: Option<&'a str>, - main_range: Option, - body: &[DisplayLine<'_>], - is_first: bool, -) -> Option> { - let display_header = if is_first { - DisplayHeaderType::Initial - } else { - DisplayHeaderType::Continuation - }; - - if let Some((main_range, path)) = zip_opt(main_range, origin) { - let mut col = 1; - let mut line_offset = 1; - - for item in body { - if let DisplayLine::Source { - line: - DisplaySourceLine::Content { - text, - range, - end_line, - }, - lineno, - .. - } = item - { - if main_range >= range.0 && main_range < range.1 + max(*end_line as usize, 1) { - let char_column = text[0..(main_range - range.0).min(text.len())] - .chars() - .count(); - col = char_column + 1; - line_offset = lineno.unwrap_or(1); - break; - } - } - } - - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: Some((line_offset, col)), - header_type: display_header, - })); - } - - if let Some(path) = origin { - return Some(DisplayLine::Raw(DisplayRawLine::Origin { - path, - pos: None, - header_type: display_header, - })); - } - - None -} - -fn fold_prefix_suffix(mut snippet: snippet::Snippet<'_>) -> snippet::Snippet<'_> { - if !snippet.fold { - return snippet; - } - - let ann_start = snippet - .annotations - .iter() - .map(|ann| ann.range.start) - .min() - .unwrap_or(0); - if let Some(before_new_start) = snippet.source[0..ann_start].rfind('\n') { - let new_start = before_new_start + 1; - - let line_offset = newline_count(&snippet.source[..new_start]); - snippet.line_start += line_offset; - - snippet.source = &snippet.source[new_start..]; - - for ann in &mut snippet.annotations { - let range_start = ann.range.start - new_start; - let range_end = ann.range.end - new_start; - ann.range = range_start..range_end; - } - } - - let ann_end = snippet - .annotations - .iter() - .map(|ann| ann.range.end) - .max() - .unwrap_or(snippet.source.len()); - if let Some(end_offset) = snippet.source[ann_end..].find('\n') { - let new_end = ann_end + end_offset; - snippet.source = &snippet.source[..new_end]; - } - - snippet -} - -fn newline_count(body: &str) -> usize { - #[cfg(feature = "simd")] - { - memchr::memchr_iter(b'\n', body.as_bytes()).count() - } - #[cfg(not(feature = "simd"))] - { - body.lines().count() - } -} - -fn fold_body(body: Vec>) -> Vec> { - const INNER_CONTEXT: usize = 1; - const INNER_UNFOLD_SIZE: usize = INNER_CONTEXT * 2 + 1; - - let mut lines = vec![]; - let mut unhighlighted_lines = vec![]; - for line in body { - match &line { - DisplayLine::Source { annotations, .. } => { - if annotations.is_empty() { - unhighlighted_lines.push(line); - } else { - if lines.is_empty() { - // Ignore leading unhighlighted lines - unhighlighted_lines.clear(); - } - match unhighlighted_lines.len() { - 0 => {} - n if n <= INNER_UNFOLD_SIZE => { - // Rather than render `...`, don't fold - lines.append(&mut unhighlighted_lines); - } - _ => { - lines.extend(unhighlighted_lines.drain(..INNER_CONTEXT)); - let inline_marks = lines - .last() - .and_then(|line| { - if let DisplayLine::Source { - ref inline_marks, .. - } = line - { - let inline_marks = inline_marks.clone(); - Some(inline_marks) - } else { - None - } - }) - .unwrap_or_default(); - lines.push(DisplayLine::Fold { - inline_marks: inline_marks.clone(), - }); - unhighlighted_lines - .drain(..unhighlighted_lines.len().saturating_sub(INNER_CONTEXT)); - lines.append(&mut unhighlighted_lines); - } - } - lines.push(line); - } - } - _ => { - unhighlighted_lines.push(line); - } - } - } - - lines -} - -fn format_body( - snippet: snippet::Snippet<'_>, - need_empty_header: bool, - term_width: usize, - anonymized_line_numbers: bool, -) -> DisplaySet<'_> { - let source_len = snippet.source.len(); - if let Some(bigger) = snippet.annotations.iter().find_map(|x| { - // Allow highlighting one past the last character in the source. - if source_len + 1 < x.range.end { - Some(&x.range) - } else { - None - } - }) { - panic!("SourceAnnotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") - } - - let mut body = vec![]; - let mut current_line = snippet.line_start; - let mut current_index = 0; - - let mut whitespace_margin = usize::MAX; - let mut span_left_margin = usize::MAX; - let mut span_right_margin = 0; - let mut label_right_margin = 0; - let mut max_line_len = 0; - - let mut depth_map: HashMap = HashMap::new(); - let mut current_depth = 0; - let mut annotations = snippet.annotations; - let ranges = annotations - .iter() - .map(|a| a.range.clone()) - .collect::>(); - // We want to merge multiline annotations that have the same range into one - // multiline annotation to save space. This is done by making any duplicate - // multiline annotations into a single-line annotation pointing at the end - // of the range. - // - // 3 | X0 Y0 Z0 - // | _____^ - // | | ____| - // | || ___| - // | ||| - // 4 | ||| X1 Y1 Z1 - // 5 | ||| X2 Y2 Z2 - // | ||| ^ - // | |||____| - // | ||____`X` is a good letter - // | |____`Y` is a good letter too - // | `Z` label - // Should be - // error: foo - // --> test.rs:3:3 - // | - // 3 | / X0 Y0 Z0 - // 4 | | X1 Y1 Z1 - // 5 | | X2 Y2 Z2 - // | | ^ - // | |____| - // | `X` is a good letter - // | `Y` is a good letter too - // | `Z` label - // | - ranges.iter().enumerate().for_each(|(r_idx, range)| { - annotations - .iter_mut() - .enumerate() - .skip(r_idx + 1) - .for_each(|(ann_idx, ann)| { - // Skip if the annotation's index matches the range index - if ann_idx != r_idx - // We only want to merge multiline annotations - && snippet.source[ann.range.clone()].lines().count() > 1 - // We only want to merge annotations that have the same range - && ann.range.start == range.start - && ann.range.end == range.end - { - ann.range.start = ann.range.end.saturating_sub(1); - } - }); - }); - annotations.sort_by_key(|a| a.range.start); - let mut annotations = annotations.into_iter().enumerate().collect::>(); - - for (idx, (line, end_line)) in CursorLines::new(snippet.source).enumerate() { - let line_length: usize = line.len(); - let line_range = (current_index, current_index + line_length); - let end_line_size = end_line.len(); - body.push(DisplayLine::Source { - lineno: Some(current_line), - inline_marks: vec![], - line: DisplaySourceLine::Content { - text: line, - range: line_range, - end_line, - }, - annotations: vec![], - }); - - let leading_whitespace = line - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| { - match c { - // Tabs are displayed as 4 spaces - '\t' => 4, - _ => 1, - } - }) - .sum(); - if line.chars().any(|c| !c.is_whitespace()) { - whitespace_margin = min(whitespace_margin, leading_whitespace); - } - max_line_len = max(max_line_len, line_length); - - let line_start_index = line_range.0; - let line_end_index = line_range.1; - current_line += 1; - current_index += line_length + end_line_size; - - // It would be nice to use filter_drain here once it's stable. - annotations.retain(|(key, annotation)| { - let body_idx = idx; - let annotation_type = match annotation.level { - snippet::Level::Error => DisplayAnnotationType::None, - snippet::Level::Warning => DisplayAnnotationType::None, - _ => DisplayAnnotationType::from(annotation.level), - }; - let label_right = annotation.label.map_or(0, |label| label.len() + 1); - match annotation.range { - // This handles if the annotation is on the next line. We add - // the `end_line_size` to account for annotating the line end. - Range { start, .. } if start > line_end_index + end_line_size => true, - // This handles the case where an annotation is contained - // within the current line including any line-end characters. - Range { start, end } - if start >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - let mut annotation_end_col = line - [0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - if annotation_start_col == annotation_end_col { - // At least highlight something - annotation_end_col += 1; - } - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::Standalone, - }); - } - false - } - // This handles the case where a multiline annotation starts - // somewhere on the current line, including any line-end chars - Range { start, end } - if start >= line_start_index - // The annotation can start on a line ending - && start <= line_end_index + end_line_size.saturating_sub(1) - && end > line_end_index => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let annotation_start_col = line - [0..(start - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::(); - let annotation_end_col = annotation_start_col + 1; - - span_left_margin = min(span_left_margin, annotation_start_col); - span_right_margin = max(span_right_margin, annotation_end_col); - label_right_margin = - max(label_right_margin, annotation_end_col + label_right); - - let range = (annotation_start_col, annotation_end_col); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: vec![], - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), - }); - depth_map.insert(*key, current_depth); - current_depth += 1; - } - true - } - // This handles the case where a multiline annotation starts - // somewhere before this line and ends after it as well - Range { start, end } - if start < line_start_index && end > line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut inline_marks, - .. - } = body[body_idx] - { - let depth = depth_map.get(key).cloned().unwrap_or_default(); - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough(depth), - annotation_type: DisplayAnnotationType::from(annotation.level), - }); - } - true - } - // This handles the case where a multiline annotation ends - // somewhere on the current line, including any line-end chars - Range { start, end } - if start < line_start_index - && end >= line_start_index - // We add at least one to `line_end_index` to allow - // highlighting the end of a file - && end <= line_end_index + max(end_line_size, 1) => - { - if let DisplayLine::Source { - ref mut annotations, - .. - } = body[body_idx] - { - let end_mark = line[0..(end - line_start_index).min(line_length)] - .chars() - .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) - .sum::() - .saturating_sub(1); - // If the annotation ends on a line-end character, we - // need to annotate one past the end of the line - let (end_mark, end_plus_one) = if end > line_end_index - // Special case for highlighting the end of a file - || (end == line_end_index + 1 && end_line_size == 0) - { - (end_mark + 1, end_mark + 2) - } else { - (end_mark, end_mark + 1) - }; - - span_left_margin = min(span_left_margin, end_mark); - span_right_margin = max(span_right_margin, end_plus_one); - label_right_margin = max(label_right_margin, end_plus_one + label_right); - - let range = (end_mark, end_plus_one); - let depth = depth_map.remove(key).unwrap_or(0); - annotations.push(DisplaySourceAnnotation { - annotation: Annotation { - annotation_type, - id: None, - label: format_label(annotation.label, None), - }, - range, - annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineEnd(depth), - }); - } - false - } - _ => true, - } - }); - // Reset the depth counter, but only after we've processed all - // annotations for a given line. - let max = depth_map.len(); - if current_depth > max { - current_depth = max; - } - } - - if snippet.fold { - body = fold_body(body); - } - - if need_empty_header { - body.insert( - 0, - DisplayLine::Source { - lineno: None, - inline_marks: vec![], - line: DisplaySourceLine::Empty, - annotations: vec![], - }, - ); - } - - let max_line_num_len = if anonymized_line_numbers { - ANONYMIZED_LINE_NUM.len() - } else { - current_line.to_string().len() - }; - - let width_offset = 3 + max_line_num_len; - - if span_left_margin == usize::MAX { - span_left_margin = 0; - } - - let margin = Margin::new( - whitespace_margin, - span_left_margin, - span_right_margin, - label_right_margin, - term_width.saturating_sub(width_offset), - max_line_len, - ); - - DisplaySet { - display_lines: body, - margin, - } -} - -#[inline] -fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT, - DisplayAnnotationType::Help => HELP_TXT, - DisplayAnnotationType::Info => INFO_TXT, - DisplayAnnotationType::Note => NOTE_TXT, - DisplayAnnotationType::Warning => WARNING_TXT, - DisplayAnnotationType::None => "", - } -} - -fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { - match annotation_type { - DisplayAnnotationType::Error => ERROR_TXT.len(), - DisplayAnnotationType::Help => HELP_TXT.len(), - DisplayAnnotationType::Info => INFO_TXT.len(), - DisplayAnnotationType::Note => NOTE_TXT.len(), - DisplayAnnotationType::Warning => WARNING_TXT.len(), - DisplayAnnotationType::None => 0, - } -} - -fn get_annotation_style<'a>( - annotation_type: &DisplayAnnotationType, - stylesheet: &'a Stylesheet, -) -> &'a Style { - match annotation_type { - DisplayAnnotationType::Error => stylesheet.error(), - DisplayAnnotationType::Warning => stylesheet.warning(), - DisplayAnnotationType::Info => stylesheet.info(), - DisplayAnnotationType::Note => stylesheet.note(), - DisplayAnnotationType::Help => stylesheet.help(), - DisplayAnnotationType::None => stylesheet.none(), - } -} - -#[inline] -fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { - annotation - .label - .iter() - .all(|fragment| fragment.content.is_empty()) -} - -// We replace some characters so the CLI output is always consistent and underlines aligned. -const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ - ('\t', " "), // We do our own tab replacement - ('\u{200D}', ""), // Replace ZWJ with nothing for consistent terminal output of grapheme clusters. - ('\u{202A}', ""), // The following unicode text flow control characters are inconsistently - ('\u{202B}', ""), // supported across CLIs and can cause confusion due to the bytes on disk - ('\u{202D}', ""), // not corresponding to the visible source code, so we replace them always. - ('\u{202E}', ""), - ('\u{2066}', ""), - ('\u{2067}', ""), - ('\u{2068}', ""), - ('\u{202C}', ""), - ('\u{2069}', ""), -]; - -fn normalize_whitespace(str: &str) -> String { - let mut s = str.to_owned(); - for (c, replacement) in OUTPUT_REPLACEMENTS { - s = s.replace(*c, replacement); - } - s -} - -fn overlaps( - a1: &DisplaySourceAnnotation<'_>, - a2: &DisplaySourceAnnotation<'_>, - padding: usize, -) -> bool { - (a2.range.0..a2.range.1).contains(&a1.range.0) - || (a1.range.0..a1.range.1 + padding).contains(&a2.range.0) -} - -fn format_inline_marks( - line: usize, - inline_marks: &[DisplayMark], - lineno_width: usize, - stylesheet: &Stylesheet, - buf: &mut StyledBuffer, -) -> fmt::Result { - for mark in inline_marks.iter() { - let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); - match mark.mark_type { - DisplayMarkType::AnnotationThrough(depth) => { - buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); - } - }; - } - Ok(()) -} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index b9edcc6c..73bbd2f9 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -2,26 +2,58 @@ //! //! # Example //! ``` -//! use annotate_snippets::{Renderer, Snippet, Level}; -//! let snippet = Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); +//! use annotate_snippets::*; +//! use annotate_snippets::level::Level; //! -//! let renderer = Renderer::styled(); -//! println!("{}", renderer.render(snippet)); +//! let source = r#" +//! use baz::zed::bar; +//! +//! mod baz {} +//! mod zed { +//! pub fn bar() { println!("bar3"); } +//! } +//! fn main() { +//! bar(); +//! } +//! "#; +//! Level::ERROR +//! .message("unresolved import `baz::zed`") +//! .id("E0432") +//! .group( +//! Group::new().element( +//! Snippet::source(source) +//! .origin("temp.rs") +//! .line_start(1) +//! .fold(true) +//! .annotation( +//! AnnotationKind::Primary +//! .span(10..13) +//! .label("could not find `zed` in `baz`"), +//! ) +//! ) +//! ); +//! ``` -mod display_list; mod margin; +pub(crate) mod source_map; mod styled_buffer; pub(crate) mod stylesheet; -use crate::snippet::Message; +use crate::level::{Level, LevelInner}; +use crate::renderer::source_map::{ + AnnotatedLineInfo, LineInfo, Loc, SourceMap, SubstitutionHighlight, +}; +use crate::renderer::styled_buffer::StyledBuffer; +use crate::{Annotation, AnnotationKind, Element, Group, Message, Origin, Patch, Snippet, Title}; pub use anstyle::*; -use display_list::DisplayList; use margin::Margin; -use std::fmt::Display; +use std::borrow::Cow; +use std::cmp::{max, min, Ordering, Reverse}; +use std::collections::{HashMap, VecDeque}; +use std::ops::Range; use stylesheet::Stylesheet; +const ANONYMIZED_LINE_NUM: &str = "LL"; pub const DEFAULT_TERM_WIDTH: usize = 140; /// A renderer for [`Message`]s @@ -29,6 +61,7 @@ pub const DEFAULT_TERM_WIDTH: usize = 140; pub struct Renderer { anonymized_line_numbers: bool, term_width: usize, + theme: OutputTheme, stylesheet: Stylesheet, } @@ -38,6 +71,7 @@ impl Renderer { Self { anonymized_line_numbers: false, term_width: DEFAULT_TERM_WIDTH, + theme: OutputTheme::Ascii, stylesheet: Stylesheet::plain(), } } @@ -73,6 +107,9 @@ impl Renderer { } .effects(Effects::BOLD), none: Style::new(), + context: BRIGHT_BLUE.effects(Effects::BOLD), + addition: AnsiColor::BrightGreen.on_default(), + removal: AnsiColor::BrightRed.on_default(), }, ..Self::plain() } @@ -103,6 +140,11 @@ impl Renderer { self } + pub const fn theme(mut self, output_theme: OutputTheme) -> Self { + self.theme = output_theme; + self + } + /// Set the output style for `error` pub const fn error(mut self, style: Style) -> Self { self.stylesheet.error = style; @@ -150,14 +192,2543 @@ impl Renderer { self.stylesheet.none = style; self } +} + +impl Renderer { + pub fn render(&self, mut message: Message<'_>) -> String { + let mut buffer = StyledBuffer::new(); + let max_line_num_len = if self.anonymized_line_numbers { + ANONYMIZED_LINE_NUM.len() + } else { + let n = message.max_line_number(); + num_decimal_digits(n) + }; + let title = message.groups.remove(0).elements.remove(0); + let level = if let Element::Title(title) = &title { + title.level.clone() + } else { + panic!("Expected a title as the first element of the message") + }; + if let Some(first) = message.groups.first_mut() { + first.elements.insert(0, title); + } else { + message.groups.push(Group::new().element(title)); + } + self.render_message(&mut buffer, message, max_line_num_len); + + buffer.render(level, &self.stylesheet).unwrap() + } + + fn render_message( + &self, + buffer: &mut StyledBuffer, + message: Message<'_>, + max_line_num_len: usize, + ) { + let og_primary_origin = message + .groups + .iter() + .find_map(|group| { + group.elements.iter().find_map(|s| match &s { + Element::Cause(cause) => { + if cause.markers.iter().any(|m| m.kind.is_primary()) { + Some(cause.origin) + } else { + None + } + } + Element::Origin(origin) => { + if origin.primary { + Some(Some(origin.origin)) + } else { + None + } + } + _ => None, + }) + }) + .unwrap_or( + message + .groups + .iter() + .find_map(|group| { + group.elements.iter().find_map(|s| match &s { + Element::Cause(cause) => Some(cause.origin), + Element::Origin(origin) => Some(Some(origin.origin)), + _ => None, + }) + }) + .unwrap_or_default(), + ); + let group_len = message.groups.len(); + for (g, group) in message.groups.into_iter().enumerate() { + let primary_origin = group + .elements + .iter() + .find_map(|s| match &s { + Element::Cause(cause) => { + if cause.markers.iter().any(|m| m.kind.is_primary()) { + Some(cause.origin) + } else { + None + } + } + Element::Origin(origin) => { + if origin.primary { + Some(Some(origin.origin)) + } else { + None + } + } + _ => None, + }) + .unwrap_or( + group + .elements + .iter() + .find_map(|s| match &s { + Element::Cause(cause) => Some(cause.origin), + Element::Origin(origin) => Some(Some(origin.origin)), + _ => None, + }) + .unwrap_or_default(), + ); + let mut source_map_annotated_lines = VecDeque::new(); + let mut max_depth = 0; + for e in &group.elements { + if let Element::Cause(cause) = e { + let source_map = SourceMap::new(cause.source, cause.line_start); + let (depth, annotated_lines) = + source_map.annotated_lines(cause.markers.clone(), cause.fold); + max_depth = max(max_depth, depth); + source_map_annotated_lines.push_back((source_map, annotated_lines)); + } + } + let mut message_iter = group.elements.iter().enumerate().peekable(); + let mut last_was_suggestion = false; + while let Some((i, section)) = message_iter.next() { + let peek = message_iter.peek().map(|(_, s)| s).copied(); + match §ion { + Element::Title(title) => { + self.render_title( + buffer, + title, + peek, + max_line_num_len, + if i == 0 { false } else { !title.primary }, + message.id.as_ref().and_then(|id| { + if g == 0 && i == 0 { + Some(id) + } else { + None + } + }), + matches!(peek, Some(Element::Title(_))), + ); + last_was_suggestion = false; + } + Element::Cause(cause) => { + if let Some((source_map, annotated_lines)) = + source_map_annotated_lines.pop_front() + { + self.render_snippet_annotations( + buffer, + max_line_num_len, + cause, + primary_origin, + &source_map, + &annotated_lines, + max_depth, + peek.is_some() || (g == 0 && group_len > 1), + ); + + if g == 0 && group_len > 1 { + if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None)) + { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + // We want to draw the separator when it is + // requested, or when it is the last element + } else if peek.is_none() { + self.draw_col_separator_end( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + } + + last_was_suggestion = false; + } + Element::Suggestion(suggestion) => { + let source_map = SourceMap::new(suggestion.source, suggestion.line_start); + self.emit_suggestion_default( + buffer, + suggestion, + max_line_num_len, + &source_map, + primary_origin.or(og_primary_origin), + last_was_suggestion, + ); + last_was_suggestion = true; + } + + Element::Origin(origin) => { + self.render_origin(buffer, max_line_num_len, origin); + last_was_suggestion = false; + } + Element::ColumnSeparator(_) => { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + if g == 0 + && (matches!(section, Element::Origin(_)) + || (matches!(section, Element::Title(_)) && i == 0) + || matches!(section, Element::Title(level) if level.level.name == Some(None))) + { + if peek.is_none() && group_len > 1 { + self.draw_col_separator_end( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } else if matches!(peek, Some(Element::Title(level)) if level.level.name != Some(None)) + { + self.draw_col_separator_no_space( + buffer, + buffer.num_lines(), + max_line_num_len + 1, + ); + } + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn render_title( + &self, + buffer: &mut StyledBuffer, + title: &Title<'_>, + next_section: Option<&Element<'_>>, + max_line_num_len: usize, + is_secondary: bool, + id: Option<&&str>, + is_cont: bool, + ) { + let line_offset = buffer.num_lines(); + + let (has_primary_spans, has_span_labels) = + next_section.map_or((false, false), |s| match s { + Element::Title(_) | Element::ColumnSeparator(_) => (false, false), + Element::Cause(cause) => ( + cause.markers.iter().any(|m| m.kind.is_primary()), + cause.markers.iter().any(|m| m.label.is_some()), + ), + Element::Suggestion(_) => (true, false), + Element::Origin(_) => (false, true), + }); + + if !has_primary_spans && !has_span_labels && is_secondary { + // This is a secondary message with no span info + for _ in 0..max_line_num_len { + buffer.prepend(line_offset, " ", ElementStyle::NoStyle); + } + + if title.level.name != Some(None) { + self.draw_note_separator(buffer, line_offset, max_line_num_len + 1, is_cont); + buffer.append( + line_offset, + title.level.as_str(), + ElementStyle::MainHeaderMsg, + ); + buffer.append(line_offset, ": ", ElementStyle::NoStyle); + } + + let printed_lines = + self.msgs_to_buffer(buffer, title.title, max_line_num_len, "note", None); + if is_cont && matches!(self.theme, OutputTheme::Unicode) { + // There's another note after this one, associated to the subwindow above. + // We write additional vertical lines to join them: + // ╭▸ test.rs:3:3 + // │ + // 3 │ code + // │ ━━━━ + // │ + // ├ note: foo + // │ bar + // ╰ note: foo + // bar + for i in line_offset + 1..=printed_lines { + self.draw_col_separator_no_space(buffer, i, max_line_num_len + 1); + } + } + } else { + let mut label_width = 0; + + if title.level.name != Some(None) { + buffer.append( + line_offset, + title.level.as_str(), + ElementStyle::Level(title.level.level), + ); + } + label_width += title.level.as_str().len(); + if let Some(id) = id { + buffer.append(line_offset, "[", ElementStyle::Level(title.level.level)); + buffer.append(line_offset, id, ElementStyle::Level(title.level.level)); + buffer.append(line_offset, "]", ElementStyle::Level(title.level.level)); + label_width += 2 + id.len(); + } + let header_style = if is_secondary { + ElementStyle::HeaderMsg + } else { + ElementStyle::MainHeaderMsg + }; + if title.level.name != Some(None) { + buffer.append(line_offset, ": ", header_style); + label_width += 2; + } + if !title.title.is_empty() { + for (line, text) in normalize_whitespace(title.title).lines().enumerate() { + buffer.append( + line_offset + line, + &format!( + "{}{}", + if line == 0 { + String::new() + } else { + " ".repeat(label_width) + }, + text + ), + header_style, + ); + } + } + } + } + + /// Adds a left margin to every line but the first, given a padding length and the label being + /// displayed, keeping the provided highlighting. + fn msgs_to_buffer( + &self, + buffer: &mut StyledBuffer, + title: &str, + padding: usize, + label: &str, + override_style: Option, + ) -> usize { + // The extra 5 ` ` is padding that's always needed to align to the `note: `: + // + // error: message + // --> file.rs:13:20 + // | + // 13 | + // | ^^^^ + // | + // = note: multiline + // message + // ++^^^----xx + // | | | | + // | | | magic `2` + // | | length of label + // | magic `3` + // `max_line_num_len` + let padding = " ".repeat(padding + label.len() + 5); + + let mut line_number = buffer.num_lines().saturating_sub(1); + + // Provided the following diagnostic message: + // + // let msgs = vec![ + // (" + // ("highlighted multiline\nstring to\nsee how it ", Style::NoStyle), + // ("looks", Style::Highlight), + // ("with\nvery ", Style::NoStyle), + // ("weird", Style::Highlight), + // (" formats\n", Style::NoStyle), + // ("see?", Style::Highlight), + // ]; + // + // the expected output on a note is (* surround the highlighted text) + // + // = note: highlighted multiline + // string to + // see how it *looks* with + // very *weird* formats + // see? + let style = if let Some(override_style) = override_style { + override_style + } else { + ElementStyle::NoStyle + }; + let lines = title.split('\n').collect::>(); + if lines.len() > 1 { + for (i, line) in lines.iter().enumerate() { + if i != 0 { + line_number += 1; + buffer.append(line_number, &padding, ElementStyle::NoStyle); + } + buffer.append(line_number, line, style); + } + } else { + buffer.append(line_number, title, style); + } + line_number + } + + fn render_origin( + &self, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + origin: &Origin<'_>, + ) { + let buffer_msg_line_offset = buffer.num_lines(); + if origin.primary { + buffer.prepend( + buffer_msg_line_offset, + self.file_start(), + ElementStyle::LineNumber, + ); + } else { + // if !origin.standalone { + // // Add spacing line, as shown: + // // --> $DIR/file:54:15 + // // | + // // LL | code + // // | ^^^^ + // // | (<- It prints *this* line) + // // ::: $DIR/other_file.rs:15:5 + // // | + // // LL | code + // // | ---- + // self.draw_col_separator_no_space( + // buffer, + // buffer_msg_line_offset, + // max_line_num_len + 1, + // ); + // + // buffer_msg_line_offset += 1; + // } + // Then, the secondary file indicator + buffer.prepend( + buffer_msg_line_offset, + self.secondary_file_start(), + ElementStyle::LineNumber, + ); + } + + let str = match (&origin.line, &origin.char_column) { + (Some(line), Some(col)) => { + format!("{}:{}:{}", origin.origin, line, col) + } + (Some(line), None) => format!("{}:{}", origin.origin, line), + _ => origin.origin.to_owned(), + }; + buffer.append(buffer_msg_line_offset, &str, ElementStyle::LineAndColumn); + for _ in 0..max_line_num_len { + buffer.prepend(buffer_msg_line_offset, " ", ElementStyle::NoStyle); + } + + if let Some(label) = &origin.label { + self.draw_col_separator_no_space( + buffer, + buffer_msg_line_offset + 1, + max_line_num_len + 1, + ); + let title = Level::NOTE.title(label); + self.render_title(buffer, &title, None, max_line_num_len, true, None, false); + } + } + + #[allow(clippy::too_many_arguments)] + fn render_snippet_annotations( + &self, + buffer: &mut StyledBuffer, + max_line_num_len: usize, + snippet: &Snippet<'_, Annotation<'_>>, + primary_origin: Option<&str>, + sm: &SourceMap<'_>, + annotated_lines: &[AnnotatedLineInfo<'_>], + multiline_depth: usize, + is_cont: bool, + ) { + if let Some(origin) = snippet.origin { + let mut origin = Origin::new(origin); + // print out the span location and spacer before we print the annotated source + // to do this, we need to know if this span will be primary + let is_primary = primary_origin == Some(origin.origin); + + if is_primary { + origin.primary = true; + if let Some(primary_line) = annotated_lines + .iter() + .find(|l| l.annotations.iter().any(LineAnnotation::is_primary)) + .or(annotated_lines.iter().find(|l| !l.annotations.is_empty())) + { + origin.line = Some(primary_line.line_index); + if let Some(first_annotation) = primary_line + .annotations + .iter() + .find(|a| a.is_primary()) + .or(primary_line.annotations.first()) + { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } else { + let buffer_msg_line_offset = buffer.num_lines(); + // Add spacing line, as shown: + // --> $DIR/file:54:15 + // | + // LL | code + // | ^^^^ + // | (<- It prints *this* line) + // ::: $DIR/other_file.rs:15:5 + // | + // LL | code + // | ---- + self.draw_col_separator_no_space( + buffer, + buffer_msg_line_offset, + max_line_num_len + 1, + ); + if let Some(first_line) = annotated_lines.first() { + origin.line = Some(first_line.line_index); + if let Some(first_annotation) = first_line.annotations.first() { + origin.char_column = Some(first_annotation.start.char + 1); + } + } + } + self.render_origin(buffer, max_line_num_len, &origin); + } + + // Put in the spacer between the location and annotated source + let buffer_msg_line_offset = buffer.num_lines(); + self.draw_col_separator_no_space(buffer, buffer_msg_line_offset, max_line_num_len + 1); + + // Contains the vertical lines' positions for active multiline annotations + let mut multilines = Vec::new(); + + // Get the left-side margin to remove it + let mut whitespace_margin = usize::MAX; + for line_info in annotated_lines { + // Whitespace can only be removed (aka considered leading) + // if the lexer considers it whitespace. + // non-rustc_lexer::is_whitespace() chars are reported as an + // error (ex. no-break-spaces \u{a0}), and thus can't be considered + // for removal during error reporting. + let leading_whitespace = line_info + .line + .chars() + .take_while(|c| c.is_whitespace()) + .map(|c| { + match c { + // Tabs are displayed as 4 spaces + '\t' => 4, + _ => 1, + } + }) + .sum(); + if line_info.line.chars().any(|c| !c.is_whitespace()) { + whitespace_margin = min(whitespace_margin, leading_whitespace); + } + } + if whitespace_margin == usize::MAX { + whitespace_margin = 0; + } + + // Left-most column any visible span points at. + let mut span_left_margin = usize::MAX; + for line_info in annotated_lines { + for ann in &line_info.annotations { + span_left_margin = min(span_left_margin, ann.start.display); + span_left_margin = min(span_left_margin, ann.end.display); + } + } + if span_left_margin == usize::MAX { + span_left_margin = 0; + } + + // Right-most column any visible span points at. + let mut span_right_margin = 0; + let mut label_right_margin = 0; + let mut max_line_len = 0; + for line_info in annotated_lines { + max_line_len = max(max_line_len, line_info.line.len()); + for ann in &line_info.annotations { + span_right_margin = max(span_right_margin, ann.start.display); + span_right_margin = max(span_right_margin, ann.end.display); + // FIXME: account for labels not in the same line + let label_right = ann.label.as_ref().map_or(0, |l| l.len() + 1); + label_right_margin = max(label_right_margin, ann.end.display + label_right); + } + } + let width_offset = 3 + max_line_num_len; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + let column_width = self.term_width.saturating_sub(code_offset); + + let margin = Margin::new( + whitespace_margin, + span_left_margin, + span_right_margin, + label_right_margin, + column_width, + max_line_len, + ); + + // Next, output the annotate source for this file + for annotated_line_idx in 0..annotated_lines.len() { + let previous_buffer_line = buffer.num_lines(); + + let depths = self.render_source_line( + &annotated_lines[annotated_line_idx], + buffer, + width_offset, + code_offset, + max_line_num_len, + margin, + !is_cont && annotated_line_idx + 1 == annotated_lines.len(), + ); + + let mut to_add = HashMap::new(); + + for (depth, style) in depths { + if let Some(index) = multilines.iter().position(|(d, _)| d == &depth) { + multilines.swap_remove(index); + } else { + to_add.insert(depth, style); + } + } + + // Set the multiline annotation vertical lines to the left of + // the code in this line. + for (depth, style) in &multilines { + for line in previous_buffer_line..buffer.num_lines() { + self.draw_multiline_line(buffer, line, width_offset, *depth, *style); + } + } + // check to see if we need to print out or elide lines that come between + // this annotated line and the next one. + if annotated_line_idx < (annotated_lines.len() - 1) { + let line_idx_delta = annotated_lines[annotated_line_idx + 1].line_index + - annotated_lines[annotated_line_idx].line_index; + match line_idx_delta.cmp(&2) { + Ordering::Greater => { + let last_buffer_line_num = buffer.num_lines(); + + self.draw_line_separator(buffer, last_buffer_line_num, width_offset); + + // Set the multiline annotation vertical lines on `...` bridging line. + for (depth, style) in &multilines { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type + { + // In the case where we have elided the entire start of the + // multispan because those lines were empty, we still need + // to draw the `|`s across the `...`. + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + ); + } + } + } + } + + Ordering::Equal => { + let unannotated_line = sm + .get_line(annotated_lines[annotated_line_idx].line_index + 1) + .unwrap_or(""); + + let last_buffer_line_num = buffer.num_lines(); + + self.draw_line( + buffer, + &normalize_whitespace(unannotated_line), + annotated_lines[annotated_line_idx + 1].line_index - 1, + last_buffer_line_num, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + for (depth, style) in &multilines { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + *depth, + *style, + ); + } + if let Some(line) = annotated_lines.get(annotated_line_idx) { + for ann in &line.annotations { + if let LineAnnotationType::MultilineStart(pos) = ann.annotation_type + { + self.draw_multiline_line( + buffer, + last_buffer_line_num, + width_offset, + pos, + if ann.is_primary() { + ElementStyle::UnderlinePrimary + } else { + ElementStyle::UnderlineSecondary + }, + ); + } + } + } + } + Ordering::Less => {} + } + } + + multilines.extend(to_add); + } + } + + #[allow(clippy::too_many_arguments)] + fn render_source_line( + &self, + line_info: &AnnotatedLineInfo<'_>, + buffer: &mut StyledBuffer, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + close_window: bool, + ) -> Vec<(usize, ElementStyle)> { + // Draw: + // + // LL | ... code ... + // | ^^-^ span label + // | | + // | secondary span label + // + // ^^ ^ ^^^ ^^^^ ^^^ we don't care about code too far to the right of a span, we trim it + // | | | | + // | | | actual code found in your source code and the spans we use to mark it + // | | when there's too much wasted space to the left, trim it + // | vertical divider between the column number and the code + // column number + + if line_info.line_index == 0 { + return Vec::new(); + } + + let source_string = normalize_whitespace(line_info.line); + + let line_offset = buffer.num_lines(); + + // Left trim + let left = margin.left(str_width(&source_string)); + + // FIXME: This looks fishy. See #132860. + // Account for unicode characters of width !=0 that were removed. + let mut taken = 0; + source_string.chars().for_each(|ch| { + let next = char_width(ch); + if taken + next <= left { + taken += next; + } + }); + + let left = taken; + self.draw_line( + buffer, + &source_string, + line_info.line_index, + line_offset, + width_offset, + code_offset, + max_line_num_len, + margin, + ); + + // Special case when there's only one annotation involved, it is the start of a multiline + // span and there's no text at the beginning of the code line. Instead of doing the whole + // graph: + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + let mut buffer_ops = vec![]; + let mut annotations = vec![]; + let mut short_start = true; + for ann in &line_info.annotations { + if let LineAnnotationType::MultilineStart(depth) = ann.annotation_type { + if source_string + .chars() + .take(ann.start.display) + .all(char::is_whitespace) + { + let uline = self.underline(ann.is_primary()); + let chr = uline.multiline_whole_line; + annotations.push((depth, uline.style)); + buffer_ops.push((line_offset, width_offset + depth - 1, chr, uline.style)); + } else { + short_start = false; + break; + } + } else if let LineAnnotationType::MultilineLine(_) = ann.annotation_type { + } else { + short_start = false; + break; + } + } + if short_start { + for (y, x, c, s) in buffer_ops { + buffer.putc(y, x, c, s); + } + return annotations; + } + + // We want to display like this: + // + // vec.push(vec.pop().unwrap()); + // --- ^^^ - previous borrow ends here + // | | + // | error occurs here + // previous borrow of `vec` occurs here + // + // But there are some weird edge cases to be aware of: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here + // || + // |this makes no sense + // previous borrow of `vec` occurs here + // + // For this reason, we group the lines into "highlight lines" + // and "annotations lines", where the highlight lines have the `^`. + + // Sort the annotations by (start, end col) + // The labels are reversed, sort and then reversed again. + // Consider a list of annotations (A1, A2, C1, C2, B1, B2) where + // the letter signifies the span. Here we are only sorting by the + // span and hence, the order of the elements with the same span will + // not change. On reversing the ordering (|a, b| but b.cmp(a)), you get + // (C1, C2, B1, B2, A1, A2). All the elements with the same span are + // still ordered first to last, but all the elements with different + // spans are ordered by their spans in last to first order. Last to + // first order is important, because the jiggly lines and | are on + // the left, so the rightmost span needs to be rendered first, + // otherwise the lines would end up needing to go over a message. + + let mut annotations = line_info.annotations.clone(); + annotations.sort_by_key(|a| Reverse(a.start.display)); + + // First, figure out where each label will be positioned. + // + // In the case where you have the following annotations: + // + // vec.push(vec.pop().unwrap()); + // -------- - previous borrow ends here [C] + // || + // |this makes no sense [B] + // previous borrow of `vec` occurs here [A] + // + // `annotations_position` will hold [(2, A), (1, B), (0, C)]. + // + // We try, when possible, to stick the rightmost annotation at the end + // of the highlight line: + // + // vec.push(vec.pop().unwrap()); + // --- --- - previous borrow ends here + // + // But sometimes that's not possible because one of the other + // annotations overlaps it. For example, from the test + // `span_overlap_label`, we have the following annotations + // (written on distinct lines for clarity): + // + // fn foo(x: u32) { + // -------------- + // - + // + // In this case, we can't stick the rightmost-most label on + // the highlight line, or we would get: + // + // fn foo(x: u32) { + // -------- x_span + // | + // fn_span + // + // which is totally weird. Instead we want: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // fn_span + // + // which is...less weird, at least. In fact, in general, if + // the rightmost span overlaps with any other span, we should + // use the "hang below" version, so we can at least make it + // clear where the span *starts*. There's an exception for this + // logic, when the labels do not have a message: + // + // fn foo(x: u32) { + // -------------- + // | + // x_span + // + // instead of: + // + // fn foo(x: u32) { + // -------------- + // | | + // | x_span + // + // + let mut annotations_position = vec![]; + let mut line_len: usize = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + if overlaps(next, annotation, 0) // This label overlaps with another one and both + && annotation.has_label() // take space (they have text and are not + && j > i // multiline lines). + && p == 0 + // We're currently on the first line, move the label one line down + { + // If we're overlapping with an un-labelled annotation with the same span + // we can just merge them in the output + if next.start.display == annotation.start.display + && next.end.display == annotation.end.display + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_position.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next.label.as_ref().map_or(0, |label| label.len() + 2); + if (overlaps(next, annotation, l) // Do not allow two labels to be in the same + // line if they overlap including padding, to + // avoid situations like: + // + // fn foo(x: u32) { + // -------^------ + // | | + // fn_spanx_span + // + && annotation.has_label() // Both labels must have some text, otherwise + && next.has_label()) // they are not overlapping. + // Do not add a new line if this annotation + // or the next are vertical line placeholders. + || (annotation.takes_space() // If either this or the next annotation is + && next.has_label()) // multiline start/end, move it to a new line + || (annotation.has_label() // so as not to overlap the horizontal lines. + && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && next.end.display <= annotation.end.display + && next.has_label() + && p == 0) + // Avoid #42595. + { + // This annotation needs a new line in the output. + p += 1; + break; + } + } + } + line_len = max(line_len, p); + } + + if line_len != 0 { + line_len += 1; + } + + // If there are no annotations or the only annotations on this line are + // MultilineLine, then there's only code being shown, stop processing. + if line_info.annotations.iter().all(LineAnnotation::is_line) { + return vec![]; + } + + if annotations_position + .iter() + .all(|(_, ann)| matches!(ann.annotation_type, LineAnnotationType::MultilineStart(_))) + { + if let Some(max_pos) = annotations_position.iter().map(|(pos, _)| *pos).max() { + // Special case the following, so that we minimize overlapping multiline spans. + // + // 3 │ X0 Y0 Z0 + // │ ┏━━━━━┛ │ │ < We are writing these lines + // │ ┃┌───────┘ │ < by reverting the "depth" of + // │ ┃│┌─────────┘ < their multiline spans. + // 4 │ ┃││ X1 Y1 Z1 + // 5 │ ┃││ X2 Y2 Z2 + // │ ┃│└────╿──│──┘ `Z` label + // │ ┃└─────│──┤ + // │ ┗━━━━━━┥ `Y` is a good letter too + // ╰╴ `X` is a good letter + for (pos, _) in &mut annotations_position { + *pos = max_pos - *pos; + } + // We know then that we don't need an additional line for the span label, saving us + // one line of vertical space. + line_len = line_len.saturating_sub(1); + } + } + + // Write the column separator. + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + for pos in 0..=line_len { + self.draw_col_separator_no_space(buffer, line_offset + pos + 1, width_offset - 2); + } + if close_window { + self.draw_col_separator_end(buffer, line_offset + line_len + 1, width_offset - 2); + } + // Write the horizontal lines for multiline annotations + // (only the first and last lines need this). + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | + // | + // 3 | + // 4 | } + // | _ + for &(pos, annotation) in &annotations_position { + let underline = self.underline(annotation.is_primary()); + let pos = pos + 1; + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) + | LineAnnotationType::MultilineEnd(depth) => { + self.draw_range( + buffer, + underline.multiline_horizontal, + line_offset + pos, + width_offset + depth, + (code_offset + annotation.start.display).saturating_sub(left), + underline.style, + ); + } + _ if annotation.highlight_source => { + buffer.set_style_range( + line_offset, + (code_offset + annotation.start.display).saturating_sub(left), + (code_offset + annotation.end.display).saturating_sub(left), + underline.style, + annotation.is_primary(), + ); + } + _ => {} + } + } + + // Write the vertical lines for labels that are on a different line as the underline. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | | + // | | + // 3 | | + // 4 | | } + // | |_ + for &(pos, annotation) in &annotations_position { + let underline = self.underline(annotation.is_primary()); + let pos = pos + 1; + + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 1..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineLine(_) => underline.multiline_vertical, + _ => underline.vertical_text_line, + }, + underline.style, + ); + } + if let LineAnnotationType::MultilineStart(_) = annotation.annotation_type { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.bottom_right, + underline.style, + ); + } + if matches!( + annotation.annotation_type, + LineAnnotationType::MultilineEnd(_) + ) && annotation.has_label() + { + buffer.putc( + line_offset + pos, + (code_offset + annotation.start.display).saturating_sub(left), + underline.multiline_bottom_right_with_text, + underline.style, + ); + } + } + match annotation.annotation_type { + LineAnnotationType::MultilineStart(depth) => { + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.top_left, + underline.style, + ); + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + } + LineAnnotationType::MultilineEnd(depth) => { + for p in line_offset..line_offset + pos { + buffer.putc( + p, + width_offset + depth - 1, + underline.multiline_vertical, + underline.style, + ); + } + buffer.putc( + line_offset + pos, + width_offset + depth - 1, + underline.bottom_left, + underline.style, + ); + } + _ => (), + } + } + + // Write the labels on the annotations that actually have a label. + // + // After this we will have: + // + // 2 | fn foo() { + // | __________ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _ test + for &(pos, annotation) in &annotations_position { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + let (pos, col) = if pos == 0 { + if annotation.end.display == 0 { + (pos + 1, (annotation.end.display + 2).saturating_sub(left)) + } else { + (pos + 1, (annotation.end.display + 1).saturating_sub(left)) + } + } else { + (pos + 2, annotation.start.display.saturating_sub(left)) + }; + if let Some(label) = annotation.label { + buffer.puts(line_offset + pos, code_offset + col, label, style); + } + } + + // Sort from biggest span to smallest span so that smaller spans are + // represented in the output: + // + // x | fn foo() + // | ^^^---^^ + // | | | + // | | something about `foo` + // | something about `fn foo()` + annotations_position.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + (Reverse(ann.len()), ann.is_primary()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(pos, annotation) in &annotations_position { + let uline = self.underline(annotation.is_primary()); + for p in annotation.start.display..annotation.end.display { + // The default span label underline. + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + uline.underline, + uline.style, + ); + } + + if pos == 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on the same line. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.top_right_flat, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_same_line, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 + && matches!( + annotation.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) + ) + { + // The beginning of a multiline span with its leftward moving line on another line, + // so we start going down first. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + match annotation.annotation_type { + LineAnnotationType::MultilineStart(_) => uline.multiline_start_down, + LineAnnotationType::MultilineEnd(_) => uline.multiline_end_up, + _ => panic!("unexpected annotation type: {annotation:?}"), + }, + uline.style, + ); + } else if pos != 0 && annotation.has_label() { + // The beginning of a span label with an actual label, we'll point down. + buffer.putc( + line_offset + 1, + (code_offset + annotation.start.display).saturating_sub(left), + uline.label_start, + uline.style, + ); + } + } + annotations_position + .iter() + .filter_map(|&(_, annotation)| match annotation.annotation_type { + LineAnnotationType::MultilineStart(p) | LineAnnotationType::MultilineEnd(p) => { + let style = if annotation.is_primary() { + ElementStyle::LabelPrimary + } else { + ElementStyle::LabelSecondary + }; + Some((p, style)) + } + _ => None, + }) + .collect::>() + } + + fn emit_suggestion_default( + &self, + buffer: &mut StyledBuffer, + suggestion: &Snippet<'_, Patch<'_>>, + max_line_num_len: usize, + sm: &SourceMap<'_>, + primary_origin: Option<&str>, + is_cont: bool, + ) { + let suggestions = sm.splice_lines(suggestion.markers.clone()); + + let buffer_offset = buffer.num_lines(); + let mut row_num = buffer_offset + usize::from(!is_cont); + for (i, (complete, parts, highlights)) in suggestions.iter().enumerate() { + let has_deletion = parts + .iter() + .any(|p| p.is_deletion(sm) || p.is_destructive_replacement(sm)); + let is_multiline = complete.lines().count() > 1; + + if i == 0 { + self.draw_col_separator_start(buffer, row_num - 1, max_line_num_len + 1); + } else { + buffer.puts( + row_num - 1, + max_line_num_len + 1, + self.multi_suggestion_separator(), + ElementStyle::LineNumber, + ); + } + if suggestion.origin != primary_origin { + if let Some(origin) = suggestion.origin { + let (loc, _) = sm.span_to_locations(parts[0].range.clone()); + // --> file.rs:line:col + // | + let arrow = self.file_start(); + buffer.puts(row_num - 1, 0, arrow, ElementStyle::LineNumber); + let message = format!("{}:{}:{}", origin, loc.line, loc.char + 1); + if is_cont { + buffer.append(row_num - 1, &message, ElementStyle::LineAndColumn); + } else { + let col = usize::max(max_line_num_len + 1, arrow.len()); + buffer.puts(row_num - 1, col, &message, ElementStyle::LineAndColumn); + } + for _ in 0..max_line_num_len { + buffer.prepend(row_num - 1, " ", ElementStyle::NoStyle); + } + self.draw_col_separator_no_space(buffer, row_num, max_line_num_len + 1); + row_num += 1; + } + } + let show_code_change = if has_deletion && !is_multiline { + DisplaySuggestion::Diff + } else if parts.len() == 1 + && parts.first().map_or(false, |p| { + p.replacement.ends_with('\n') && p.replacement.trim() == complete.trim() + }) + { + // We are adding a line(s) of code before code that was already there. + DisplaySuggestion::Add + } else if (parts.len() != 1 || parts[0].replacement.trim() != complete.trim()) + && !is_multiline + { + DisplaySuggestion::Underline + } else { + DisplaySuggestion::None + }; + + if let DisplaySuggestion::Diff = show_code_change { + row_num += 1; + } + + let file_lines = sm.span_to_lines(parts[0].range.clone()); + let (line_start, line_end) = sm.span_to_locations(parts[0].range.clone()); + let mut lines = complete.lines(); + if lines.clone().next().is_none() { + // Account for a suggestion to completely remove a line(s) with whitespace (#94192). + for line in line_start.line..=line_end.line { + buffer.puts( + row_num - 1 + line - line_start.line, + 0, + &self.maybe_anonymized(line), + ElementStyle::LineNumber, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + row_num - 1 + line - line_start.line, + max_line_num_len + 3, + &normalize_whitespace(sm.get_line(line).unwrap()), + ElementStyle::Removal, + ); + } + row_num += line_end.line - line_start.line; + } + let mut last_pos = 0; + let mut is_item_attribute = false; + let mut unhighlighted_lines = Vec::new(); + for (line_pos, (line, highlight_parts)) in lines.by_ref().zip(highlights).enumerate() { + last_pos = line_pos; + + // Remember lines that are not highlighted to hide them if needed + if highlight_parts.is_empty() { + unhighlighted_lines.push((line_pos, line)); + continue; + } + if highlight_parts.len() == 1 + && line.trim().starts_with("#[") + && line.trim().ends_with(']') + { + is_item_attribute = true; + } + + match unhighlighted_lines.len() { + 0 => (), + // Since we show first line, "..." line and last line, + // There is no reason to hide if there are 3 or less lines + // (because then we just replace a line with ... which is + // not helpful) + n if n <= 3 => unhighlighted_lines.drain(..).for_each(|(p, l)| { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + }), + // Print first unhighlighted line, "..." and last unhighlighted line, like so: + // + // LL | this line was highlighted + // LL | this line is just for context + // ... + // LL | this line is just for context + // LL | this line was highlighted + _ => { + let last_line = unhighlighted_lines.pop(); + let first_line = unhighlighted_lines.drain(..).next(); + + if let Some((p, l)) = first_line { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + ElementStyle::LineNumber, + ); + row_num += 1; + + if let Some((p, l)) = last_line { + self.draw_code_line( + buffer, + &mut row_num, + &[], + p + line_start.line, + l, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + } + self.draw_code_line( + buffer, + &mut row_num, + highlight_parts, + line_pos + line_start.line, + line, + show_code_change, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + + if matches!(show_code_change, DisplaySuggestion::Add) && is_item_attribute { + // The suggestion adds an entire line of code, ending on a newline, so we'll also + // print the *following* line, to provide context of what we're advising people to + // do. Otherwise you would only see contextless code that can be confused for + // already existing code, despite the colors and UI elements. + // We special case `#[derive(_)]\n` and other attribute suggestions, because those + // are the ones where context is most useful. + let file_lines = sm.span_to_lines(parts[0].range.end..parts[0].range.end); + let (lo, _) = sm.span_to_locations(parts[0].range.clone()); + let line_num = lo.line; + if let Some(line) = sm.get_line(line_num) { + let line = normalize_whitespace(line); + self.draw_code_line( + buffer, + &mut row_num, + &[], + line_num + last_pos + 1, + &line, + DisplaySuggestion::None, + max_line_num_len, + &file_lines, + is_multiline, + ); + } + } + // This offset and the ones below need to be signed to account for replacement code + // that is shorter than the original code. + let mut offsets: Vec<(usize, isize)> = Vec::new(); + // Only show an underline in the suggestions if the suggestion is not the + // entirety of the code being shown and the displayed code is not multiline. + if let DisplaySuggestion::Diff | DisplaySuggestion::Underline | DisplaySuggestion::Add = + show_code_change + { + for part in parts { + let (span_start, span_end) = sm.span_to_locations(part.range.clone()); + let span_start_pos = span_start.display; + let span_end_pos = span_end.display; + + // If this addition is _only_ whitespace, then don't trim it, + // or else we're just not rendering anything. + let is_whitespace_addition = part.replacement.trim().is_empty(); + + // Do not underline the leading... + let start = if is_whitespace_addition { + 0 + } else { + part.replacement + .len() + .saturating_sub(part.replacement.trim_start().len()) + }; + // ...or trailing spaces. Account for substitutions containing unicode + // characters. + let sub_len: usize = str_width(if is_whitespace_addition { + part.replacement + } else { + part.replacement.trim() + }); + + let offset: isize = offsets + .iter() + .filter_map(|(start, v)| { + if span_start_pos < *start { + None + } else { + Some(v) + } + }) + .sum(); + let underline_start = (span_start_pos + start) as isize + offset; + let underline_end = (span_start_pos + start + sub_len) as isize + offset; + assert!(underline_start >= 0 && underline_end >= 0); + let padding: usize = max_line_num_len + 3; + for p in underline_start..underline_end { + if matches!(show_code_change, DisplaySuggestion::Underline) + && is_different(sm, part.replacement, part.range.clone()) + { + // If this is a replacement, underline with `~`, if this is an addition + // underline with `+`. + buffer.putc( + row_num, + (padding as isize + p) as usize, + if part.is_addition(sm) { + '+' + } else { + self.diff() + }, + ElementStyle::Addition, + ); + } + } + if let DisplaySuggestion::Diff = show_code_change { + // Colorize removal with red in diff format. + buffer.set_style_range( + row_num - 2, + (padding as isize + span_start_pos as isize) as usize, + (padding as isize + span_end_pos as isize) as usize, + ElementStyle::Removal, + true, + ); + } + + // length of the code after substitution + let full_sub_len = str_width(part.replacement) as isize; + + // length of the code to be substituted + let snippet_len = span_end_pos as isize - span_start_pos as isize; + // For multiple substitutions, use the position *after* the previous + // substitutions have happened, only when further substitutions are + // located strictly after. + offsets.push((span_end_pos, full_sub_len - snippet_len)); + } + row_num += 1; + } + + // if we elided some lines, add an ellipsis + if lines.next().is_some() { + let placeholder = self.margin(); + let padding = str_width(placeholder); + buffer.puts( + row_num, + max_line_num_len.saturating_sub(padding), + placeholder, + ElementStyle::LineNumber, + ); + } else { + let row = match show_code_change { + DisplaySuggestion::Diff + | DisplaySuggestion::Add + | DisplaySuggestion::Underline => row_num - 1, + DisplaySuggestion::None => row_num, + }; + self.draw_col_separator_end(buffer, row, max_line_num_len + 1); + row_num = row + 1; + } + } + } + + #[allow(clippy::too_many_arguments)] + fn draw_code_line( + &self, + buffer: &mut StyledBuffer, + row_num: &mut usize, + highlight_parts: &[SubstitutionHighlight], + line_num: usize, + line_to_add: &str, + show_code_change: DisplaySuggestion, + max_line_num_len: usize, + file_lines: &[&LineInfo<'_>], + is_multiline: bool, + ) { + if let DisplaySuggestion::Diff = show_code_change { + // We need to print more than one line if the span we need to remove is multiline. + // For more info: https://github.com/rust-lang/rust/issues/92741 + let lines_to_remove = file_lines.iter().take(file_lines.len() - 1); + for (index, line_to_remove) in lines_to_remove.enumerate() { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + index), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + let line = normalize_whitespace(line_to_remove.line); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &line, + ElementStyle::NoStyle, + ); + *row_num += 1; + } + // If the last line is exactly equal to the line we need to add, we can skip both of + // them. This allows us to avoid output like the following: + // 2 - & + // 2 + if true { true } else { false } + // 3 - if true { true } else { false } + // If those lines aren't equal, we print their diff + let last_line = &file_lines.last().unwrap(); + if last_line.line == line_to_add { + *row_num -= 2; + } else { + buffer.puts( + *row_num - 1, + 0, + &self.maybe_anonymized(line_num + file_lines.len() - 1), + ElementStyle::LineNumber, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 1, + "- ", + ElementStyle::Removal, + ); + buffer.puts( + *row_num - 1, + max_line_num_len + 3, + &normalize_whitespace(last_line.line), + ElementStyle::NoStyle, + ); + if line_to_add.trim().is_empty() { + *row_num -= 1; + } else { + // Check if after the removal, the line is left with only whitespace. If so, we + // will not show an "addition" line, as removing the whole line is what the user + // would really want. + // For example, for the following: + // | + // 2 - .await + // 2 + (note the left over whitespace) + // | + // We really want + // | + // 2 - .await + // | + // *row_num -= 1; + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + } + } else if is_multiline { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + match &highlight_parts { + [SubstitutionHighlight { start: 0, end }] if *end == line_to_add.len() => { + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + } + [] => { + // FIXME: needed? Doesn't get exercised in any test. + self.draw_col_separator_no_space(buffer, *row_num, max_line_num_len + 1); + } + _ => { + let diff = self.diff(); + buffer.puts( + *row_num, + max_line_num_len + 1, + &format!("{diff} "), + ElementStyle::Addition, + ); + } + } + // LL | line_to_add + // ++^^^ + // | | + // | magic `3` + // `max_line_num_len` + buffer.puts( + *row_num, + max_line_num_len + 3, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else if let DisplaySuggestion::Add = show_code_change { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + buffer.puts(*row_num, max_line_num_len + 1, "+ ", ElementStyle::Addition); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } else { + buffer.puts( + *row_num, + 0, + &self.maybe_anonymized(line_num), + ElementStyle::LineNumber, + ); + self.draw_col_separator(buffer, *row_num, max_line_num_len + 1); + buffer.append( + *row_num, + &normalize_whitespace(line_to_add), + ElementStyle::NoStyle, + ); + } + + // Colorize addition/replacements with green. + for &SubstitutionHighlight { start, end } in highlight_parts { + // This is a no-op for empty ranges + if start != end { + // Account for tabs when highlighting (#87972). + let tabs: usize = line_to_add + .chars() + .take(start) + .map(|ch| match ch { + '\t' => 3, + _ => 0, + }) + .sum(); + buffer.set_style_range( + *row_num, + max_line_num_len + 3 + start + tabs, + max_line_num_len + 3 + end + tabs, + ElementStyle::Addition, + true, + ); + } + } + *row_num += 1; + } + + #[allow(clippy::too_many_arguments)] + fn draw_line( + &self, + buffer: &mut StyledBuffer, + source_string: &str, + line_index: usize, + line_offset: usize, + width_offset: usize, + code_offset: usize, + max_line_num_len: usize, + margin: Margin, + ) { + // Tabs are assumed to have been replaced by spaces in calling code. + debug_assert!(!source_string.contains('\t')); + let line_len = str_width(source_string); + // Create the source line we will highlight. + let left = margin.left(line_len); + let right = margin.right(line_len); + // FIXME: The following code looks fishy. See #132860. + // On long lines, we strip the source line, accounting for unicode. + let mut taken = 0; + let mut skipped = 0; + let code: String = source_string + .chars() + .skip_while(|ch| { + skipped += char_width(*ch); + skipped <= left + }) + .take_while(|ch| { + // Make sure that the trimming on the right will fall within the terminal width. + taken += char_width(*ch); + taken <= (right - left) + }) + .collect(); + + buffer.puts(line_offset, code_offset, &code, ElementStyle::Quotation); + let placeholder = self.margin(); + let padding = str_width(placeholder); + let (width_taken, bytes_taken) = if margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut bytes_taken = 0; + let mut width_taken = 0; + for ch in code.chars() { + width_taken += char_width(ch); + bytes_taken += ch.len_utf8(); + + if width_taken >= padding { + break; + } + } + buffer.puts( + line_offset, + code_offset, + &format!("{placeholder:>width_taken$}"), + ElementStyle::LineNumber, + ); + (width_taken, bytes_taken) + } else { + (0, 0) + }; + + buffer.puts( + line_offset, + code_offset + width_taken, + &code[bytes_taken..], + ElementStyle::Quotation, + ); + + if margin.was_cut_right(line_len) { + // We have stripped some code/whitespace from the beginning, make it clear. + let mut char_taken = 0; + let mut width_taken_inner = 0; + for ch in code.chars().rev() { + width_taken_inner += char_width(ch); + char_taken += 1; + + if width_taken_inner >= padding { + break; + } + } + + buffer.puts( + line_offset, + code_offset + width_taken + code[bytes_taken..].chars().count() - char_taken, + placeholder, + ElementStyle::LineNumber, + ); + } + + buffer.puts( + line_offset, + 0, + &format!("{:>max_line_num_len$}", self.maybe_anonymized(line_index)), + ElementStyle::LineNumber, + ); + + self.draw_col_separator_no_space(buffer, line_offset, width_offset - 2); + } + + fn draw_range( + &self, + buffer: &mut StyledBuffer, + symbol: char, + line: usize, + col_from: usize, + col_to: usize, + style: ElementStyle, + ) { + for col in col_from..col_to { + buffer.putc(line, col, symbol, style); + } + } + + fn draw_multiline_line( + &self, + buffer: &mut StyledBuffer, + line: usize, + offset: usize, + depth: usize, + style: ElementStyle, + ) { + let chr = match (style, self.theme) { + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Ascii) => { + '|' + } + (_, OutputTheme::Ascii) => '|', + (ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary, OutputTheme::Unicode) => { + '┃' + } + (_, OutputTheme::Unicode) => '│', + }; + buffer.putc(line, offset + depth - 1, chr, style); + } + + fn col_separator(&self) -> char { + match self.theme { + OutputTheme::Ascii => '|', + OutputTheme::Unicode => '│', + } + } + + fn multi_suggestion_separator(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "|", + OutputTheme::Unicode => "├╴", + } + } + + fn draw_col_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + buffer.puts(line, col, &format!("{chr} "), ElementStyle::LineNumber); + } + + fn draw_col_separator_no_space(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let chr = self.col_separator(); + self.draw_col_separator_no_space_with_style( + buffer, + chr, + line, + col, + ElementStyle::LineNumber, + ); + } + + fn draw_col_separator_start(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╭', + line, + col, + ElementStyle::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } + } + + fn draw_col_separator_end(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + match self.theme { + OutputTheme::Ascii => { + self.draw_col_separator_no_space_with_style( + buffer, + '|', + line, + col, + ElementStyle::LineNumber, + ); + } + OutputTheme::Unicode => { + self.draw_col_separator_no_space_with_style( + buffer, + '╰', + line, + col, + ElementStyle::LineNumber, + ); + self.draw_col_separator_no_space_with_style( + buffer, + '╴', + line, + col + 1, + ElementStyle::LineNumber, + ); + } + } + } + + fn draw_col_separator_no_space_with_style( + &self, + buffer: &mut StyledBuffer, + chr: char, + line: usize, + col: usize, + style: ElementStyle, + ) { + buffer.putc(line, col, chr, style); + } + + fn maybe_anonymized(&self, line_num: usize) -> Cow<'static, str> { + if self.anonymized_line_numbers { + Cow::Borrowed(ANONYMIZED_LINE_NUM) + } else { + Cow::Owned(line_num.to_string()) + } + } + + fn file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "--> ", + OutputTheme::Unicode => " ╭▸ ", + } + } + + fn secondary_file_start(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "::: ", + OutputTheme::Unicode => " ⸬ ", + } + } + + fn draw_note_separator( + &self, + buffer: &mut StyledBuffer, + line: usize, + col: usize, + is_cont: bool, + ) { + let chr = match self.theme { + OutputTheme::Ascii => "= ", + OutputTheme::Unicode if is_cont => "├ ", + OutputTheme::Unicode => "╰ ", + }; + buffer.puts(line, col, chr, ElementStyle::LineNumber); + } + + fn diff(&self) -> char { + match self.theme { + OutputTheme::Ascii => '~', + OutputTheme::Unicode => '±', + } + } + + fn draw_line_separator(&self, buffer: &mut StyledBuffer, line: usize, col: usize) { + let (column, dots) = match self.theme { + OutputTheme::Ascii => (0, "..."), + OutputTheme::Unicode => (col - 2, "‡"), + }; + buffer.puts(line, column, dots, ElementStyle::LineNumber); + } + + fn margin(&self) -> &'static str { + match self.theme { + OutputTheme::Ascii => "...", + OutputTheme::Unicode => "…", + } + } + + fn underline(&self, is_primary: bool) -> UnderlineParts { + // X0 Y0 + // label_start > ┯━━━━ < underline + // │ < vertical_text_line + // text + + // multiline_start_down ⤷ X0 Y0 + // top_left > ┌───╿──┘ < top_right_flat + // top_left > ┏│━━━┙ < top_right + // multiline_vertical > ┃│ + // ┃│ X1 Y1 + // ┃│ X2 Y2 + // ┃└────╿──┘ < multiline_end_same_line + // bottom_left > ┗━━━━━┥ < bottom_right_with_text + // multiline_horizontal ^ `X` is a good letter + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┗━━━━┛ < multiline_end_same_line + + // multiline_whole_line > ┏ X0 Y0 + // ┃ X1 Y1 + // ┃ ╿ < multiline_end_up + // ┗━━┛ < bottom_right + + match (self.theme, is_primary) { + (OutputTheme::Ascii, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '^', + label_start: '^', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '^', + bottom_right: '|', + top_left: ' ', + top_right_flat: '^', + bottom_left: '|', + multiline_end_up: '^', + multiline_end_same_line: '^', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Ascii, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '-', + label_start: '-', + vertical_text_line: '|', + multiline_vertical: '|', + multiline_horizontal: '_', + multiline_whole_line: '/', + multiline_start_down: '-', + bottom_right: '|', + top_left: ' ', + top_right_flat: '-', + bottom_left: '|', + multiline_end_up: '-', + multiline_end_same_line: '-', + multiline_bottom_right_with_text: '|', + }, + (OutputTheme::Unicode, true) => UnderlineParts { + style: ElementStyle::UnderlinePrimary, + underline: '━', + label_start: '┯', + vertical_text_line: '│', + multiline_vertical: '┃', + multiline_horizontal: '━', + multiline_whole_line: '┏', + multiline_start_down: '╿', + bottom_right: '┙', + top_left: '┏', + top_right_flat: '┛', + bottom_left: '┗', + multiline_end_up: '╿', + multiline_end_same_line: '┛', + multiline_bottom_right_with_text: '┥', + }, + (OutputTheme::Unicode, false) => UnderlineParts { + style: ElementStyle::UnderlineSecondary, + underline: '─', + label_start: '┬', + vertical_text_line: '│', + multiline_vertical: '│', + multiline_horizontal: '─', + multiline_whole_line: '┌', + multiline_start_down: '│', + bottom_right: '┘', + top_left: '┌', + top_right_flat: '┘', + bottom_left: '└', + multiline_end_up: '│', + multiline_end_same_line: '┘', + multiline_bottom_right_with_text: '┤', + }, + } + } +} + +// instead of taking the String length or dividing by 10 while > 0, we multiply a limit by 10 until +// we're higher. If the loop isn't exited by the `return`, the last multiplication will wrap, which +// is OK, because while we cannot fit a higher power of 10 in a usize, the loop will end anyway. +// This is also why we need the max number of decimal digits within a `usize`. +fn num_decimal_digits(num: usize) -> usize { + #[cfg(target_pointer_width = "64")] + const MAX_DIGITS: usize = 20; + + #[cfg(target_pointer_width = "32")] + const MAX_DIGITS: usize = 10; + + #[cfg(target_pointer_width = "16")] + const MAX_DIGITS: usize = 5; + + let mut lim = 10; + for num_digits in 1..MAX_DIGITS { + if num < lim { + return num_digits; + } + lim = lim.wrapping_mul(10); + } + MAX_DIGITS +} + +pub fn str_width(s: &str) -> usize { + s.chars().map(char_width).sum() +} + +pub fn char_width(ch: char) -> usize { + // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. For now, + // just accept that sometimes the code line will be longer than desired. + match ch { + '\t' => 4, + // Keep the following list in sync with `rustc_errors::emitter::OUTPUT_REPLACEMENTS`. These + // are control points that we replace before printing with a visible codepoint for the sake + // of being able to point at them with underlines. + '\u{0000}' | '\u{0001}' | '\u{0002}' | '\u{0003}' | '\u{0004}' | '\u{0005}' + | '\u{0006}' | '\u{0007}' | '\u{0008}' | '\u{000B}' | '\u{000C}' | '\u{000D}' + | '\u{000E}' | '\u{000F}' | '\u{0010}' | '\u{0011}' | '\u{0012}' | '\u{0013}' + | '\u{0014}' | '\u{0015}' | '\u{0016}' | '\u{0017}' | '\u{0018}' | '\u{0019}' + | '\u{001A}' | '\u{001B}' | '\u{001C}' | '\u{001D}' | '\u{001E}' | '\u{001F}' + | '\u{007F}' | '\u{202A}' | '\u{202B}' | '\u{202D}' | '\u{202E}' | '\u{2066}' + | '\u{2067}' | '\u{2068}' | '\u{202C}' | '\u{2069}' => 1, + _ => unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1), + } +} + +fn num_overlap( + a_start: usize, + a_end: usize, + b_start: usize, + b_end: usize, + inclusive: bool, +) -> bool { + let extra = usize::from(inclusive); + (b_start..b_end + extra).contains(&a_start) || (a_start..a_end + extra).contains(&b_start) +} + +fn overlaps(a1: &LineAnnotation<'_>, a2: &LineAnnotation<'_>, padding: usize) -> bool { + num_overlap( + a1.start.display, + a1.end.display + padding, + a2.start.display, + a2.end.display, + false, + ) +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum LineAnnotationType { + /// Annotation under a single line of code + Singleline, + + // The Multiline type above is replaced with the following three in order + // to reuse the current label drawing code. + // + // Each of these corresponds to one part of the following diagram: + // + // x | foo(1 + bar(x, + // | _________^ < MultilineStart + // x | | y), < MultilineLine + // | |______________^ label < MultilineEnd + // x | z); + /// Annotation marking the first character of a fully shown multiline span + MultilineStart(usize), + /// Annotation marking the last character of a fully shown multiline span + MultilineEnd(usize), + /// Line at the left enclosing the lines of a fully shown multiline span + // Just a placeholder for the drawing algorithm, to know that it shouldn't skip the first 4 + // and last 2 lines of code. The actual line is drawn in `emit_message_default` and not in + // `draw_multiline_line`. + MultilineLine(usize), +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct LineAnnotation<'a> { + /// Start column. + /// Note that it is important that this field goes + /// first, so that when we sort, we sort orderings by start + /// column. + pub start: Loc, + + /// End column within the line (exclusive) + pub end: Loc, + + /// level + pub kind: AnnotationKind, + + /// Optional label to display adjacent to the annotation. + pub label: Option<&'a str>, + + /// Is this a single line, multiline or multiline span minimized down to a + /// smaller span. + pub annotation_type: LineAnnotationType, - /// Render a snippet into a `Display`able object - pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a { - DisplayList::new( - msg, - &self.stylesheet, - self.anonymized_line_numbers, - self.term_width, + /// Whether the source code should be highlighted + pub highlight_source: bool, +} + +impl LineAnnotation<'_> { + pub(crate) fn is_primary(&self) -> bool { + self.kind == AnnotationKind::Primary + } + + /// Whether this annotation is a vertical line placeholder. + pub(crate) fn is_line(&self) -> bool { + matches!(self.annotation_type, LineAnnotationType::MultilineLine(_)) + } + + /// Length of this annotation as displayed in the stderr output + pub(crate) fn len(&self) -> usize { + // Account for usize underflows + if self.end.display > self.start.display { + self.end.display - self.start.display + } else { + self.start.display - self.end.display + } + } + + pub(crate) fn has_label(&self) -> bool { + if let Some(label) = self.label { + // Consider labels with no text as effectively not being there + // to avoid weird output with unnecessary vertical lines, like: + // + // X | fn foo(x: u32) { + // | -------^------ + // | | | + // | | + // | + // + // Note that this would be the complete output users would see. + !label.is_empty() + } else { + false + } + } + + pub(crate) fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_type, + LineAnnotationType::MultilineStart(_) | LineAnnotationType::MultilineEnd(_) ) } } + +#[derive(Clone, Copy, Debug)] +pub(crate) enum DisplaySuggestion { + Underline, + Diff, + None, + Add, +} + +// We replace some characters so the CLI output is always consistent and underlines aligned. +// Keep the following list in sync with `rustc_span::char_width`. +const OUTPUT_REPLACEMENTS: &[(char, &str)] = &[ + // In terminals without Unicode support the following will be garbled, but in *all* terminals + // the underlying codepoint will be as well. We could gate this replacement behind a "unicode + // support" gate. + ('\0', "␀"), + ('\u{0001}', "␁"), + ('\u{0002}', "␂"), + ('\u{0003}', "␃"), + ('\u{0004}', "␄"), + ('\u{0005}', "␅"), + ('\u{0006}', "␆"), + ('\u{0007}', "␇"), + ('\u{0008}', "␈"), + ('\t', " "), // We do our own tab replacement + ('\u{000b}', "␋"), + ('\u{000c}', "␌"), + ('\u{000d}', "␍"), + ('\u{000e}', "␎"), + ('\u{000f}', "␏"), + ('\u{0010}', "␐"), + ('\u{0011}', "␑"), + ('\u{0012}', "␒"), + ('\u{0013}', "␓"), + ('\u{0014}', "␔"), + ('\u{0015}', "␕"), + ('\u{0016}', "␖"), + ('\u{0017}', "␗"), + ('\u{0018}', "␘"), + ('\u{0019}', "␙"), + ('\u{001a}', "␚"), + ('\u{001b}', "␛"), + ('\u{001c}', "␜"), + ('\u{001d}', "␝"), + ('\u{001e}', "␞"), + ('\u{001f}', "␟"), + ('\u{007f}', "␡"), + ('\u{200d}', ""), // Replace ZWJ for consistent terminal output of grapheme clusters. + ('\u{202a}', "�"), // The following unicode text flow control characters are inconsistently + ('\u{202b}', "�"), // supported across CLIs and can cause confusion due to the bytes on disk + ('\u{202c}', "�"), // not corresponding to the visible source code, so we replace them always. + ('\u{202d}', "�"), + ('\u{202e}', "�"), + ('\u{2066}', "�"), + ('\u{2067}', "�"), + ('\u{2068}', "�"), + ('\u{2069}', "�"), +]; + +pub(crate) fn normalize_whitespace(s: &str) -> String { + // Scan the input string for a character in the ordered table above. + // If it's present, replace it with its alternative string (it can be more than 1 char!). + // Otherwise, retain the input char. + s.chars().fold(String::with_capacity(s.len()), |mut s, c| { + match OUTPUT_REPLACEMENTS.binary_search_by_key(&c, |(k, _)| *k) { + Ok(i) => s.push_str(OUTPUT_REPLACEMENTS[i].1), + _ => s.push(c), + } + s + }) +} + +#[derive(Clone, Copy, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) enum ElementStyle { + MainHeaderMsg, + HeaderMsg, + LineAndColumn, + LineNumber, + Quotation, + UnderlinePrimary, + UnderlineSecondary, + LabelPrimary, + LabelSecondary, + NoStyle, + Level(LevelInner), + Addition, + Removal, +} + +impl ElementStyle { + fn color_spec(&self, level: &Level<'_>, stylesheet: &Stylesheet) -> Style { + match self { + ElementStyle::Addition => stylesheet.addition, + ElementStyle::Removal => stylesheet.removal, + ElementStyle::LineAndColumn => stylesheet.none, + ElementStyle::LineNumber => stylesheet.line_no, + ElementStyle::Quotation => stylesheet.none, + ElementStyle::MainHeaderMsg => stylesheet.emphasis, + ElementStyle::UnderlinePrimary | ElementStyle::LabelPrimary => level.style(stylesheet), + ElementStyle::UnderlineSecondary | ElementStyle::LabelSecondary => stylesheet.context, + ElementStyle::HeaderMsg | ElementStyle::NoStyle => stylesheet.none, + ElementStyle::Level(lvl) => lvl.style(stylesheet), + } + } +} + +#[derive(Debug, Clone, Copy)] +struct UnderlineParts { + style: ElementStyle, + underline: char, + label_start: char, + vertical_text_line: char, + multiline_vertical: char, + multiline_horizontal: char, + multiline_whole_line: char, + multiline_start_down: char, + bottom_right: char, + top_left: char, + top_right_flat: char, + bottom_left: char, + multiline_end_up: char, + multiline_end_same_line: char, + multiline_bottom_right_with_text: char, +} + +/// Whether the original and suggested code are the same. +pub(crate) fn is_different(sm: &SourceMap<'_>, suggested: &str, range: Range) -> bool { + match sm.span_to_snippet(range) { + Some(s) => s != suggested, + None => true, + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputTheme { + Ascii, + Unicode, +} + +#[cfg(test)] +mod test { + use super::OUTPUT_REPLACEMENTS; + use snapbox::IntoData; + + fn format_replacements(replacements: Vec<(char, &str)>) -> String { + replacements + .into_iter() + .map(|r| format!(" {r:?}")) + .collect::>() + .join("\n") + } + + #[test] + /// The [`OUTPUT_REPLACEMENTS`] array must be sorted (for binary search to + /// work) and must contain no duplicate entries + fn ensure_output_replacements_is_sorted() { + let mut expected = OUTPUT_REPLACEMENTS.to_owned(); + expected.sort_by_key(|r| r.0); + expected.dedup_by_key(|r| r.0); + let expected = format_replacements(expected); + let actual = format_replacements(OUTPUT_REPLACEMENTS.to_owned()); + snapbox::assert_data_eq!(actual, expected.into_data().raw()); + } +} diff --git a/src/renderer/source_map.rs b/src/renderer/source_map.rs new file mode 100644 index 00000000..33fe1897 --- /dev/null +++ b/src/renderer/source_map.rs @@ -0,0 +1,662 @@ +use crate::renderer::{char_width, is_different, num_overlap, LineAnnotation, LineAnnotationType}; +use crate::{Annotation, AnnotationKind, Patch}; +use std::cmp::{max, min}; +use std::ops::Range; + +#[derive(Debug)] +pub(crate) struct SourceMap<'a> { + lines: Vec>, + pub(crate) source: &'a str, +} + +impl<'a> SourceMap<'a> { + pub(crate) fn new(source: &'a str, line_start: usize) -> Self { + let mut current_index = 0; + + let mut mapping = vec![]; + for (idx, (line, end_line)) in CursorLines::new(source).enumerate() { + let line_length = line.len(); + let line_range = current_index..current_index + line_length; + let end_line_size = end_line.len(); + + mapping.push(LineInfo { + line, + line_index: line_start + idx, + start_byte: line_range.start, + end_byte: line_range.end + end_line_size, + end_line_size, + }); + + current_index += line_length + end_line_size; + } + Self { + lines: mapping, + source, + } + } + + pub(crate) fn get_line(&self, idx: usize) -> Option<&'a str> { + self.lines + .iter() + .find(|l| l.line_index == idx) + .map(|info| info.line) + } + + pub(crate) fn span_to_locations(&self, span: Range) -> (Loc, Loc) { + let start_info = self + .lines + .iter() + .find(|info| span.start >= info.start_byte && span.start < info.end_byte) + .unwrap_or(self.lines.last().unwrap()); + let (mut start_char_pos, start_display_pos) = start_info.line + [0..(span.start - start_info.start_byte).min(start_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + // correct the char pos if we are highlighting the end of a line + if (span.start - start_info.start_byte).saturating_sub(start_info.line.len()) > 0 { + start_char_pos += 1; + } + let start = Loc { + line: start_info.line_index, + char: start_char_pos, + display: start_display_pos, + byte: span.start, + }; + + if span.start == span.end { + return (start, start); + } + + let end_info = self + .lines + .iter() + .find(|info| info.end_byte > span.end.saturating_sub(1)) + .unwrap_or(self.lines.last().unwrap()); + let (mut end_char_pos, end_display_pos) = end_info.line + [0..(span.end - end_info.start_byte).min(end_info.line.len())] + .chars() + .fold((0, 0), |(char_pos, byte_pos), c| { + let display = char_width(c); + (char_pos + 1, byte_pos + display) + }); + + // correct the char pos if we are highlighting the end of a line + if (span.end - end_info.start_byte).saturating_sub(end_info.line.len()) > 0 { + end_char_pos += 1; + } + let mut end = Loc { + line: end_info.line_index, + char: end_char_pos, + display: end_display_pos, + byte: span.end, + }; + if start.line != end.line && end.byte > end_info.end_byte - end_info.end_line_size { + end.char += 1; + end.display += 1; + } + + (start, end) + } + + pub(crate) fn span_to_snippet(&self, span: Range) -> Option<&str> { + self.source.get(span) + } + + pub(crate) fn span_to_lines(&self, span: Range) -> Vec<&LineInfo<'a>> { + let mut lines = vec![]; + let start = span.start; + let end = span.end; + for line_info in &self.lines { + if start >= line_info.end_byte { + continue; + } + if end <= line_info.start_byte { + break; + } + lines.push(line_info); + } + lines + } + + pub(crate) fn annotated_lines( + &self, + annotations: Vec>, + fold: bool, + ) -> (usize, Vec>) { + let source_len = self.source.len(); + if let Some(bigger) = annotations.iter().find_map(|x| { + // Allow highlighting one past the last character in the source. + if source_len + 1 < x.range.end { + Some(&x.range) + } else { + None + } + }) { + panic!("Annotation range `{bigger:?}` is beyond the end of buffer `{source_len}`") + } + + let mut annotated_line_infos = self + .lines + .iter() + .map(|info| AnnotatedLineInfo { + line: info.line, + line_index: info.line_index, + annotations: vec![], + }) + .collect::>(); + let mut multiline_annotations = vec![]; + + for Annotation { + range, + label, + kind, + highlight_source, + } in annotations + { + let (lo, mut hi) = self.span_to_locations(range.clone()); + + // Watch out for "empty spans". If we get a span like 6..6, we + // want to just display a `^` at 6, so convert that to + // 6..7. This is degenerate input, but it's best to degrade + // gracefully -- and the parser likes to supply a span like + // that for EOF, in particular. + + if lo.display == hi.display && lo.line == hi.line { + hi.display += 1; + } + + if lo.line == hi.line { + let line_ann = LineAnnotation { + start: lo, + end: hi, + kind, + label, + annotation_type: LineAnnotationType::Singleline, + highlight_source, + }; + self.add_annotation_to_file(&mut annotated_line_infos, lo.line, line_ann); + } else { + multiline_annotations.push(MultilineAnnotation { + depth: 1, + start: lo, + end: hi, + kind, + label, + overlaps_exactly: false, + highlight_source, + }); + } + } + + let mut primary_spans = vec![]; + + // Find overlapping multiline annotations, put them at different depths + multiline_annotations + .sort_by_key(|ml| (ml.start.line, usize::MAX - ml.end.line, ml.start.byte)); + for ann in multiline_annotations.clone() { + if ann.kind.is_primary() { + primary_spans.push((ann.start, ann.end)); + } + for a in &mut multiline_annotations { + // Move all other multiline annotations overlapping with this one + // one level to the right. + if !ann.same_span(a) + && num_overlap(ann.start.line, ann.end.line, a.start.line, a.end.line, true) + { + a.increase_depth(); + } else if ann.same_span(a) && &ann != a { + a.overlaps_exactly = true; + } else { + if primary_spans + .iter() + .any(|(s, e)| a.start == *s && a.end == *e) + { + a.kind = AnnotationKind::Primary; + } + break; + } + } + } + + let mut max_depth = 0; // max overlapping multiline spans + for ann in &multiline_annotations { + max_depth = max(max_depth, ann.depth); + } + // Change order of multispan depth to minimize the number of overlaps in the ASCII art. + for a in &mut multiline_annotations { + a.depth = max_depth - a.depth + 1; + } + for ann in multiline_annotations { + let mut end_ann = ann.as_end(); + if ann.overlaps_exactly { + end_ann.annotation_type = LineAnnotationType::Singleline; + } else { + // avoid output like + // + // | foo( + // | _____^ + // | |_____| + // | || bar, + // | || ); + // | || ^ + // | ||______| + // | |______foo + // | baz + // + // and instead get + // + // | foo( + // | _____^ + // | | bar, + // | | ); + // | | ^ + // | | | + // | |______foo + // | baz + self.add_annotation_to_file( + &mut annotated_line_infos, + ann.start.line, + ann.as_start(), + ); + // 4 is the minimum vertical length of a multiline span when presented: two lines + // of code and two lines of underline. This is not true for the special case where + // the beginning doesn't have an underline, but the current logic seems to be + // working correctly. + let middle = min(ann.start.line + 4, ann.end.line); + // We'll show up to 4 lines past the beginning of the multispan start. + // We will *not* include the tail of lines that are only whitespace, a comment or + // a bare delimiter. + let filter = |s: &str| { + let s = s.trim(); + // Consider comments as empty, but don't consider docstrings to be empty. + !(s.starts_with("//") && !(s.starts_with("///") || s.starts_with("//!"))) + // Consider lines with nothing but whitespace, a single delimiter as empty. + && !["", "{", "}", "(", ")", "[", "]"].contains(&s) + }; + let until = (ann.start.line..middle) + .rev() + .filter_map(|line| self.get_line(line).map(|s| (line + 1, s))) + .find(|(_, s)| filter(s)) + .map_or(ann.start.line, |(line, _)| line); + for line in ann.start.line + 1..until { + // Every `|` that joins the beginning of the span (`___^`) to the end (`|__^`). + self.add_annotation_to_file(&mut annotated_line_infos, line, ann.as_line()); + } + let line_end = ann.end.line - 1; + let end_is_empty = self.get_line(line_end).map_or(false, |s| !filter(s)); + if middle < line_end && !end_is_empty { + self.add_annotation_to_file(&mut annotated_line_infos, line_end, ann.as_line()); + } + } + self.add_annotation_to_file(&mut annotated_line_infos, end_ann.end.line, end_ann); + } + + if fold { + annotated_line_infos.retain(|l| !l.annotations.is_empty()); + } + + annotated_line_infos + .iter_mut() + .for_each(|l| l.annotations.sort_by(|a, b| a.start.cmp(&b.start))); + + (max_depth, annotated_line_infos) + } + + fn add_annotation_to_file( + &self, + annotated_line_infos: &mut Vec>, + line_index: usize, + line_ann: LineAnnotation<'a>, + ) { + if let Some(line_info) = annotated_line_infos + .iter_mut() + .find(|line_info| line_info.line_index == line_index) + { + line_info.annotations.push(line_ann); + } else { + let info = self + .lines + .iter() + .find(|l| l.line_index == line_index) + .unwrap(); + annotated_line_infos.push(AnnotatedLineInfo { + line: info.line, + line_index, + annotations: vec![line_ann], + }); + annotated_line_infos.sort_by_key(|l| l.line_index); + } + } + + pub(crate) fn splice_lines<'b>( + &'b self, + mut patches: Vec>, + ) -> Vec<(String, Vec>, Vec>)> { + fn push_trailing( + buf: &mut String, + line_opt: Option<&str>, + lo: &Loc, + hi_opt: Option<&Loc>, + ) -> usize { + let mut line_count = 0; + // Convert CharPos to Usize, as CharPose is character offset + // Extract low index and high index + let (lo, hi_opt) = (lo.char, hi_opt.map(|hi| hi.char)); + if let Some(line) = line_opt { + if let Some(lo) = line.char_indices().map(|(i, _)| i).nth(lo) { + // Get high index while account for rare unicode and emoji with char_indices + let hi_opt = hi_opt.and_then(|hi| line.char_indices().map(|(i, _)| i).nth(hi)); + match hi_opt { + // If high index exist, take string from low to high index + Some(hi) if hi > lo => { + // count how many '\n' exist + line_count = line[lo..hi].matches('\n').count(); + buf.push_str(&line[lo..hi]); + } + Some(_) => (), + // If high index absence, take string from low index till end string.len + None => { + // count how many '\n' exist + line_count = line[lo..].matches('\n').count(); + buf.push_str(&line[lo..]); + } + } + } + // If high index is None + if hi_opt.is_none() { + buf.push('\n'); + } + } + line_count + } + // Assumption: all spans are in the same file, and all spans + // are disjoint. Sort in ascending order. + patches.sort_by_key(|p| p.range.start); + + // Find the bounding span. + let Some(lo) = patches.iter().map(|p| p.range.start).min() else { + return Vec::new(); + }; + let Some(hi) = patches.iter().map(|p| p.range.end).max() else { + return Vec::new(); + }; + + let lines = self.span_to_lines(lo..hi); + + let mut highlights = vec![]; + // To build up the result, we do this for each span: + // - push the line segment trailing the previous span + // (at the beginning a "phantom" span pointing at the start of the line) + // - push lines between the previous and current span (if any) + // - if the previous and current span are not on the same line + // push the line segment leading up to the current span + // - splice in the span substitution + // + // Finally push the trailing line segment of the last span + let (mut prev_hi, _) = self.span_to_locations(lo..hi); + prev_hi.char = 0; + let mut prev_line = lines.first().map(|line| line.line); + let mut buf = String::new(); + + let mut line_highlight = vec![]; + // We need to keep track of the difference between the existing code and the added + // or deleted code in order to point at the correct column *after* substitution. + let mut acc = 0; + for part in &mut patches { + // If this is a replacement of, e.g. `"a"` into `"ab"`, adjust the + // suggestion and snippet to look as if we just suggested to add + // `"b"`, which is typically much easier for the user to understand. + part.trim_trivial_replacements(self); + let (cur_lo, cur_hi) = self.span_to_locations(part.range.clone()); + if prev_hi.line == cur_lo.line { + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, Some(&cur_lo)); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + acc = 0; + count -= 1; + } + } else { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let mut count = push_trailing(&mut buf, prev_line, &prev_hi, None); + while count > 0 { + highlights.push(std::mem::take(&mut line_highlight)); + count -= 1; + } + // push lines between the previous and current span (if any) + for idx in prev_hi.line + 1..(cur_lo.line) { + if let Some(line) = self.get_line(idx) { + buf.push_str(line.as_ref()); + buf.push('\n'); + highlights.push(std::mem::take(&mut line_highlight)); + } + } + if let Some(cur_line) = self.get_line(cur_lo.line) { + let end = match cur_line.char_indices().nth(cur_lo.char) { + Some((i, _)) => i, + None => cur_line.len(), + }; + buf.push_str(&cur_line[..end]); + } + } + // Add a whole line highlight per line in the snippet. + let len: isize = part + .replacement + .split('\n') + .next() + .unwrap_or(part.replacement) + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + if !is_different(self, part.replacement, part.range.clone()) { + // Account for cases where we are suggesting the same code that's already + // there. This shouldn't happen often, but in some cases for multipart + // suggestions it's much easier to handle it here than in the origin. + } else { + line_highlight.push(SubstitutionHighlight { + start: (cur_lo.char as isize + acc) as usize, + end: (cur_lo.char as isize + acc + len) as usize, + }); + } + buf.push_str(part.replacement); + // Account for the difference between the width of the current code and the + // snippet being suggested, so that the *later* suggestions are correctly + // aligned on the screen. Note that cur_hi and cur_lo can be on different + // lines, so cur_hi.col can be smaller than cur_lo.col + acc += len - (cur_hi.char as isize - cur_lo.char as isize); + prev_hi = cur_hi; + prev_line = self.get_line(prev_hi.line); + for line in part.replacement.split('\n').skip(1) { + acc = 0; + highlights.push(std::mem::take(&mut line_highlight)); + let end: usize = line + .chars() + .map(|c| match c { + '\t' => 4, + _ => 1, + }) + .sum(); + line_highlight.push(SubstitutionHighlight { start: 0, end }); + } + } + highlights.push(std::mem::take(&mut line_highlight)); + // if the replacement already ends with a newline, don't print the next line + if !buf.ends_with('\n') { + push_trailing(&mut buf, prev_line, &prev_hi, None); + } + // remove trailing newlines + while buf.ends_with('\n') { + buf.pop(); + } + if highlights.iter().all(|parts| parts.is_empty()) { + Vec::new() + } else { + vec![(buf, patches, highlights)] + } + } +} + +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct MultilineAnnotation<'a> { + pub depth: usize, + pub start: Loc, + pub end: Loc, + pub kind: AnnotationKind, + pub label: Option<&'a str>, + pub overlaps_exactly: bool, + pub highlight_source: bool, +} + +impl<'a> MultilineAnnotation<'a> { + pub(crate) fn increase_depth(&mut self) { + self.depth += 1; + } + + /// Compare two `MultilineAnnotation`s considering only the `Span` they cover. + pub(crate) fn same_span(&self, other: &MultilineAnnotation<'_>) -> bool { + self.start == other.start && self.end == other.end + } + + pub(crate) fn as_start(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: self.start, + end: Loc { + line: self.start.line, + char: self.start.char + 1, + display: self.start.display + 1, + byte: self.start.byte + 1, + }, + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineStart(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_end(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc { + line: self.end.line, + char: self.end.char.saturating_sub(1), + display: self.end.display.saturating_sub(1), + byte: self.end.byte.saturating_sub(1), + }, + end: self.end, + kind: self.kind, + label: self.label, + annotation_type: LineAnnotationType::MultilineEnd(self.depth), + highlight_source: self.highlight_source, + } + } + + pub(crate) fn as_line(&self) -> LineAnnotation<'a> { + LineAnnotation { + start: Loc::default(), + end: Loc::default(), + kind: self.kind, + label: None, + annotation_type: LineAnnotationType::MultilineLine(self.depth), + highlight_source: self.highlight_source, + } + } +} + +#[derive(Debug)] +pub(crate) struct LineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) start_byte: usize, + pub(crate) end_byte: usize, + end_line_size: usize, +} + +#[derive(Debug)] +pub(crate) struct AnnotatedLineInfo<'a> { + pub(crate) line: &'a str, + pub(crate) line_index: usize, + pub(crate) annotations: Vec>, +} + +/// A source code location used for error reporting. +#[derive(Clone, Copy, Debug, Default, PartialOrd, Ord, PartialEq, Eq)] +pub(crate) struct Loc { + /// The (1-based) line number. + pub(crate) line: usize, + /// The (0-based) column offset. + pub(crate) char: usize, + /// The (0-based) column offset when displayed. + pub(crate) display: usize, + /// The (0-based) byte offset. + pub(crate) byte: usize, +} + +struct CursorLines<'a>(&'a str); + +impl CursorLines<'_> { + fn new(src: &str) -> CursorLines<'_> { + CursorLines(src) + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +enum EndLine { + Eof, + Lf, + Crlf, +} + +impl EndLine { + /// The number of characters this line ending occupies in bytes. + pub(crate) fn len(self) -> usize { + match self { + EndLine::Eof => 0, + EndLine::Lf => 1, + EndLine::Crlf => 2, + } + } +} + +impl<'a> Iterator for CursorLines<'a> { + type Item = (&'a str, EndLine); + + fn next(&mut self) -> Option { + if self.0.is_empty() { + None + } else { + self.0 + .find('\n') + .map(|x| { + let ret = if 0 < x { + if self.0.as_bytes()[x - 1] == b'\r' { + (&self.0[..x - 1], EndLine::Crlf) + } else { + (&self.0[..x], EndLine::Lf) + } + } else { + ("", EndLine::Lf) + }; + self.0 = &self.0[x + 1..]; + ret + }) + .or_else(|| { + let ret = Some((self.0, EndLine::Eof)); + self.0 = ""; + ret + }) + } + } +} + +/// Used to translate between `Span`s and byte positions within a single output line in highlighted +/// code of structured suggestions. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SubstitutionHighlight { + pub(crate) start: usize, + pub(crate) end: usize, +} diff --git a/src/renderer/styled_buffer.rs b/src/renderer/styled_buffer.rs index ec834e1b..7114683b 100644 --- a/src/renderer/styled_buffer.rs +++ b/src/renderer/styled_buffer.rs @@ -2,8 +2,10 @@ //! //! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs +use crate::level::Level; use crate::renderer::stylesheet::Stylesheet; -use anstyle::Style; +use crate::renderer::ElementStyle; + use std::fmt; use std::fmt::Write; @@ -15,13 +17,13 @@ pub(crate) struct StyledBuffer { #[derive(Clone, Copy, Debug, PartialEq)] pub(crate) struct StyledChar { ch: char, - style: Style, + style: ElementStyle, } impl StyledChar { - pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new()); + pub(crate) const SPACE: Self = StyledChar::new(' ', ElementStyle::NoStyle); - pub(crate) const fn new(ch: char, style: Style) -> StyledChar { + pub(crate) const fn new(ch: char, style: ElementStyle) -> StyledChar { StyledChar { ch, style } } } @@ -37,19 +39,24 @@ impl StyledBuffer { } } - pub(crate) fn render(&self, stylesheet: &Stylesheet) -> Result { + pub(crate) fn render( + &self, + level: Level<'_>, + stylesheet: &Stylesheet, + ) -> Result { let mut str = String::new(); for (i, line) in self.lines.iter().enumerate() { let mut current_style = stylesheet.none; - for ch in line { - if ch.style != current_style { + for StyledChar { ch, style } in line { + let ch_style = style.color_spec(&level, stylesheet); + if ch_style != current_style { if !line.is_empty() { write!(str, "{}", current_style.render_reset())?; } - current_style = ch.style; + current_style = ch_style; write!(str, "{}", current_style.render())?; } - write!(str, "{}", ch.ch)?; + write!(str, "{ch}")?; } write!(str, "{}", current_style.render_reset())?; if i != self.lines.len() - 1 { @@ -62,7 +69,7 @@ impl StyledBuffer { /// Sets `chr` with `style` for given `line`, `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) { + pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: ElementStyle) { self.ensure_lines(line); if col >= self.lines[line].len() { self.lines[line].resize(col + 1, StyledChar::SPACE); @@ -73,16 +80,17 @@ impl StyledBuffer { /// Sets `string` with `style` for given `line`, starting from `col`. /// If `line` does not exist in our buffer, adds empty lines up to the given /// and fills the last line with unstyled whitespace. - pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) { + pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: ElementStyle) { let mut n = col; for c in string.chars() { self.putc(line, n, c, style); n += 1; } } + /// For given `line` inserts `string` with `style` after old content of that line, /// adding lines if needed - pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) { + pub(crate) fn append(&mut self, line: usize, string: &str, style: ElementStyle) { if line >= self.lines.len() { self.puts(line, 0, string, style); } else { @@ -91,7 +99,58 @@ impl StyledBuffer { } } + /// For given `line` inserts `string` with `style` before old content of that line, + /// adding lines if needed + pub(crate) fn prepend(&mut self, line: usize, string: &str, style: ElementStyle) { + self.ensure_lines(line); + let string_len = string.chars().count(); + + if !self.lines[line].is_empty() { + // Push the old content over to make room for new content + for _ in 0..string_len { + self.lines[line].insert(0, StyledChar::SPACE); + } + } + + self.puts(line, 0, string, style); + } + pub(crate) fn num_lines(&self) -> usize { self.lines.len() } + + /// Set `style` for `line`, `col_start..col_end` range if: + /// 1. That line and column range exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style_range( + &mut self, + line: usize, + col_start: usize, + col_end: usize, + style: ElementStyle, + overwrite: bool, + ) { + for col in col_start..col_end { + self.set_style(line, col, style, overwrite); + } + } + + /// Set `style` for `line`, `col` if: + /// 1. That line and column exist in `StyledBuffer` + /// 2. `overwrite` is `true` or existing style is `Style::NoStyle` or `Style::Quotation` + pub(crate) fn set_style( + &mut self, + line: usize, + col: usize, + style: ElementStyle, + overwrite: bool, + ) { + if let Some(ref mut line) = self.lines.get_mut(line) { + if let Some(StyledChar { style: s, .. }) = line.get_mut(col) { + if overwrite || matches!(s, ElementStyle::NoStyle | ElementStyle::Quotation) { + *s = style; + } + } + } + } } diff --git a/src/renderer/stylesheet.rs b/src/renderer/stylesheet.rs index ee1ab937..4aa21a5a 100644 --- a/src/renderer/stylesheet.rs +++ b/src/renderer/stylesheet.rs @@ -10,6 +10,9 @@ pub(crate) struct Stylesheet { pub(crate) line_no: Style, pub(crate) emphasis: Style, pub(crate) none: Style, + pub(crate) context: Style, + pub(crate) addition: Style, + pub(crate) removal: Style, } impl Default for Stylesheet { @@ -29,40 +32,9 @@ impl Stylesheet { line_no: Style::new(), emphasis: Style::new(), none: Style::new(), + context: Style::new(), + addition: Style::new(), + removal: Style::new(), } } } - -impl Stylesheet { - pub(crate) fn error(&self) -> &Style { - &self.error - } - - pub(crate) fn warning(&self) -> &Style { - &self.warning - } - - pub(crate) fn info(&self) -> &Style { - &self.info - } - - pub(crate) fn note(&self) -> &Style { - &self.note - } - - pub(crate) fn help(&self) -> &Style { - &self.help - } - - pub(crate) fn line_no(&self) -> &Style { - &self.line_no - } - - pub(crate) fn emphasis(&self) -> &Style { - &self.emphasis - } - - pub(crate) fn none(&self) -> &Style { - &self.none - } -} diff --git a/src/snippet.rs b/src/snippet.rs index 8e9a3a88..fe1239d4 100644 --- a/src/snippet.rs +++ b/src/snippet.rs @@ -1,27 +1,19 @@ //! Structures used as an input for the library. -//! -//! Example: -//! -//! ``` -//! use annotate_snippets::*; -//! -//! Level::Error.title("mismatched types") -//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs")) -//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs")); -//! ``` +use crate::level::Level; +use crate::renderer::source_map::SourceMap; use std::ops::Range; -/// Primary structure provided for formatting -/// -/// See [`Level::title`] to create a [`Message`] +pub(crate) const ERROR_TXT: &str = "error"; +pub(crate) const HELP_TXT: &str = "help"; +pub(crate) const INFO_TXT: &str = "info"; +pub(crate) const NOTE_TXT: &str = "note"; +pub(crate) const WARNING_TXT: &str = "warning"; + #[derive(Debug)] pub struct Message<'a> { - pub(crate) level: Level, - pub(crate) id: Option<&'a str>, - pub(crate) title: &'a str, - pub(crate) snippets: Vec>, - pub(crate) footer: Vec>, + pub(crate) id: Option<&'a str>, // for "correctness", could be sloppy and be on Title + pub(crate) groups: Vec>, } impl<'a> Message<'a> { @@ -30,50 +22,157 @@ impl<'a> Message<'a> { self } - pub fn snippet(mut self, slice: Snippet<'a>) -> Self { - self.snippets.push(slice); + pub fn group(mut self, group: Group<'a>) -> Self { + self.groups.push(group); self } - pub fn snippets(mut self, slice: impl IntoIterator>) -> Self { - self.snippets.extend(slice); + pub(crate) fn max_line_number(&self) -> usize { + self.groups + .iter() + .map(|v| { + v.elements + .iter() + .map(|s| match s { + Element::Title(_) | Element::Origin(_) | Element::ColumnSeparator(_) => 0, + Element::Cause(cause) => { + let end = cause + .markers + .iter() + .map(|a| a.range.end) + .max() + .unwrap_or(cause.source.len()) + .min(cause.source.len()); + + cause.line_start + newline_count(&cause.source[..end]) + } + Element::Suggestion(suggestion) => { + let end = suggestion + .markers + .iter() + .map(|a| a.range.end) + .max() + .unwrap_or(suggestion.source.len()) + .min(suggestion.source.len()); + + suggestion.line_start + newline_count(&suggestion.source[..end]) + } + }) + .max() + .unwrap_or(1) + }) + .max() + .unwrap_or(1) + } +} + +#[derive(Debug)] +pub struct Group<'a> { + pub(crate) elements: Vec>, +} + +impl Default for Group<'_> { + fn default() -> Self { + Self::new() + } +} + +impl<'a> Group<'a> { + pub fn new() -> Self { + Self { elements: vec![] } + } + + pub fn element(mut self, section: impl Into>) -> Self { + self.elements.push(section.into()); self } - pub fn footer(mut self, footer: Message<'a>) -> Self { - self.footer.push(footer); + pub fn elements(mut self, sections: impl IntoIterator>>) -> Self { + self.elements.extend(sections.into_iter().map(Into::into)); self } - pub fn footers(mut self, footer: impl IntoIterator>) -> Self { - self.footer.extend(footer); + pub fn is_empty(&self) -> bool { + self.elements.is_empty() + } +} + +#[derive(Debug)] +#[non_exhaustive] +pub enum Element<'a> { + Title(Title<'a>), + Cause(Snippet<'a, Annotation<'a>>), + Suggestion(Snippet<'a, Patch<'a>>), + Origin(Origin<'a>), + ColumnSeparator(ColumnSeparator), +} + +impl<'a> From> for Element<'a> { + fn from(value: Title<'a>) -> Self { + Element::Title(value) + } +} + +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Annotation<'a>>) -> Self { + Element::Cause(value) + } +} + +impl<'a> From>> for Element<'a> { + fn from(value: Snippet<'a, Patch<'a>>) -> Self { + Element::Suggestion(value) + } +} + +impl<'a> From> for Element<'a> { + fn from(value: Origin<'a>) -> Self { + Element::Origin(value) + } +} + +impl From for Element<'_> { + fn from(value: ColumnSeparator) -> Self { + Self::ColumnSeparator(value) + } +} + +#[derive(Debug)] +pub struct ColumnSeparator; + +#[derive(Debug)] +pub struct Title<'a> { + pub(crate) level: Level<'a>, + pub(crate) title: &'a str, + pub(crate) primary: bool, +} + +impl Title<'_> { + pub fn primary(mut self, primary: bool) -> Self { + self.primary = primary; self } } -/// Structure containing the slice of text to be annotated and -/// basic information about the location of the slice. -/// -/// One `Snippet` is meant to represent a single, continuous, -/// slice of source code that you want to annotate. #[derive(Debug)] -pub struct Snippet<'a> { +pub struct Snippet<'a, T> { pub(crate) origin: Option<&'a str>, pub(crate) line_start: usize, - pub(crate) source: &'a str, - pub(crate) annotations: Vec>, - + pub(crate) markers: Vec, pub(crate) fold: bool, } -impl<'a> Snippet<'a> { +impl<'a, T: Clone> Snippet<'a, T> { + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. pub fn source(source: &'a str) -> Self { Self { origin: None, line_start: 1, source, - annotations: vec![], + markers: vec![], fold: false, } } @@ -83,75 +182,229 @@ impl<'a> Snippet<'a> { self } + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. pub fn origin(mut self, origin: &'a str) -> Self { self.origin = Some(origin); self } - pub fn annotation(mut self, annotation: Annotation<'a>) -> Self { - self.annotations.push(annotation); + pub fn fold(mut self, fold: bool) -> Self { + self.fold = fold; + self + } +} + +impl<'a> Snippet<'a, Annotation<'a>> { + pub fn annotation(mut self, annotation: Annotation<'a>) -> Snippet<'a, Annotation<'a>> { + self.markers.push(annotation); self } pub fn annotations(mut self, annotation: impl IntoIterator>) -> Self { - self.annotations.extend(annotation); + self.markers.extend(annotation); + self + } +} + +impl<'a> Snippet<'a, Patch<'a>> { + pub fn patch(mut self, patch: Patch<'a>) -> Snippet<'a, Patch<'a>> { + self.markers.push(patch); self } - /// Hide lines without [`Annotation`]s - pub fn fold(mut self, fold: bool) -> Self { - self.fold = fold; + pub fn patches(mut self, patches: impl IntoIterator>) -> Self { + self.markers.extend(patches); self } } -/// An annotation for a [`Snippet`]. -/// -/// See [`Level::span`] to create a [`Annotation`] -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Annotation<'a> { - /// The byte range of the annotation in the `source` string pub(crate) range: Range, pub(crate) label: Option<&'a str>, - pub(crate) level: Level, + pub(crate) kind: AnnotationKind, + pub(crate) highlight_source: bool, } impl<'a> Annotation<'a> { + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. pub fn label(mut self, label: &'a str) -> Self { self.label = Some(label); self } -} -/// Types of annotations. -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum Level { - /// Error annotations are displayed using red color and "^" character. - Error, - /// Warning annotations are displayed using blue color and "-" character. - Warning, - Info, - Note, - Help, + pub fn highlight_source(mut self, highlight_source: bool) -> Self { + self.highlight_source = highlight_source; + self + } } -impl Level { - pub fn title(self, title: &str) -> Message<'_> { - Message { - level: self, - id: None, - title, - snippets: vec![], - footer: vec![], - } - } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum AnnotationKind { + /// Color to [`Message`]'s [`Level`] + Primary, + /// "secondary"; fixed color + Context, +} - /// Create a [`Annotation`] with the given span for a [`Snippet`] +impl AnnotationKind { pub fn span<'a>(self, span: Range) -> Annotation<'a> { Annotation { range: span, label: None, - level: self, + kind: self, + highlight_source: false, + } + } + + pub(crate) fn is_primary(&self) -> bool { + matches!(self, AnnotationKind::Primary) + } +} + +#[derive(Clone, Debug)] +pub struct Patch<'a> { + pub(crate) range: Range, + pub(crate) replacement: &'a str, +} + +impl<'a> Patch<'a> { + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + pub fn new(range: Range, replacement: &'a str) -> Self { + Self { range, replacement } + } + + pub(crate) fn is_addition(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && !self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_deletion(&self, sm: &SourceMap<'_>) -> bool { + self.replacement.trim().is_empty() && self.replaces_meaningful_content(sm) + } + + pub(crate) fn is_replacement(&self, sm: &SourceMap<'_>) -> bool { + !self.replacement.is_empty() && self.replaces_meaningful_content(sm) + } + + /// Whether this is a replacement that overwrites source with a snippet + /// in a way that isn't a superset of the original string. For example, + /// replacing "abc" with "abcde" is not destructive, but replacing it + /// it with "abx" is, since the "c" character is lost. + pub(crate) fn is_destructive_replacement(&self, sm: &SourceMap<'_>) -> bool { + self.is_replacement(sm) + && !sm + .span_to_snippet(self.range.clone()) + // This should use `is_some_and` when our MSRV is >= 1.70 + .map_or(false, |s| { + as_substr(s.trim(), self.replacement.trim()).is_some() + }) + } + + fn replaces_meaningful_content(&self, sm: &SourceMap<'_>) -> bool { + sm.span_to_snippet(self.range.clone()) + .map_or(!self.range.is_empty(), |snippet| !snippet.trim().is_empty()) + } + + /// Try to turn a replacement into an addition when the span that is being + /// overwritten matches either the prefix or suffix of the replacement. + pub(crate) fn trim_trivial_replacements(&mut self, sm: &'a SourceMap<'a>) { + if self.replacement.is_empty() { + return; + } + let Some(snippet) = sm.span_to_snippet(self.range.clone()) else { + return; + }; + + if let Some((prefix, substr, suffix)) = as_substr(snippet, self.replacement) { + self.range = self.range.start + prefix..self.range.end.saturating_sub(suffix); + self.replacement = substr; + } + } +} + +#[derive(Clone, Debug)] +pub struct Origin<'a> { + pub(crate) origin: &'a str, + pub(crate) line: Option, + pub(crate) char_column: Option, + pub(crate) primary: bool, + pub(crate) label: Option<&'a str>, +} + +impl<'a> Origin<'a> { + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + pub fn new(origin: &'a str) -> Self { + Self { + origin, + line: None, + char_column: None, + primary: false, + label: None, } } + + pub fn line(mut self, line: usize) -> Self { + self.line = Some(line); + self + } + + pub fn char_column(mut self, char_column: usize) -> Self { + self.char_column = Some(char_column); + self + } + + pub fn primary(mut self, primary: bool) -> Self { + self.primary = primary; + self + } + + /// Text passed to this function is considered "untrusted input", as such + /// all text is passed through a normalization function. Pre-styled text is + /// not allowed to be passed to this function. + pub fn label(mut self, label: &'a str) -> Self { + self.label = Some(label); + self + } +} + +fn newline_count(body: &str) -> usize { + #[cfg(feature = "simd")] + { + memchr::memchr_iter(b'\n', body.as_bytes()) + .count() + .saturating_sub(1) + } + #[cfg(not(feature = "simd"))] + { + body.lines().count().saturating_sub(1) + } +} + +/// Given an original string like `AACC`, and a suggestion like `AABBCC`, try to detect +/// the case where a substring of the suggestion is "sandwiched" in the original, like +/// `BB` is. Return the length of the prefix, the "trimmed" suggestion, and the length +/// of the suffix. +fn as_substr<'a>(original: &'a str, suggestion: &'a str) -> Option<(usize, &'a str, usize)> { + let common_prefix = original + .chars() + .zip(suggestion.chars()) + .take_while(|(c1, c2)| c1 == c2) + .map(|(c, _)| c.len_utf8()) + .sum(); + let original = &original[common_prefix..]; + let suggestion = &suggestion[common_prefix..]; + if let Some(stripped) = suggestion.strip_suffix(original) { + let common_suffix = original.len(); + Some((common_prefix, stripped, common_suffix)) + } else { + None + } } diff --git a/tests/examples.rs b/tests/examples.rs index b6576629..66dd94be 100644 --- a/tests/examples.rs +++ b/tests/examples.rs @@ -1,3 +1,17 @@ +#[test] +fn custom_error() { + let target = "custom_error"; + let expected = snapbox::file!["../examples/custom_error.svg": TermSvg]; + assert_example(target, expected); +} + +#[test] +fn custom_level() { + let target = "custom_level"; + let expected = snapbox::file!["../examples/custom_level.svg": TermSvg]; + assert_example(target, expected); +} + #[test] fn expected_type() { let target = "expected_type"; @@ -19,6 +33,20 @@ fn format() { assert_example(target, expected); } +#[test] +fn highlight_source() { + let target = "highlight_source"; + let expected = snapbox::file!["../examples/highlight_source.svg": TermSvg]; + assert_example(target, expected); +} + +#[test] +fn highlight_title() { + let target = "highlight_title"; + let expected = snapbox::file!["../examples/highlight_title.svg": TermSvg]; + assert_example(target, expected); +} + #[test] fn multislice() { let target = "multislice"; diff --git a/tests/fixtures/color/ann_eof.svg b/tests/fixtures/color/ann_eof.svg index b0fb8b6c..aeb4f8cf 100644 --- a/tests/fixtures/color/ann_eof.svg +++ b/tests/fixtures/color/ann_eof.svg @@ -19,13 +19,13 @@ - error: expected `.`, `=` + error: expected `.`, `=` - --> Cargo.toml:1:5 + --> Cargo.toml:1:5 | - 1 | asdf + 1 | asdf | ^ diff --git a/tests/fixtures/color/ann_eof.toml b/tests/fixtures/color/ann_eof.toml index cee5f0fb..ac129f3f 100644 --- a/tests/fixtures/color/ann_eof.toml +++ b/tests/fixtures/color/ann_eof.toml @@ -2,14 +2,14 @@ level = "Error" title = "expected `.`, `=`" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = "asdf" line_start = 1 origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [4, 4] +annotations = [ + { label = "", kind = "Primary", range = [4, 4] }, +] [renderer] color = true diff --git a/tests/fixtures/color/ann_insertion.svg b/tests/fixtures/color/ann_insertion.svg index 35d65a05..57c90a23 100644 --- a/tests/fixtures/color/ann_insertion.svg +++ b/tests/fixtures/color/ann_insertion.svg @@ -19,13 +19,13 @@ - error: expected `.`, `=` + error: expected `.`, `=` - --> Cargo.toml:1:3 + --> Cargo.toml:1:3 | - 1 | asf + 1 | asf | ^ 'd' belongs here diff --git a/tests/fixtures/color/ann_insertion.toml b/tests/fixtures/color/ann_insertion.toml index bf7411ef..13bc13ca 100644 --- a/tests/fixtures/color/ann_insertion.toml +++ b/tests/fixtures/color/ann_insertion.toml @@ -2,14 +2,14 @@ level = "Error" title = "expected `.`, `=`" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = "asf" line_start = 1 origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "'d' belongs here" -level = "Error" -range = [2, 2] +annotations = [ + { label = "'d' belongs here", kind = "Primary", range = [2, 2] } +] [renderer] color = true diff --git a/tests/fixtures/color/ann_multiline.svg b/tests/fixtures/color/ann_multiline.svg index 949eddcb..2ff0364b 100644 --- a/tests/fixtures/color/ann_multiline.svg +++ b/tests/fixtures/color/ann_multiline.svg @@ -19,19 +19,19 @@ - error[E0027]: pattern does not mention fields `lineno`, `content` + error[E0027]: pattern does not mention fields `lineno`, `content` - --> src/display_list.rs:139:32 + --> src/display_list.rs:139:32 | - 139 | if let DisplayLine::Source { + 139 | if let DisplayLine::Source { - | ________________________________^ + | ________________________________^ - 140 | | ref mut inline_marks, + 140 | | ref mut inline_marks, - 141 | | } = body[body_idx] + 141 | | } = body[body_idx] | |_________________________^ missing fields `lineno`, `content` diff --git a/tests/fixtures/color/ann_multiline.toml b/tests/fixtures/color/ann_multiline.toml index 9d8c30f9..722c3e18 100644 --- a/tests/fixtures/color/ann_multiline.toml +++ b/tests/fixtures/color/ann_multiline.toml @@ -3,7 +3,8 @@ level = "Error" id = "E0027" title = "pattern does not mention fields `lineno`, `content`" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = """ if let DisplayLine::Source { ref mut inline_marks, @@ -12,10 +13,9 @@ source = """ line_start = 139 origin = "src/display_list.rs" fold = false -[[message.snippets.annotations]] -label = "missing fields `lineno`, `content`" -level = "Error" -range = [31, 128] +annotations = [ + { label = "missing fields `lineno`, `content`", kind = "Primary", range = [31, 128] } +] [renderer] color = true diff --git a/tests/fixtures/color/ann_multiline2.svg b/tests/fixtures/color/ann_multiline2.svg index 064826a3..24827f66 100644 --- a/tests/fixtures/color/ann_multiline2.svg +++ b/tests/fixtures/color/ann_multiline2.svg @@ -19,19 +19,19 @@ - error[E####]: spacing error found + error[E####]: spacing error found - --> foo.txt:26:12 + --> foo.txt:26:12 | - 26 | This is an example + 26 | This is an example | ^^^^^^^ this should not be on separate lines - 27 | of an edge case of an annotation overflowing + 27 | of an edge case of an annotation overflowing - 28 | to exactly one character on next line. + 28 | to exactly one character on next line. diff --git a/tests/fixtures/color/ann_multiline2.toml b/tests/fixtures/color/ann_multiline2.toml index 259d94b4..329beb49 100644 --- a/tests/fixtures/color/ann_multiline2.toml +++ b/tests/fixtures/color/ann_multiline2.toml @@ -3,7 +3,8 @@ level = "Error" id = "E####" title = "spacing error found" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = """ This is an example of an edge case of an annotation overflowing @@ -12,10 +13,9 @@ to exactly one character on next line. line_start = 26 origin = "foo.txt" fold = false -[[message.snippets.annotations]] -label = "this should not be on separate lines" -level = "Error" -range = [11, 19] +annotations = [ + { label = "this should not be on separate lines", kind = "Primary", range = [11, 19] }, +] [renderer] color = true diff --git a/tests/fixtures/color/ann_removed_nl.svg b/tests/fixtures/color/ann_removed_nl.svg index b0fb8b6c..aeb4f8cf 100644 --- a/tests/fixtures/color/ann_removed_nl.svg +++ b/tests/fixtures/color/ann_removed_nl.svg @@ -19,13 +19,13 @@ - error: expected `.`, `=` + error: expected `.`, `=` - --> Cargo.toml:1:5 + --> Cargo.toml:1:5 | - 1 | asdf + 1 | asdf | ^ diff --git a/tests/fixtures/color/ann_removed_nl.toml b/tests/fixtures/color/ann_removed_nl.toml index 36f74ef6..8ed96bcc 100644 --- a/tests/fixtures/color/ann_removed_nl.toml +++ b/tests/fixtures/color/ann_removed_nl.toml @@ -2,14 +2,14 @@ level = "Error" title = "expected `.`, `=`" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = "asdf" line_start = 1 origin = "Cargo.toml" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [4, 5] +annotations = [ + { label = "", kind = "Primary", range = [4, 5] }, +] [renderer] color = true diff --git a/tests/fixtures/color/ensure-emoji-highlight-width.svg b/tests/fixtures/color/ensure-emoji-highlight-width.svg index 077bca20..14624fb6 100644 --- a/tests/fixtures/color/ensure-emoji-highlight-width.svg +++ b/tests/fixtures/color/ensure-emoji-highlight-width.svg @@ -19,13 +19,13 @@ - error: invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) + error: invalid character ` ` in package name: `haha this isn't a valid name 🐛`, characters must be Unicode XID characters (numbers, `-`, `_`, or most letters) - --> <file>:7:1 + --> <file>:7:1 | - 7 | "haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } + 7 | "haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/tests/fixtures/color/ensure-emoji-highlight-width.toml b/tests/fixtures/color/ensure-emoji-highlight-width.toml index 52168b48..8d7a14aa 100644 --- a/tests/fixtures/color/ensure-emoji-highlight-width.toml +++ b/tests/fixtures/color/ensure-emoji-highlight-width.toml @@ -3,16 +3,16 @@ title = "invalid character ` ` in package name: `haha this isn't a valid name level = "Error" -[[message.snippets]] +[[message.sections]] +type = "Cause" source = """ "haha this isn't a valid name 🐛" = { package = "libc", version = "0.1" } """ line_start = 7 origin = "" -[[message.snippets.annotations]] -label = "" -level = "Error" -range = [0, 35] +annotations = [ + { label = "", kind = "Primary", range = [0, 35] }, +] [renderer] color = true diff --git a/tests/fixtures/color/fold_ann_multiline.svg b/tests/fixtures/color/fold_ann_multiline.svg index 39323c5f..80197e5c 100644 --- a/tests/fixtures/color/fold_ann_multiline.svg +++ b/tests/fixtures/color/fold_ann_multiline.svg @@ -1,10 +1,9 @@ - +