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), @"
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_for_hex() {
923        assert_eq!(
924            color_for_hex("#000000"),
925            Some(Color::Rgb { r: 0, g: 0, b: 0 })
926        );
927        assert_eq!(
928            color_for_hex("#fab123"),
929            Some(Color::Rgb {
930                r: 0xfa,
931                g: 0xb1,
932                b: 0x23
933            })
934        );
935        assert_eq!(
936            color_for_hex("#F00D13"),
937            Some(Color::Rgb {
938                r: 0xf0,
939                g: 0x0d,
940                b: 0x13
941            })
942        );
943        assert_eq!(
944            color_for_hex("#ffffff"),
945            Some(Color::Rgb {
946                r: 255,
947                g: 255,
948                b: 255
949            })
950        );
951
952        assert_eq!(color_for_hex("000000"), None);
953        assert_eq!(color_for_hex("0000000"), None);
954        assert_eq!(color_for_hex("#00000g"), None);
955        assert_eq!(color_for_hex("#á00000"), None);
956    }
957
958    #[test]
959    fn test_color_formatter_ansi256() {
960        let config = config_from_string(
961            r#"
962        [colors]
963        purple-bg = { fg = "ansi-color-15", bg = "ansi-color-93" }
964        gray = "ansi-color-244"
965        "#,
966        );
967        let mut output: Vec<u8> = vec![];
968        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
969        formatter.push_label("purple-bg");
970        write!(formatter, " purple background ").unwrap();
971        formatter.pop_label();
972        writeln!(formatter).unwrap();
973        formatter.push_label("gray");
974        write!(formatter, " gray ").unwrap();
975        formatter.pop_label();
976        writeln!(formatter).unwrap();
977        drop(formatter);
978        insta::assert_snapshot!(to_snapshot_string(output), @"
979         purple background 
980         gray 
981        [EOF]
982        ");
983    }
984
985    #[test]
986    fn test_color_formatter_hex_colors() {
987        // Test the color code for each color.
988        let config = config_from_string(indoc! {"
989            [colors]
990            black = '#000000'
991            white = '#ffffff'
992            pastel-blue = '#AFE0D9'
993        "});
994        let colors: IndexMap<String, String> = config.get("colors").unwrap();
995        let mut output: Vec<u8> = vec![];
996        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
997        for label in colors.keys() {
998            formatter.push_label(&label.replace(' ', "-"));
999            write!(formatter, " {label} ").unwrap();
1000            formatter.pop_label();
1001            writeln!(formatter).unwrap();
1002        }
1003        drop(formatter);
1004        insta::assert_snapshot!(to_snapshot_string(output), @"
1005         black 
1006         white 
1007         pastel-blue 
1008        [EOF]
1009        ");
1010    }
1011
1012    #[test]
1013    fn test_color_formatter_single_label() {
1014        // Test that a single label can be colored and that the color is reset
1015        // afterwards.
1016        let config = config_from_string(
1017            r#"
1018        colors.inside = "green"
1019        "#,
1020        );
1021        let mut output: Vec<u8> = vec![];
1022        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1023        write!(formatter, " before ").unwrap();
1024        formatter.push_label("inside");
1025        write!(formatter, " inside ").unwrap();
1026        formatter.pop_label();
1027        write!(formatter, " after ").unwrap();
1028        drop(formatter);
1029        insta::assert_snapshot!(
1030            to_snapshot_string(output), @" before  inside  after [EOF]");
1031    }
1032
1033    #[test]
1034    fn test_color_formatter_attributes() {
1035        // Test that each attribute of the style can be set and that they can be
1036        // combined in a single rule or by using multiple rules.
1037        let config = config_from_string(
1038            r#"
1039        colors.red_fg = { fg = "red" }
1040        colors.blue_bg = { bg = "blue" }
1041        colors.bold_font = { bold = true }
1042        colors.dim_font = { dim = true }
1043        colors.italic_text = { italic = true }
1044        colors.underlined_text = { underline = true }
1045        colors.reversed_colors = { reverse = true }
1046        colors.multiple = { fg = "green", bg = "yellow", bold = true, italic = true, underline = true, reverse = true }
1047        "#,
1048        );
1049        let mut output: Vec<u8> = vec![];
1050        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1051        formatter.push_label("red_fg");
1052        write!(formatter, " fg only ").unwrap();
1053        formatter.pop_label();
1054        writeln!(formatter).unwrap();
1055        formatter.push_label("blue_bg");
1056        write!(formatter, " bg only ").unwrap();
1057        formatter.pop_label();
1058        writeln!(formatter).unwrap();
1059        formatter.push_label("bold_font");
1060        write!(formatter, " bold only ").unwrap();
1061        formatter.pop_label();
1062        writeln!(formatter).unwrap();
1063        formatter.push_label("dim_font");
1064        write!(formatter, " dim only ").unwrap();
1065        formatter.pop_label();
1066        writeln!(formatter).unwrap();
1067        formatter.push_label("italic_text");
1068        write!(formatter, " italic only ").unwrap();
1069        formatter.pop_label();
1070        writeln!(formatter).unwrap();
1071        formatter.push_label("underlined_text");
1072        write!(formatter, " underlined only ").unwrap();
1073        formatter.pop_label();
1074        writeln!(formatter).unwrap();
1075        formatter.push_label("reversed_colors");
1076        write!(formatter, " reverse only ").unwrap();
1077        formatter.pop_label();
1078        writeln!(formatter).unwrap();
1079        formatter.push_label("multiple");
1080        write!(formatter, " single rule ").unwrap();
1081        formatter.pop_label();
1082        writeln!(formatter).unwrap();
1083        formatter.push_label("red_fg");
1084        formatter.push_label("blue_bg");
1085        write!(formatter, " two rules ").unwrap();
1086        formatter.pop_label();
1087        formatter.pop_label();
1088        writeln!(formatter).unwrap();
1089        drop(formatter);
1090        insta::assert_snapshot!(to_snapshot_string(output), @"
1091         fg only 
1092         bg only 
1093         bold only 
1094         dim only 
1095         italic only 
1096         underlined only 
1097         reverse only 
1098         single rule 
1099         two rules 
1100        [EOF]
1101        ");
1102    }
1103
1104    #[test]
1105    fn test_color_formatter_bold_reset() {
1106        // Test that we don't lose other attributes when we reset the bold attribute.
1107        let config = config_from_string(indoc! {"
1108            [colors]
1109            not_bold = { fg = 'red', bg = 'blue', italic = true, underline = true }
1110            bold_font = { bold = true }
1111            stop_bold = { bold = false }
1112        "});
1113        let mut output: Vec<u8> = vec![];
1114        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1115        formatter.push_label("not_bold");
1116        write!(formatter, " not bold ").unwrap();
1117        formatter.push_label("bold_font");
1118        write!(formatter, " bold ").unwrap();
1119        formatter.push_label("stop_bold");
1120        write!(formatter, " stop bold ").unwrap();
1121        formatter.pop_label();
1122        write!(formatter, " bold again ").unwrap();
1123        formatter.pop_label();
1124        write!(formatter, " not bold again ").unwrap();
1125        formatter.pop_label();
1126        drop(formatter);
1127        insta::assert_snapshot!(
1128            to_snapshot_string(output),
1129            @" not bold  bold  stop bold  bold again  not bold again [EOF]");
1130    }
1131
1132    #[test]
1133    fn test_color_formatter_dim_reset() {
1134        // Test that we don't lose other attributes when we reset the dim attribute.
1135        let config = config_from_string(indoc! {"
1136            [colors]
1137            not_dim = { fg = 'red', bg = 'blue', italic = true, underline = true }
1138            dim_font = { dim = true }
1139            stop_dim = { dim = false }
1140        "});
1141        let mut output: Vec<u8> = vec![];
1142        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1143        formatter.push_label("not_dim");
1144        write!(formatter, " not dim ").unwrap();
1145        formatter.push_label("dim_font");
1146        write!(formatter, " dim ").unwrap();
1147        formatter.push_label("stop_dim");
1148        write!(formatter, " stop dim ").unwrap();
1149        formatter.pop_label();
1150        write!(formatter, " dim again ").unwrap();
1151        formatter.pop_label();
1152        write!(formatter, " not dim again ").unwrap();
1153        formatter.pop_label();
1154        drop(formatter);
1155        insta::assert_snapshot!(
1156            to_snapshot_string(output),
1157            @" not dim  dim  stop dim  dim again  not dim again [EOF]");
1158    }
1159
1160    #[test]
1161    fn test_color_formatter_bold_to_dim() {
1162        // Test that we don't lose bold when we reset the dim attribute.
1163        let config = config_from_string(indoc! {"
1164            [colors]
1165            bold_font = { bold = true }
1166            dim_font = { dim = true }
1167        "});
1168        let mut output: Vec<u8> = vec![];
1169        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1170        formatter.push_label("bold_font");
1171        write!(formatter, " bold ").unwrap();
1172        formatter.push_label("dim_font");
1173        write!(formatter, " bold&dim ").unwrap();
1174        formatter.pop_label();
1175        write!(formatter, " bold again ").unwrap();
1176        formatter.pop_label();
1177        drop(formatter);
1178        insta::assert_snapshot!(
1179            to_snapshot_string(output),
1180            @" bold  bold&dim  bold again [EOF]");
1181    }
1182
1183    #[test]
1184    fn test_formatter_reset_on_flush() {
1185        let config = config_from_string("colors.red = 'red'");
1186        let mut output: Vec<u8> = vec![];
1187        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1188        formatter.push_label("red");
1189        write!(formatter, "foo").unwrap();
1190        formatter.pop_label();
1191
1192        // without flush()
1193        insta::assert_snapshot!(
1194            to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1195
1196        // flush() should emit the reset sequence.
1197        formatter.flush().unwrap();
1198        insta::assert_snapshot!(
1199            to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1200
1201        // New color sequence should be emitted as the state was reset.
1202        formatter.push_label("red");
1203        write!(formatter, "bar").unwrap();
1204        formatter.pop_label();
1205
1206        // drop() should emit the reset sequence.
1207        drop(formatter);
1208        insta::assert_snapshot!(
1209            to_snapshot_string(output), @"foobar[EOF]");
1210
1211        // plaintext and sanitizing formatters produce no special behavior
1212        let mut output: Vec<u8> = vec![];
1213        let mut formatter = PlainTextFormatter::new(&mut output);
1214        formatter.push_label("red");
1215        write!(formatter, "foo").unwrap();
1216        formatter.pop_label();
1217        formatter.flush().unwrap();
1218        insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1219
1220        let mut output: Vec<u8> = vec![];
1221        let mut formatter = SanitizingFormatter::new(&mut output);
1222        formatter.push_label("red");
1223        write!(formatter, "foo").unwrap();
1224        formatter.pop_label();
1225        formatter.flush().unwrap();
1226        insta::assert_snapshot!(to_snapshot_string(formatter.output.clone()), @"foo[EOF]");
1227    }
1228
1229    #[test]
1230    fn test_color_formatter_no_space() {
1231        // Test that two different colors can touch.
1232        let config = config_from_string(
1233            r#"
1234        colors.red = "red"
1235        colors.green = "green"
1236        "#,
1237        );
1238        let mut output: Vec<u8> = vec![];
1239        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1240        write!(formatter, "before").unwrap();
1241        formatter.push_label("red");
1242        write!(formatter, "first").unwrap();
1243        formatter.pop_label();
1244        formatter.push_label("green");
1245        write!(formatter, "second").unwrap();
1246        formatter.pop_label();
1247        write!(formatter, "after").unwrap();
1248        drop(formatter);
1249        insta::assert_snapshot!(
1250            to_snapshot_string(output), @"beforefirstsecondafter[EOF]");
1251    }
1252
1253    #[test]
1254    fn test_color_formatter_ansi_codes_in_text() {
1255        // Test that ANSI codes in the input text are escaped.
1256        let config = config_from_string(
1257            r#"
1258        colors.red = "red"
1259        "#,
1260        );
1261        let mut output: Vec<u8> = vec![];
1262        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1263        formatter.push_label("red");
1264        write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
1265        formatter.pop_label();
1266        drop(formatter);
1267        insta::assert_snapshot!(
1268            to_snapshot_string(output), @"␛[1mnot actually bold␛[0m[EOF]");
1269    }
1270
1271    #[test]
1272    fn test_color_formatter_nested() {
1273        // A color can be associated with a combination of labels. A more specific match
1274        // overrides a less specific match. After the inner label is removed, the outer
1275        // color is used again (we don't reset).
1276        let config = config_from_string(
1277            r#"
1278        colors.outer = "blue"
1279        colors.inner = "red"
1280        colors."outer inner" = "green"
1281        "#,
1282        );
1283        let mut output: Vec<u8> = vec![];
1284        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1285        write!(formatter, " before outer ").unwrap();
1286        formatter.push_label("outer");
1287        write!(formatter, " before inner ").unwrap();
1288        formatter.push_label("inner");
1289        write!(formatter, " inside inner ").unwrap();
1290        formatter.pop_label();
1291        write!(formatter, " after inner ").unwrap();
1292        formatter.pop_label();
1293        write!(formatter, " after outer ").unwrap();
1294        drop(formatter);
1295        insta::assert_snapshot!(
1296            to_snapshot_string(output),
1297            @" before outer  before inner  inside inner  after inner  after outer [EOF]");
1298    }
1299
1300    #[test]
1301    fn test_color_formatter_partial_match() {
1302        // A partial match doesn't count
1303        let config = config_from_string(
1304            r#"
1305        colors."outer inner" = "green"
1306        "#,
1307        );
1308        let mut output: Vec<u8> = vec![];
1309        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1310        formatter.push_label("outer");
1311        write!(formatter, " not colored ").unwrap();
1312        formatter.push_label("inner");
1313        write!(formatter, " colored ").unwrap();
1314        formatter.pop_label();
1315        write!(formatter, " not colored ").unwrap();
1316        formatter.pop_label();
1317        drop(formatter);
1318        insta::assert_snapshot!(
1319            to_snapshot_string(output),
1320            @" not colored  colored  not colored [EOF]");
1321    }
1322
1323    #[test]
1324    fn test_color_formatter_unrecognized_color() {
1325        // An unrecognized color causes an error.
1326        let config = config_from_string(
1327            r#"
1328        colors."outer" = "red"
1329        colors."outer inner" = "bloo"
1330        "#,
1331        );
1332        let mut output: Vec<u8> = vec![];
1333        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1334        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1335        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: bloo");
1336    }
1337
1338    #[test]
1339    fn test_color_formatter_unrecognized_ansi256_color() {
1340        // An unrecognized ANSI color causes an error.
1341        let config = config_from_string(
1342            r##"
1343            colors."outer" = "red"
1344            colors."outer inner" = "ansi-color-256"
1345            "##,
1346        );
1347        let mut output: Vec<u8> = vec![];
1348        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1349        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1350        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: ansi-color-256");
1351    }
1352
1353    #[test]
1354    fn test_color_formatter_unrecognized_hex_color() {
1355        // An unrecognized hex color causes an error.
1356        let config = config_from_string(
1357            r##"
1358            colors."outer" = "red"
1359            colors."outer inner" = "#ffgggg"
1360            "##,
1361        );
1362        let mut output: Vec<u8> = vec![];
1363        let err = ColorFormatter::for_config(&mut output, &config, false).unwrap_err();
1364        insta::assert_snapshot!(err, @r#"Invalid type or value for colors."outer inner""#);
1365        insta::assert_snapshot!(err.source().unwrap(), @"Invalid color: #ffgggg");
1366    }
1367
1368    #[test]
1369    fn test_color_formatter_invalid_type_of_color() {
1370        let config = config_from_string("colors.foo = []");
1371        let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1372        insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1373        insta::assert_snapshot!(
1374            err.source().unwrap(),
1375            @"invalid type: array, expected a color name or a table of styles");
1376    }
1377
1378    #[test]
1379    fn test_color_formatter_invalid_type_of_style() {
1380        let config = config_from_string("colors.foo = { bold = 1 }");
1381        let err = ColorFormatter::for_config(&mut Vec::new(), &config, false).unwrap_err();
1382        insta::assert_snapshot!(err, @"Invalid type or value for colors.foo");
1383        insta::assert_snapshot!(err.source().unwrap(), @"
1384        invalid type: integer `1`, expected a boolean
1385        in `bold`
1386        ");
1387    }
1388
1389    #[test]
1390    fn test_color_formatter_normal_color() {
1391        // The "default" color resets the color. It is possible to reset only the
1392        // background or only the foreground.
1393        let config = config_from_string(
1394            r#"
1395        colors."outer" = {bg="yellow", fg="blue"}
1396        colors."outer default_fg" = "default"
1397        colors."outer default_bg" = {bg = "default"}
1398        "#,
1399        );
1400        let mut output: Vec<u8> = vec![];
1401        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1402        formatter.push_label("outer");
1403        write!(formatter, "Blue on yellow, ").unwrap();
1404        formatter.push_label("default_fg");
1405        write!(formatter, " default fg, ").unwrap();
1406        formatter.pop_label();
1407        write!(formatter, " and back.\nBlue on yellow, ").unwrap();
1408        formatter.push_label("default_bg");
1409        write!(formatter, " default bg, ").unwrap();
1410        formatter.pop_label();
1411        write!(formatter, " and back.").unwrap();
1412        drop(formatter);
1413        insta::assert_snapshot!(to_snapshot_string(output), @"
1414        Blue on yellow,  default fg,  and back.
1415        Blue on yellow,  default bg,  and back.[EOF]
1416        ");
1417    }
1418
1419    #[test]
1420    fn test_color_formatter_sibling() {
1421        // A partial match on one rule does not eliminate other rules.
1422        let config = config_from_string(
1423            r#"
1424        colors."outer1 inner1" = "red"
1425        colors.inner2 = "green"
1426        "#,
1427        );
1428        let mut output: Vec<u8> = vec![];
1429        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1430        formatter.push_label("outer1");
1431        formatter.push_label("inner2");
1432        write!(formatter, " hello ").unwrap();
1433        formatter.pop_label();
1434        formatter.pop_label();
1435        drop(formatter);
1436        insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1437    }
1438
1439    #[test]
1440    fn test_color_formatter_reverse_order() {
1441        // Rules don't match labels out of order
1442        let config = config_from_string(
1443            r#"
1444        colors."inner outer" = "green"
1445        "#,
1446        );
1447        let mut output: Vec<u8> = vec![];
1448        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1449        formatter.push_label("outer");
1450        formatter.push_label("inner");
1451        write!(formatter, " hello ").unwrap();
1452        formatter.pop_label();
1453        formatter.pop_label();
1454        drop(formatter);
1455        insta::assert_snapshot!(to_snapshot_string(output), @" hello [EOF]");
1456    }
1457
1458    #[test]
1459    fn test_color_formatter_innermost_wins() {
1460        // When two labels match, the innermost one wins.
1461        let config = config_from_string(
1462            r#"
1463        colors."a" = "red"
1464        colors."b" = "green"
1465        colors."a c" = "blue"
1466        colors."b c" = "yellow"
1467        "#,
1468        );
1469        let mut output: Vec<u8> = vec![];
1470        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1471        formatter.push_label("a");
1472        write!(formatter, " a1 ").unwrap();
1473        formatter.push_label("b");
1474        write!(formatter, " b1 ").unwrap();
1475        formatter.push_label("c");
1476        write!(formatter, " c ").unwrap();
1477        formatter.pop_label();
1478        write!(formatter, " b2 ").unwrap();
1479        formatter.pop_label();
1480        write!(formatter, " a2 ").unwrap();
1481        formatter.pop_label();
1482        drop(formatter);
1483        insta::assert_snapshot!(
1484            to_snapshot_string(output),
1485            @" a1  b1  c  b2  a2 [EOF]");
1486    }
1487
1488    #[test]
1489    fn test_color_formatter_dropped() {
1490        // Test that the style gets reset if the formatter is dropped without popping
1491        // all labels.
1492        let config = config_from_string(
1493            r#"
1494        colors.outer = "green"
1495        "#,
1496        );
1497        let mut output: Vec<u8> = vec![];
1498        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1499        formatter.push_label("outer");
1500        formatter.push_label("inner");
1501        write!(formatter, " inside ").unwrap();
1502        drop(formatter);
1503        insta::assert_snapshot!(to_snapshot_string(output), @" inside [EOF]");
1504    }
1505
1506    #[test]
1507    fn test_color_formatter_debug() {
1508        // Behaves like the color formatter, but surrounds each write with <<...>>,
1509        // adding the active labels before the actual content separated by a ::.
1510        let config = config_from_string(
1511            r#"
1512        colors.outer = "green"
1513        "#,
1514        );
1515        let mut output: Vec<u8> = vec![];
1516        let mut formatter = ColorFormatter::for_config(&mut output, &config, true).unwrap();
1517        formatter.push_label("outer");
1518        formatter.push_label("inner");
1519        write!(formatter, " inside ").unwrap();
1520        formatter.pop_label();
1521        formatter.pop_label();
1522        // Matching debug styles are not separated.
1523        formatter.push_label("outer");
1524        formatter.push_label("inner");
1525        write!(formatter, " inside two ").unwrap();
1526        formatter.pop_label();
1527        formatter.pop_label();
1528        drop(formatter);
1529        insta::assert_snapshot!(
1530            to_snapshot_string(output),
1531            @"<<outer inner:: inside  inside two >>[EOF]",
1532        );
1533    }
1534
1535    #[test]
1536    fn test_labeled_scope() {
1537        let config = config_from_string(indoc! {"
1538            [colors]
1539            outer = 'blue'
1540            inner = 'red'
1541            'outer inner' = 'green'
1542        "});
1543        let mut output: Vec<u8> = vec![];
1544        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1545        writeln!(formatter.labeled("outer"), "outer").unwrap();
1546        writeln!(formatter.labeled("outer").labeled("inner"), "outer-inner").unwrap();
1547        writeln!(formatter.labeled("inner"), "inner").unwrap();
1548        drop(formatter);
1549        insta::assert_snapshot!(to_snapshot_string(output), @"
1550        outer
1551        outer-inner
1552        inner
1553        [EOF]
1554        ");
1555    }
1556
1557    #[test]
1558    fn test_heading_labeled_writer() {
1559        let config = config_from_string(
1560            r#"
1561        colors.inner = "green"
1562        colors."inner heading" = "red"
1563        "#,
1564        );
1565        let mut output: Vec<u8> = vec![];
1566        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1567        formatter.labeled("inner").with_heading("Should be noop: ");
1568        let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1569        write!(writer, "Message").unwrap();
1570        writeln!(writer, " continues").unwrap();
1571        drop(writer);
1572        drop(formatter);
1573        insta::assert_snapshot!(to_snapshot_string(output), @"
1574        Heading: Message continues
1575        [EOF]
1576        ");
1577    }
1578
1579    #[test]
1580    fn test_heading_labeled_writer_empty_string() {
1581        let mut output: Vec<u8> = vec![];
1582        let mut formatter = PlainTextFormatter::new(&mut output);
1583        let mut writer = formatter.labeled("inner").with_heading("Heading: ");
1584        // write_fmt() is called even if the format string is empty. I don't
1585        // know if that's guaranteed, but let's record the current behavior.
1586        write!(writer, "").unwrap();
1587        write!(writer, "").unwrap();
1588        drop(writer);
1589        insta::assert_snapshot!(to_snapshot_string(output), @"Heading: [EOF]");
1590    }
1591
1592    #[test]
1593    fn test_format_recorder() {
1594        let mut recorder = FormatRecorder::new(false);
1595        write!(recorder, " outer1 ").unwrap();
1596        recorder.push_label("inner");
1597        write!(recorder, " inner1 ").unwrap();
1598        write!(recorder, " inner2 ").unwrap();
1599        recorder.pop_label();
1600        write!(recorder, " outer2 ").unwrap();
1601
1602        insta::assert_snapshot!(
1603            to_snapshot_string(recorder.data()),
1604            @" outer1  inner1  inner2  outer2 [EOF]");
1605
1606        // Replayed output should be labeled.
1607        let config = config_from_string(r#" colors.inner = "red" "#);
1608        let mut output: Vec<u8> = vec![];
1609        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1610        recorder.replay(&mut formatter).unwrap();
1611        drop(formatter);
1612        insta::assert_snapshot!(
1613            to_snapshot_string(output),
1614            @" outer1  inner1  inner2  outer2 [EOF]");
1615
1616        // Replayed output should be split at push/pop_label() call.
1617        let mut output: Vec<u8> = vec![];
1618        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1619        recorder
1620            .replay_with(&mut formatter, |formatter, range| {
1621                let data = &recorder.data()[range];
1622                write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
1623            })
1624            .unwrap();
1625        drop(formatter);
1626        insta::assert_snapshot!(
1627            to_snapshot_string(output),
1628            @"<< outer1 >><< inner1  inner2 >><< outer2 >>[EOF]");
1629    }
1630
1631    #[test]
1632    fn test_raw_format_recorder() {
1633        // Note: similar to test_format_recorder above
1634        let mut recorder = FormatRecorder::new(false);
1635        write!(recorder.raw().unwrap(), " outer1 ").unwrap();
1636        recorder.push_label("inner");
1637        write!(recorder.raw().unwrap(), " inner1 ").unwrap();
1638        write!(recorder.raw().unwrap(), " inner2 ").unwrap();
1639        recorder.pop_label();
1640        write!(recorder.raw().unwrap(), " outer2 ").unwrap();
1641
1642        // Replayed raw escape sequences are labeled.
1643        let config = config_from_string(r#" colors.inner = "red" "#);
1644        let mut output: Vec<u8> = vec![];
1645        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1646        recorder.replay(&mut formatter).unwrap();
1647        drop(formatter);
1648        insta::assert_snapshot!(
1649            to_snapshot_string(output), @" outer1  inner1  inner2  outer2 [EOF]");
1650
1651        let mut output: Vec<u8> = vec![];
1652        let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
1653        recorder
1654            .replay_with(&mut formatter, |_formatter, range| {
1655                panic!(
1656                    "Called with {:?} when all output should be raw",
1657                    str::from_utf8(&recorder.data()[range]).unwrap()
1658                );
1659            })
1660            .unwrap();
1661        drop(formatter);
1662        insta::assert_snapshot!(
1663            to_snapshot_string(output), @" outer1  inner1  inner2  outer2 [EOF]");
1664    }
1665}