// Copyright 2020 The Jujutsu Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // https://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. use std::borrow::BorrowMut; use std::collections::HashMap; use std::io::{Error, Write}; use std::ops::Range; use std::sync::Arc; use std::{fmt, io, mem}; use crossterm::queue; use crossterm::style::{Attribute, Color, SetAttribute, SetBackgroundColor, SetForegroundColor}; use itertools::Itertools; // Lets the caller label strings and translates the labels to colors pub trait Formatter: Write { fn write_str(&mut self, text: &str) -> io::Result<()> { self.write_all(text.as_bytes()) } /// Returns the backing `Write`. This is useful for writing data that is /// already formatted, such as in the graphical log. fn raw(&mut self) -> &mut dyn Write; fn push_label(&mut self, label: &str) -> io::Result<()>; fn pop_label(&mut self) -> io::Result<()>; } impl dyn Formatter + '_ { pub fn labeled>(&mut self, label: S) -> LabeledWriter<&mut Self, S> { LabeledWriter { formatter: self, label, } } pub fn with_label( &mut self, label: &str, write_inner: impl FnOnce(&mut dyn Formatter) -> io::Result<()>, ) -> io::Result<()> { self.push_label(label)?; // Call `pop_label()` whether or not `write_inner()` fails, but don't let // its error replace the one from `write_inner()`. write_inner(self).and(self.pop_label()) } } /// `Formatter` wrapper to write a labeled message with `write!()` or /// `writeln!()`. pub struct LabeledWriter { formatter: T, label: S, } impl LabeledWriter { pub fn new(formatter: T, label: S) -> Self { LabeledWriter { formatter, label } } } impl<'a, T, S> LabeledWriter where T: BorrowMut, S: AsRef, { pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> { self.formatter .borrow_mut() .with_label(self.label.as_ref(), |formatter| formatter.write_fmt(args)) } } type Rules = Vec<(Vec, Style)>; /// Creates `Formatter` instances with preconfigured parameters. #[derive(Clone, Debug)] pub struct FormatterFactory { kind: FormatterFactoryKind, } #[derive(Clone, Debug)] enum FormatterFactoryKind { PlainText, Sanitized, Color { rules: Arc }, } impl FormatterFactory { pub fn prepare( config: &config::Config, color: bool, sanitized: bool, ) -> Result { let kind = if color { let rules = Arc::new(rules_from_config(config)?); FormatterFactoryKind::Color { rules } } else if sanitized { FormatterFactoryKind::Sanitized } else { FormatterFactoryKind::PlainText }; Ok(FormatterFactory { kind }) } pub fn new_formatter<'output, W: Write + 'output>( &self, output: W, ) -> Box { match &self.kind { FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)), FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)), FormatterFactoryKind::Color { rules } => { Box::new(ColorFormatter::new(output, rules.clone())) } } } } pub struct PlainTextFormatter { output: W, } impl PlainTextFormatter { pub fn new(output: W) -> PlainTextFormatter { Self { output } } } impl Write for PlainTextFormatter { fn write(&mut self, data: &[u8]) -> Result { self.output.write(data) } fn flush(&mut self) -> Result<(), Error> { self.output.flush() } } impl Formatter for PlainTextFormatter { fn raw(&mut self) -> &mut dyn Write { &mut self.output } fn push_label(&mut self, _label: &str) -> io::Result<()> { Ok(()) } fn pop_label(&mut self) -> io::Result<()> { Ok(()) } } pub struct SanitizingFormatter { output: W, } impl SanitizingFormatter { pub fn new(output: W) -> SanitizingFormatter { Self { output } } } impl Write for SanitizingFormatter { fn write(&mut self, data: &[u8]) -> Result { write_sanitized(&mut self.output, data)?; Ok(data.len()) } fn flush(&mut self) -> Result<(), Error> { self.output.flush() } } impl Formatter for SanitizingFormatter { fn raw(&mut self) -> &mut dyn Write { &mut self.output } fn push_label(&mut self, _label: &str) -> io::Result<()> { Ok(()) } fn pop_label(&mut self) -> io::Result<()> { Ok(()) } } #[derive(Clone, Debug, Default, PartialEq, Eq)] pub struct Style { pub fg_color: Option, pub bg_color: Option, pub bold: Option, pub underlined: Option, } impl Style { fn merge(&mut self, other: &Style) { self.fg_color = other.fg_color.or(self.fg_color); self.bg_color = other.bg_color.or(self.bg_color); self.bold = other.bold.or(self.bold); self.underlined = other.underlined.or(self.underlined); } } #[derive(Clone, Debug)] pub struct ColorFormatter { output: W, rules: Arc, /// The stack of currently applied labels. These determine the desired /// style. labels: Vec, cached_styles: HashMap, Style>, /// The style we last wrote to the output. current_style: Style, } impl ColorFormatter { pub fn new(output: W, rules: Arc) -> ColorFormatter { ColorFormatter { output, rules, labels: vec![], cached_styles: HashMap::new(), current_style: Style::default(), } } pub fn for_config(output: W, config: &config::Config) -> Result { let rules = rules_from_config(config)?; Ok(Self::new(output, Arc::new(rules))) } fn requested_style(&mut self) -> Style { if let Some(cached) = self.cached_styles.get(&self.labels) { cached.clone() } else { // We use the reverse list of matched indices as a measure of how well the rule // matches the actual labels. For example, for rule "a d" and the actual labels // "a b c d", we'll get [3,0]. We compare them by Rust's default Vec comparison. // That means "a d" will trump both rule "d" (priority [3]) and rule // "a b c" (priority [2,1,0]). let mut matched_styles = vec![]; for (labels, style) in self.rules.as_ref() { let mut labels_iter = self.labels.iter().enumerate(); // The indexes in the current label stack that match the required label. let mut matched_indices = vec![]; for required_label in labels { for (label_index, label) in &mut labels_iter { if label == required_label { matched_indices.push(label_index); break; } } } if matched_indices.len() == labels.len() { matched_indices.reverse(); matched_styles.push((style, matched_indices)); } } matched_styles.sort_by_key(|(_, indices)| indices.clone()); let mut style = Style::default(); for (matched_style, _) in matched_styles { style.merge(matched_style); } self.cached_styles .insert(self.labels.clone(), style.clone()); style } } fn write_new_style(&mut self) -> io::Result<()> { let new_style = self.requested_style(); if new_style != self.current_style { if new_style.bold != self.current_style.bold { if new_style.bold.unwrap_or_default() { queue!(self.output, SetAttribute(Attribute::Bold))?; } else { // NoBold results in double underlining on some terminals, so we use reset // instead. However, that resets other attributes as well, so we reset // our record of the current style so we re-apply the other attributes // below. queue!(self.output, SetAttribute(Attribute::Reset))?; self.current_style = Style::default(); } } if new_style.underlined != self.current_style.underlined { if new_style.underlined.unwrap_or_default() { queue!(self.output, SetAttribute(Attribute::Underlined))?; } else { queue!(self.output, SetAttribute(Attribute::NoUnderline))?; } } if new_style.fg_color != self.current_style.fg_color { queue!( self.output, SetForegroundColor(new_style.fg_color.unwrap_or(Color::Reset)) )?; } if new_style.bg_color != self.current_style.bg_color { queue!( self.output, SetBackgroundColor(new_style.bg_color.unwrap_or(Color::Reset)) )?; } self.current_style = new_style; } Ok(()) } } fn rules_from_config(config: &config::Config) -> Result { let mut result = vec![]; let table = config.get_table("colors")?; for (key, value) in table { let labels = key .split_whitespace() .map(ToString::to_string) .collect_vec(); match value.kind { config::ValueKind::String(color_name) => { let style = Style { fg_color: Some(color_for_name(&color_name)?), bg_color: None, bold: None, underlined: None, }; result.push((labels, style)); } config::ValueKind::Table(style_table) => { let mut style = Style::default(); if let Some(value) = style_table.get("fg") { if let config::ValueKind::String(color_name) = &value.kind { style.fg_color = Some(color_for_name(color_name)?); } } if let Some(value) = style_table.get("bg") { if let config::ValueKind::String(color_name) = &value.kind { style.bg_color = Some(color_for_name(color_name)?); } } if let Some(value) = style_table.get("bold") { if let config::ValueKind::Boolean(value) = &value.kind { style.bold = Some(*value); } } if let Some(value) = style_table.get("underline") { if let config::ValueKind::Boolean(value) = &value.kind { style.underlined = Some(*value); } } result.push((labels, style)); } _ => {} } } Ok(result) } fn color_for_name(color_name: &str) -> Result { match color_name { "default" => Ok(Color::Reset), "black" => Ok(Color::Black), "red" => Ok(Color::DarkRed), "green" => Ok(Color::DarkGreen), "yellow" => Ok(Color::DarkYellow), "blue" => Ok(Color::DarkBlue), "magenta" => Ok(Color::DarkMagenta), "cyan" => Ok(Color::DarkCyan), "white" => Ok(Color::Grey), "bright black" => Ok(Color::DarkGrey), "bright red" => Ok(Color::Red), "bright green" => Ok(Color::Green), "bright yellow" => Ok(Color::Yellow), "bright blue" => Ok(Color::Blue), "bright magenta" => Ok(Color::Magenta), "bright cyan" => Ok(Color::Cyan), "bright white" => Ok(Color::White), _ => Err(config::ConfigError::Message(format!( "invalid color: {color_name}" ))), } } impl Write for ColorFormatter { fn write(&mut self, data: &[u8]) -> Result { /* We clear the current style at the end of each line, and then we re-apply the style after the newline. There are several reasons for this: * We can more easily skip styling a trailing blank line, which other internal code then can correctly detect as having a trailing newline. * Some tools (like `less -R`) add an extra newline if the final character is not a newline (e.g. if there's a color reset after it), which led to an annoying blank line after the diff summary in e.g. `jj status`. * Since each line is styled independently, you get all the necessary escapes even when grepping through the output. * Some terminals extend background color to the end of the terminal (i.e. past the newline character), which is probably not what the user wanted. * Some tools (like `less -R`) get confused and lose coloring of lines after a newline. */ for line in data.split_inclusive(|b| *b == b'\n') { if line.ends_with(b"\n") { self.write_new_style()?; write_sanitized(&mut self.output, &line[..line.len() - 1])?; let labels = mem::take(&mut self.labels); self.write_new_style()?; self.output.write_all(b"\n")?; self.labels = labels; } else { self.write_new_style()?; write_sanitized(&mut self.output, line)?; } } Ok(data.len()) } fn flush(&mut self) -> Result<(), Error> { self.output.flush() } } impl Formatter for ColorFormatter { fn raw(&mut self) -> &mut dyn Write { &mut self.output } fn push_label(&mut self, label: &str) -> io::Result<()> { self.labels.push(label.to_owned()); Ok(()) } fn pop_label(&mut self) -> io::Result<()> { self.labels.pop(); if self.labels.is_empty() { self.write_new_style()? } Ok(()) } } /// Like buffered formatter, but records `push`/`pop_label()` calls. /// /// This allows you to manipulate the recorded data without losing labels. /// The recorded data and labels can be written to another formatter. If /// the destination formatter has already been labeled, the recorded labels /// will be stacked on top of the existing labels, and the subsequent data /// may be colorized differently. #[derive(Debug, Default)] pub struct FormatRecorder { data: Vec, label_ops: Vec<(usize, LabelOp)>, } #[derive(Clone, Debug, Eq, PartialEq)] enum LabelOp { PushLabel(String), PopLabel, } impl FormatRecorder { pub fn new() -> Self { FormatRecorder::default() } pub fn data(&self) -> &[u8] { &self.data } fn push_label_op(&mut self, op: LabelOp) { self.label_ops.push((self.data.len(), op)); } pub fn replay(&self, formatter: &mut dyn Formatter) -> io::Result<()> { self.replay_with(formatter, |formatter, range| { formatter.write_all(&self.data[range]) }) } pub fn replay_with( &self, formatter: &mut dyn Formatter, mut write_data: impl FnMut(&mut dyn Formatter, Range) -> io::Result<()>, ) -> io::Result<()> { let mut last_pos = 0; let mut flush_data = |formatter: &mut dyn Formatter, pos| -> io::Result<()> { if last_pos != pos { write_data(formatter, last_pos..pos)?; last_pos = pos; } Ok(()) }; for (pos, op) in &self.label_ops { flush_data(formatter, *pos)?; match op { LabelOp::PushLabel(label) => formatter.push_label(label)?, LabelOp::PopLabel => formatter.pop_label()?, } } flush_data(formatter, self.data.len()) } } impl Write for FormatRecorder { fn write(&mut self, data: &[u8]) -> io::Result { self.data.extend_from_slice(data); Ok(data.len()) } fn flush(&mut self) -> io::Result<()> { Ok(()) } } impl Formatter for FormatRecorder { fn raw(&mut self) -> &mut dyn Write { panic!("raw output isn't supported by FormatRecorder") } fn push_label(&mut self, label: &str) -> io::Result<()> { self.push_label_op(LabelOp::PushLabel(label.to_owned())); Ok(()) } fn pop_label(&mut self) -> io::Result<()> { self.push_label_op(LabelOp::PopLabel); Ok(()) } } fn write_sanitized(output: &mut impl Write, buf: &[u8]) -> Result<(), Error> { if buf.contains(&b'\x1b') { let mut sanitized = Vec::with_capacity(buf.len()); for b in buf { if *b == b'\x1b' { sanitized.extend_from_slice("␛".as_bytes()); } else { sanitized.push(*b); } } output.write_all(&sanitized) } else { output.write_all(buf) } } #[cfg(test)] mod tests { use std::str; use super::*; fn config_from_string(text: &str) -> config::Config { config::Config::builder() .add_source(config::File::from_str(text, config::FileFormat::Toml)) .build() .unwrap() } #[test] fn test_plaintext_formatter() { // Test that PlainTextFormatter ignores labels. let mut output: Vec = vec![]; let mut formatter = PlainTextFormatter::new(&mut output); formatter.push_label("warning").unwrap(); formatter.write_str("hello").unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"hello"); } #[test] fn test_plaintext_formatter_ansi_codes_in_text() { // Test that ANSI codes in the input text are NOT escaped. let mut output: Vec = vec![]; let mut formatter = PlainTextFormatter::new(&mut output); formatter.write_str("\x1b[1mactually bold\x1b[0m").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"actually bold"); } #[test] fn test_sanitizing_formatter_ansi_codes_in_text() { // Test that ANSI codes in the input text are escaped. let mut output: Vec = vec![]; let mut formatter = SanitizingFormatter::new(&mut output); formatter .write_str("\x1b[1mnot actually bold\x1b[0m") .unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m"); } #[test] fn test_color_formatter_color_codes() { // Test the color code for each color. let colors = [ "black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "bright black", "bright red", "bright green", "bright yellow", "bright blue", "bright magenta", "bright cyan", "bright white", ]; let mut config_builder = config::Config::builder(); for color in colors { // Use the color name as the label. config_builder = config_builder .set_override(format!("colors.{}", color.replace(' ', "-")), color) .unwrap(); } let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config_builder.build().unwrap()).unwrap(); for color in colors { formatter.push_label(&color.replace(' ', "-")).unwrap(); formatter.write_str(&format!(" {color} ")).unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); } insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"  black   red   green   yellow   blue   magenta   cyan   white   bright black   bright red   bright green   bright yellow   bright blue   bright magenta   bright cyan   bright white  "###); } #[test] fn test_color_formatter_single_label() { // Test that a single label can be colored and that the color is reset // afterwards. let config = config_from_string( r#" colors.inside = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.write_str(" before ").unwrap(); formatter.push_label("inside").unwrap(); formatter.write_str(" inside ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" after ").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" before  inside  after "); } #[test] fn test_color_formatter_attributes() { // Test that each attribute of the style can be set and that they can be // combined in a single rule or by using multiple rules. let config = config_from_string( r#" colors.red_fg = { fg = "red" } colors.blue_bg = { bg = "blue" } colors.bold_font = { bold = true } colors.underlined_text = { underline = true } colors.multiple = { fg = "green", bg = "yellow", bold = true, underline = true } "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("red_fg").unwrap(); formatter.write_str(" fg only ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); formatter.push_label("blue_bg").unwrap(); formatter.write_str(" bg only ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); formatter.push_label("bold_font").unwrap(); formatter.write_str(" bold only ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); formatter.push_label("underlined_text").unwrap(); formatter.write_str(" underlined only ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); formatter.push_label("multiple").unwrap(); formatter.write_str(" single rule ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); formatter.push_label("red_fg").unwrap(); formatter.push_label("blue_bg").unwrap(); formatter.write_str(" two rules ").unwrap(); formatter.pop_label().unwrap(); formatter.pop_label().unwrap(); formatter.write_str("\n").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"  fg only   bg only   bold only   underlined only   single rule   two rules  "###); } #[test] fn test_color_formatter_bold_reset() { // Test that we don't lose other attributes when we reset the bold attribute. let config = config_from_string( r#" colors.not_bold = { fg = "red", bg = "blue", underline = true } colors.bold_font = { bold = true } "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("not_bold").unwrap(); formatter.write_str(" not bold ").unwrap(); formatter.push_label("bold_font").unwrap(); formatter.write_str(" bold ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" not bold again ").unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" not bold  bold  not bold again "); } #[test] fn test_color_formatter_no_space() { // Test that two different colors can touch. let config = config_from_string( r#" colors.red = "red" colors.green = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.write_str("before").unwrap(); formatter.push_label("red").unwrap(); formatter.write_str("first").unwrap(); formatter.pop_label().unwrap(); formatter.push_label("green").unwrap(); formatter.write_str("second").unwrap(); formatter.pop_label().unwrap(); formatter.write_str("after").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"beforefirstsecondafter"); } #[test] fn test_color_formatter_ansi_codes_in_text() { // Test that ANSI codes in the input text are escaped. let config = config_from_string( r#" colors.red = "red" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("red").unwrap(); formatter .write_str("\x1b[1mnot actually bold\x1b[0m") .unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m"); } #[test] fn test_color_formatter_nested() { // A color can be associated with a combination of labels. A more specific match // overrides a less specific match. After the inner label is removed, the outer // color is used again (we don't reset). let config = config_from_string( r#" colors.outer = "blue" colors.inner = "red" colors."outer inner" = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.write_str(" before outer ").unwrap(); formatter.push_label("outer").unwrap(); formatter.write_str(" before inner ").unwrap(); formatter.push_label("inner").unwrap(); formatter.write_str(" inside inner ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" after inner ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" after outer ").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" before outer  before inner  inside inner  after inner  after outer "); } #[test] fn test_color_formatter_partial_match() { // A partial match doesn't count let config = config_from_string( r#" colors."outer inner" = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("outer").unwrap(); formatter.write_str(" not colored ").unwrap(); formatter.push_label("inner").unwrap(); formatter.write_str(" colored ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" not colored ").unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" not colored  colored  not colored "); } #[test] fn test_color_formatter_unrecognized_color() { // An unrecognized color causes an error. let config = config_from_string( r#" colors."outer" = "red" colors."outer inner" = "bloo" "#, ); let mut output: Vec = vec![]; let err = ColorFormatter::for_config(&mut output, &config) .unwrap_err() .to_string(); insta::assert_snapshot!(err, @"invalid color: bloo"); } #[test] fn test_color_formatter_normal_color() { // The "default" color resets the color. It is possible to reset only the // background or only the foreground. let config = config_from_string( r#" colors."outer" = {bg="yellow", fg="blue"} colors."outer default_fg" = "default" colors."outer default_bg" = {bg = "default"} "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("outer").unwrap(); formatter.write_str("Blue on yellow, ").unwrap(); formatter.push_label("default_fg").unwrap(); formatter.write_str(" default fg, ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" and back.\nBlue on yellow, ").unwrap(); formatter.push_label("default_bg").unwrap(); formatter.write_str(" default bg, ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" and back.").unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###" Blue on yellow,  default fg,  and back. Blue on yellow,  default bg,  and back. "###); } #[test] fn test_color_formatter_sibling() { // A partial match on one rule does not eliminate other rules. let config = config_from_string( r#" colors."outer1 inner1" = "red" colors.inner2 = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("outer1").unwrap(); formatter.push_label("inner2").unwrap(); formatter.write_str(" hello ").unwrap(); formatter.pop_label().unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" hello "); } #[test] fn test_color_formatter_reverse_order() { // Rules don't match labels out of order let config = config_from_string( r#" colors."inner outer" = "green" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("outer").unwrap(); formatter.push_label("inner").unwrap(); formatter.write_str(" hello ").unwrap(); formatter.pop_label().unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" hello "); } #[test] fn test_color_formatter_innermost_wins() { // When two labels match, the innermost one wins. let config = config_from_string( r#" colors."a" = "red" colors."b" = "green" colors."a c" = "blue" colors."b c" = "yellow" "#, ); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); formatter.push_label("a").unwrap(); formatter.write_str(" a1 ").unwrap(); formatter.push_label("b").unwrap(); formatter.write_str(" b1 ").unwrap(); formatter.push_label("c").unwrap(); formatter.write_str(" c ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" b2 ").unwrap(); formatter.pop_label().unwrap(); formatter.write_str(" a2 ").unwrap(); formatter.pop_label().unwrap(); insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" a1  b1  c  b2  a2 "); } #[test] fn test_format_recorder() { let mut recorder = FormatRecorder::new(); recorder.write_str(" outer1 ").unwrap(); recorder.push_label("inner").unwrap(); recorder.write_str(" inner1 ").unwrap(); recorder.write_str(" inner2 ").unwrap(); recorder.pop_label().unwrap(); recorder.write_str(" outer2 ").unwrap(); insta::assert_snapshot!( str::from_utf8(recorder.data()).unwrap(), @" outer1 inner1 inner2 outer2 "); // Replayed output should be labeled. let config = config_from_string(r#" colors.inner = "red" "#); let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); recorder.replay(&mut formatter).unwrap(); insta::assert_snapshot!( String::from_utf8(output).unwrap(), @" outer1  inner1 inner2  outer2 "); // Replayed output should be split at push/pop_label() call. let mut output: Vec = vec![]; let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); recorder .replay_with(&mut formatter, |formatter, range| { let data = &recorder.data()[range]; write!(formatter, "<<{}>>", str::from_utf8(data).unwrap()) }) .unwrap(); insta::assert_snapshot!( String::from_utf8(output).unwrap(), @"<< outer1 >><< inner1 inner2 >><< outer2 >>"); } }