Repository: zkat/miette (rust)

Issue:
Snippet message not rendered when source is not named
This goes back to #23, when the "header" concept was introduced. The header is not rendered at all when the source returns None for `.name()`. (Back in v0.12 you needed SourceSpan::new_labeled.)

Workaround atm is to give every source a name. But given Source is implemented for unnamed `String`/`Arc<String>` miette should be able to render the header without a name.

Using `String`/ `Arc<String>`

```
Error: ────[code::here]────────────────────

    × std::error::Error display message

 1 │ one two thre
   ·     ─┬─
   ·      ╰── highlight message
```

Using NamedSource

```
Error: ────[code::here]────────────────────

    × std::error::Error display message

   ╭───[example.txt:1:1] snippet message:
 1 │ one two thre
   ·     ─┬─
   ·      ╰── highlight message


```

Now that I figured out the issue, it's like a few lines to edit across a few printers, so lemme give it a go.

While I'm thinking about this, I think we could implement Source on `&'static str` because these can be cloned just as freely as the others and are obviously `'static`. Not super useful except for testing.

Pull request description:
Show snippet message for unnamed sources
Fixes #38 

Patch:
diff --git a/src/printer/graphical_printer.rs b/src/printer/graphical_printer.rs
index 897c65f..3625996 100644
--- a/src/printer/graphical_printer.rs
+++ b/src/printer/graphical_printer.rs
@@ -189,23 +189,29 @@ impl GraphicalReportPrinter {
             .len();
 
         // Header
+        write!(
+            f,
+            "{}{}{}",
+            " ".repeat(linum_width + 2),
+            self.theme.characters.ltop,
+            self.theme.characters.hbar.to_string().repeat(3),
+        )?;
         if let Some(source_name) = snippet.source.name() {
             let source_name = source_name.style(self.theme.styles.filename);
             write!(
                 f,
-                "{}{}{}[{}:{}:{}]",
-                " ".repeat(linum_width + 2),
-                self.theme.characters.ltop,
-                self.theme.characters.hbar.to_string().repeat(3),
+                "[{}:{}:{}]",
                 source_name,
                 contents.line() + 1,
                 contents.column() + 1
             )?;
-            if let Some(msg) = &snippet.message {
-                write!(f, " {}:", msg)?;
-            }
-            writeln!(f)?;
+        } else {
+            write!(f, "[{}:{}]", contents.line() + 1, contents.column() + 1)?;
         }
+        if let Some(msg) = &snippet.message {
+            write!(f, " {}:", msg)?;
+        }
+        writeln!(f)?;
 
         // Now it's time for the fun part--actually rendering everything!
         for line in &lines {
diff --git a/src/printer/narratable_printer.rs b/src/printer/narratable_printer.rs
index 37c4c83..d7ea72c 100644
--- a/src/printer/narratable_printer.rs
+++ b/src/printer/narratable_printer.rs
@@ -93,12 +93,16 @@ impl NarratableReportPrinter {
         if let Some(filename) = snippet.source.name() {
             write!(f, " for {}", filename,)?;
         }
-        writeln!(
+        write!(
             f,
             " starting at line {}, column {}",
             contents.line() + 1,
             contents.column() + 1
         )?;
+        if let Some(message) = snippet.message.as_deref() {
+            write!(f, ": {}", message)?;
+        }
+        writeln!(f)?;
         writeln!(f)?;
 
         // Highlights are the bits we're going to underline in our overall
diff --git a/src/source_impls.rs b/src/source_impls.rs
index ab355cf..74101fa 100644
--- a/src/source_impls.rs
+++ b/src/source_impls.rs
@@ -5,43 +5,73 @@ use std::{borrow::Cow, sync::Arc};
 
 use crate::{MietteError, MietteSpanContents, Source, SourceSpan, SpanContents};
 
-impl Source for String {
-    fn read_span(&self, span: &SourceSpan) -> Result<Box<dyn SpanContents + '_>, MietteError> {
-        let mut offset = 0usize;
-        let mut start_line = 0usize;
-        let mut start_column = 0usize;
-        let mut iter = self.chars().peekable();
-        while let Some(char) = iter.next() {
-            if offset < span.offset() {
-                match char {
-                    '\r' => {
-                        if iter.next_if_eq(&'\n').is_some() {
-                            offset += 1;
-                        }
-                        start_line += 1;
-                        start_column = 0;
-                    }
-                    '\n' => {
-                        start_line += 1;
-                        start_column = 0;
-                    }
-                    _ => {
-                        start_column += 1;
+fn start_line_column(string: &str, span: &SourceSpan) -> Result<(usize, usize), MietteError> {
+    let mut offset = 0usize;
+    let mut start_line = 0usize;
+    let mut start_column = 0usize;
+    let mut iter = string.chars().peekable();
+    while let Some(char) = iter.next() {
+        if offset < span.offset() {
+            match char {
+                '\r' => {
+                    if iter.next_if_eq(&'\n').is_some() {
+                        offset += 1;
                     }
+                    start_line += 1;
+                    start_column = 0;
+                }
+                '\n' => {
+                    start_line += 1;
+                    start_column = 0;
+                }
+                _ => {
+                    start_column += 1;
                 }
             }
+        }
 
-            if offset >= span.offset() + span.len() - 1 {
-                return Ok(Box::new(MietteSpanContents::new(
-                    &self.as_bytes()[span.offset()..span.offset() + span.len()],
-                    start_line,
-                    start_column,
-                )));
-            }
-
-            offset += char.len_utf8();
+        if offset >= span.offset() + span.len() - 1 {
+            return Ok((start_line, start_column));
         }
-        Err(MietteError::OutOfBounds)
+
+        offset += char.len_utf8();
+    }
+    Err(MietteError::OutOfBounds)
+}
+
+// The basic impl here is on str (not &str), because otherwise String's impl cannot reuse it
+// without creating a temporary &str inside its read_span implementation, and then returning data
+// that refers to that temporary.
+impl Source for str {
+    fn read_span<'a>(
+        &'a self,
+        span: &SourceSpan,
+    ) -> Result<Box<dyn SpanContents + 'a>, MietteError> {
+        let (start_line, start_column) = start_line_column(self, span)?;
+        return Ok(Box::new(MietteSpanContents::new(
+            &self.as_bytes()[span.offset()..span.offset() + span.len()],
+            start_line,
+            start_column,
+        )));
+    }
+}
+
+/// Makes `src: &'static str` or `struct S<'a> { src: &'a str }` usable.
+impl<'s> Source for &'s str {
+    fn read_span<'a>(
+        &'a self,
+        span: &SourceSpan,
+    ) -> Result<Box<dyn SpanContents + 'a>, MietteError> {
+        <str as Source>::read_span(self, span)
+    }
+}
+
+impl Source for String {
+    fn read_span<'a>(
+        &'a self,
+        span: &SourceSpan,
+    ) -> Result<Box<dyn SpanContents + 'a>, MietteError> {
+        <str as Source>::read_span(self, span)
     }
 }
 


Test patch:
diff --git a/tests/narrated.rs b/tests/narrated.rs
index f6f143e..778d3fe 100644
--- a/tests/narrated.rs
+++ b/tests/narrated.rs
@@ -45,7 +45,7 @@ fn single_line_highlight() -> Result<(), MietteError> {
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
 snippet line 2:   text
@@ -87,7 +87,7 @@ fn single_line_highlight_no_label() -> Result<(), MietteError> {
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
 snippet line 2:   text
@@ -132,7 +132,7 @@ fn multiple_same_line_highlights() -> Result<(), MietteError> {
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
 snippet line 2:   text text text text text
@@ -175,7 +175,7 @@ fn multiline_highlight_adjacent() -> Result<(), MietteError> {
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
 snippet line 2:   text
@@ -226,7 +226,7 @@ line5
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: line1
     highlight starting at line 1, column 1: block 1
@@ -280,7 +280,7 @@ line5
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: line1
     highlight starting at line 1, column 1: block 1
@@ -328,7 +328,7 @@ fn multiple_multiline_highlights_adjacent() -> Result<(), MietteError> {
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
     highlight starting at line 1, column 1: this bit here
@@ -377,7 +377,7 @@ fn multiple_multiline_highlights_overlapping_lines() -> Result<(), MietteError>
 oops!
     Diagnostic severity: error
 
-Begin snippet for bad_file.rs starting at line 1, column 1
+Begin snippet for bad_file.rs starting at line 1, column 1: This is the part that broke
 
 snippet line 1: source
     highlight starting at line 1, column 1: this bit here
@@ -441,3 +441,36 @@ diagnostic error code: oops::my::bad
     assert_eq!(expected, out);
     Ok(())
 }
+
+#[test]
+fn unnamed_snippet_shows_message() {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, message("This is the part that broke"))]
+        ctx: SourceSpan,
+    }
+    let src = "source_text_here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: (0, len).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    let expected = r#"
+oops!
+    Diagnostic severity: error
+
+Begin snippet starting at line 1, column 1: This is the part that broke
+
+snippet line 1: source_text_here
+
+diagnostic help: try doing it better next time?
+diagnostic error code: oops::my::bad
+"#
+    .trim_start();
+    assert_eq!(out, expected);
+}
diff --git a/tests/printer.rs b/tests/printer.rs
index 2591899..9465789 100644
--- a/tests/printer.rs
+++ b/tests/printer.rs
@@ -491,3 +491,35 @@ fn disable_url_links() -> Result<(), MietteError> {
     assert!(out.contains("oops::my::bad"));
     Ok(())
 }
+
+#[test]
+fn unnamed_snippet_shows_message() {
+    #[derive(Debug, Diagnostic, Error)]
+    #[error("oops!")]
+    #[diagnostic(code(oops::my::bad), help("try doing it better next time?"))]
+    struct MyBad {
+        src: String,
+        #[snippet(src, message("This is the part that broke"))]
+        ctx: SourceSpan,
+    }
+    let src = "source_text_here".to_string();
+    let len = src.len();
+    let err = MyBad {
+        src,
+        ctx: (0, len).into(),
+    };
+    let out = fmt_report(err.into());
+    println!("{}", out);
+    let expected = r#"
+────[oops::my::bad]────────────────────
+
+    × oops!
+
+   ╭───[1:1] This is the part that broke:
+ 1 │ source_text_here
+
+    ‽ try doing it better next time?
+"#
+    .trim_start();
+    assert_eq!(out, expected);
+}