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