Repository: zesterer/ariadne
Pull request #138: feat: add multiple and multiline help support
Author: login: Young-Flash
Created: 2025-02-27T02:39:35Z | Merged: 2025-03-13T22:34:40Z

hi, thanks for the great project. This PR add support for multiple and multiline `help`.


Diff:
diff --git a/src/lib.rs b/src/lib.rs
index ededa5f..ffeb124 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -203,7 +203,7 @@ pub struct Report<'a, S: Span = Range<usize>> {
     code: Option<String>,
     msg: Option<String>,
     notes: Vec<String>,
-    help: Option<String>,
+    help: Vec<String>,
     span: S,
     labels: Vec<Label<S>>,
     config: Config,
@@ -219,7 +219,7 @@ impl<S: Span> Report<'_, S> {
             code: None,
             msg: None,
             notes: vec![],
-            help: None,
+            help: vec![],
             span,
             labels: Vec::new(),
             config: Config::default(),
@@ -284,7 +284,7 @@ pub struct ReportBuilder<'a, S: Span> {
     code: Option<String>,
     msg: Option<String>,
     notes: Vec<String>,
-    help: Option<String>,
+    help: Vec<String>,
     span: S,
     labels: Vec<Label<S>>,
     config: Config,
@@ -333,12 +333,24 @@ impl<'a, S: Span> ReportBuilder<'a, S> {
 
     /// Set the help message of this report.
     pub fn set_help<N: ToString>(&mut self, note: N) {
-        self.help = Some(note.to_string());
+        self.help = vec![note.to_string()];
+    }
+
+    /// Add a help message to this report.
+    pub fn add_help<N: ToString>(&mut self, note: N) {
+        self.help.push(note.to_string());
+    }
+
+    /// Set the help messages of this report.
+    pub fn with_helps<N: IntoIterator<Item = impl ToString>>(&mut self, helps: N) {
+        for help in helps {
+            self.add_help(help)
+        }
     }
 
     /// Set the help message of this report.
     pub fn with_help<N: ToString>(mut self, note: N) -> Self {
-        self.set_help(note);
+        self.add_help(note);
         self
     }
 
diff --git a/src/source.rs b/src/source.rs
index acae6a7..4e48629 100644
--- a/src/source.rs
+++ b/src/source.rs
@@ -1,11 +1,11 @@
 use super::*;
 
+use std::io::Error;
 use std::{
     collections::{hash_map::Entry, HashMap},
     fs,
     path::{Path, PathBuf},
 };
-use std::io::Error;
 
 /// A trait implemented by [`Source`] caches.
 pub trait Cache<Id: ?Sized> {
@@ -332,9 +332,7 @@ impl Cache<Path> for FileCache {
         Ok::<_, Error>(match self.files.entry(path.to_path_buf()) {
             // TODO: Don't allocate here
             Entry::Occupied(entry) => entry.into_mut(),
-            Entry::Vacant(entry) => entry.insert(Source::from(
-                fs::read_to_string(path)?,
-            )),
+            Entry::Vacant(entry) => entry.insert(Source::from(fs::read_to_string(path)?)),
         })
     }
     fn display<'a>(&self, path: &'a Path) -> Option<impl fmt::Display + 'a> {
@@ -403,14 +401,12 @@ where
     I: IntoIterator<Item = (Id, S)>,
     S: AsRef<str>,
 {
-    FnCache::new(
-        (move |id| Err(format!("Failed to fetch source '{}'", id))) as fn(&_) -> _,
-    )
-    .with_sources(
-        iter.into_iter()
-            .map(|(id, s)| (id, Source::from(s)))
-            .collect(),
-    )
+    FnCache::new((move |id| Err(format!("Failed to fetch source '{}'", id))) as fn(&_) -> _)
+        .with_sources(
+            iter.into_iter()
+                .map(|(id, s)| (id, Source::from(s)))
+                .collect(),
+        )
 }
 
 #[cfg(test)]
diff --git a/src/write.rs b/src/write.rs
index ee7b1d7..8028e90 100644
--- a/src/write.rs
+++ b/src/write.rs
@@ -856,13 +856,37 @@ impl<S: Span> Report<'_, S> {
             let is_final_group = group_idx + 1 == groups_len;
 
             // Help
-            if let (Some(note), true) = (&self.help, is_final_group) {
-                if !self.config.compact {
-                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
-                    writeln!(w)?;
+            if is_final_group {
+                for (i, help) in self.help.iter().enumerate() {
+                    if !self.config.compact {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        writeln!(w)?;
+                    }
+                    let help_prefix = format!("{} {}", "Help", i + 1);
+                    let help_prefix_len = if self.help.len() > 1 {
+                        help_prefix.len()
+                    } else {
+                        4
+                    };
+                    let mut lines = help.lines();
+                    if let Some(line) = lines.next() {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        if self.help.len() > 1 {
+                            writeln!(
+                                w,
+                                "{}: {}",
+                                help_prefix.fg(self.config.note_color(), s),
+                                line
+                            )?;
+                        } else {
+                            writeln!(w, "{}: {}", "Help".fg(self.config.note_color(), s), line)?;
+                        }
+                    }
+                    for line in lines {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        writeln!(w, "{:>pad$}{}", "", line, pad = help_prefix_len + 2)?;
+                    }
                 }
-                write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
-                writeln!(w, "{}: {}", "Help".fg(self.config.note_color(), s), note)?;
             }
 
             // Note
@@ -1522,4 +1546,33 @@ mod tests {
         ---'
         "###)
     }
+
+    #[test]
+    fn multi_helps_multi_lines() {
+        let source = "apple == orange;";
+        let msg = remove_trailing(
+            Report::build(ReportKind::Error, 0..0)
+                .with_config(no_color_and_ascii())
+                .with_message("can't compare apples with oranges")
+                .with_label(Label::new(0..15).with_message("This is a strange comparison"))
+                .with_help("No need to try, they can't be compared.")
+                .with_help("Yeah, really, please stop.\nIt has no resemblance.")
+                .finish()
+                .write_to_string(Source::from(source)),
+        );
+        assert_snapshot!(msg, @r###"
+        Error: can't compare apples with oranges
+           ,-[ <unknown>:1:1 ]
+           |
+         1 | apple == orange;
+           | ^^^^^^^|^^^^^^^
+           |        `--------- This is a strange comparison
+           |
+           | Help 1: No need to try, they can't be compared.
+           |
+           | Help 2: Yeah, really, please stop.
+           |         It has no resemblance.
+        ---'
+        "###)
+    }
 }


Reviews:
user: login: zesterer
state: CHANGES_REQUESTED
author_association: OWNER
submitted_at: 2025-03-02T13:36:14Z
user: login: Young-Flash
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-03T03:46:33Z

Review comments:
diff_hunk: @@ -856,13 +856,37 @@ impl<S: Span> Report<'_, S> {
             let is_final_group = group_idx + 1 == groups_len;
 
             // Help
-            if let (Some(note), true) = (&self.help, is_final_group) {
-                if !self.config.compact {
-                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
-                    writeln!(w)?;
+            if is_final_group {
+                for (i, help) in self.help.iter().enumerate() {
+                    if !self.config.compact {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        writeln!(w)?;
+                    }
+                    let help_prefix = format!("{} {}", "Help", i + 1);
+                    let help_prefix_len = if self.help.len() > 1 {
+                        help_prefix.len()
path: src/write.rs
user: login: zesterer
body: Could this be changed to `help_prefix.chars().count()`?
created_at: 2025-03-02T13:33:44Z
author_association: OWNER
diff_hunk: @@ -856,13 +856,37 @@ impl<S: Span> Report<'_, S> {
             let is_final_group = group_idx + 1 == groups_len;
 
             // Help
-            if let (Some(note), true) = (&self.help, is_final_group) {
-                if !self.config.compact {
-                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
-                    writeln!(w)?;
+            if is_final_group {
+                for (i, help) in self.help.iter().enumerate() {
+                    if !self.config.compact {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        writeln!(w)?;
+                    }
+                    let help_prefix = format!("{} {}", "Help", i + 1);
+                    let help_prefix_len = if self.help.len() > 1 {
+                        help_prefix.len()
path: src/write.rs
user: login: zesterer
body: Also, should this logic not read `self.help.chars().count().max(4)`? Perhaps I'm misunderstanding it.
created_at: 2025-03-02T13:35:34Z
author_association: OWNER
diff_hunk: @@ -856,13 +856,37 @@ impl<S: Span> Report<'_, S> {
             let is_final_group = group_idx + 1 == groups_len;
 
             // Help
-            if let (Some(note), true) = (&self.help, is_final_group) {
-                if !self.config.compact {
-                    write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
-                    writeln!(w)?;
+            if is_final_group {
+                for (i, help) in self.help.iter().enumerate() {
+                    if !self.config.compact {
+                        write_margin(&mut w, 0, false, false, true, Some((0, false)), &[], &None)?;
+                        writeln!(w)?;
+                    }
+                    let help_prefix = format!("{} {}", "Help", i + 1);
+                    let help_prefix_len = if self.help.len() > 1 {
+                        help_prefix.len()
path: src/write.rs
user: login: Young-Flash
body: `self.help` is `Vec<String>`, IMO the current logic of `help_prefix_len` is ok
created_at: 2025-03-03T03:46:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: zesterer
created_at: 2025-03-02T13:36:28Z
body: Thanks for the PR!
author_association: OWNER
user: login: Young-Flash
created_at: 2025-03-13T08:46:27Z
body: hi, could this PR merge and make a new release when you have time?
author_association: CONTRIBUTOR
user: login: zesterer
created_at: 2025-03-13T22:38:17Z
body: `0.5.1` has now been released, which includes this change.
author_association: OWNER