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