Skip to main content

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