feat(text): improve concise debug view for Span,Line,Text,Style (#1410)

Improves https://github.com/ratatui/ratatui/pull/1383

The following now round trips when formatted for debug.
This will make it easier to use insta when testing text related views of
widgets.

```rust
Text::from_iter([
    Line::from("Hello, world!"),
    Line::from("How are you?").bold().left_aligned(),
    Line::from_iter([
        Span::from("I'm "),
        Span::from("doing ").italic(),
        Span::from("great!").bold(),
    ]),
]).on_blue().italic().centered()
```
This commit is contained in:
Josh McKinney 2024-10-07 23:08:21 -07:00 committed by GitHub
parent c32baa7cd8
commit 23c0d52c29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 216 additions and 107 deletions

View file

@ -260,46 +260,7 @@ pub struct Style {
impl fmt::Debug for Style {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str("Style::new()")?;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
self.fmt_stylize(f)?;
Ok(())
}
}
@ -489,6 +450,54 @@ impl Style {
self
}
/// Formats the style in a way that can be copy-pasted into code using the style shorthands.
///
/// This is useful for debugging and for generating code snippets.
pub(crate) fn fmt_stylize(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use fmt::Debug;
if let Some(fg) = self.fg {
fg.stylize_debug(ColorDebugKind::Foreground).fmt(f)?;
}
if let Some(bg) = self.bg {
bg.stylize_debug(ColorDebugKind::Background).fmt(f)?;
}
#[cfg(feature = "underline-color")]
if let Some(underline_color) = self.underline_color {
underline_color
.stylize_debug(ColorDebugKind::Underline)
.fmt(f)?;
}
for modifier in self.add_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".bold()")?,
Modifier::DIM => f.write_str(".dim()")?,
Modifier::ITALIC => f.write_str(".italic()")?,
Modifier::UNDERLINED => f.write_str(".underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".rapid_blink()")?,
Modifier::REVERSED => f.write_str(".reversed()")?,
Modifier::HIDDEN => f.write_str(".hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".crossed_out()")?,
_ => f.write_fmt(format_args!(".add_modifier(Modifier::{modifier:?})"))?,
}
}
for modifier in self.sub_modifier.iter() {
match modifier {
Modifier::BOLD => f.write_str(".not_bold()")?,
Modifier::DIM => f.write_str(".not_dim()")?,
Modifier::ITALIC => f.write_str(".not_italic()")?,
Modifier::UNDERLINED => f.write_str(".not_underlined()")?,
Modifier::SLOW_BLINK => f.write_str(".not_slow_blink()")?,
Modifier::RAPID_BLINK => f.write_str(".not_rapid_blink()")?,
Modifier::REVERSED => f.write_str(".not_reversed()")?,
Modifier::HIDDEN => f.write_str(".not_hidden()")?,
Modifier::CROSSED_OUT => f.write_str(".not_crossed_out()")?,
_ => f.write_fmt(format_args!(".remove_modifier(Modifier::{modifier:?})"))?,
}
}
Ok(())
}
}
impl From<Color> for Style {
@ -638,6 +647,10 @@ mod tests {
#[case(Style::new().on_blue(), "Style::new().on_blue()")]
#[case(Style::new().bold(), "Style::new().bold()")]
#[case(Style::new().not_italic(), "Style::new().not_italic()")]
#[case(
Style::new().red().on_blue().bold().italic().not_dim().not_hidden(),
"Style::new().red().on_blue().bold().italic().not_dim().not_hidden()"
)]
fn debug(#[case] style: Style, #[case] expected: &'static str) {
assert_eq!(format!("{style:?}"), expected);
}

View file

