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