diff --git a/cli/src/formatter.rs b/cli/src/formatter.rs index 9adf4dbec..d425188ca 100644 --- a/cli/src/formatter.rs +++ b/cli/src/formatter.rs @@ -73,9 +73,50 @@ where S: AsRef, { pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> { + self.with_labeled(|formatter| formatter.write_fmt(args)) + } + + fn with_labeled( + &mut self, + write_inner: impl FnOnce(&mut dyn Formatter) -> io::Result<()>, + ) -> io::Result<()> { self.formatter .borrow_mut() - .with_label(self.label.as_ref(), |formatter| formatter.write_fmt(args)) + .with_label(self.label.as_ref(), write_inner) + } +} + +/// Like `LabeledWriter`, but also prints the `heading` once. +/// +/// The `heading` will be printed within the first `write!()` or `writeln!()` +/// invocation, which is handy because `io::Error` can be handled there. +pub struct HeadingLabeledWriter { + writer: LabeledWriter, + heading: Option, +} + +impl HeadingLabeledWriter { + pub fn new(formatter: T, label: S, heading: H) -> Self { + HeadingLabeledWriter { + writer: LabeledWriter::new(formatter, label), + heading: Some(heading), + } + } +} + +impl<'a, T, S, H> HeadingLabeledWriter +where + T: BorrowMut, + S: AsRef, + H: fmt::Display, +{ + pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> { + self.writer.with_labeled(|formatter| { + if let Some(heading) = self.heading.take() { + write!(formatter.labeled("heading"), "{heading}")?; + } + formatter.write_fmt(args) + }) } } @@ -1054,6 +1095,40 @@ mod tests { insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" inside "); } + #[test] + fn test_heading_labeled_writer() { + let config = config_from_string( + r#" + colors.inner = "green" + colors."inner heading" = "red" + "#, + ); + let mut output: Vec = vec![]; + let mut formatter: Box = + Box::new(ColorFormatter::for_config(&mut output, &config).unwrap()); + HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Should be noop: "); + let mut writer = HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Heading: "); + write!(writer, "Message").unwrap(); + writeln!(writer, " continues").unwrap(); + drop(formatter); + insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###" + Heading: Message continues + "###); + } + + #[test] + fn test_heading_labeled_writer_empty_string() { + let mut output: Vec = vec![]; + let mut formatter: Box = Box::new(PlainTextFormatter::new(&mut output)); + let mut writer = HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Heading: "); + // write_fmt() is called even if the format string is empty. I don't + // know if that's guaranteed, but let's record the current behavior. + write!(writer, "").unwrap(); + write!(writer, "").unwrap(); + drop(formatter); + insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"Heading: "); + } + #[test] fn test_format_recorder() { let mut recorder = FormatRecorder::new();