@ -193,18 +193,28 @@ pub struct Line<'a> {
impl fmt::Debug for Line<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Line ")?;
return f.debug_list().entries(&self.spans).finish();
if self.spans.is_empty() {
f.write_str("Line::default()")?;
} else if self.spans.len() == 1 && self.spans[0].style == Style::default() {
f.write_str(r#"Line::from(""#)?;
f.write_str(&self.spans[0].content)?;
f.write_str(r#"")"#)?;
} else if self.spans.len() == 1 {
f.write_str("Line::from(")?;
self.spans[0].fmt(f)?;
f.write_str(")")?;
} else {
f.write_str("Line::from_iter(")?;
f.debug_list().entries(&self.spans).finish()?;
f.write_str(")")?;
}
let mut debug = f.debug_struct("Line");
if self.style != Style::default() {
debug.field("style", &self.style);
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => write!(f, ".left_aligned()"),
Some(Alignment::Center) => write!(f, ".centered()"),
Some(Alignment::Right) => write!(f, ".right_aligned()"),
None => Ok(()),
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("spans", &self.spans).finish()
}
}
@ -1590,4 +1600,49 @@ mod tests {
assert_eq!(result, "Hello world!");
}
}
#[rstest]
#[case::empty(Line::default(), "Line::default()")]
#[case::raw(Line::raw("Hello, world!"), r#"Line::from("Hello, world!")"#)]
#[case::styled(
Line::styled("Hello, world!", Color::Yellow),
r#"Line::from("Hello, world!").yellow()"#
)]
#[case::styled_complex(
Line::from(String::from("Hello, world!")).green().on_blue().bold().italic().not_dim(),
r#"Line::from("Hello, world!").green().on_blue().bold().italic().not_dim()"#
)]
#[case::styled_span(
Line::from(Span::styled("Hello, world!", Color::Yellow)),
r#"Line::from(Span::from("Hello, world!").yellow())"#
)]
#[case::styled_line_and_span(
Line::from(vec![
Span::styled("Hello", Color::Yellow),
Span::styled(" world!", Color::Green),
]).italic(),
r#"Line::from_iter([Span::from("Hello").yellow(), Span::from(" world!").green()]).italic()"#
)]
#[case::spans_vec(
Line::from(vec![
Span::styled("Hello", Color::Blue),
Span::styled(" world!", Color::Green),
]),
r#"Line::from_iter([Span::from("Hello").blue(), Span::from(" world!").green()])"#,
)]
#[case::left_aligned(
Line::from("Hello, world!").left_aligned(),
r#"Line::from("Hello, world!").left_aligned()"#
)]
#[case::centered(
Line::from("Hello, world!").centered(),
r#"Line::from("Hello, world!").centered()"#
)]
#[case::right_aligned(
Line::from("Hello, world!").right_aligned(),
r#"Line::from("Hello, world!").right_aligned()"#
)]
fn debug(#[case] line: Line, #[case] expected: &str) {
assert_eq!(format!("{line:?}"), expected);
}
}

View file

@ -107,13 +107,15 @@ pub struct Span<'a> {
impl fmt::Debug for Span<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() {
return write!(f, "Span({:?})", self.content);
if self.content.is_empty() {
write!(f, "Span::default()")?;
} else {
write!(f, "Span::from({:?})", self.content)?;
}
f.debug_struct("Span")
.field("style", &self.style)
.field("content", &self.content)
.finish()
if self.style != Style::default() {
self.style.fmt_stylize(f)?;
}
Ok(())
}
}
@ -505,7 +507,7 @@ impl fmt::Display for Span<'_> {
#[cfg(test)]
mod tests {
use rstest::fixture;
use rstest::{fixture, rstest};
use super::*;
use crate::{buffer::Cell, layout::Alignment, style::Stylize};
@ -884,4 +886,16 @@ mod tests {
Line::from(vec![Span::raw("test"), Span::raw("content")])
);
}
#[rstest]
#[case::default(Span::default(), "Span::default()")]
#[case::raw(Span::raw("test"), r#"Span::from("test")"#)]
#[case::styled(Span::styled("test", Style::new().green()), r#"Span::from("test").green()"#)]
#[case::styled_italic(
Span::styled("test", Style::new().green().italic()),
r#"Span::from("test").green().italic()"#
)]
fn debug(#[case] span: Span, #[case] expected: &str) {
assert_eq!(format!("{span:?}"), expected);
}
}

