Repository: rust-lang/annotate-snippets-rs (rust)

Issue:
Removing duplicate Slice origins and surrounding empty lines
I'm currently trying to move the suggestions of the following diagnostic over from Rust to annotate-snippet:

```
error[E0382]: use of moved value: `x`
  --> /code/rust/src/test/ui/annotate-snippet/suggestion.rs:4:5
   |
 4 |     let x = vec![1];
   |         - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
 7 |     let y = x;
   |             - value moved here
 9 |     x; //~ ERROR use of moved value
   |     ^ value used here after move
```

In short, from Rust I can only get a `Vec` that contains the three lines with the annotations. This `Vec` contains _only_ the lines with annotations and no other lines around it. Something similar to this:

```
[
  Line {
      line_index: 4,
      annotations: [
          Annotation {
              start_col: 8,
              end_col: 9,
              label: "move occurs because ...",
          },
      ],
  },
  Line {
      line_index: 7,
      annotations: [
          Annotation {
              start_col: 11,
              end_col: 12,
              label: "value moved here",
          },
      ],
  },
  // etc..
]
```

I'm currently mapping this to annotate-snippets by turning each `Line` into a `Slice` and then calling `DisplayList::from(snippet)` at the end.

The problem is that each `Slice` has the `origin` at the top:

```
error[E0382]: use of moved value: `x`
  --> /code/rust/src/test/ui/annotate-snippet/suggestion.rs:4:8
   |
 4 |     let x = vec![1];
   |         ^ move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
   |
  ::: /code/rust/src/test/ui/annotate-snippet/suggestion.rs:7:12
   |
 7 |     let y = x;
   |             ^ value moved here
   |
  ::: /code/rust/src/test/ui/annotate-snippet/suggestion.rs:9:4
   |
 9 |     x; //~ ERROR use of moved value
   |     ^ value used here after move
```

If I set the origin of consecutive slices to `None` there will still be extra empty padding lines:

```
error[E0382]: use of moved value: `x`
  --> /code/rust/src/test/ui/annotate-snippet/suggestion.rs:5:8                                                             
   |
LL |     let x = vec![1];
   |         ^ move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
   |
   |
LL |     let y = x;
   |             ^ value moved here
   |
   |
LL |     x; //~ ERROR use of moved value
   |     ^ value used here after move
   |
```

I'm not sure what the original idea for a `Slice` was, but it seems like the best approach for those rustc annotations?

I can see three ways to solve this currently:

1. annotate-snippets doesn't add empty padding lines if the `origin` is `None`
2. annotate-snippets removes the origin and empty padding lines of consecutive Slices that have the same origin. This means annotate-snippet would have to compare the `origin` of each Slice to origin of the previous Slice.
3. We implement `From<Snippet> for DisplayList` by ourselves in rustc.

I would love to hear your thoughts on this before I continue with the annotations =)

cc https://github.com/rust-lang/rust/issues/61809

Pull request description:
Removing duplicate Slice origins and surrounding empty lines
Closes #9 
@phansch ??

Patch:
diff --git a/src/display_list/from_snippet.rs b/src/display_list/from_snippet.rs
index b3ba5a3..da0deea 100644
--- a/src/display_list/from_snippet.rs
+++ b/src/display_list/from_snippet.rs
@@ -104,15 +104,16 @@ fn format_annotation(annotation: snippet::Annotation<'_>) -> Vec<DisplayLine<'_>
 }
 
 fn format_slice(
-    mut slice: snippet::Slice<'_>,
+    slice: snippet::Slice<'_>,
     is_first: bool,
     has_footer: bool,
 ) -> Vec<DisplayLine<'_>> {
     let main_range = slice.annotations.get(0).map(|x| x.range.0);
-    let row = slice.line_start;
-    let origin = slice.origin.take();
-    let mut body = format_body(slice, has_footer);
-    let header = format_header(origin, main_range, row, &body, is_first);
+    let origin = slice.origin;
+    let line_start = slice.line_start;
+    let need_empty_header = origin.is_some() || is_first;
+    let mut body = format_body(slice, need_empty_header, has_footer);
+    let header = format_header(origin, main_range, line_start, &body, is_first);
     let mut result = vec![];
 
     if let Some(header) = header {
@@ -122,6 +123,12 @@ fn format_slice(
     result
 }
 
+#[inline]
+// TODO: option_zip
+fn zip_opt<A, B>(a: Option<A>, b: Option<B>) -> Option<(A, B)> {
+    a.and_then(|a| b.map(|b| (a, b)))
+}
+
 fn format_header<'a>(
     origin: Option<&'a str>,
     main_range: Option<usize>,
@@ -135,7 +142,7 @@ fn format_header<'a>(
         DisplayHeaderType::Continuation
     };
 
