From 52bf734fc084c6a0aa5eb0a44b2d47c88c0868e4 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 19 Jun 2024 11:38:04 -0600 Subject: [PATCH 1/6] test: Add tests for multiple annotations per line --- tests/formatter.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) diff --git a/tests/formatter.rs b/tests/formatter.rs index fa3927c..d0ac369 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -732,3 +732,168 @@ error let renderer = Renderer::plain().anonymized_line_numbers(false); assert_data_eq!(renderer.render(input).to_string(), expected); } + +#[test] +fn two_single_line_same_line() { + let source = r#"bar = { version = "0.1.0", optional = true }"#; + let input = Level::Error.title("unused optional dependency").snippet( + Snippet::source(source) + .origin("Cargo.toml") + .line_start(4) + .annotation( + Level::Error + .span(0..3) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Info + .span(27..42) + .label("This should also be long but not too long"), + ), + ); + let expected = str![[r#" +error: unused optional dependency + --> Cargo.toml:4:1 + | +4 | bar = { version = "0.1.0", optional = true } + | ^^^ I need this to be really long so I can test overlaps + | --------------- info: This should also be long but not too long + | +"#]]; + let renderer = Renderer::plain().anonymized_line_numbers(false); + assert_data_eq!(renderer.render(input).to_string(), expected); +} + +#[test] +fn multi_and_single() { + let source = r#"bar = { version = "0.1.0", optional = true } +this is another line +so is this +bar = { version = "0.1.0", optional = true } +"#; + let input = Level::Error.title("unused optional dependency").snippet( + Snippet::source(source) + .line_start(4) + .annotation( + Level::Error + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Info + .span(27..42) + .label("This should also be long but not too long"), + ), + ); + let expected = str![[r#" +error: unused optional dependency + | +4 | bar = { version = "0.1.0", optional = true } + | __________________________________________^ + | --------------- info: This should also be long but not too long +5 | | this is another line +6 | | so is this +7 | | bar = { version = "0.1.0", optional = true } + | |__________________________________________^ I need this to be really long so I can test overlaps + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} + +#[test] +fn two_multi_and_single() { + let source = r#"bar = { version = "0.1.0", optional = true } +this is another line +so is this +bar = { version = "0.1.0", optional = true } +"#; + let input = Level::Error.title("unused optional dependency").snippet( + Snippet::source(source) + .line_start(4) + .annotation( + Level::Error + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Error + .span(8..102) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Info + .span(27..42) + .label("This should also be long but not too long"), + ), + ); + let expected = str![[r#" +error: unused optional dependency + | +4 | bar = { version = "0.1.0", optional = true } + | __________________________________________^ + | _________^ + | --------------- info: This should also be long but not too long +5 | || this is another line +6 | || so is this +7 | || bar = { version = "0.1.0", optional = true } + | ||__________________________________________^ I need this to be really long so I can test overlaps + | ||_________________________^ I need this to be really long so I can test overlaps + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} + +#[test] +fn three_multi_and_single() { + let source = r#"bar = { version = "0.1.0", optional = true } +this is another line +so is this +bar = { version = "0.1.0", optional = true } +this is another line +"#; + let input = Level::Error.title("unused optional dependency").snippet( + Snippet::source(source) + .line_start(4) + .annotation( + Level::Error + .span(41..119) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Error + .span(8..102) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Error + .span(48..126) + .label("I need this to be really long so I can test overlaps"), + ) + .annotation( + Level::Info + .span(27..42) + .label("This should also be long but not too long"), + ), + ); + let expected = str![[r#" +error: unused optional dependency + | +4 | bar = { version = "0.1.0", optional = true } + | __________________________________________^ + | _________^ + | --------------- info: This should also be long but not too long +5 | || this is another line + | ||____^ +6 | ||| so is this +7 | ||| bar = { version = "0.1.0", optional = true } + | |||__________________________________________^ I need this to be really long so I can test overlaps + | |||_________________________^ I need this to be really long so I can test overlaps +8 | | this is another line + | |____^ I need this to be really long so I can test overlaps + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} From d1d3a628e5eab73c5524d919820d85e2969e80ac Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 19 Jun 2024 11:39:56 -0600 Subject: [PATCH 2/6] test: Add some of Rust's parser tests --- tests/rustc_tests.rs | 778 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 778 insertions(+) create mode 100644 tests/rustc_tests.rs diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs new file mode 100644 index 0000000..f87b206 --- /dev/null +++ b/tests/rustc_tests.rs @@ -0,0 +1,778 @@ +//! These tests have been adapted from [Rust's parser tests][parser-tests]. +//! +//! [parser-tests]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_parse/src/parser/tests.rs + +use annotate_snippets::{Level, Renderer, Snippet}; + +use snapbox::{assert_data_eq, str}; + +#[test] +fn ends_on_col0() { + let source = r#" +fn foo() { +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(10..13).label("test")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:2:10 + | +2 | fn foo() { + | __________^ +3 | | } + | |_^ test + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn ends_on_col2() { + let source = r#" +fn foo() { + + + } +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(10..17).label("test")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:2:10 + | +2 | fn foo() { + | __________^ +3 | | +4 | | +5 | | } + | |___^ test + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn non_nested() { + let source = r#" +fn foo() { + X0 Y0 + X1 Y1 + X2 Y2 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..32).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(17..35) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | X0 Y0 + | ___^ + | ______- +4 | || X1 Y1 +5 | || X2 Y2 + | ||____^ `X` is a good letter + | ||_______- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn nested() { + let source = r#" +fn foo() { + X0 Y0 + Y1 X1 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(17..24) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | X0 Y0 + | ___^ + | ______- +4 | || Y1 X1 + | ||_______^ `X` is a good letter + | ||____- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn different_overlap() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(17..38).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(31..49) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:6 + | +3 | X0 Y0 Z0 + | ______^ +4 | | X1 Y1 Z1 + | |_________- +5 | || X2 Y2 Z2 + | ||____^ `X` is a good letter +6 | | X3 Y3 Z3 + | |____- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn triple_overlap() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..38).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(17..41) + .label("`Y` is a good letter too"), + ) + .annotation(Level::Warning.span(20..44).label("`Z` label")), + ); + + let expected = str![[r#" +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 + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn triple_exact_overlap() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..38).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(14..38) + .label("`Y` is a good letter too"), + ) + .annotation(Level::Warning.span(14..38).label("`Z` label")), + ); + + let expected = str![[r#" +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 + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn minimum_depth() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(17..27).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(28..44) + .label("`Y` is a good letter too"), + ) + .annotation(Level::Warning.span(36..52).label("`Z`")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:6 + | +3 | X0 Y0 Z0 + | ______^ +4 | | X1 Y1 Z1 + | |____^ `X` is a good letter + | |______- +5 | | X2 Y2 Z2 + | |__________- `Y` is a good letter too + | |___- +6 | | X3 Y3 Z3 + | |_______- `Z` + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn non_overlapping() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(39..55) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | X0 Y0 Z0 + | ___^ +4 | | X1 Y1 Z1 + | |____^ `X` is a good letter +5 | X2 Y2 Z2 + | ______- +6 | | X3 Y3 Z3 + | |__________- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn overlapping_start_and_end() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 + X2 Y2 Z2 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(17..27).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(31..55) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:6 + | +3 | X0 Y0 Z0 + | ______^ +4 | | X1 Y1 Z1 + | |____^ `X` is a good letter + | |_________- +5 | | X2 Y2 Z2 +6 | | X3 Y3 Z3 + | |__________- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_primary_without_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(18..25).label("")) + .annotation(Level::Warning.span(14..27).label("`a` is a good letter")) + .annotation(Level::Warning.span(22..23).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:7 + | +3 | a { b { c } d } + | ^^^^^^^ + | ------------- `a` is a good letter + | - + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_secondary_without_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("`a` is a good letter")) + .annotation(Level::Warning.span(18..25).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ `a` is a good letter + | ------- + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_primary_without_message_2() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(18..25).label("`b` is a good letter")) + .annotation(Level::Warning.span(14..27).label("")) + .annotation(Level::Warning.span(22..23).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:7 + | +3 | a { b { c } d } + | ^^^^^^^ `b` is a good letter + | ------------- + | - + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_secondary_without_message_2() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("")) + .annotation(Level::Warning.span(18..25).label("`b` is a good letter")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ + | ------- `b` is a good letter + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_secondary_without_message_3() { + let source = r#" +fn foo() { + a bc d +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..18).label("`a` is a good letter")) + .annotation(Level::Warning.span(18..22).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a bc d + | ^^^^ `a` is a good letter + | ---- + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_without_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("")) + .annotation(Level::Warning.span(18..25).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ + | ------- + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_without_message_2() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(18..25).label("")) + .annotation(Level::Warning.span(14..27).label("")) + .annotation(Level::Warning.span(22..23).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:7 + | +3 | a { b { c } d } + | ^^^^^^^ + | ------------- + | - + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn multiple_labels_with_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("`a` is a good letter")) + .annotation(Level::Warning.span(18..25).label("`b` is a good letter")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ `a` is a good letter + | ------- `b` is a good letter + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn ingle_label_with_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("`a` is a good letter")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ `a` is a good letter + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn single_label_without_message() { + let source = r#" +fn foo() { + a { b { c } d } +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(14..27).label("")), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:3 + | +3 | a { b { c } d } + | ^^^^^^^^^^^^^ + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn long_snippet() { + let source = r#" +fn foo() { + X0 Y0 Z0 + X1 Y1 Z1 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 + X2 Y2 Z2 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(17..27).label("`X` is a good letter")) + .annotation( + Level::Warning + .span(31..76) + .label("`Y` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:6 + | + 3 | X0 Y0 Z0 + | ______^ + 4 | | X1 Y1 Z1 + | |____^ `X` is a good letter + | |_________- + 5 | | 1 +... | +15 | | X2 Y2 Z2 +16 | | X3 Y3 Z3 + | |__________- `Y` is a good letter too + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} +#[test] +fn long_snippet_multiple_spans() { + let source = r#" +fn foo() { + X0 Y0 Z0 +1 +2 +3 + X1 Y1 Z1 +4 +5 +6 + X2 Y2 Z2 +7 +8 +9 +10 + X3 Y3 Z3 +} +"#; + let input = Level::Error.title("foo").snippet( + Snippet::source(source) + .line_start(1) + .origin("test.rs") + .fold(true) + .annotation(Level::Error.span(17..73).label("`Y` is a good letter")) + .annotation( + Level::Warning + .span(37..56) + .label("`Z` is a good letter too"), + ), + ); + + let expected = str![[r#" +error: foo + --> test.rs:3:6 + | + 3 | X0 Y0 Z0 + | ______^ + 4 | | 1 + 5 | | 2 + 6 | | 3 + 7 | | X1 Y1 Z1 + | |_________- + 8 | || 4 + 9 | || 5 +10 | || 6 +11 | || X2 Y2 Z2 + | ||__________- `Z` is a good letter too +12 | | 7 +... | +15 | | 10 +16 | | X3 Y3 Z3 + | |_______^ `Y` is a good letter + | +"#]]; + let renderer = Renderer::plain(); + assert_data_eq!(renderer.render(input).to_string(), expected); +} From 9ec2939584d9d4ad6c601020d5c68ca08bfceb66 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 19 Jun 2024 21:02:23 -0600 Subject: [PATCH 3/6] fix: Properly handle multiple annotations on one line --- examples/expected_type.svg | 8 +- examples/footer.svg | 8 +- examples/format.svg | 12 +- examples/multislice.svg | 8 +- src/renderer/display_list.rs | 768 ++++++++++++------ src/renderer/mod.rs | 1 + src/renderer/styled_buffer.rs | 97 +++ tests/fixtures/no-color/simple.svg | 2 +- tests/fixtures/no-color/strip_line_non_ws.svg | 10 +- tests/formatter.rs | 46 +- tests/rustc_tests.rs | 177 ++-- 11 files changed, 750 insertions(+), 387 deletions(-) create mode 100644 src/renderer/styled_buffer.rs diff --git a/examples/expected_type.svg b/examples/expected_type.svg index a355dbc..ed19ef3 100644 --- a/examples/expected_type.svg +++ b/examples/expected_type.svg @@ -23,11 +23,11 @@ --> examples/footer.rs:29:25 - | + | 26 | annotations: vec![SourceAnnotation { - | ---------------- info: while parsing this struct + | ---------------- info: while parsing this struct 27 | label: "expected struct `annotate_snippets::snippet::Slice`, found reference" @@ -35,9 +35,9 @@ 29 | range: <22, 25>, - | ^^ expected struct `annotate_snippets::snippet::Slice`, found reference + | ^^ expected struct `annotate_snippets::snippet::Slice`, found reference - | + | diff --git a/examples/footer.svg b/examples/footer.svg index 34f81c8..76e7d77 100644 --- a/examples/footer.svg +++ b/examples/footer.svg @@ -24,15 +24,15 @@ --> src/multislice.rs:13:22 - | + | 13 | slices: vec!["A", - | ^^^ expected struct `annotate_snippets::snippet::Slice`, found reference + | ^^^ expected struct `annotate_snippets::snippet::Slice`, found reference - | + | - = note: expected type: `snippet::Annotation` + = note: expected type: `snippet::Annotation` found type: `__&__snippet::Annotation` diff --git a/examples/format.svg b/examples/format.svg index dd6c1c0..a427c94 100644 --- a/examples/format.svg +++ b/examples/format.svg @@ -24,15 +24,15 @@ --> src/format.rs:51:6 - | + | 51 | ) -> Option<String> { - | -------------- expected `Option<String>` because of return type + | -------------- expected `Option<String>` because of return type 52 | for ann in annotations { - | _____^ + | _____^ 53 | | match (ann.range.0, ann.range.1) { @@ -52,7 +52,7 @@ 61 | | }; - 62 | | + 62 | | 63 | | return Some(format!( @@ -74,9 +74,9 @@ 72 | | } - | |____^ expected enum `std::option::Option` + | |____^ expected enum `std::option::Option` - | + | diff --git a/examples/multislice.svg b/examples/multislice.svg index 2ab959a..216a359 100644 --- a/examples/multislice.svg +++ b/examples/multislice.svg @@ -23,19 +23,19 @@ --> src/format.rs - | + | 51 | Foo - | + | ::: src/display.rs - | + | 129 | Faa - | + | diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs index bc46f7f..2c8dbf9 100644 --- a/src/renderer/display_list.rs +++ b/src/renderer/display_list.rs @@ -32,11 +32,13 @@ //! //! The above snippet has been built out of the following structure: use crate::snippet; -use std::cmp::{max, min}; -use std::fmt::{Display, Write}; +use std::cmp::{max, min, Reverse}; +use std::collections::HashMap; +use std::fmt::Display; use std::ops::Range; use std::{cmp, fmt}; +use crate::renderer::styled_buffer::StyledBuffer; use crate::renderer::{stylesheet::Stylesheet, Margin, Style, DEFAULT_TERM_WIDTH}; const ANONYMIZED_LINE_NUM: &str = "LL"; @@ -83,19 +85,31 @@ impl<'a> Display for DisplayList<'a> { } else { ((lineno_width as f64).log10().floor() as usize) + 1 }; - let inline_marks_width = self.body.iter().fold(0, |max, set| { - set.display_lines.iter().fold(max, |max, line| match line { - DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max), - _ => max, + + let multiline_depth = self.body.iter().fold(0, |max, set| { + set.display_lines.iter().fold(max, |max2, line| match line { + DisplayLine::Source { annotations, .. } => cmp::max( + annotations.iter().fold(max2, |max3, line| { + cmp::max( + match line.annotation_part { + DisplayAnnotationPart::Standalone => 0, + DisplayAnnotationPart::LabelContinuation => 0, + DisplayAnnotationPart::MultilineStart(depth) => depth + 1, + DisplayAnnotationPart::MultilineEnd(depth) => depth + 1, + }, + max3, + ) + }), + max, + ), + _ => max2, }) }); - - let mut count_offset = 0; + let mut buffer = StyledBuffer::new(); for set in self.body.iter() { - self.format_set(set, lineno_width, inline_marks_width, count_offset, f)?; - count_offset += set.display_lines.len(); + self.format_set(set, lineno_width, multiline_depth, &mut buffer)?; } - Ok(()) + write!(f, "{}", buffer.render(self.stylesheet)?) } } @@ -119,27 +133,18 @@ impl<'a> DisplayList<'a> { &self, set: &DisplaySet<'_>, lineno_width: usize, - inline_marks_width: usize, - count_offset: usize, - f: &mut fmt::Formatter<'_>, + multiline_depth: usize, + buffer: &mut StyledBuffer, ) -> fmt::Result { - let body_len = self - .body - .iter() - .map(|set| set.display_lines.len()) - .sum::(); - for (i, line) in set.display_lines.iter().enumerate() { + for line in &set.display_lines { set.format_line( line, lineno_width, - inline_marks_width, + multiline_depth, self.stylesheet, self.anonymized_line_numbers, - f, + buffer, )?; - if i + count_offset + 1 < body_len { - f.write_char('\n')?; - } } Ok(()) } @@ -154,36 +159,27 @@ pub(crate) struct DisplaySet<'a> { impl<'a> DisplaySet<'a> { fn format_label( &self, + line_offset: usize, label: &[DisplayTextFragment<'_>], stylesheet: &Stylesheet, - f: &mut fmt::Formatter<'_>, + buffer: &mut StyledBuffer, ) -> fmt::Result { - let emphasis_style = stylesheet.emphasis(); - for fragment in label { - match fragment.style { - DisplayTextStyle::Regular => fragment.content.fmt(f)?, - DisplayTextStyle::Emphasis => { - write!( - f, - "{}{}{}", - emphasis_style.render(), - fragment.content, - emphasis_style.render_reset() - )?; - } - } + let style = match fragment.style { + DisplayTextStyle::Regular => stylesheet.none(), + DisplayTextStyle::Emphasis => stylesheet.emphasis(), + }; + buffer.append(line_offset, fragment.content, *style); } Ok(()) } - fn format_annotation( &self, + line_offset: usize, annotation: &Annotation<'_>, continuation: bool, - in_source: bool, stylesheet: &Stylesheet, - f: &mut fmt::Formatter<'_>, + buffer: &mut StyledBuffer, ) -> fmt::Result { let color = get_annotation_style(&annotation.annotation_type, stylesheet); let formatted_len = if let Some(id) = &annotation.id { @@ -193,31 +189,27 @@ impl<'a> DisplaySet<'a> { }; if continuation { - format_repeat_char(' ', formatted_len + 2, f)?; - return self.format_label(&annotation.label, stylesheet, f); + for _ in 0..formatted_len + 2 { + buffer.append(line_offset, " ", Style::new()); + } + return self.format_label(line_offset, &annotation.label, stylesheet, buffer); } if formatted_len == 0 { - self.format_label(&annotation.label, stylesheet, f) + self.format_label(line_offset, &annotation.label, stylesheet, buffer) } else { - write!(f, "{}", color.render())?; - format_annotation_type(&annotation.annotation_type, f)?; - if let Some(id) = &annotation.id { - f.write_char('[')?; - f.write_str(id)?; - f.write_char(']')?; - } - write!(f, "{}", color.render_reset())?; + let id = match &annotation.id { + Some(id) => format!("[{}]", id), + None => String::new(), + }; + buffer.append( + line_offset, + &format!("{}{}", annotation_type_str(&annotation.annotation_type), id), + *color, + ); if !is_annotation_empty(annotation) { - if in_source { - write!(f, "{}", color.render())?; - f.write_str(": ")?; - self.format_label(&annotation.label, stylesheet, f)?; - write!(f, "{}", color.render_reset())?; - } else { - f.write_str(": ")?; - self.format_label(&annotation.label, stylesheet, f)?; - } + buffer.append(line_offset, ": ", stylesheet.none); + self.format_label(line_offset, &annotation.label, stylesheet, buffer)?; } Ok(()) } @@ -226,10 +218,11 @@ impl<'a> DisplaySet<'a> { #[inline] fn format_raw_line( &self, + line_offset: usize, line: &DisplayRawLine<'_>, lineno_width: usize, stylesheet: &Stylesheet, - f: &mut fmt::Formatter<'_>, + buffer: &mut StyledBuffer, ) -> fmt::Result { match line { DisplayRawLine::Origin { @@ -242,34 +235,15 @@ impl<'a> DisplaySet<'a> { DisplayHeaderType::Continuation => ":::", }; let lineno_color = stylesheet.line_no(); - + buffer.puts(line_offset, lineno_width, header_sigil, *lineno_color); + buffer.puts(line_offset, lineno_width + 4, path, stylesheet.none); if let Some((col, row)) = pos { - format_repeat_char(' ', lineno_width, f)?; - write!( - f, - "{}{}{}", - lineno_color.render(), - header_sigil, - lineno_color.render_reset() - )?; - f.write_char(' ')?; - path.fmt(f)?; - f.write_char(':')?; - col.fmt(f)?; - f.write_char(':')?; - row.fmt(f) - } else { - format_repeat_char(' ', lineno_width, f)?; - write!( - f, - "{}{}{}", - lineno_color.render(), - header_sigil, - lineno_color.render_reset() - )?; - f.write_char(' ')?; - path.fmt(f) + buffer.append(line_offset, ":", stylesheet.none); + buffer.append(line_offset, col.to_string().as_str(), stylesheet.none); + buffer.append(line_offset, ":", stylesheet.none); + buffer.append(line_offset, row.to_string().as_str(), stylesheet.none); } + Ok(()) } DisplayRawLine::Annotation { annotation, @@ -278,35 +252,35 @@ impl<'a> DisplaySet<'a> { } => { if *source_aligned { if *continuation { - format_repeat_char(' ', lineno_width + 3, f)?; + for _ in 0..lineno_width + 3 { + buffer.append(line_offset, " ", stylesheet.none); + } } else { let lineno_color = stylesheet.line_no(); - format_repeat_char(' ', lineno_width, f)?; - f.write_char(' ')?; - write!( - f, - "{}={}", - lineno_color.render(), - lineno_color.render_reset() - )?; - f.write_char(' ')?; + for _ in 0..lineno_width + 1 { + buffer.append(line_offset, " ", stylesheet.none); + } + buffer.append(line_offset, "=", *lineno_color); + buffer.append(line_offset, " ", *lineno_color); } } - self.format_annotation(annotation, *continuation, false, stylesheet, f) + self.format_annotation(line_offset, annotation, *continuation, stylesheet, buffer) } } } + // Adapted from https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/emitter.rs#L706-L1155 #[inline] fn format_line( &self, dl: &DisplayLine<'_>, lineno_width: usize, - inline_marks_width: usize, + multiline_depth: usize, stylesheet: &Stylesheet, anonymized_line_numbers: bool, - f: &mut fmt::Formatter<'_>, + buffer: &mut StyledBuffer, ) -> fmt::Result { + let line_offset = buffer.num_lines(); match dl { DisplayLine::Source { lineno, @@ -316,36 +290,45 @@ impl<'a> DisplaySet<'a> { } => { let lineno_color = stylesheet.line_no(); if anonymized_line_numbers && lineno.is_some() { - write!(f, "{}", lineno_color.render())?; - f.write_str(ANONYMIZED_LINE_NUM)?; - f.write_str(" |")?; - write!(f, "{}", lineno_color.render_reset())?; + let num = format!("{:>width$} |", ANONYMIZED_LINE_NUM, width = lineno_width); + buffer.puts(line_offset, 0, &num, *lineno_color); } else { - write!(f, "{}", lineno_color.render())?; match lineno { - Some(n) => write!(f, "{:>width$}", n, width = lineno_width), - None => format_repeat_char(' ', lineno_width, f), - }?; - f.write_str(" |")?; - write!(f, "{}", lineno_color.render_reset())?; + Some(n) => { + let num = format!("{:>width$} |", n, width = lineno_width); + buffer.puts(line_offset, 0, &num, *lineno_color); + } + None => { + buffer.putc(line_offset, lineno_width + 1, '|', *lineno_color); + } + }; } - if let DisplaySourceLine::Content { text, .. } = line { - if !inline_marks.is_empty() || 0 < inline_marks_width { - f.write_char(' ')?; - self.format_inline_marks(inline_marks, inline_marks_width, stylesheet, f)?; + // The width of the line number, a space, pipe, and a space + // `123 | ` is `lineno_width + 3`. + let width_offset = lineno_width + 3; + let code_offset = if multiline_depth == 0 { + width_offset + } else { + width_offset + multiline_depth + 1 + }; + + // Add any inline marks to the code line + if !inline_marks.is_empty() || 0 < multiline_depth { + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; } - f.write_char(' ')?; let text = normalize_whitespace(text); let line_len = text.as_bytes().len(); - let mut left = self.margin.left(line_len); + let left = self.margin.left(line_len); let right = self.margin.right(line_len); - if self.margin.was_cut_left() { - "...".fmt(f)?; - left += 3; - } // On long lines, we strip the source line, accounting for unicode. let mut taken = 0; let code: String = text @@ -364,135 +347,341 @@ impl<'a> DisplaySet<'a> { true }) .collect(); - + buffer.puts(line_offset, code_offset, &code, Style::new()); + if self.margin.was_cut_left() { + // We have stripped some code/whitespace from the beginning, make it clear. + buffer.puts(line_offset, code_offset, "...", *lineno_color); + } if self.margin.was_cut_right(line_len) { - code[..taken.saturating_sub(3)].fmt(f)?; - "...".fmt(f)?; - } else { - code.fmt(f)?; + buffer.puts(line_offset, code_offset + taken - 3, "...", *lineno_color); } - let mut left: usize = text + let left: usize = text .chars() .take(left) .map(|ch| unicode_width::UnicodeWidthChar::width(ch).unwrap_or(1)) .sum(); - if self.margin.was_cut_left() { - left = left.saturating_sub(3); + let mut annotations = annotations.clone(); + annotations.sort_by_key(|a| Reverse(a.range.0)); + + let mut annotations_positions = vec![]; + let mut line_len = 0; + let mut p = 0; + for (i, annotation) in annotations.iter().enumerate() { + for (j, next) in annotations.iter().enumerate() { + // This label overlaps with another one and both take space ( + // they have text and are not multiline lines). + if overlaps(next, annotation, 0) + && annotation.has_label() + && j > i + && 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.range.0 == annotation.range.0 + && next.range.1 == annotation.range.1 + && !next.has_label() + { + continue; + } + + // This annotation needs a new line in the output. + p += 1; + break; + } + } + annotations_positions.push((p, annotation)); + for (j, next) in annotations.iter().enumerate() { + if j > i { + let l = next + .annotation + .label + .iter() + .map(|label| label.content) + .collect::>() + .join("") + .len() + + 2; + // 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 + // + // Both labels must have some text, otherwise they are not + // overlapping. Do not add a new line if this annotation or + // the next are vertical line placeholders. If either this + // or the next annotation is multiline start/end, move it + // to a new line so as not to overlap the horizontal lines. + if (overlaps(next, annotation, l) + && annotation.has_label() + && next.has_label()) + || (annotation.takes_space() && next.has_label()) + || (annotation.has_label() && next.takes_space()) + || (annotation.takes_space() && next.takes_space()) + || (overlaps(next, annotation, l) + && next.range.1 <= annotation.range.1 + && 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); } - for annotation in annotations { - // Each annotation should be on its own line - f.write_char('\n')?; - // Add the line number and the line number delimiter - write!(f, "{}", stylesheet.line_no.render())?; - format_repeat_char(' ', lineno_width, f)?; - f.write_str(" |")?; - write!(f, "{}", stylesheet.line_no.render_reset())?; - - if !inline_marks.is_empty() || 0 < inline_marks_width { - f.write_char(' ')?; - self.format_inline_marks( - inline_marks, - inline_marks_width, - stylesheet, - f, - )?; + if line_len != 0 { + line_len += 1; + } + + // Draw the column separator for any extra lines that were + // created + // + // After this we will have: + // + // 2 | fn foo() { + // | + // | + // | + // 3 | + // 4 | } + // | + if !annotations_positions.is_empty() { + for pos in 0..=line_len { + buffer.putc( + line_offset + pos + 1, + lineno_width + 1, + '|', + stylesheet.line_no, + ); + } + } + + // 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_positions { + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + let pos = pos + 1; + match annotation.annotation_part { + DisplayAnnotationPart::MultilineStart(depth) + | DisplayAnnotationPart::MultilineEnd(depth) => { + for col in width_offset + depth + ..(code_offset + annotation.range.0).saturating_sub(left) + { + buffer.putc(line_offset + pos, col + 1, '_', *style); + } + } + _ => {} + } + } + + // 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_positions { + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + let pos = pos + 1; + if pos > 1 && (annotation.has_label() || annotation.takes_space()) { + for p in line_offset + 2..=line_offset + pos { + buffer.putc( + p, + (code_offset + annotation.range.0).saturating_sub(left), + '|', + *style, + ); + } + } + match annotation.annotation_part { + DisplayAnnotationPart::MultilineStart(depth) => { + for p in line_offset + pos + 1..line_offset + line_len + 2 { + buffer.putc(p, width_offset + depth, '|', *style); + } + } + DisplayAnnotationPart::MultilineEnd(depth) => { + for p in line_offset..=line_offset + pos { + buffer.putc(p, width_offset + depth, '|', *style); + } + } + _ => {} + } + } + + // Add in any inline marks for any extra lines that have + // been created. Output should look like above. + for inline_mark in inline_marks { + if let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type { + let style = + get_annotation_style(&inline_mark.annotation_type, stylesheet); + if annotations_positions.is_empty() { + buffer.putc(line_offset, width_offset + depth, '|', *style); + } else { + for p in line_offset..=line_offset + line_len + 1 { + buffer.putc(p, width_offset + depth, '|', *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_positions { + if !is_annotation_empty(&annotation.annotation) { + let style = + get_annotation_style(&annotation.annotation_type, stylesheet); + let mut formatted_len = if let Some(id) = &annotation.annotation.id { + 2 + id.len() + + annotation_type_len(&annotation.annotation.annotation_type) + } else { + annotation_type_len(&annotation.annotation.annotation_type) + }; + let (pos, col) = if pos == 0 { + (pos + 1, (annotation.range.1 + 1).saturating_sub(left)) + } else { + (pos + 2, annotation.range.0.saturating_sub(left)) + }; + if annotation.annotation_part + == DisplayAnnotationPart::LabelContinuation + { + formatted_len = 0; + } else if formatted_len != 0 { + formatted_len += 2; + let id = match &annotation.annotation.id { + Some(id) => format!("[{}]", id), + None => String::new(), + }; + buffer.puts( + line_offset + pos, + col + code_offset, + &format!( + "{}{}: ", + annotation_type_str(&annotation.annotation_type), + id + ), + *style, + ); + } else { + formatted_len = 0; + } + let mut before = 0; + for fragment in &annotation.annotation.label { + let inner_col = before + formatted_len + col + code_offset; + buffer.puts(line_offset + pos, inner_col, fragment.content, *style); + before += fragment.content.len(); + } + } + } + + // 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_positions.sort_by_key(|(_, ann)| { + // Decreasing order. When annotations share the same length, prefer `Primary`. + Reverse(ann.len()) + }); + + // Write the underlines. + // + // After this we will have: + // + // 2 | fn foo() { + // | ____-_____^ + // | | + // | something about `foo` + // 3 | + // 4 | } + // | _^ test + for &(_, annotation) in &annotations_positions { + let mark = match annotation.annotation_type { + DisplayAnnotationType::Error => '^', + DisplayAnnotationType::Warning => '-', + DisplayAnnotationType::Info => '-', + DisplayAnnotationType::Note => '-', + DisplayAnnotationType::Help => '-', + DisplayAnnotationType::None => ' ', + }; + let style = get_annotation_style(&annotation.annotation_type, stylesheet); + for p in annotation.range.0..annotation.range.1 { + buffer.putc( + line_offset + 1, + (code_offset + p).saturating_sub(left), + mark, + *style, + ); } - self.format_source_annotation(annotation, left, stylesheet, f)?; } } else if !inline_marks.is_empty() { - f.write_char(' ')?; - self.format_inline_marks(inline_marks, inline_marks_width, stylesheet, f)?; + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; } Ok(()) } DisplayLine::Fold { inline_marks } => { - f.write_str("...")?; - if !inline_marks.is_empty() || 0 < inline_marks_width { - format_repeat_char(' ', lineno_width, f)?; - self.format_inline_marks(inline_marks, inline_marks_width, stylesheet, f)?; + buffer.puts(line_offset, 0, "...", *stylesheet.line_no()); + if !inline_marks.is_empty() || 0 < multiline_depth { + format_inline_marks( + line_offset, + inline_marks, + lineno_width, + stylesheet, + buffer, + )?; } Ok(()) } - DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width, stylesheet, f), - } - } - - fn format_inline_marks( - &self, - inline_marks: &[DisplayMark], - inline_marks_width: usize, - stylesheet: &Stylesheet, - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { - format_repeat_char(' ', inline_marks_width - inline_marks.len(), f)?; - for mark in inline_marks { - let annotation_style = get_annotation_style(&mark.annotation_type, stylesheet); - write!(f, "{}", annotation_style.render())?; - f.write_char(match mark.mark_type { - DisplayMarkType::AnnotationThrough => '|', - DisplayMarkType::AnnotationStart => '/', - })?; - write!(f, "{}", annotation_style.render_reset())?; - } - Ok(()) - } - - fn format_source_annotation( - &self, - annotation: &DisplaySourceAnnotation<'_>, - left: usize, - stylesheet: &Stylesheet, - f: &mut fmt::Formatter<'_>, - ) -> fmt::Result { - let indent_char = match annotation.annotation_part { - DisplayAnnotationPart::Standalone => ' ', - DisplayAnnotationPart::LabelContinuation => ' ', - DisplayAnnotationPart::MultilineStart => '_', - DisplayAnnotationPart::MultilineEnd => '_', - }; - let mark = match annotation.annotation_type { - DisplayAnnotationType::Error => '^', - DisplayAnnotationType::Warning => '-', - DisplayAnnotationType::Info => '-', - DisplayAnnotationType::Note => '-', - DisplayAnnotationType::Help => '-', - DisplayAnnotationType::None => ' ', - }; - let color = get_annotation_style(&annotation.annotation_type, stylesheet); - let range = ( - annotation.range.0.saturating_sub(left), - annotation.range.1.saturating_sub(left), - ); - let indent_length = match annotation.annotation_part { - DisplayAnnotationPart::LabelContinuation => range.1, - _ => range.0, - }; - write!(f, "{}", color.render())?; - format_repeat_char(indent_char, indent_length + 1, f)?; - format_repeat_char(mark, range.1 - indent_length, f)?; - write!(f, "{}", color.render_reset())?; - - if !is_annotation_empty(&annotation.annotation) { - f.write_char(' ')?; - write!(f, "{}", color.render())?; - self.format_annotation( - &annotation.annotation, - annotation.annotation_part == DisplayAnnotationPart::LabelContinuation, - true, - stylesheet, - f, - )?; - write!(f, "{}", color.render_reset())?; + DisplayLine::Raw(line) => { + self.format_raw_line(line_offset, line, lineno_width, stylesheet, buffer) + } } - Ok(()) } } /// Inline annotation which can be used in either Raw or Source line. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct Annotation<'a> { pub(crate) annotation_type: DisplayAnnotationType, pub(crate) id: Option<&'a str>, @@ -530,7 +719,7 @@ pub(crate) enum DisplaySourceLine<'a> { Empty, } -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct DisplaySourceAnnotation<'a> { pub(crate) annotation: Annotation<'a>, pub(crate) range: (usize, usize), @@ -538,6 +727,34 @@ pub(crate) struct DisplaySourceAnnotation<'a> { pub(crate) annotation_part: DisplayAnnotationPart, } +impl<'a> DisplaySourceAnnotation<'a> { + fn has_label(&self) -> bool { + !self + .annotation + .label + .iter() + .all(|label| label.content.is_empty()) + } + + // Length of this annotation as displayed in the stderr output + fn len(&self) -> usize { + // Account for usize underflows + if self.range.1 > self.range.0 { + self.range.1 - self.range.0 + } else { + self.range.0 - self.range.1 + } + } + + fn takes_space(&self) -> bool { + // Multiline annotations always have to keep vertical space. + matches!( + self.annotation_part, + DisplayAnnotationPart::MultilineStart(_) | DisplayAnnotationPart::MultilineEnd(_) + ) + } +} + /// Raw line - a line which does not have the `lineno` part and is not considered /// a part of the snippet. #[derive(Debug, PartialEq)] @@ -566,7 +783,7 @@ pub(crate) enum DisplayRawLine<'a> { } /// An inline text fragment which any label is composed of. -#[derive(Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq)] pub(crate) struct DisplayTextFragment<'a> { pub(crate) content: &'a str, pub(crate) style: DisplayTextStyle, @@ -589,9 +806,9 @@ pub(crate) enum DisplayAnnotationPart { /// A continuation of a multi-line label of an annotation. LabelContinuation, /// A line starting a multiline annotation. - MultilineStart, + MultilineStart(usize), /// A line ending a multiline annotation. - MultilineEnd, + MultilineEnd(usize), } /// A visual mark used in `inline_marks` field of the `DisplaySourceLine`. @@ -605,7 +822,7 @@ pub(crate) struct DisplayMark { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DisplayMarkType { /// A mark indicating a multiline annotation going through the current line. - AnnotationThrough, + AnnotationThrough(usize), /// A mark indicating a multiline annotation starting on the given line. AnnotationStart, } @@ -969,10 +1186,7 @@ fn fold_body(body: Vec>) -> Vec> { ref inline_marks, .. } = line { - let mut inline_marks = inline_marks.clone(); - for mark in &mut inline_marks { - mark.mark_type = DisplayMarkType::AnnotationThrough; - } + let inline_marks = inline_marks.clone(); Some(inline_marks) } else { None @@ -1031,7 +1245,12 @@ fn format_body( 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; + 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); @@ -1069,7 +1288,7 @@ fn format_body( current_index += line_length + end_line_size; // It would be nice to use filter_drain here once it's stable. - annotations.retain(|annotation| { + annotations.retain(|(key, annotation)| { let body_idx = idx; let annotation_type = match annotation.level { snippet::Level::Error => DisplayAnnotationType::None, @@ -1175,8 +1394,10 @@ fn format_body( }, range, annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineStart, + annotation_part: DisplayAnnotationPart::MultilineStart(current_depth), }); + depth_map.insert(*key, current_depth); + current_depth += 1; } true } @@ -1190,8 +1411,9 @@ fn format_body( .. } = body[body_idx] { + let depth = depth_map.get(key).cloned().unwrap_or_default(); inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough, + mark_type: DisplayMarkType::AnnotationThrough(depth), annotation_type: DisplayAnnotationType::from(annotation.level), }); } @@ -1207,15 +1429,10 @@ fn format_body( && end <= line_end_index + max(end_line_size, 1) => { if let DisplayLine::Source { - ref mut inline_marks, ref mut annotations, .. } = body[body_idx] { - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationThrough, - annotation_type: DisplayAnnotationType::from(annotation.level), - }); let end_mark = line[0..(end - line_start_index).min(line_length)] .chars() .map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(0)) @@ -1237,6 +1454,7 @@ fn format_body( 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, @@ -1245,7 +1463,7 @@ fn format_body( }, range, annotation_type: DisplayAnnotationType::from(annotation.level), - annotation_part: DisplayAnnotationPart::MultilineEnd, + annotation_part: DisplayAnnotationPart::MultilineEnd(depth), }); } false @@ -1253,6 +1471,12 @@ fn format_body( _ => 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 { @@ -1313,25 +1537,15 @@ fn format_body( } } -fn format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for _ in 0..n { - f.write_char(c)?; - } - Ok(()) -} - #[inline] -fn format_annotation_type( - annotation_type: &DisplayAnnotationType, - f: &mut fmt::Formatter<'_>, -) -> fmt::Result { +fn annotation_type_str(annotation_type: &DisplayAnnotationType) -> &'static str { match annotation_type { - DisplayAnnotationType::Error => f.write_str(ERROR_TXT), - DisplayAnnotationType::Help => f.write_str(HELP_TXT), - DisplayAnnotationType::Info => f.write_str(INFO_TXT), - DisplayAnnotationType::Note => f.write_str(NOTE_TXT), - DisplayAnnotationType::Warning => f.write_str(WARNING_TXT), - DisplayAnnotationType::None => Ok(()), + DisplayAnnotationType::Error => ERROR_TXT, + DisplayAnnotationType::Help => HELP_TXT, + DisplayAnnotationType::Info => INFO_TXT, + DisplayAnnotationType::Note => NOTE_TXT, + DisplayAnnotationType::Warning => WARNING_TXT, + DisplayAnnotationType::None => "", } } @@ -1390,3 +1604,33 @@ fn normalize_whitespace(str: &str) -> String { } 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); + } + DisplayMarkType::AnnotationStart => { + buf.putc(line, 3 + lineno_width, '/', *annotation_style); + } + }; + } + Ok(()) +} diff --git a/src/renderer/mod.rs b/src/renderer/mod.rs index 845d293..b9edcc6 100644 --- a/src/renderer/mod.rs +++ b/src/renderer/mod.rs @@ -12,6 +12,7 @@ mod display_list; mod margin; +mod styled_buffer; pub(crate) mod stylesheet; use crate::snippet::Message; diff --git a/src/renderer/styled_buffer.rs b/src/renderer/styled_buffer.rs new file mode 100644 index 0000000..ec834e1 --- /dev/null +++ b/src/renderer/styled_buffer.rs @@ -0,0 +1,97 @@ +//! Adapted from [styled_buffer] +//! +//! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs + +use crate::renderer::stylesheet::Stylesheet; +use anstyle::Style; +use std::fmt; +use std::fmt::Write; + +#[derive(Debug)] +pub(crate) struct StyledBuffer { + lines: Vec>, +} + +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct StyledChar { + ch: char, + style: Style, +} + +impl StyledChar { + pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new()); + + pub(crate) const fn new(ch: char, style: Style) -> StyledChar { + StyledChar { ch, style } + } +} + +impl StyledBuffer { + pub(crate) fn new() -> StyledBuffer { + StyledBuffer { lines: vec![] } + } + + fn ensure_lines(&mut self, line: usize) { + if line >= self.lines.len() { + self.lines.resize(line + 1, Vec::new()); + } + } + + pub(crate) fn render(&self, 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 { + if !line.is_empty() { + write!(str, "{}", current_style.render_reset())?; + } + current_style = ch.style; + write!(str, "{}", current_style.render())?; + } + write!(str, "{}", ch.ch)?; + } + write!(str, "{}", current_style.render_reset())?; + if i != self.lines.len() - 1 { + writeln!(str)?; + } + } + Ok(str) + } + + /// 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) { + self.ensure_lines(line); + if col >= self.lines[line].len() { + self.lines[line].resize(col + 1, StyledChar::SPACE); + } + self.lines[line][col] = StyledChar::new(chr, style); + } + + /// 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) { + 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) { + if line >= self.lines.len() { + self.puts(line, 0, string, style); + } else { + let col = self.lines[line].len(); + self.puts(line, col, string, style); + } + } + + pub(crate) fn num_lines(&self) -> usize { + self.lines.len() + } +} diff --git a/tests/fixtures/no-color/simple.svg b/tests/fixtures/no-color/simple.svg index 51a3a65..ae7b03c 100644 --- a/tests/fixtures/no-color/simple.svg +++ b/tests/fixtures/no-color/simple.svg @@ -26,7 +26,7 @@ | - expected one of `.`, `;`, `?`, or an operator here - 170 | + 170 | 171 | for line in &self.body { diff --git a/tests/fixtures/no-color/strip_line_non_ws.svg b/tests/fixtures/no-color/strip_line_non_ws.svg index 2be3890..f1977dc 100644 --- a/tests/fixtures/no-color/strip_line_non_ws.svg +++ b/tests/fixtures/no-color/strip_line_non_ws.svg @@ -1,4 +1,4 @@ - + LL | ... = (); let _: () = (); let _: () = (); let _: () = 42; let _: () = (); let _: () = (); let _: () = (); let _: () = (); let _: () ... - | ^^ expected `()`, found integer + | ^^ ^^ expected `()`, found integer - | ^^ expected due to this + | | - | + | expected due to this + + | diff --git a/tests/formatter.rs b/tests/formatter.rs index d0ac369..7f914de 100644 --- a/tests/formatter.rs +++ b/tests/formatter.rs @@ -262,8 +262,10 @@ fn test_source_annotation_standalone_multiline() { error | 1 | tests - | ----- help: Example string - | ----- help: Second line + | ----- + | | + | help: Example string + | help: Second line | "#]]; let renderer = Renderer::plain(); @@ -296,7 +298,7 @@ error | LL | This is an example LL | of content lines -LL | +LL | LL | abc | "#]]; @@ -756,8 +758,9 @@ error: unused optional dependency --> Cargo.toml:4:1 | 4 | bar = { version = "0.1.0", optional = true } - | ^^^ I need this to be really long so I can test overlaps - | --------------- info: This should also be long but not too long + | ^^^ --------------- info: This should also be long but not too long + | | + | I need this to be really long so I can test overlaps | "#]]; let renderer = Renderer::plain().anonymized_line_numbers(false); @@ -789,8 +792,9 @@ bar = { version = "0.1.0", optional = true } error: unused optional dependency | 4 | bar = { version = "0.1.0", optional = true } - | __________________________________________^ - | --------------- info: This should also be long but not too long + | ____________________________--------------^ + | | | + | | info: This should also be long but not too long 5 | | this is another line 6 | | so is this 7 | | bar = { version = "0.1.0", optional = true } @@ -831,14 +835,16 @@ bar = { version = "0.1.0", optional = true } error: unused optional dependency | 4 | bar = { version = "0.1.0", optional = true } - | __________________________________________^ - | _________^ - | --------------- info: This should also be long but not too long + | _________^__________________--------------^ + | | | | + | |_________| info: This should also be long but not too long + | || 5 | || this is another line 6 | || so is this 7 | || bar = { version = "0.1.0", optional = true } - | ||__________________________________________^ I need this to be really long so I can test overlaps - | ||_________________________^ I need this to be really long so I can test overlaps + | ||_________________________^________________^ I need this to be really long so I can test overlaps + | |__________________________| + | I need this to be really long so I can test overlaps | "#]]; let renderer = Renderer::plain(); @@ -881,15 +887,17 @@ this is another line error: unused optional dependency | 4 | bar = { version = "0.1.0", optional = true } - | __________________________________________^ - | _________^ - | --------------- info: This should also be long but not too long -5 | || this is another line - | ||____^ + | __________^__________________--------------^ + | | | | + | |__________| info: This should also be long but not too long + | || +5 | || this is another line + | || ____^ 6 | ||| so is this 7 | ||| bar = { version = "0.1.0", optional = true } - | |||__________________________________________^ I need this to be really long so I can test overlaps - | |||_________________________^ I need this to be really long so I can test overlaps + | |||_________________________^________________^ I need this to be really long so I can test overlaps + | |_|_________________________| + | | I need this to be really long so I can test overlaps 8 | | this is another line | |____^ I need this to be really long so I can test overlaps | diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index f87b206..ed2c7ad 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -55,8 +55,8 @@ error: foo | 2 | fn foo() { | __________^ -3 | | -4 | | +3 | | +4 | | 5 | | } | |___^ test | @@ -91,12 +91,14 @@ error: foo --> test.rs:3:3 | 3 | X0 Y0 - | ___^ - | ______- + | ___^__- + | |___| + | || 4 | || X1 Y1 5 | || X2 Y2 - | ||____^ `X` is a good letter - | ||_______- `Y` is a good letter too + | ||____^__- `Y` is a good letter too + | |_____| + | `X` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -128,11 +130,13 @@ error: foo --> test.rs:3:3 | 3 | X0 Y0 - | ___^ - | ______- + | ___^__- + | |___| + | || 4 | || Y1 X1 - | ||_______^ `X` is a good letter - | ||____- `Y` is a good letter too + | ||____-__^ `X` is a good letter + | |____| + | `Y` is a good letter too | "#]]; let renderer = Renderer::plain(); @@ -166,9 +170,9 @@ error: foo --> test.rs:3:6 | 3 | X0 Y0 Z0 - | ______^ -4 | | X1 Y1 Z1 - | |_________- + | _______^ +4 | | X1 Y1 Z1 + | | _________- 5 | || X2 Y2 Z2 | ||____^ `X` is a good letter 6 | | X3 Y3 Z3 @@ -206,14 +210,16 @@ 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 + | |||____^__-__- `Z` label + | ||_____|__| + | |______| `Y` is a good letter too + | `X` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -247,14 +253,17 @@ 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 + | ||| - + | |||____| + | ||____`X` is a good letter + | |____`Y` is a good letter too + | `Z` label | "#]]; let renderer = Renderer::plain(); @@ -288,16 +297,18 @@ fn foo() { error: foo --> test.rs:3:6 | -3 | X0 Y0 Z0 - | ______^ -4 | | X1 Y1 Z1 - | |____^ `X` is a good letter - | |______- -5 | | X2 Y2 Z2 - | |__________- `Y` is a good letter too - | |___- -6 | | X3 Y3 Z3 - | |_______- `Z` +3 | X0 Y0 Z0 + | _______^ +4 | | X1 Y1 Z1 + | | ____^_- + | ||____| + | | `X` is a good letter +5 | | X2 Y2 Z2 + | |___-______- `Y` is a good letter too + | ___| + | | +6 | | X3 Y3 Z3 + | |_______- `Z` | "#]]; let renderer = Renderer::plain(); @@ -370,14 +381,15 @@ fn foo() { error: foo --> test.rs:3:6 | -3 | X0 Y0 Z0 - | ______^ -4 | | X1 Y1 Z1 - | |____^ `X` is a good letter - | |_________- -5 | | X2 Y2 Z2 -6 | | X3 Y3 Z3 - | |__________- `Y` is a good letter too +3 | X0 Y0 Z0 + | _______^ +4 | | X1 Y1 Z1 + | | ____^____- + | ||____| + | | `X` is a good letter +5 | | X2 Y2 Z2 +6 | | X3 Y3 Z3 + | |__________- `Y` is a good letter too | "#]]; let renderer = Renderer::plain(); @@ -405,9 +417,7 @@ error: foo --> test.rs:3:7 | 3 | a { b { c } d } - | ^^^^^^^ - | ------------- `a` is a good letter - | - + | ----^^^^-^^-- `a` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -434,8 +444,7 @@ error: foo --> test.rs:3:3 | 3 | a { b { c } d } - | ^^^^^^^^^^^^^ `a` is a good letter - | ------- + | ^^^^-------^^ `a` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -463,9 +472,9 @@ error: foo --> test.rs:3:7 | 3 | a { b { c } d } - | ^^^^^^^ `b` is a good letter - | ------------- - | - + | ----^^^^-^^-- + | | + | `b` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -492,8 +501,9 @@ error: foo --> test.rs:3:3 | 3 | a { b { c } d } - | ^^^^^^^^^^^^^ - | ------- `b` is a good letter + | ^^^^-------^^ + | | + | `b` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -520,8 +530,9 @@ error: foo --> test.rs:3:3 | 3 | a bc d - | ^^^^ `a` is a good letter - | ---- + | ^^^^---- + | | + | `a` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -548,8 +559,7 @@ error: foo --> test.rs:3:3 | 3 | a { b { c } d } - | ^^^^^^^^^^^^^ - | ------- + | ^^^^-------^^ | "#]]; let renderer = Renderer::plain(); @@ -577,9 +587,7 @@ error: foo --> test.rs:3:7 | 3 | a { b { c } d } - | ^^^^^^^ - | ------------- - | - + | ----^^^^-^^-- | "#]]; let renderer = Renderer::plain(); @@ -606,8 +614,10 @@ error: foo --> test.rs:3:3 | 3 | a { b { c } d } - | ^^^^^^^^^^^^^ `a` is a good letter - | ------- `b` is a good letter + | ^^^^-------^^ + | | | + | | `b` is a good letter + | `a` is a good letter | "#]]; let renderer = Renderer::plain(); @@ -702,16 +712,17 @@ fn foo() { error: foo --> test.rs:3:6 | - 3 | X0 Y0 Z0 - | ______^ - 4 | | X1 Y1 Z1 - | |____^ `X` is a good letter - | |_________- - 5 | | 1 -... | -15 | | X2 Y2 Z2 -16 | | X3 Y3 Z3 - | |__________- `Y` is a good letter too + 3 | X0 Y0 Z0 + | _______^ + 4 | | X1 Y1 Z1 + | | ____^____- + | ||____| + | | `X` is a good letter + 5 | | 1 +... | +15 | | X2 Y2 Z2 +16 | | X3 Y3 Z3 + | |__________- `Y` is a good letter too | "#]]; let renderer = Renderer::plain(); @@ -755,22 +766,22 @@ error: foo --> test.rs:3:6 | 3 | X0 Y0 Z0 - | ______^ - 4 | | 1 - 5 | | 2 - 6 | | 3 - 7 | | X1 Y1 Z1 - | |_________- + | _______^ + 4 | | 1 + 5 | | 2 + 6 | | 3 + 7 | | X1 Y1 Z1 + | | _________- 8 | || 4 9 | || 5 10 | || 6 11 | || X2 Y2 Z2 | ||__________- `Z` is a good letter too -12 | | 7 -... | -15 | | 10 -16 | | X3 Y3 Z3 - | |_______^ `Y` is a good letter +12 | | 7 +... | +15 | | 10 +16 | | X3 Y3 Z3 + | |________^ `Y` is a good letter | "#]]; let renderer = Renderer::plain(); From 0175871363182516e0b69fbe95d7a997f58564a3 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 19 Jun 2024 21:13:36 -0600 Subject: [PATCH 4/6] feat: Merge multiline annotations with matching spans --- src/renderer/display_list.rs | 52 ++++++++++++++++++++++++++++++++++++ tests/rustc_tests.rs | 23 ++++++++-------- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs index 2c8dbf9..8d0c1f0 100644 --- a/src/renderer/display_list.rs +++ b/src/renderer/display_list.rs @@ -1248,6 +1248,58 @@ fn format_body( 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::>(); diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index ed2c7ad..8db9b80 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -248,22 +248,21 @@ fn foo() { .annotation(Level::Warning.span(14..38).label("`Z` label")), ); + // This should have a `^` but we currently don't support the idea of a + // "primary" annotation, which would solve this let expected = str![[r#" 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 +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 | "#]]; let renderer = Renderer::plain(); From 66bbd827167c037a2105719edbae4e8fd50f86a9 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Wed, 19 Jun 2024 21:21:54 -0600 Subject: [PATCH 5/6] feat: Match Rust's multiline start special case --- examples/format.svg | 52 +++++++++++----------- src/renderer/display_list.rs | 86 +++++++++++++++++++----------------- tests/rustc_tests.rs | 6 +-- 3 files changed, 73 insertions(+), 71 deletions(-) diff --git a/examples/format.svg b/examples/format.svg index a427c94..ac196c0 100644 --- a/examples/format.svg +++ b/examples/format.svg @@ -1,4 +1,4 @@ - + | -------------- expected `Option<String>` because of return type - 52 | for ann in annotations { + 52 | / for ann in annotations { - | _____^ + 53 | | match (ann.range.0, ann.range.1) { - 53 | | match (ann.range.0, ann.range.1) { + 54 | | (None, None) => continue, - 54 | | (None, None) => continue, + 55 | | (Some(start), Some(end)) if start > end_index => continue, - 55 | | (Some(start), Some(end)) if start > end_index => continue, + 56 | | (Some(start), Some(end)) if start >= start_index => { - 56 | | (Some(start), Some(end)) if start >= start_index => { + 57 | | let label = if let Some(ref label) = ann.label { - 57 | | let label = if let Some(ref label) = ann.label { + 58 | | format!(" {}", label) - 58 | | format!(" {}", label) + 59 | | } else { - 59 | | } else { + 60 | | String::from("") - 60 | | String::from("") + 61 | | }; - 61 | | }; + 62 | | - 62 | | + 63 | | return Some(format!( - 63 | | return Some(format!( + 64 | | "{}{}{}", - 64 | | "{}{}{}", + 65 | | " ".repeat(start - start_index), - 65 | | " ".repeat(start - start_index), + 66 | | "^".repeat(end - start), - 66 | | "^".repeat(end - start), + 67 | | label - 67 | | label + 68 | | )); - 68 | | )); + 69 | | } - 69 | | } + 70 | | _ => continue, - 70 | | _ => continue, + 71 | | } - 71 | | } + 72 | | } - 72 | | } + | |____^ expected enum `std::option::Option` - | |____^ expected enum `std::option::Option` + | - | - - + diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs index 8d0c1f0..955b4b2 100644 --- a/src/renderer/display_list.rs +++ b/src/renderer/display_list.rs @@ -442,6 +442,42 @@ impl<'a> DisplaySet<'a> { line_len += 1; } + // This is a special case where we have a multiline + // annotation that is at the start of the line disregarding + // any leading whitespace, and no other multiline + // annotations overlap it. In this case, we want to draw + // + // 2 | fn foo() { + // | _^ + // 3 | | + // 4 | | } + // | |_^ test + // + // we simplify the output to: + // + // 2 | / fn foo() { + // 3 | | + // 4 | | } + // | |_^ test + if multiline_depth == 1 + && annotations_positions.len() == 1 + && annotations_positions + .first() + .map_or(false, |(_, annotation)| { + matches!( + annotation.annotation_part, + DisplayAnnotationPart::MultilineStart(_) + ) && text + .chars() + .take(annotation.range.0) + .all(|c| c.is_whitespace()) + }) + { + let (_, ann) = annotations_positions.remove(0); + let style = get_annotation_style(&ann.annotation_type, stylesheet); + buffer.putc(line_offset, 3 + lineno_width, '/', *style); + } + // Draw the column separator for any extra lines that were // created // @@ -535,15 +571,13 @@ impl<'a> DisplaySet<'a> { // Add in any inline marks for any extra lines that have // been created. Output should look like above. for inline_mark in inline_marks { - if let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type { - let style = - get_annotation_style(&inline_mark.annotation_type, stylesheet); - if annotations_positions.is_empty() { - buffer.putc(line_offset, width_offset + depth, '|', *style); - } else { - for p in line_offset..=line_offset + line_len + 1 { - buffer.putc(p, width_offset + depth, '|', *style); - } + let DisplayMarkType::AnnotationThrough(depth) = inline_mark.mark_type; + let style = get_annotation_style(&inline_mark.annotation_type, stylesheet); + if annotations_positions.is_empty() { + buffer.putc(line_offset, width_offset + depth, '|', *style); + } else { + for p in line_offset..=line_offset + line_len + 1 { + buffer.putc(p, width_offset + depth, '|', *style); } } } @@ -823,8 +857,6 @@ pub(crate) struct DisplayMark { pub(crate) enum DisplayMarkType { /// A mark indicating a multiline annotation going through the current line. AnnotationThrough(usize), - /// A mark indicating a multiline annotation starting on the given line. - AnnotationStart, } /// A type of the `Annotation` which may impact the sigils, style or text displayed. @@ -1153,18 +1185,8 @@ fn fold_body(body: Vec>) -> Vec> { let mut unhighlighed_lines = vec![]; for line in body { match &line { - DisplayLine::Source { - annotations, - inline_marks, - .. - } => { - if annotations.is_empty() - // A multiline start mark (`/`) needs be treated as an - // annotation or the line could get folded. - && inline_marks - .iter() - .all(|m| m.mark_type != DisplayMarkType::AnnotationStart) - { + DisplayLine::Source { annotations, .. } => { + if annotations.is_empty() { unhighlighed_lines.push(line); } else { if lines.is_empty() { @@ -1407,20 +1429,7 @@ fn format_body( && start <= line_end_index + end_line_size.saturating_sub(1) && end > line_end_index => { - // Special case for multiline annotations that start at the - // beginning of a line, which requires a special mark (`/`) - if start - line_start_index == 0 { - if let DisplayLine::Source { - ref mut inline_marks, - .. - } = body[body_idx] - { - inline_marks.push(DisplayMark { - mark_type: DisplayMarkType::AnnotationStart, - annotation_type: DisplayAnnotationType::from(annotation.level), - }); - } - } else if let DisplayLine::Source { + if let DisplayLine::Source { ref mut annotations, .. } = body[body_idx] @@ -1679,9 +1688,6 @@ fn format_inline_marks( DisplayMarkType::AnnotationThrough(depth) => { buf.putc(line, 3 + lineno_width + depth, '|', *annotation_style); } - DisplayMarkType::AnnotationStart => { - buf.putc(line, 3 + lineno_width, '/', *annotation_style); - } }; } Ok(()) diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index 8db9b80..620ca45 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -254,8 +254,7 @@ fn foo() { error: foo --> test.rs:3:3 | -3 | X0 Y0 Z0 - | ___^ +3 | / X0 Y0 Z0 4 | | X1 Y1 Z1 5 | | X2 Y2 Z2 | | - @@ -340,8 +339,7 @@ fn foo() { error: foo --> test.rs:3:3 | -3 | X0 Y0 Z0 - | ___^ +3 | / X0 Y0 Z0 4 | | X1 Y1 Z1 | |____^ `X` is a good letter 5 | X2 Y2 Z2 From b25bd3e02f2256f34daad1efca730ec581c139cf Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Mon, 24 Jun 2024 11:05:33 -0600 Subject: [PATCH 6/6] feat: Match Rust's overlapping multiline starts --- src/renderer/display_list.rs | 34 ++++++++++++++++++++++++++++++++-- tests/rustc_tests.rs | 17 +++++++---------- 2 files changed, 39 insertions(+), 12 deletions(-) diff --git a/src/renderer/display_list.rs b/src/renderer/display_list.rs index 955b4b2..eea4971 100644 --- a/src/renderer/display_list.rs +++ b/src/renderer/display_list.rs @@ -269,7 +269,7 @@ impl<'a> DisplaySet<'a> { } } - // Adapted from https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/emitter.rs#L706-L1155 + // Adapted from https://github.com/rust-lang/rust/blob/d371d17496f2ce3a56da76aa083f4ef157572c20/compiler/rustc_errors/src/emitter.rs#L706-L1211 #[inline] fn format_line( &self, @@ -366,7 +366,7 @@ impl<'a> DisplaySet<'a> { annotations.sort_by_key(|a| Reverse(a.range.0)); let mut annotations_positions = vec![]; - let mut line_len = 0; + let mut line_len: usize = 0; let mut p = 0; for (i, annotation) in annotations.iter().enumerate() { for (j, next) in annotations.iter().enumerate() { @@ -442,6 +442,36 @@ impl<'a> DisplaySet<'a> { line_len += 1; } + if annotations_positions.iter().all(|(_, ann)| { + matches!( + ann.annotation_part, + DisplayAnnotationPart::MultilineStart(_) + ) + }) { + if let Some(max_pos) = + annotations_positions.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 multilne 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_positions { + *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); + } + } + // This is a special case where we have a multiline // annotation that is at the start of the line disregarding // any leading whitespace, and no other multiline diff --git a/tests/rustc_tests.rs b/tests/rustc_tests.rs index 620ca45..54b7321 100644 --- a/tests/rustc_tests.rs +++ b/tests/rustc_tests.rs @@ -91,9 +91,8 @@ error: foo --> test.rs:3:3 | 3 | X0 Y0 - | ___^__- - | |___| - | || + | ____^ - + | | ______| 4 | || X1 Y1 5 | || X2 Y2 | ||____^__- `Y` is a good letter too @@ -130,9 +129,8 @@ error: foo --> test.rs:3:3 | 3 | X0 Y0 - | ___^__- - | |___| - | || + | ____^ - + | | ______| 4 | || Y1 X1 | ||____-__^ `X` is a good letter | |____| @@ -210,10 +208,9 @@ error: foo --> test.rs:3:3 | 3 | X0 Y0 Z0 - | ___^__-__- - | |___|__| - | ||___| - | ||| + | _____^ - - + | | _______| | + | || _________| 4 | ||| X1 Y1 Z1 5 | ||| X2 Y2 Z2 | |||____^__-__- `Z` label