View file

@ -202,19 +202,23 @@ pub struct Text<'a> {
impl fmt::Debug for Text<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.style == Style::default() && self.alignment.is_none() {
f.write_str("Text ")?;
f.debug_list().entries(&self.lines).finish()
if self.lines.is_empty() {
f.write_str("Text::default()")?;
} else if self.lines.len() == 1 {
write!(f, "Text::from({:?})", self.lines[0])?;
} else {
let mut debug = f.debug_struct("Text");
if self.style != Style::default() {
debug.field("style", &self.style);
}
if let Some(alignment) = self.alignment {
debug.field("alignment", &format!("Alignment::{alignment}"));
}
debug.field("lines", &self.lines).finish()
f.write_str("Text::from_iter(")?;
f.debug_list().entries(self.lines.iter()).finish()?;
f.write_str(")")?;
}
self.style.fmt_stylize(f)?;
match self.alignment {
Some(Alignment::Left) => f.write_str(".left_aligned()")?,
Some(Alignment::Center) => f.write_str(".centered()")?,
Some(Alignment::Right) => f.write_str(".right_aligned()")?,
_ => (),
}
Ok(())
}
}
@ -1315,45 +1319,68 @@ mod tests {
}
}
mod debug {
use super::*;
#[rstest]
#[case::default(Text::default(), "Text::default()")]
// TODO jm: these could be improved to inspect the line / span if there's only one. e.g.
// Text::from("Hello, world!") and Text::from("Hello, world!".blue()) but the current
// implementation is good enough for now.
#[case::raw(
Text::raw("Hello, world!"),
r#"Text::from(Line::from("Hello, world!"))"#
)]
#[case::styled(
Text::styled("Hello, world!", Color::Yellow),
r#"Text::from(Line::from("Hello, world!")).yellow()"#
)]
#[case::complex_styled(
Text::from("Hello, world!").yellow().on_blue().bold().italic().not_dim().not_hidden(),
r#"Text::from(Line::from("Hello, world!")).yellow().on_blue().bold().italic().not_dim().not_hidden()"#
)]
#[case::alignment(
Text::from("Hello, world!").centered(),
r#"Text::from(Line::from("Hello, world!")).centered()"#
)]
#[case::styled_alignment(
Text::styled("Hello, world!", Color::Yellow).centered(),
r#"Text::from(Line::from("Hello, world!")).yellow().centered()"#
)]
#[case::multiple_lines(
Text::from(vec![
Line::from("Hello, world!"),
Line::from("How are you?")
]),
r#"Text::from_iter([Line::from("Hello, world!"), Line::from("How are you?")])"#
)]
fn debug(#[case] text: Text, #[case] expected: &str) {
assert_eq!(format!("{text:?}"), expected);
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn no_style() {
let text = Text::from("single unstyled line");
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn text_style() {
let text = Text::from("single styled line")
.red()
.on_black()
.bold()
.not_italic();
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn line_style() {
let text = Text::from(vec![
Line::from("first line").red().alignment(Alignment::Right),
Line::from("second line").on_black(),
]);
assert_eq!(text, Text::default());
}
#[test]
#[ignore = "This is just showing the debug output of the assertions"]
fn span_style() {
let text = Text::from(Line::from(vec![
Span::from("first span").red(),
Span::from("second span").on_black(),
]));
assert_eq!(text, Text::default());
}
#[test]
fn debug_alternate() {
let text = Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
])
.on_blue()
.italic()
.centered();
assert_eq!(
format!("{text:#?}"),
indoc::indoc! {r#"
Text::from_iter([
Line::from("Hello, world!"),
Line::from("How are you?").bold().left_aligned(),
Line::from_iter([
Span::from("I'm "),
Span::from("doing ").italic(),
Span::from("great!").bold(),
]),
]).on_blue().italic().centered()"#}
);
}
}