-    if let Some(main_range) = main_range {
+    if let Some((main_range, path)) = zip_opt(main_range, origin) {
         let mut col = 1;
 
         for item in body {
@@ -151,14 +158,14 @@ fn format_header<'a>(
                 row += 1;
             }
         }
-        if let Some(path) = origin {
-            return Some(DisplayLine::Raw(DisplayRawLine::Origin {
-                path,
-                pos: Some((row, col)),
-                header_type: display_header,
-            }));
-        }
+
+        return Some(DisplayLine::Raw(DisplayRawLine::Origin {
+            path,
+            pos: Some((row, col)),
+            header_type: display_header,
+        }));
     }
+
     if let Some(path) = origin {
         return Some(DisplayLine::Raw(DisplayRawLine::Origin {
             path,
@@ -166,6 +173,7 @@ fn format_header<'a>(
             header_type: display_header,
         }));
     }
+
     None
 }
 
@@ -261,7 +269,11 @@ fn fold_body(mut body: Vec<DisplayLine<'_>>) -> Vec<DisplayLine<'_>> {
     new_body
 }
 
-fn format_body(slice: snippet::Slice<'_>, has_footer: bool) -> Vec<DisplayLine<'_>> {
+fn format_body(
+    slice: snippet::Slice<'_>,
+    need_empty_header: bool,
+    has_footer: bool,
+) -> Vec<DisplayLine<'_>> {
     let source_len = slice.source.chars().count();
     if let Some(bigger) = slice.annotations.iter().find_map(|x| {
         if source_len < x.range.1 {
@@ -445,14 +457,17 @@ fn format_body(slice: snippet::Slice<'_>, has_footer: bool) -> Vec<DisplayLine<'
         body = fold_body(body);
     }
 
-    body.insert(
-        0,
-        DisplayLine::Source {
-            lineno: None,
-            inline_marks: vec![],
-            line: DisplaySourceLine::Empty,
-        },
-    );
+    if need_empty_header {
+        body.insert(
+            0,
+            DisplayLine::Source {
+                lineno: None,
+                inline_marks: vec![],
+                line: DisplaySourceLine::Empty,
+            },
+        );
+    }
+
     if has_footer {
         body.push(DisplayLine::Source {
             lineno: None,


Test patch:
diff --git a/tests/fixtures/no-color/issue_9.toml b/tests/fixtures/no-color/issue_9.toml
new file mode 100644
index 0000000..a30563b
--- /dev/null
+++ b/tests/fixtures/no-color/issue_9.toml
@@ -0,0 +1,28 @@
+[title]
+label = "expected one of `.`, `;`, `?`, or an operator, found `for`"
+annotation_type = "Error"
+
+[[slices]]
+source = "let x = vec![1];"
+line_start = 4
+origin = "/code/rust/src/test/ui/annotate-snippet/suggestion.rs"
+[[slices.annotations]]
+label = "move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait"
+annotation_type = "Warning"
+range = [4, 5]
+
+[[slices]]
+source = "let y = x;"
+line_start = 7
+[[slices.annotations]]
+label = "value moved here"
+annotation_type = "Warning"
+range = [8, 9]
+
+[[slices]]
+source = "x;"
+line_start = 9
+[[slices.annotations]]
+label = "value used here after move"
+annotation_type = "Error"
+range = [0, 1]
diff --git a/tests/fixtures/no-color/issue_9.txt b/tests/fixtures/no-color/issue_9.txt
new file mode 100644
index 0000000..affe6bc
--- /dev/null
+++ b/tests/fixtures/no-color/issue_9.txt
@@ -0,0 +1,12 @@
+error: expected one of `.`, `;`, `?`, or an operator, found `for`
+ --> /code/rust/src/test/ui/annotate-snippet/suggestion.rs:4:5
+  |
+4 | let x = vec![1];
+  |     - move occurs because `x` has type `std::vec::Vec<i32>`, which does not implement the `Copy` trait
+  |
+7 | let y = x;
+  |         - value moved here
+  |
+9 | x;
+  | ^ value used here after move
+  |
\ No newline at end of file
diff --git a/tests/snippet/mod.rs b/tests/snippet/mod.rs
index c334874..cc747a4 100644
--- a/tests/snippet/mod.rs
+++ b/tests/snippet/mod.rs
@@ -53,7 +53,9 @@ where
 #[derive(Deserialize)]
 #[serde(remote = "FormatOptions")]
 pub struct FormatOptionsDef {
+    #[serde(default)]
     pub color: bool,
+    #[serde(default)]
     pub anonymized_line_numbers: bool,
 }