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