jj_cli/
formatter.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::collections::HashMap;
16use std::fmt;
17use std::io;
18use std::io::Error;
19use std::io::Write;
20use std::mem;
21use std::ops::Deref;
22use std::ops::DerefMut;
23use std::ops::Range;
24use std::sync::Arc;
25
26use crossterm::queue;
27use crossterm::style::Attribute;
28use crossterm::style::Color;
29use crossterm::style::SetAttribute;
30use crossterm::style::SetBackgroundColor;
31use crossterm::style::SetForegroundColor;
32use itertools::Itertools as _;
33use jj_lib::config::ConfigGetError;
34use jj_lib::config::StackedConfig;
35use serde::de::Deserialize as _;
36use serde::de::Error as _;
37use serde::de::IntoDeserializer as _;
38
39// Lets the caller label strings and translates the labels to colors
40pub trait Formatter: Write {
41    /// Returns the backing `Write`. This is useful for writing data that is
42    /// already formatted, such as in the graphical log.
43    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>>;
44
45    fn push_label(&mut self, label: &str);
46
47    fn pop_label(&mut self);
48}
49
50impl<T: Formatter + ?Sized> Formatter for &mut T {
51    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
52        <T as Formatter>::raw(self)
53    }
54
55    fn push_label(&mut self, label: &str) {
56        <T as Formatter>::push_label(self, label);
57    }
58
59    fn pop_label(&mut self) {
60        <T as Formatter>::pop_label(self);
61    }
62}
63
64impl<T: Formatter + ?Sized> Formatter for Box<T> {
65    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
66        <T as Formatter>::raw(self)
67    }
68
69    fn push_label(&mut self, label: &str) {
70        <T as Formatter>::push_label(self, label);
71    }
72
73    fn pop_label(&mut self) {
74        <T as Formatter>::pop_label(self);
75    }
76}
77
78/// [`Formatter`] adapters.
79pub trait FormatterExt: Formatter {
80    fn labeled(&mut self, label: &str) -> LabeledScope<&mut Self> {
81        LabeledScope::new(self, label)
82    }
83
84    fn into_labeled(self, label: &str) -> LabeledScope<Self>
85    where
86        Self: Sized,
87    {
88        LabeledScope::new(self, label)
89    }
90}
91
92impl<T: Formatter + ?Sized> FormatterExt for T {}
93
94/// [`Formatter`] wrapper to apply a label within a lexical scope.
95#[must_use]
96pub struct LabeledScope<T: Formatter> {
97    formatter: T,
98}
99
100impl<T: Formatter> LabeledScope<T> {
101    pub fn new(mut formatter: T, label: &str) -> Self {
102        formatter.push_label(label);
103        Self { formatter }
104    }
105
106    // TODO: move to FormatterExt?
107    /// Turns into writer that prints labeled message with the `heading`.
108    pub fn with_heading<H>(self, heading: H) -> HeadingLabeledWriter<T, H> {
109        HeadingLabeledWriter::new(self, heading)
110    }
111}
112
113impl<T: Formatter> Drop for LabeledScope<T> {
114    fn drop(&mut self) {
115        self.formatter.pop_label();
116    }
117}
118
119impl<T: Formatter> Deref for LabeledScope<T> {
120    type Target = T;
121
122    fn deref(&self) -> &Self::Target {
123        &self.formatter
124    }
125}
126
127impl<T: Formatter> DerefMut for LabeledScope<T> {
128    fn deref_mut(&mut self) -> &mut Self::Target {
129        &mut self.formatter
130    }
131}
132
133// There's no `impl Formatter for LabeledScope<T>` so nested .labeled() calls
134// wouldn't construct `LabeledScope<LabeledScope<T>>`.
135
136/// [`Formatter`] wrapper that prints the `heading` once.
137///
138/// The `heading` will be printed within the first `write!()` or `writeln!()`
139/// invocation, which is handy because `io::Error` can be handled there.
140pub struct HeadingLabeledWriter<T: Formatter, H> {
141    formatter: LabeledScope<T>,
142    heading: Option<H>,
143}
144
145impl<T: Formatter, H> HeadingLabeledWriter<T, H> {
146    pub fn new(formatter: LabeledScope<T>, heading: H) -> Self {
147        Self {
148            formatter,
149            heading: Some(heading),
150        }
151    }
152}
153
154impl<T: Formatter, H: fmt::Display> HeadingLabeledWriter<T, H> {
155    pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
156        if let Some(heading) = self.heading.take() {
157            write!(self.formatter.labeled("heading"), "{heading}")?;
158        }
159        self.formatter.write_fmt(args)
160    }
161}
162
163type Rules = Vec<(Vec<String>, Style)>;
164
165/// Creates `Formatter` instances with preconfigured parameters.
166#[derive(Clone, Debug)]
167pub struct FormatterFactory {
168    kind: FormatterFactoryKind,
169}
170
171#[derive(Clone, Debug)]
172enum FormatterFactoryKind {
173    PlainText,
174    Sanitized,
175    Color { rules: Arc<Rules>, debug: bool },
176}
177
178impl FormatterFactory {
179    pub fn plain_text() -> Self {
180        let kind = FormatterFactoryKind::PlainText;
181        Self { kind }
182    }
183
184    pub fn sanitized() -> Self {
185        let kind = FormatterFactoryKind::Sanitized;
186        Self { kind }
187    }
188
189    pub fn color(config: &StackedConfig, debug: bool) -> Result<Self, ConfigGetError> {
190        let rules = Arc::new(rules_from_config(config)?);
191        let kind = FormatterFactoryKind::Color { rules, debug };
192        Ok(Self { kind })
193    }
194
195    pub fn new_formatter<'output, W: Write + 'output>(
196        &self,
197        output: W,
198    ) -> Box<dyn Formatter + 'output> {
199        match &self.kind {
200            FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)),
201            FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)),
202            FormatterFactoryKind::Color { rules, debug } => {
203                Box::new(ColorFormatter::new(output, rules.clone(), *debug))
204            }
205        }
206    }
207
208    pub fn is_color(&self) -> bool {
209        matches!(self.kind, FormatterFactoryKind::Color { .. })
210    }
211}
212
213pub struct PlainTextFormatter<W> {
214    output: W,
215}
216
217impl<W> PlainTextFormatter<W> {
218    pub fn new(output: W) -> Self {
219        Self { output }
220    }
221}
222
223impl<W: Write> Write for PlainTextFormatter<W> {
224    fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
225        self.output.write(data)
226    }
227
228    fn flush(&mut self) -> Result<(), Error> {
229        self.output.flush()
230    }
231}
232
233impl<W: Write> Formatter for PlainTextFormatter<W> {
234    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
235        Ok(Box::new(self.output.by_ref()))
236    }
237
238    fn push_label(&mut self, _label: &str) {}
239
240    fn pop_label(&mut self) {}
241}
242
243pub struct SanitizingFormatter<W> {
244    output: W,
245}
246
247impl<W> SanitizingFormatter<W> {
248    pub fn new(output: W) -> Self {
249        Self { output }
250    }
251}
252
253impl<W: Write> Write for SanitizingFormatter<W> {
254    fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
255        write_sanitized(&mut self.output, data)?;
256        Ok(data.len())
257    }
258
259    fn flush(&mut self) -> Result<(), Error> {
260        self.output.flush()
261    }
262}
263
264impl<W: Write> Formatter for SanitizingFormatter<W> {
265    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
266        Ok(Box::new(self.output.by_ref()))
267    }
268
269    fn push_label(&mut self, _label: &str) {}
270
271    fn pop_label(&mut self) {}
272}
273
274#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize)]
275#[serde(default, rename_all = "kebab-case")]
276pub struct Style {
277    #[serde(deserialize_with = "deserialize_color_opt")]
278    pub fg: Option<Color>,
279    #[serde(deserialize_with = "deserialize_color_opt")]
280    pub bg: Option<Color>,
281    pub bold: Option<bool>,
282    pub italic: Option<bool>,
283    pub underline: Option<bool>,
284    pub reverse: Option<bool>,
285}
286
287impl Style {
288    fn merge(&mut self, other: &Self) {
289        self.fg = other.fg.or(self.fg);
290        self.bg = other.bg.or(self.bg);
291        self.bold = other.bold.or(self.bold);
292        self.italic = other.italic.or(self.italic);
293        self.underline = other.underline.or(self.underline);
294        self.reverse = other.reverse.or(self.reverse);
295    }
296}
297
298#[derive(Clone, Debug)]
299pub struct ColorFormatter<W: Write> {
300    output: W,
301    rules: Arc<Rules>,
302    /// The stack of currently applied labels. These determine the desired
303    /// style.
304    labels: Vec<String>,
305    cached_styles: HashMap<Vec<String>, Style>,
306    /// The style we last wrote to the output.
307    current_style: Style,
308    /// The debug string (space-separated labels) we last wrote to the output.
309    /// Initialize to None to turn debug strings off.
310    current_debug: Option<String>,
311}
312
313impl<W: Write> ColorFormatter<W> {
314    pub fn new(output: W, rules: Arc<Rules>, debug: bool) -> Self {
315        Self {
316            output,
317            rules,
318            labels: vec![],
319            cached_styles: HashMap::new(),
320            current_style: Style::default(),
321            current_debug: debug.then(String::new),
322        }
323    }
324
325    pub fn for_config(
326        output: W,
327        config: &StackedConfig,
328        debug: bool,
329    ) -> Result<Self, ConfigGetError> {
330        let rules = rules_from_config(config)?;
331        Ok(Self::new(output, Arc::new(rules), debug))
332    }
333
334    fn requested_style(&mut self) -> Style {
335        if let Some(cached) = self.cached_styles.get(&self.labels) {
336            cached.clone()
337        } else {
338            // We use the reverse list of matched indices as a measure of how well the rule
339            // matches the actual labels. For example, for rule "a d" and the actual labels
340            // "a b c d", we'll get [3,0]. We compare them by Rust's default Vec comparison.
341            // That means "a d" will trump both rule "d" (priority [3]) and rule
342            // "a b c" (priority [2,1,0]).
343            let mut matched_styles = vec![];
344            for (labels, style) in self.rules.as_ref() {
345                let mut labels_iter = self.labels.iter().enumerate();
346                // The indexes in the current label stack that match the required label.
347                let mut matched_indices = vec![];
348                for required_label in labels {
349                    for (label_index, label) in &mut labels_iter {
350                        if label == required_label {
351                            matched_indices.push(label_index);
352                            break;
353                        }
354                    }
355                }
356                if matched_indices.len() == labels.len() {
357                    matched_indices.reverse();
358                    matched_styles.push((style, matched_indices));
359                }
360            }
361            matched_styles.sort_by_key(|(_, indices)| indices.clone());
362
363            let mut style = Style::default();
364            for (matched_style, _) in matched_styles {
365                style.merge(matched_style);
366            }
367            self.cached_styles
368                .insert(self.labels.clone(), style.clone());
369            style
370        }
371    }
372
373    fn write_new_style(&mut self) -> io::Result<()> {
374        let new_debug = match &self.current_debug {
375            Some(current) => {
376                let joined = self.labels.join(" ");
377                if joined == *current {
378                    None
379                } else {
380                    if !current.is_empty() {
381                        write!(self.output, ">>")?;
382                    }
383                    Some(joined)
384                }
385            }
386            None => None,
387        };
388        let new_style = self.requested_style();
389        if new_style != self.current_style {
390            if new_style.bold != self.current_style.bold {
391                if new_style.bold.unwrap_or_default() {
392                    queue!(self.output, SetAttribute(Attribute::Bold))?;
393                } else {
394                    // NoBold results in double underlining on some terminals, so we use reset
395                    // instead. However, that resets other attributes as well, so we reset
396                    // our record of the current style so we re-apply the other attributes
397                    // below.
398                    queue!(self.output, SetAttribute(Attribute::Reset))?;
399                    self.current_style = Style::default();
400                }
401            }
402            if new_style.italic != self.current_style.italic {
403                if new_style.italic.unwrap_or_default() {
404                    queue!(self.output, SetAttribute(Attribute::Italic))?;
405                } else {
406                    queue!(self.output, SetAttribute(Attribute::NoItalic))?;
407                }
408            }
409            if new_style.underline != self.current_style.underline {
410                if new_style.underline.unwrap_or_default() {
411                    queue!(self.output, SetAttribute(Attribute::Underlined))?;
412                } else {
413                    queue!(self.output, SetAttribute(Attribute::NoUnderline))?;
414                }
415            }
416            if new_style.reverse != self.current_style.reverse {
417                if new_style.reverse.unwrap_or_default() {
418                    queue!(self.output, SetAttribute(Attribute::Reverse))?;
419                } else {
420                    queue!(self.output, SetAttribute(Attribute::NoReverse))?;
421                }
422            }
423            if new_style.fg != self.current_style.fg {
424                queue!(
425                    self.output,
426                    SetForegroundColor(new_style.fg.unwrap_or(Color::Reset))
427                )?;
428            }
429            if new_style.bg != self.current_style.bg {
430                queue!(
431                    self.output,
432                    SetBackgroundColor(new_style.bg.unwrap_or(Color::Reset))
433                )?;
434            }
435            self.current_style = new_style;
436        }
437        if let Some(d) = new_debug {
438            if !d.is_empty() {
439                write!(self.output, "<<{d}::")?;
440            }
441            self.current_debug = Some(d);
442        }
443        Ok(())
444    }
445}
446
447fn rules_from_config(config: &StackedConfig) -> Result<Rules, ConfigGetError> {
448    config
449        .table_keys("colors")
450        .map(|key| {
451            let labels = key
452                .split_whitespace()
453                .map(ToString::to_string)
454                .collect_vec();
455            let style = config.get_value_with(["colors", key], |value| {
456                if value.is_str() {
457                    Ok(Style {
458                        fg: Some(deserialize_color(value.into_deserializer())?),
459                        bg: None,
460                        bold: None,
461                        italic: None,
462                        underline: None,
463                        reverse: None,
464                    })
465                } else if value.is_inline_table() {
466                    Style::deserialize(value.into_deserializer())
467                } else {
468                    Err(toml_edit::de::Error::custom(format!(
469                        "invalid type: {}, expected a color name or a table of styles",
470                        value.type_name()
471                    )))
472                }
473            })?;
474            Ok((labels, style))
475        })
476        .collect()
477}
478
479fn deserialize_color<'de, D>(deserializer: D) -> Result<Color, D::Error>
480where
481    D: serde::Deserializer<'de>,
482{
483    let color_str = String::deserialize(deserializer)?;
484    color_for_string(&color_str).map_err(D::Error::custom)
485}
486
487fn deserialize_color_opt<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
488where
489    D: serde::Deserializer<'de>,
490{
491    deserialize_color(deserializer).map(Some)
492}
493
494fn color_for_string(color_str: &str) -> Result<Color, String> {
495    match color_str {
496        "default" => Ok(Color::Reset),
497        "black" => Ok(Color::Black),
498        "red" => Ok(Color::DarkRed),
499        "green" => Ok(Color::DarkGreen),
500        "yellow" => Ok(Color::DarkYellow),
501        "blue" => Ok(Color::DarkBlue),
502        "magenta" => Ok(Color::DarkMagenta),
503        "cyan" => Ok(Color::DarkCyan),
504        "white" => Ok(Color::Grey),
505        "bright black" => Ok(Color::DarkGrey),
506        "bright red" => Ok(Color::Red),
507        "bright green" => Ok(Color::Green),
508        "bright yellow" => Ok(Color::Yellow),
509        "bright blue" => Ok(Color::Blue),
510        "bright magenta" => Ok(Color::Magenta),
511        "bright cyan" => Ok(Color::Cyan),
512        "bright white" => Ok(Color::White),
513        _ => color_for_ansi256_index(color_str)
514            .or_else(|| color_for_hex(color_str))
515            .ok_or_else(|| format!("Invalid color: {color_str}")),
516    }
517}
518
519fn color_for_ansi256_index(color: &str) -> Option<Color> {
520    color
521        .strip_prefix("ansi-color-")
522        .filter(|s| *s == "0" || !s.starts_with('0'))
523        .and_then(|n| n.parse::<u8>().ok())
524        .map(Color::AnsiValue)
525}
526
527fn color_for_hex(color: &str) -> Option<Color> {
528    if color.len() == 7
529        && color.starts_with('#')
530        && color[1..].chars().all(|c| c.is_ascii_hexdigit())
531    {
532        let r = u8::from_str_radix(&color[1..3], 16);
533        let g = u8::from_str_radix(&color[3..5], 16);
534        let b = u8::from_str_radix(&color[5..7], 16);
535        match (r, g, b) {
536            (Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb { r, g, b }),
537            _ => None,
538        }
539    } else {
540        None
541    }
542}
543
544impl<W: Write> Write for ColorFormatter<W> {
545    fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
546        /*
547        We clear the current style at the end of each line, and then we re-apply the style
548        after the newline. There are several reasons for this:
549
550         * We can more easily skip styling a trailing blank line, which other
551           internal code then can correctly detect as having a trailing
552           newline.
553
554         * Some tools (like `less -R`) add an extra newline if the final
555           character is not a newline (e.g. if there's a color reset after
556           it), which led to an annoying blank line after the diff summary in
557           e.g. `jj status`.
558
559         * Since each line is styled independently, you get all the necessary
560           escapes even when grepping through the output.
561
562         * Some terminals extend background color to the end of the terminal
563           (i.e. past the newline character), which is probably not what the
564           user wanted.
565
566         * Some tools (like `less -R`) get confused and lose coloring of lines
567           after a newline.
568         */
569
570        for line in data.split_inclusive(|b| *b == b'\n') {
571            if line.ends_with(b"\n") {
572                self.write_new_style()?;
573                write_sanitized(&mut self.output, &line[..line.len() - 1])?;
574                let labels = mem::take(&mut self.labels);
575                self.write_new_style()?;
576                self.output.write_all(b"\n")?;
577                self.labels = labels;
578            } else {
579                self.write_new_style()?;
580                write_sanitized(&mut self.output, line)?;
581            }
582        }
583
584        Ok(data.len())
585    }
586
587    fn flush(&mut self) -> Result<(), Error> {
588        self.write_new_style()?;
589        self.output.flush()
590    }
591}
592
593impl<W: Write> Formatter for ColorFormatter<W> {
594    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
595        self.write_new_style()?;
596        Ok(Box::new(self.output.by_ref()))
597    }
598
599    fn push_label(&mut self, label: &str) {
600        self.labels.push(label.to_owned());
601    }
602
603    fn pop_label(&mut self) {
604        self.labels.pop();
605    }
606}
607
608impl<W: Write> Drop for ColorFormatter<W> {
609    fn drop(&mut self) {
610        // If a `ColorFormatter` was dropped without flushing, let's try to
611        // reset any currently active style.
612        self.labels.clear();
613        self.write_new_style().ok();
614    }
615}
616
617/// Like buffered formatter, but records `push`/`pop_label()` calls.
618///
619/// This allows you to manipulate the recorded data without losing labels.
620/// The recorded data and labels can be written to another formatter. If
621/// the destination formatter has already been labeled, the recorded labels
622/// will be stacked on top of the existing labels, and the subsequent data
623/// may be colorized differently.
624#[derive(Clone, Debug, Default)]
625pub struct FormatRecorder {
626    data: Vec<u8>,
627    ops: Vec<(usize, FormatOp)>,
628}
629
630#[derive(Clone, Debug, Eq, PartialEq)]
631enum FormatOp {
632    PushLabel(String),
633    PopLabel,
634    RawEscapeSequence(Vec<u8>),
635}
636
637impl FormatRecorder {
638    pub fn new() -> Self {
639        Self::default()
640    }
641
642    /// Creates new buffer containing the given `data`.
643    pub fn with_data(data: impl Into<Vec<u8>>) -> Self {
644        Self {
645            data: data.into(),
646            ops: vec![],
647        }
648    }
649
650    pub fn data(&self) -> &[u8] {
651        &self.data
652    }
653
654    fn push_op(&mut self, op: FormatOp) {
655        self.ops.push((self.data.len(), op));
656    }
657
658    pub fn replay(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
659        self.replay_with(formatter, |formatter, range| {
660            formatter.write_all(&self.data[range])
661        })
662    }
663
664    pub fn replay_with(
665        &self,
666        formatter: &mut dyn Formatter,
667        mut write_data: impl FnMut(&mut dyn Formatter, Range<usize>) -> io::Result<()>,
668    ) -> io::Result<()> {
669        let mut last_pos = 0;
670        let mut flush_data = |formatter: &mut dyn Formatter, pos| -> io::Result<()> {
671            if last_pos != pos {
672                write_data(formatter, last_pos..pos)?;
673                last_pos = pos;
674            }
675            Ok(())
676        };
677        for (pos, op) in &self.ops {
678            flush_data(formatter, *pos)?;
679            match op {
680                FormatOp::PushLabel(label) => formatter.push_label(label),
681                FormatOp::PopLabel => formatter.pop_label(),
682                FormatOp::RawEscapeSequence(raw_escape_sequence) => {
683                    formatter.raw()?.write_all(raw_escape_sequence)?;
684                }
685            }
686        }
687        flush_data(formatter, self.data.len())
688    }
689}
690
691impl Write for FormatRecorder {
692    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
693        self.data.extend_from_slice(data);
694        Ok(data.len())
695    }
696
697    fn flush(&mut self) -> io::Result<()> {
698        Ok(())
699    }
700}
701
702struct RawEscapeSequenceRecorder<'a>(&'a mut FormatRecorder);
703
704impl Write for RawEscapeSequenceRecorder<'_> {
705    fn write(&mut self, data: &[u8]) -> io::Result<usize> {
706        self.0.push_op(FormatOp::RawEscapeSequence(data.to_vec()));
707        Ok(data.len())
708    }
709
710    fn flush(&mut self) -> io::Result<()> {
711        self.0.flush()
712    }
713}
714
715impl Formatter for FormatRecorder {
716    fn raw(&mut self) -> io::Result<Box<dyn Write + '_>> {
717        Ok(Box::new(RawEscapeSequenceRecorder(self)))
718    }
719
720    fn push_label(&mut self, label: &str) {
721        self.push_op(FormatOp::PushLabel(label.to_owned()));
722    }
723
724    fn pop_label(&mut self) {
725        self.push_op(FormatOp::PopLabel);
726    }
727}
728
729fn write_sanitized(output: &mut impl Write, buf: &[u8]) -> Result<(), Error> {
730    if buf.contains(&b'\x1b') {
731        let mut sanitized = Vec::with_capacity(buf.len());
732        for b in buf {
733            if *b == b'\x1b' {
734                sanitized.extend_from_slice("␛".as_bytes());
735            } else {
736                sanitized.push(*b);
737            }
738        }
739        output.write_all(&sanitized)
740    } else {
741        output.write_all(buf)
742    }
743}
744
745#[cfg(test)]
746mod tests {
747    use std::error::Error as _;
748
749    use bstr::BString;
750    use indexmap::IndexMap;
751    use indoc::indoc;
752    use jj_lib::config::ConfigLayer;
753    use jj_lib::config::ConfigSource;
754    use jj_lib::config::StackedConfig;
755
756    use super::*;
757
758    fn config_from_string(text: &str) -> StackedConfig {
759        let mut config = StackedConfig::empty();
760        config.add_layer(ConfigLayer::parse(ConfigSource::User, text).unwrap());
761        config
762    }
763
764    /// Appends "[EOF]" marker to the output text.
765    ///
766    /// This is a workaround for https://github.com/mitsuhiko/insta/issues/384.
767    fn to_snapshot_string(output: impl Into<Vec<u8>>) -> BString {
768        let mut output = output.into();
769        output.extend_from_slice(b"[EOF]\n");
770        BString::new(output)
771    }
772
773    #[test]
774    fn test_plaintext_formatter() {
775        // Test that PlainTextFormatter ignores labels.
776        let mut output: Vec<u8> = vec![];
777        let mut formatter = PlainTextFormatter::new(&mut output);
778        formatter.push_label("warning");
779        write!(formatter, "hello").unwrap();
780        formatter.pop_label();
781        insta::assert_snapshot!(to_snapshot_string(output), @"hello[EOF]");
782    }
783
784    #[test]
785    fn test_plaintext_formatter_ansi_codes_in_text() {
786        // Test that ANSI codes in the input text are NOT escaped.
787        let mut output: Vec<u8> = vec![];
788        let mut formatter = PlainTextFormatter::new(&mut output);
789        write!(formatter, "\x1b[1mactually bold\x1b[0m").unwrap();
790        insta::assert_snapshot!(to_snapshot_string(output), @"actually bold[EOF]");
791    }
792
793    #[test]
794    fn test_sanitizing_formatter_ansi_codes_in_text() {
795        // Test that ANSI codes in the input text are escaped.
796        let mut output: Vec<u8> = vec![];
797        let mut formatter = SanitizingFormatter::new(&mut output);
798        write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
799        insta::assert_snapshot!(to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
800    }
801
802    #[test]
803    fn test_color_formatter_color_codes() {
804        // Test the color code for each color.
805        // Use the color name as the label.
806        let config = config_from_string(indoc! {"
807            [colors]
808            black = 'black'
809            red = 'red'
810            green = 'green'
811            yellow = 'yellow'
812            blue = 'blue'
813            magenta = 'magenta'
814            cyan = 'cyan'
815            white = 'white'
816            bright-black = 'bright black'
817            bright-red = 'bright red'
818            bright-green = 'bright green'
819            bright-yellow = 'bright yellow'
820            bright-blue = 'bright blue'
821            bright-magenta = 'bright magenta'
822            bright-cyan = 'bright cyan'
823            bright-white = 'bright white'
824        "});
825        let colors: IndexMap<String, String> = config.get("colors").unwrap();
826        let mut output: Vec<u8> = vec![];
827        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
828        for (label, color) in &colors {
829            formatter.push_label(label);
830            write!(formatter, " {color} ").unwrap();
831            formatter.pop_label();
832            writeln!(formatter).unwrap();
833        }
834        drop(formatter);
835        insta::assert_snapshot!(to_snapshot_string(output), @r"
836         black 
837         red 
838         green 
839         yellow 
840         blue 
841         magenta 
842         cyan 
843         white 
844         bright black 
845         bright red 
846         bright green 
847         bright yellow 
848         bright blue 
849         bright magenta 
850         bright cyan 
851         bright white 
852        [EOF]
853        ");
854    }
855
856    #[test]
857    fn test_color_for_ansi256_index() {
858        assert_eq!(
859            color_for_ansi256_index("ansi-color-0"),
860            Some(Color::AnsiValue(0))
861        );
862        assert_eq!(
863            color_for_ansi256_index("ansi-color-10"),
864            Some(Color::AnsiValue(10))
865        );
866        assert_eq!(
867            color_for_ansi256_index("ansi-color-255"),
868            Some(Color::AnsiValue(255))
869        );
870        assert_eq!(color_for_ansi256_index("ansi-color-256"), None);
871
872        assert_eq!(color_for_ansi256_index("ansi-color-00"), None);
873        assert_eq!(color_for_ansi256_index("ansi-color-010"), None);
874        assert_eq!(color_for_ansi256_index("ansi-color-0255"), None);
875    }
876
877    #[test]
878    fn test_color_formatter_ansi256() {
879        let config = config_from_string(
880            r#"
881        [colors]
882        purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
883        gray = "ansi-color-244"
884        "#,
885        );
886        let mut output: Vec<u8> = vec![];
887        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
888        formatter.push_label("purple-bg");
889        write!(formatter, " purple background ").unwrap();
890        formatter.pop_label();
891        writeln!(formatter).unwrap();
892        formatter.push_label("gray");
893        write!(formatter, " gray ").unwrap();
894        formatter.pop_label();
895        writeln!(formatter).unwrap();
896        drop(formatter);
897        insta::assert_snapshot!(to_snapshot_string(output), @r"
898         purple background 
899         gray 
900        [EOF]
901        ");
902    }
903
904    #[test]
905    fn test_color_formatter_hex_colors() {
906        // Test the color code for each color.
907        let config = config_from_string(indoc! {"
908            [colors]
909            black = '#000000'
910            white = '#ffffff'
911            pastel-blue = '#AFE0D9'
912        "});
913        let colors: IndexMap<String, String> = config.get("colors").unwrap();
914        let mut output: Vec<u8> = vec![];
915        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
916        for label in colors.keys() {
917            formatter.push_label(&label.replace(' ', "-"));
918            write!(formatter, " {label} ").unwrap();
919            formatter.pop_label();
920            writeln!(formatter).unwrap();
921        }
922        drop(formatter);
923        insta::assert_snapshot!(to_snapshot_string(output), @r"
924         black 
925         white 
926         pastel-blue 
927        [EOF]
928        ");
929    }
930
931    #[test]
932    fn test_color_formatter_single_label() {
933        // Test that a single label can be colored and that the color is reset
934        // afterwards.
935        let config = config_from_string(
936            r#"
937        colors.inside = "green"
938        "#,
939        );
940        let mut output: Vec<u8> = vec![];
941        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
942        write!(formatter, " before ").unwrap();
943        formatter.push_label("inside");
944        write!(formatter, " inside ").unwrap();
945        formatter.pop_label();
946        write!(formatter, " after ").unwrap();
947        drop(formatter);
948        insta::assert_snapshot!(
949            to_snapshot_string(output), @" before  inside  after [EOF]");
950    }
951
952    #[test]
953    fn test_color_formatter_attributes() {
954        // Test that each attribute of the style can be set and that they can be
955        // combined in a single rule or by using multiple rules.
956        let config = config_from_string(
957            r#"
958        colors.red_fg = { fg = "red" }
959        colors.blue_bg = { bg = "blue" }
960        colors.bold_font = { bold = true }
961        colors.italic_text = { italic = true }
962        colors.underlined_text = { underline = true }
963        colors.reversed_colors = { reverse = true }
964        colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, reverse = true }
965        "#,
966        );
967        let mut output: Vec<u8> = vec![];
968        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
969        formatter.push_label("red_fg");
970        write!(formatter, " fg only ").unwrap();
971        formatter.pop_label();
972        writeln!(formatter).unwrap();
973        formatter.push_label("blue_bg");
974        write!(formatter, " bg only ").unwrap();
975        formatter.pop_label();
976        writeln!(formatter).unwrap();
977        formatter.push_label("bold_font");
978        write!(formatter, " bold only ").unwrap();
979        formatter.pop_label();
980        writeln!(formatter).unwrap();
981        formatter.push_label("italic_text");
982        write!(formatter, " italic only ").unwrap();
983        formatter.pop_label();
984        writeln!(formatter).unwrap();
985        formatter.push_label("underlined_text");
986        write!(formatter, " underlined only ").unwrap();
987        formatter.pop_label();
988        writeln!(formatter).unwrap();
989        formatter.push_label("reversed_colors");
990        write!(formatter, " reverse only ").unwrap();
991        formatter.pop_label();
992        writeln!(formatter).unwrap();
993        formatter.push_label("multiple");
994        write!(formatter, " single rule ").unwrap();
995        formatter.pop_label();
996        writeln!(formatter).unwrap();
997        formatter.push_label("red_fg");
998        formatter.push_label("blue_bg");
999        write!(formatter, " two rules ").unwrap();
1000        formatter.pop_label();
1001        formatter.pop_label();
1002        writeln!(formatter).unwrap();
1003        drop(formatter);
1004        insta::assert_snapshot!(to_snapshot_string(output), @r"
1005         fg only 
1006         bg only 
1007         bold only 
1008         italic only 
1009         underlined only 
1010         reverse only 
1011         single rule 
1012         two rules 
1013        [EOF]
1014        ");
1015    }
1016
1017    #[test]
1018    fn test_color_formatter_bold_reset() {
1019        // Test that we don't lose other attributes when we reset the bold attribute.
1020        let config = config_from_string(
1021            r#"
1022        colors.not_bold = { fg = "red", bg = "blue", italic = true, underline = true }
1023        colors.bold_font = { bold = true }
1024        "#,
1025        );
1026        let mut output: Vec<u8> = vec![];
1027        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1028        formatter.push_label("not_bold");
1029        write!(formatter, " not bold ").unwrap();
1030        formatter.push_label("bold_font");
1031        write!(formatter, " bold ").unwrap();
1032        formatter.pop_label();
1033        write!(formatter, " not bold again ").unwrap();
1034        formatter.pop_label();
1035        drop(formatter);
1036        insta::assert_snapshot!(
1037            to_snapshot_string(output),
1038            @" not bold  bold  not bold again [EOF]");
1039    }
1040
1041    #[test]
1042    fn test_color_formatter_reset_on_flush() {
1043        let config = config_from_string("colors.red = 'red'");
1044        let mut output: Vec<u8> = vec![];
1045        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1046        formatter.push_label("red");
1047        write!(formatter, "foo").unwrap();
1048        formatter.pop_label();
1049
1050        // without flush()
1051        insta::assert_snapshot!(
1052            to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1053
1054        // flush() should emit the reset sequence.
1055        formatter.flush().unwrap();
1056        insta::assert_snapshot!(
1057            to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1058
1059        // New color sequence should be emitted as the state was reset.
1060        formatter.push_label("red");
1061        write!(formatter, "bar").unwrap();
1062        formatter.pop_label();
1063
1064        // drop() should emit the reset sequence.
1065        drop(formatter);
1066        insta::assert_snapshot!(
1067            to_snapshot_string(output), @"foobar[EOF]");
1068    }
1069
1070    #[test]
1071    fn test_color_formatter_no_space() {
1072        // Test that two different colors can touch.
1073        let config = config_from_string(
1074            r#"
1075        colors.red = "red"
1076        colors.green = "green"
1077        "#,
1078        );
1079        let mut output: Vec<u8> = vec![];
1080        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1081        write!(formatter, "before").unwrap();
1082        formatter.push_label("red");
1083        write!(formatter, "first").unwrap();
1084        formatter.pop_label();
1085        formatter.push_label("green");
1086        write!(formatter, "second").unwrap();
1087        formatter.pop_label();
1088        write!(formatter, "after").unwrap();
1089        drop(formatter);
1090        insta::assert_snapshot!(
1091            to_snapshot_string(output), @"beforefirstsecondafter[EOF]");
1092    }
1093
1094    #[test]
1095    fn test_color_formatter_ansi_codes_in_text() {
1096        // Test that ANSI codes in the input text are escaped.
1097        let config = config_from_string(
1098            r#"
1099        colors.red = "red"
1100        "#,
1101        );
1102        let mut output: Vec<u8> = vec![];
1103        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1104        formatter.push_label("red");
1105        write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
1106        formatter.pop_label();
1107        drop(formatter);
1108        insta::assert_snapshot!(
1109            to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
1110    }
1111
1112    #[test]
1113    fn test_color_formatter_nested() {
1114        // A color can be associated with a combination of labels. A more specific match
1115        // overrides a less specific match. After the inner label is removed, the outer
1116        // color is used again (we don't reset).
1117        let config = config_from_string(
1118            r#"
1119        colors.outer = "blue"
1120        colors.inner = "red"
1121        colors."outer inner" = "green"
1122        "#,
1123        );
1124        let mut output: Vec<u8> = vec![];
1125        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1126        write!(formatter, " before outer ").unwrap();
1127        formatter.push_label("outer");
1128        write!(formatter, " before inner ").unwrap();
1129        formatter.push_label("inner");
1130        write!(formatter, " inside inner ").unwrap();
1131        formatter.pop_label();
1132        write!(formatter, " after inner ").unwrap();
1133        formatter.pop_label();
1134        write!(formatter, " after outer ").unwrap();
1135        drop(formatter);
1136        insta::assert_snapshot!(
1137            to_snapshot_string(output),
1138            @" before outer  before inner  inside inner  after inner  after outer [EOF]");
1139    }
1140
1141    #[test]
1142    fn test_color_formatter_partial_match() {
1143        // A partial match doesn't count
1144        let config = config_from_string(
1145            r#"
1146        colors."outer inner" = "green"
1147        "#,
1148        );
1149        let mut output: Vec<u8> = vec![];
1150        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1151        formatter.push_label("outer");
1152        write!(formatter, " not colored ").unwrap();
1153        formatter.push_label("inner");
1154        write!(formatter, " colored ").unwrap();
1155        formatter.pop_label();
1156        write!(formatter, " not colored ").unwrap();
1157        formatter.pop_label();
1158        drop(formatter);
1159        insta::assert_snapshot!(
1160            to_snapshot_string(output),
1161            @" not colored  colored  not colored [EOF]");
1162    }
1163
1164    #[test]
1165    fn test_color_formatter_unrecognized_color() {
1166        // An unrecognized color causes an error.
1167        let config = config_from_string(
1168            r#"
1169        colors."outer" = "red"
1170        colors."outer inner" = "bloo"
1171        "#,
1172        );
1173        let mut output: Vec<u8> = vec![];
1174        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1175        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1176        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1177    }
1178
1179    #[test]
1180    fn test_color_formatter_unrecognized_ansi256_color() {
1181        // An unrecognized ANSI color causes an error.
1182        let config = config_from_string(
1183            r##"
1184            colors."outer" = "red"
1185            colors."outer inner" = "ansi-color-256"
1186            "##,
1187        );
1188        let mut output: Vec<u8> = vec![];
1189        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1190        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1191        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1192    }
1193
1194    #[test]
1195    fn test_color_formatter_unrecognized_hex_color() {
1196        // An unrecognized hex color causes an error.
1197        let config = config_from_string(
1198            r##"
1199            colors."outer" = "red"
1200            colors."outer inner" = "#ffgggg"
1201            "##,
1202        );
1203        let mut output: Vec<u8> = vec![];
1204        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1205        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1206        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1207    }
1208
1209    #[test]
1210    fn test_color_formatter_invalid_type_of_color() {
1211        let config = config_from_string("colors.foo = []");
1212        let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1213        insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1214        insta::assert_snapshot!(
1215            err.source().unwrap(),
1216            @"invalid type: array, expected a color name or a table of styles");
1217    }
1218
1219    #[test]
1220    fn test_color_formatter_invalid_type_of_style() {
1221        let config = config_from_string("colors.foo = { bold = 1 }");
1222        let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1223        insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1224        insta::assert_snapshot!(err.source().unwrap(), @r"
1225        invalid type: integer `1`, expected a boolean
1226        in `bold`
1227        ");
1228    }
1229
1230    #[test]
1231    fn test_color_formatter_normal_color() {
1232        // The "default" color resets the color. It is possible to reset only the
1233        // background or only the foreground.
1234        let config = config_from_string(
1235            r#"
1236        colors."outer" = {bg="yellow", fg="blue"}
1237        colors."outer default_fg" = "default"
1238        colors."outer default_bg" = {bg = "default"}
1239        "#,
1240        );
1241        let mut output: Vec<u8> = vec![];
1242        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1243        formatter.push_label("outer");
1244        write!(formatter, "Blue on yellow, ").unwrap();
1245        formatter.push_label("default_fg");
1246        write!(formatter, " default fg, ").unwrap();
1247        formatter.pop_label();
1248        write!(formatter, " and back.\nBlue on yellow, ").unwrap();
1249        formatter.push_label("default_bg");
1250        write!(formatter, " default bg, ").unwrap();
1251        formatter.pop_label();
1252        write!(formatter, " and back.").unwrap();
1253        drop(formatter);
1254        insta::assert_snapshot!(to_snapshot_string(output), @r"
1255        Blue on yellow,  default fg,  and back.
1256        Blue on yellow,  default bg,  and back.[EOF]
1257        ");
1258    }
1259
1260    #[test]
1261    fn test_color_formatter_sibling() {
1262        // A partial match on one rule does not eliminate other rules.
1263        let config = config_from_string(
1264            r#"
1265        colors."outer1 inner1" = "red"
1266        colors.inner2 = "green"
1267        "#,
1268        );
1269        let mut output: Vec<u8> = vec![];
1270        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1271        formatter.push_label("outer1");
1272        formatter.push_label("inner2");
1273        write!(formatter, " hello ").unwrap();
1274        formatter.pop_label();
1275        formatter.pop_label();
1276        drop(formatter);
1277        insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1278    }
1279
1280    #[test]
1281    fn test_color_formatter_reverse_order() {
1282        // Rules don't match labels out of order
1283        let config = config_from_string(
1284            r#"
1285        colors."inner outer" = "green"
1286        "#,
1287        );
1288        let mut output: Vec<u8> = vec![];
1289        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1290        formatter.push_label("outer");
1291        formatter.push_label("inner");
1292        write!(formatter, " hello ").unwrap();
1293        formatter.pop_label();
1294        formatter.pop_label();
1295        drop(formatter);
1296        insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1297    }
1298
1299    #[test]
1300    fn test_color_formatter_innermost_wins() {
1301        // When two labels match, the innermost one wins.
1302        let config = config_from_string(
1303            r#"
1304        colors."a" = "red"
1305        colors."b" = "green"
1306        colors."a c" = "blue"
1307        colors."b c" = "yellow"
1308        "#,
1309        );
1310        let mut output: Vec<u8> = vec![];
1311        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1312        formatter.push_label("a");
1313        write!(formatter, " a1 ").unwrap();
1314        formatter.push_label("b");
1315        write!(formatter, " b1 ").unwrap();
1316        formatter.push_label("c");
1317        write!(formatter, " c ").unwrap();
1318        formatter.pop_label();
1319        write!(formatter, " b2 ").unwrap();
1320        formatter.pop_label();
1321        write!(formatter, " a2 ").unwrap();
1322        formatter.pop_label();
1323        drop(formatter);
1324        insta::assert_snapshot!(
1325            to_snapshot_string(output),
1326            @" a1  b1  c  b2  a2 [EOF]");
1327    }
1328
1329    #[test]
1330    fn test_color_formatter_dropped() {
1331        // Test that the style gets reset if the formatter is dropped without popping
1332        // all labels.
1333        let config = config_from_string(
1334            r#"
1335        colors.outer = "green"
1336        "#,
1337        );
1338        let mut output: Vec<u8> = vec![];
1339        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1340        formatter.push_label("outer");
1341        formatter.push_label("inner");
1342        write!(formatter, " inside ").unwrap();
1343        drop(formatter);
1344        insta::assert_snapshot!(to_snapshot_string(output), @" inside [EOF]");
1345    }
1346
1347    #[test]
1348    fn test_color_formatter_debug() {
1349        // Behaves like the color formatter, but surrounds each write with <<...>>,
1350        // adding the active labels before the actual content separated by a ::.
1351        let config = config_from_string(
1352            r#"
1353        colors.outer = "green"
1354        "#,
1355        );
1356        let mut output: Vec<u8> = vec![];
1357        let mut formatter = ColorFormatter::for_config(&mut output, &config, true).unwrap();
1358        formatter.push_label("outer");
1359        formatter.push_label("inner");
1360        write!(formatter, " inside ").unwrap();
1361        formatter.pop_label();
1362        formatter.pop_label();
1363        drop(formatter);
1364        insta::assert_snapshot!(
1365            to_snapshot_string(output), @"<<outer inner:: inside >>[EOF]");
1366    }
1367
1368    #[test]
1369    fn test_labeled_scope() {
1370        let config = config_from_string(indoc! {"
1371            [colors]
1372            outer = 'blue'
1373            inner = 'red'
1374            'outer inner' = 'green'
1375        "});
1376        let mut output: Vec<u8> = vec![];
1377        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1378        writeln!(formatter.labeled("outer"), "outer").unwrap();
1379        writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner").unwrap();
1380        writeln!(formatter.labeled("inner"), "inner").unwrap();
1381        drop(formatter);
1382        insta::assert_snapshot!(to_snapshot_string(output), @r"
1383        outer
1384        outer-inner
1385        inner
1386        [EOF]
1387        ");
1388    }
1389
1390    #[test]
1391    fn test_heading_labeled_writer() {
1392        let config = config_from_string(
1393            r#"
1394        colors.inner = "green"
1395        colors."inner heading" = "red"
1396        "#,
1397        );
1398        let mut output: Vec<u8> = vec![];
1399        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1400        formatter.labeled("inner").with_heading("Should be noop: ");
1401        let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1402        write!(writer, "Message").unwrap();
1403        writeln!(writer, " continues").unwrap();
1404        drop(writer);
1405        drop(formatter);
1406        insta::assert_snapshot!(to_snapshot_string(output), @r"
1407        Heading: Message continues
1408        [EOF]
1409        ");
1410    }
1411
1412    #[test]
1413    fn test_heading_labeled_writer_empty_string() {
1414        let mut output: Vec<u8> = vec![];
1415        let mut formatter = PlainTextFormatter::new(&mut output);
1416        let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1417        // write_fmt() is called even if the format string is empty. I don't
1418        // know if that's guaranteed, but let's record the current behavior.
1419        write!(writer, "").unwrap();
1420        write!(writer, "").unwrap();
1421        drop(writer);
1422        insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1423    }
1424
1425    #[test]
1426    fn test_format_recorder() {
1427        let mut recorder = FormatRecorder::new();
1428        write!(recorder, " outer1 ").unwrap();
1429        recorder.push_label("inner");
1430        write!(recorder, " inner1 ").unwrap();
1431        write!(recorder, " inner2 ").unwrap();
1432        recorder.pop_label();
1433        write!(recorder, " outer2 ").unwrap();
1434
1435        insta::assert_snapshot!(
1436            to_snapshot_string(recorder.data()),
1437            @" outer1  inner1  inner2  outer2 [EOF]");
1438
1439        // Replayed output should be labeled.
1440        let config = config_from_string(r#" colors.inner = "red" "#);
1441        let mut output: Vec<u8> = vec![];
1442        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1443        recorder.replay(&mut formatter).unwrap();
1444        drop(formatter);
1445        insta::assert_snapshot!(
1446            to_snapshot_string(output),
1447            @" outer1  inner1  inner2  outer2 [EOF]");
1448
1449        // Replayed output should be split at push/pop_label() call.
1450        let mut output: Vec<u8> = vec![];
1451        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1452        recorder
1453            .replay_with(&mut formatter, |formatter, range| {
1454                let data = &recorder.data()[range];
1455                write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1456            })
1457            .unwrap();
1458        drop(formatter);
1459        insta::assert_snapshot!(
1460            to_snapshot_string(output),
1461            @"<< outer1 >><< inner1  inner2 >><< outer2 >>[EOF]");
1462    }
1463
1464    #[test]
1465    fn test_raw_format_recorder() {
1466        // Note: similar to test_format_recorder above
1467        let mut recorder = FormatRecorder::new();
1468        write!(recorder.raw().unwrap(), " outer1 ").unwrap();
1469        recorder.push_label("inner");
1470        write!(recorder.raw().unwrap(), " inner1 ").unwrap();
1471        write!(recorder.raw().unwrap(), " inner2 ").unwrap();
1472        recorder.pop_label();
1473        write!(recorder.raw().unwrap(), " outer2 ").unwrap();
1474
1475        // Replayed raw escape sequences are labeled.
1476        let config = config_from_string(r#" colors.inner = "red" "#);
1477        let mut output: Vec<u8> = vec![];
1478        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1479        recorder.replay(&mut formatter).unwrap();
1480        drop(formatter);
1481        insta::assert_snapshot!(
1482            to_snapshot_string(output), @" outer1  inner1  inner2  outer2 [EOF]");
1483
1484        let mut output: Vec<u8> = vec![];
1485        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1486        recorder
1487            .replay_with(&mut formatter, |_formatter, range| {
1488                panic!(
1489                    "Called with {:?} when all output should be raw",
1490                    str::from_utf8(&recorder.data()[range]).unwrap()
1491                );
1492            })
1493            .unwrap();
1494        drop(formatter);
1495        insta::assert_snapshot!(
1496            to_snapshot_string(output), @" outer1  inner1  inner2  outer2 [EOF]");
1497    }
1498}