ok/jj
1
0
Fork 0
forked from mirrors/jj
jj/cli/src/formatter.rs

1247 lines
43 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright 2020 The Jujutsu Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::borrow::BorrowMut;
use std::collections::HashMap;
use std::fmt;
use std::io;
use std::io::Error;
use std::io::Write;
use std::mem;
use std::ops::Range;
use std::sync::Arc;
use crossterm::queue;
use crossterm::style::Attribute;
use crossterm::style::Color;
use crossterm::style::SetAttribute;
use crossterm::style::SetBackgroundColor;
use crossterm::style::SetForegroundColor;
use itertools::Itertools;
// Lets the caller label strings and translates the labels to colors
pub trait Formatter: Write {
/// Returns the backing `Write`. This is useful for writing data that is
/// already formatted, such as in the graphical log.
fn raw(&mut self) -> &mut dyn Write;
fn push_label(&mut self, label: &str) -> io::Result<()>;
fn pop_label(&mut self) -> io::Result<()>;
}
impl dyn Formatter + '_ {
pub fn labeled<S: AsRef<str>>(&mut self, label: S) -> LabeledWriter<&mut Self, S> {
LabeledWriter {
formatter: self,
label,
}
}
pub fn with_label<E: From<io::Error>>(
&mut self,
label: &str,
write_inner: impl FnOnce(&mut dyn Formatter) -> Result<(), E>,
) -> Result<(), E> {
self.push_label(label)?;
// Call `pop_label()` whether or not `write_inner()` fails, but don't let
// its error replace the one from `write_inner()`.
write_inner(self).and(self.pop_label().map_err(Into::into))
}
}
/// `Formatter` wrapper to write a labeled message with `write!()` or
/// `writeln!()`.
pub struct LabeledWriter<T, S> {
formatter: T,
label: S,
}
impl<T, S> LabeledWriter<T, S> {
pub fn new(formatter: T, label: S) -> Self {
LabeledWriter { formatter, label }
}
/// Turns into writer that prints labeled message with the `heading`.
pub fn with_heading<H>(self, heading: H) -> HeadingLabeledWriter<T, S, H> {
HeadingLabeledWriter::new(self, heading)
}
}
impl<'a, T, S> LabeledWriter<T, S>
where
T: BorrowMut<dyn Formatter + 'a>,
S: AsRef<str>,
{
pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
self.with_labeled(|formatter| formatter.write_fmt(args))
}
fn with_labeled(
&mut self,
write_inner: impl FnOnce(&mut dyn Formatter) -> io::Result<()>,
) -> io::Result<()> {
self.formatter
.borrow_mut()
.with_label(self.label.as_ref(), write_inner)
}
}
/// Like `LabeledWriter`, but also prints the `heading` once.
///
/// The `heading` will be printed within the first `write!()` or `writeln!()`
/// invocation, which is handy because `io::Error` can be handled there.
pub struct HeadingLabeledWriter<T, S, H> {
writer: LabeledWriter<T, S>,
heading: Option<H>,
}
impl<T, S, H> HeadingLabeledWriter<T, S, H> {
pub fn new(writer: LabeledWriter<T, S>, heading: H) -> Self {
HeadingLabeledWriter {
writer,
heading: Some(heading),
}
}
}
impl<'a, T, S, H> HeadingLabeledWriter<T, S, H>
where
T: BorrowMut<dyn Formatter + 'a>,
S: AsRef<str>,
H: fmt::Display,
{
pub fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> io::Result<()> {
self.writer.with_labeled(|formatter| {
if let Some(heading) = self.heading.take() {
write!(formatter.labeled("heading"), "{heading}")?;
}
formatter.write_fmt(args)
})
}
}
type Rules = Vec<(Vec<String>, Style)>;
/// Creates `Formatter` instances with preconfigured parameters.
#[derive(Clone, Debug)]
pub struct FormatterFactory {
kind: FormatterFactoryKind,
}
#[derive(Clone, Debug)]
enum FormatterFactoryKind {
PlainText,
Sanitized,
Color { rules: Arc<Rules>, debug: bool },
}
impl FormatterFactory {
pub fn plain_text() -> Self {
let kind = FormatterFactoryKind::PlainText;
FormatterFactory { kind }
}
pub fn sanitized() -> Self {
let kind = FormatterFactoryKind::Sanitized;
FormatterFactory { kind }
}
pub fn color(config: &config::Config, debug: bool) -> Result<Self, config::ConfigError> {
let rules = Arc::new(rules_from_config(config)?);
let kind = FormatterFactoryKind::Color { rules, debug };
Ok(FormatterFactory { kind })
}
pub fn new_formatter<'output, W: Write + 'output>(
&self,
output: W,
) -> Box<dyn Formatter + 'output> {
match &self.kind {
FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)),
FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)),
FormatterFactoryKind::Color { rules, debug } => {
Box::new(ColorFormatter::new(output, rules.clone(), *debug))
}
}
}
pub fn is_color(&self) -> bool {
matches!(self.kind, FormatterFactoryKind::Color { .. })
}
}
pub struct PlainTextFormatter<W> {
output: W,
}
impl<W> PlainTextFormatter<W> {
pub fn new(output: W) -> PlainTextFormatter<W> {
Self { output }
}
}
impl<W: Write> Write for PlainTextFormatter<W> {
fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
self.output.write(data)
}
fn flush(&mut self) -> Result<(), Error> {
self.output.flush()
}
}
impl<W: Write> Formatter for PlainTextFormatter<W> {
fn raw(&mut self) -> &mut dyn Write {
&mut self.output
}
fn push_label(&mut self, _label: &str) -> io::Result<()> {
Ok(())
}
fn pop_label(&mut self) -> io::Result<()> {
Ok(())
}
}
pub struct SanitizingFormatter<W> {
output: W,
}
impl<W> SanitizingFormatter<W> {
pub fn new(output: W) -> SanitizingFormatter<W> {
Self { output }
}
}
impl<W: Write> Write for SanitizingFormatter<W> {
fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
write_sanitized(&mut self.output, data)?;
Ok(data.len())
}
fn flush(&mut self) -> Result<(), Error> {
self.output.flush()
}
}
impl<W: Write> Formatter for SanitizingFormatter<W> {
fn raw(&mut self) -> &mut dyn Write {
&mut self.output
}
fn push_label(&mut self, _label: &str) -> io::Result<()> {
Ok(())
}
fn pop_label(&mut self) -> io::Result<()> {
Ok(())
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Style {
pub fg_color: Option<Color>,
pub bg_color: Option<Color>,
pub bold: Option<bool>,
pub underlined: Option<bool>,
}
impl Style {
fn merge(&mut self, other: &Style) {
self.fg_color = other.fg_color.or(self.fg_color);
self.bg_color = other.bg_color.or(self.bg_color);
self.bold = other.bold.or(self.bold);
self.underlined = other.underlined.or(self.underlined);
}
}
#[derive(Clone, Debug)]
pub struct ColorFormatter<W: Write> {
output: W,
rules: Arc<Rules>,
/// The stack of currently applied labels. These determine the desired
/// style.
labels: Vec<String>,
cached_styles: HashMap<Vec<String>, Style>,
/// The style we last wrote to the output.
current_style: Style,
/// The debug string (space-separated labels) we last wrote to the output.
/// Initialize to None to turn debug strings off.
current_debug: Option<String>,
}
impl<W: Write> ColorFormatter<W> {
pub fn new(output: W, rules: Arc<Rules>, debug: bool) -> ColorFormatter<W> {
ColorFormatter {
output,
rules,
labels: vec![],
cached_styles: HashMap::new(),
current_style: Style::default(),
current_debug: debug.then(String::new),
}
}
pub fn for_config(
output: W,
config: &config::Config,
debug: bool,
) -> Result<Self, config::ConfigError> {
let rules = rules_from_config(config)?;
Ok(Self::new(output, Arc::new(rules), debug))
}
fn requested_style(&mut self) -> Style {
if let Some(cached) = self.cached_styles.get(&self.labels) {
cached.clone()
} else {
// We use the reverse list of matched indices as a measure of how well the rule
// matches the actual labels. For example, for rule "a d" and the actual labels
// "a b c d", we'll get [3,0]. We compare them by Rust's default Vec comparison.
// That means "a d" will trump both rule "d" (priority [3]) and rule
// "a b c" (priority [2,1,0]).
let mut matched_styles = vec![];
for (labels, style) in self.rules.as_ref() {
let mut labels_iter = self.labels.iter().enumerate();
// The indexes in the current label stack that match the required label.
let mut matched_indices = vec![];
for required_label in labels {
for (label_index, label) in &mut labels_iter {
if label == required_label {
matched_indices.push(label_index);
break;
}
}
}
if matched_indices.len() == labels.len() {
matched_indices.reverse();
matched_styles.push((style, matched_indices));
}
}
matched_styles.sort_by_key(|(_, indices)| indices.clone());
let mut style = Style::default();
for (matched_style, _) in matched_styles {
style.merge(matched_style);
}
self.cached_styles
.insert(self.labels.clone(), style.clone());
style
}
}
fn write_new_style(&mut self) -> io::Result<()> {
let new_debug = match &self.current_debug {
Some(current) => {
let joined = self.labels.join(" ");
if joined == *current {
None
} else {
if !current.is_empty() {
write!(self.output, ">>")?;
}
Some(joined)
}
}
None => None,
};
let new_style = self.requested_style();
if new_style != self.current_style {
if new_style.bold != self.current_style.bold {
if new_style.bold.unwrap_or_default() {
queue!(self.output, SetAttribute(Attribute::Bold))?;
} else {
// NoBold results in double underlining on some terminals, so we use reset
// instead. However, that resets other attributes as well, so we reset
// our record of the current style so we re-apply the other attributes
// below.
queue!(self.output, SetAttribute(Attribute::Reset))?;
self.current_style = Style::default();
}
}
if new_style.underlined != self.current_style.underlined {
if new_style.underlined.unwrap_or_default() {
queue!(self.output, SetAttribute(Attribute::Underlined))?;
} else {
queue!(self.output, SetAttribute(Attribute::NoUnderline))?;
}
}
if new_style.fg_color != self.current_style.fg_color {
queue!(
self.output,
SetForegroundColor(new_style.fg_color.unwrap_or(Color::Reset))
)?;
}
if new_style.bg_color != self.current_style.bg_color {
queue!(
self.output,
SetBackgroundColor(new_style.bg_color.unwrap_or(Color::Reset))
)?;
}
self.current_style = new_style;
}
if let Some(d) = new_debug {
if !d.is_empty() {
write!(self.output, "<<{}::", d)?;
}
self.current_debug = Some(d);
}
Ok(())
}
}
fn rules_from_config(config: &config::Config) -> Result<Rules, config::ConfigError> {
let mut result = vec![];
let table = config.get_table("colors")?;
for (key, value) in table {
let labels = key
.split_whitespace()
.map(ToString::to_string)
.collect_vec();
match value.kind {
config::ValueKind::String(color_name) => {
let style = Style {
fg_color: Some(color_for_name_or_hex(&color_name)?),
bg_color: None,
bold: None,
underlined: None,
};
result.push((labels, style));
}
config::ValueKind::Table(style_table) => {
let mut style = Style::default();
if let Some(value) = style_table.get("fg") {
if let config::ValueKind::String(color_name) = &value.kind {
style.fg_color = Some(color_for_name_or_hex(color_name)?);
}
}
if let Some(value) = style_table.get("bg") {
if let config::ValueKind::String(color_name) = &value.kind {
style.bg_color = Some(color_for_name_or_hex(color_name)?);
}
}
if let Some(value) = style_table.get("bold") {
if let config::ValueKind::Boolean(value) = &value.kind {
style.bold = Some(*value);
}
}
if let Some(value) = style_table.get("underline") {
if let config::ValueKind::Boolean(value) = &value.kind {
style.underlined = Some(*value);
}
}
result.push((labels, style));
}
_ => {}
}
}
Ok(result)
}
fn color_for_name_or_hex(name_or_hex: &str) -> Result<Color, config::ConfigError> {
match name_or_hex {
"default" => Ok(Color::Reset),
"black" => Ok(Color::Black),
"red" => Ok(Color::DarkRed),
"green" => Ok(Color::DarkGreen),
"yellow" => Ok(Color::DarkYellow),
"blue" => Ok(Color::DarkBlue),
"magenta" => Ok(Color::DarkMagenta),
"cyan" => Ok(Color::DarkCyan),
"white" => Ok(Color::Grey),
"bright black" => Ok(Color::DarkGrey),
"bright red" => Ok(Color::Red),
"bright green" => Ok(Color::Green),
"bright yellow" => Ok(Color::Yellow),
"bright blue" => Ok(Color::Blue),
"bright magenta" => Ok(Color::Magenta),
"bright cyan" => Ok(Color::Cyan),
"bright white" => Ok(Color::White),
_ => color_for_hex(name_or_hex)
.ok_or_else(|| config::ConfigError::Message(format!("invalid color: {}", name_or_hex))),
}
}
fn color_for_hex(color: &str) -> Option<Color> {
if color.len() == 7
&& color.starts_with('#')
&& color[1..].chars().all(|c| c.is_ascii_hexdigit())
{
let r = u8::from_str_radix(&color[1..3], 16);
let g = u8::from_str_radix(&color[3..5], 16);
let b = u8::from_str_radix(&color[5..7], 16);
match (r, g, b) {
(Ok(r), Ok(g), Ok(b)) => Some(Color::Rgb { r, g, b }),
_ => None,
}
} else {
None
}
}
impl<W: Write> Write for ColorFormatter<W> {
fn write(&mut self, data: &[u8]) -> Result<usize, Error> {
/*
We clear the current style at the end of each line, and then we re-apply the style
after the newline. There are several reasons for this:
* We can more easily skip styling a trailing blank line, which other
internal code then can correctly detect as having a trailing
newline.
* Some tools (like `less -R`) add an extra newline if the final
character is not a newline (e.g. if there's a color reset after
it), which led to an annoying blank line after the diff summary in
e.g. `jj status`.
* Since each line is styled independently, you get all the necessary
escapes even when grepping through the output.
* Some terminals extend background color to the end of the terminal
(i.e. past the newline character), which is probably not what the
user wanted.
* Some tools (like `less -R`) get confused and lose coloring of lines
after a newline.
*/
for line in data.split_inclusive(|b| *b == b'\n') {
if line.ends_with(b"\n") {
self.write_new_style()?;
write_sanitized(&mut self.output, &line[..line.len() - 1])?;
let labels = mem::take(&mut self.labels);
self.write_new_style()?;
self.output.write_all(b"\n")?;
self.labels = labels;
} else {
self.write_new_style()?;
write_sanitized(&mut self.output, line)?;
}
}
Ok(data.len())
}
fn flush(&mut self) -> Result<(), Error> {
self.output.flush()
}
}
impl<W: Write> Formatter for ColorFormatter<W> {
fn raw(&mut self) -> &mut dyn Write {
&mut self.output
}
fn push_label(&mut self, label: &str) -> io::Result<()> {
self.labels.push(label.to_owned());
Ok(())
}
fn pop_label(&mut self) -> io::Result<()> {
self.labels.pop();
if self.labels.is_empty() {
self.write_new_style()?
}
Ok(())
}
}
impl<W: Write> Drop for ColorFormatter<W> {
fn drop(&mut self) {
// If a `ColorFormatter` was dropped without popping all labels first (perhaps
// because of an error), let's still try to reset any currently active style.
self.labels.clear();
self.write_new_style().ok();
}
}
/// Like buffered formatter, but records `push`/`pop_label()` calls.
///
/// This allows you to manipulate the recorded data without losing labels.
/// The recorded data and labels can be written to another formatter. If
/// the destination formatter has already been labeled, the recorded labels
/// will be stacked on top of the existing labels, and the subsequent data
/// may be colorized differently.
#[derive(Clone, Debug, Default)]
pub struct FormatRecorder {
data: Vec<u8>,
label_ops: Vec<(usize, LabelOp)>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
enum LabelOp {
PushLabel(String),
PopLabel,
}
impl FormatRecorder {
pub fn new() -> Self {
FormatRecorder::default()
}
pub fn data(&self) -> &[u8] {
&self.data
}
fn push_label_op(&mut self, op: LabelOp) {
self.label_ops.push((self.data.len(), op));
}
pub fn replay(&self, formatter: &mut dyn Formatter) -> io::Result<()> {
self.replay_with(formatter, |formatter, range| {
formatter.write_all(&self.data[range])
})
}
pub fn replay_with(
&self,
formatter: &mut dyn Formatter,
mut write_data: impl FnMut(&mut dyn Formatter, Range<usize>) -> io::Result<()>,
) -> io::Result<()> {
let mut last_pos = 0;
let mut flush_data = |formatter: &mut dyn Formatter, pos| -> io::Result<()> {
if last_pos != pos {
write_data(formatter, last_pos..pos)?;
last_pos = pos;
}
Ok(())
};
for (pos, op) in &self.label_ops {
flush_data(formatter, *pos)?;
match op {
LabelOp::PushLabel(label) => formatter.push_label(label)?,
LabelOp::PopLabel => formatter.pop_label()?,
}
}
flush_data(formatter, self.data.len())
}
}
impl Write for FormatRecorder {
fn write(&mut self, data: &[u8]) -> io::Result<usize> {
self.data.extend_from_slice(data);
Ok(data.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl Formatter for FormatRecorder {
fn raw(&mut self) -> &mut dyn Write {
panic!("raw output isn't supported by FormatRecorder")
}
fn push_label(&mut self, label: &str) -> io::Result<()> {
self.push_label_op(LabelOp::PushLabel(label.to_owned()));
Ok(())
}
fn pop_label(&mut self) -> io::Result<()> {
self.push_label_op(LabelOp::PopLabel);
Ok(())
}
}
fn write_sanitized(output: &mut impl Write, buf: &[u8]) -> Result<(), Error> {
if buf.contains(&b'\x1b') {
let mut sanitized = Vec::with_capacity(buf.len());
for b in buf {
if *b == b'\x1b' {
sanitized.extend_from_slice("".as_bytes());
} else {
sanitized.push(*b);
}
}
output.write_all(&sanitized)
} else {
output.write_all(buf)
}
}
#[cfg(test)]
mod tests {
use std::str;
use super::*;
fn config_from_string(text: &str) -> config::Config {
config::Config::builder()
.add_source(config::File::from_str(text, config::FileFormat::Toml))
.build()
.unwrap()
}
#[test]
fn test_plaintext_formatter() {
// Test that PlainTextFormatter ignores labels.
let mut output: Vec<u8> = vec![];
let mut formatter = PlainTextFormatter::new(&mut output);
formatter.push_label("warning").unwrap();
write!(formatter, "hello").unwrap();
formatter.pop_label().unwrap();
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"hello");
}
#[test]
fn test_plaintext_formatter_ansi_codes_in_text() {
// Test that ANSI codes in the input text are NOT escaped.
let mut output: Vec<u8> = vec![];
let mut formatter = PlainTextFormatter::new(&mut output);
write!(formatter, "\x1b[1mactually bold\x1b[0m").unwrap();
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"actually bold");
}
#[test]
fn test_sanitizing_formatter_ansi_codes_in_text() {
// Test that ANSI codes in the input text are escaped.
let mut output: Vec<u8> = vec![];
let mut formatter = SanitizingFormatter::new(&mut output);
write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m");
}
#[test]
fn test_color_formatter_color_codes() {
// Test the color code for each color.
let colors = [
"black",
"red",
"green",
"yellow",
"blue",
"magenta",
"cyan",
"white",
"bright black",
"bright red",
"bright green",
"bright yellow",
"bright blue",
"bright magenta",
"bright cyan",
"bright white",
];
let mut config_builder = config::Config::builder();
for color in colors {
// Use the color name as the label.
config_builder = config_builder
.set_override(format!("colors.{}", color.replace(' ', "-")), color)
.unwrap();
}
let mut output: Vec<u8> = vec![];
let mut formatter =
ColorFormatter::for_config(&mut output, &config_builder.build().unwrap(), false)
.unwrap();
for color in colors {
formatter.push_label(&color.replace(' ', "-")).unwrap();
write!(formatter, " {color} ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
}
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"
 black 
 red 
 green 
 yellow 
 blue 
 magenta 
 cyan 
 white 
 bright black 
 bright red 
 bright green 
 bright yellow 
 bright blue 
 bright magenta 
 bright cyan 
 bright white 
"###);
}
#[test]
fn test_color_formatter_hex_colors() {
// Test the color code for each color.
let labels_and_colors = [
["black", "#000000"],
["white", "#ffffff"],
["pastel-blue", "#AFE0D9"],
];
let mut config_builder = config::Config::builder();
for [label, color] in labels_and_colors {
// Use the color name as the label.
config_builder = config_builder
.set_override(format!("colors.{}", label), color)
.unwrap();
}
let mut output: Vec<u8> = vec![];
let mut formatter =
ColorFormatter::for_config(&mut output, &config_builder.build().unwrap(), false)
.unwrap();
for [label, _] in labels_and_colors {
formatter.push_label(&label.replace(' ', "-")).unwrap();
write!(formatter, " {label} ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
}
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"
 black 
 white 
 pastel-blue 
"###);
}
#[test]
fn test_color_formatter_single_label() {
// Test that a single label can be colored and that the color is reset
// afterwards.
let config = config_from_string(
r#"
colors.inside = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
write!(formatter, " before ").unwrap();
formatter.push_label("inside").unwrap();
write!(formatter, " inside ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " after ").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" before  inside  after ");
}
#[test]
fn test_color_formatter_attributes() {
// Test that each attribute of the style can be set and that they can be
// combined in a single rule or by using multiple rules.
let config = config_from_string(
r#"
colors.red_fg = { fg = "red" }
colors.blue_bg = { bg = "blue" }
colors.bold_font = { bold = true }
colors.underlined_text = { underline = true }
colors.multiple = { fg = "green", bg = "yellow", bold = true, underline = true }
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("red_fg").unwrap();
write!(formatter, " fg only ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
formatter.push_label("blue_bg").unwrap();
write!(formatter, " bg only ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
formatter.push_label("bold_font").unwrap();
write!(formatter, " bold only ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
formatter.push_label("underlined_text").unwrap();
write!(formatter, " underlined only ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
formatter.push_label("multiple").unwrap();
write!(formatter, " single rule ").unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
formatter.push_label("red_fg").unwrap();
formatter.push_label("blue_bg").unwrap();
write!(formatter, " two rules ").unwrap();
formatter.pop_label().unwrap();
formatter.pop_label().unwrap();
writeln!(formatter).unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"
 fg only 
 bg only 
 bold only 
 underlined only 
 single rule 
 two rules 
"###);
}
#[test]
fn test_color_formatter_bold_reset() {
// Test that we don't lose other attributes when we reset the bold attribute.
let config = config_from_string(
r#"
colors.not_bold = { fg = "red", bg = "blue", underline = true }
colors.bold_font = { bold = true }
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("not_bold").unwrap();
write!(formatter, " not bold ").unwrap();
formatter.push_label("bold_font").unwrap();
write!(formatter, " bold ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " not bold again ").unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" not bold  bold  not bold again ");
}
#[test]
fn test_color_formatter_no_space() {
// Test that two different colors can touch.
let config = config_from_string(
r#"
colors.red = "red"
colors.green = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
write!(formatter, "before").unwrap();
formatter.push_label("red").unwrap();
write!(formatter, "first").unwrap();
formatter.pop_label().unwrap();
formatter.push_label("green").unwrap();
write!(formatter, "second").unwrap();
formatter.pop_label().unwrap();
write!(formatter, "after").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"beforefirstsecondafter");
}
#[test]
fn test_color_formatter_ansi_codes_in_text() {
// Test that ANSI codes in the input text are escaped.
let config = config_from_string(
r#"
colors.red = "red"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("red").unwrap();
write!(formatter, "\x1b[1mnot actually bold\x1b[0m").unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"␛[1mnot actually bold␛[0m");
}
#[test]
fn test_color_formatter_nested() {
// A color can be associated with a combination of labels. A more specific match
// overrides a less specific match. After the inner label is removed, the outer
// color is used again (we don't reset).
let config = config_from_string(
r#"
colors.outer = "blue"
colors.inner = "red"
colors."outer inner" = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
write!(formatter, " before outer ").unwrap();
formatter.push_label("outer").unwrap();
write!(formatter, " before inner ").unwrap();
formatter.push_label("inner").unwrap();
write!(formatter, " inside inner ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " after inner ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " after outer ").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
@" before outer  before inner  inside inner  after inner  after outer ");
}
#[test]
fn test_color_formatter_partial_match() {
// A partial match doesn't count
let config = config_from_string(
r#"
colors."outer inner" = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("outer").unwrap();
write!(formatter, " not colored ").unwrap();
formatter.push_label("inner").unwrap();
write!(formatter, " colored ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " not colored ").unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
@" not colored  colored  not colored ");
}
#[test]
fn test_color_formatter_unrecognized_color() {
// An unrecognized color causes an error.
let config = config_from_string(
r#"
colors."outer" = "red"
colors."outer inner" = "bloo"
"#,
);
let mut output: Vec<u8> = vec![];
let err = ColorFormatter::for_config(&mut output, &config, false)
.unwrap_err()
.to_string();
insta::assert_snapshot!(err,
@"invalid color: bloo");
}
#[test]
fn test_color_formatter_unrecognized_hex_color() {
// An unrecognized hex color causes an error.
let config = config_from_string(
r##"
colors."outer" = "red"
colors."outer inner" = "#ffgggg"
"##,
);
let mut output: Vec<u8> = vec![];
let err = ColorFormatter::for_config(&mut output, &config, false)
.unwrap_err()
.to_string();
insta::assert_snapshot!(err,
@"invalid color: #ffgggg");
}
#[test]
fn test_color_formatter_normal_color() {
// The "default" color resets the color. It is possible to reset only the
// background or only the foreground.
let config = config_from_string(
r#"
colors."outer" = {bg="yellow", fg="blue"}
colors."outer default_fg" = "default"
colors."outer default_bg" = {bg = "default"}
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("outer").unwrap();
write!(formatter, "Blue on yellow, ").unwrap();
formatter.push_label("default_fg").unwrap();
write!(formatter, " default fg, ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " and back.\nBlue on yellow, ").unwrap();
formatter.push_label("default_bg").unwrap();
write!(formatter, " default bg, ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " and back.").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
@r###"
Blue on yellow,  default fg,  and back.
Blue on yellow,  default bg,  and back.
"###);
}
#[test]
fn test_color_formatter_sibling() {
// A partial match on one rule does not eliminate other rules.
let config = config_from_string(
r#"
colors."outer1 inner1" = "red"
colors.inner2 = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("outer1").unwrap();
formatter.push_label("inner2").unwrap();
write!(formatter, " hello ").unwrap();
formatter.pop_label().unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
@" hello ");
}
#[test]
fn test_color_formatter_reverse_order() {
// Rules don't match labels out of order
let config = config_from_string(
r#"
colors."inner outer" = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("outer").unwrap();
formatter.push_label("inner").unwrap();
write!(formatter, " hello ").unwrap();
formatter.pop_label().unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" hello ");
}
#[test]
fn test_color_formatter_innermost_wins() {
// When two labels match, the innermost one wins.
let config = config_from_string(
r#"
colors."a" = "red"
colors."b" = "green"
colors."a c" = "blue"
colors."b c" = "yellow"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("a").unwrap();
write!(formatter, " a1 ").unwrap();
formatter.push_label("b").unwrap();
write!(formatter, " b1 ").unwrap();
formatter.push_label("c").unwrap();
write!(formatter, " c ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " b2 ").unwrap();
formatter.pop_label().unwrap();
write!(formatter, " a2 ").unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(),
@" a1  b1  c  b2  a2 ");
}
#[test]
fn test_color_formatter_dropped() {
// Test that the style gets reset if the formatter is dropped without popping
// all labels.
let config = config_from_string(
r#"
colors.outer = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
formatter.push_label("outer").unwrap();
formatter.push_label("inner").unwrap();
write!(formatter, " inside ").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @" inside ");
}
#[test]
fn test_color_formatter_debug() {
// Behaves like the color formatter, but surrounds each write with <<...>>,
// adding the active labels before the actual content separated by a ::.
let config = config_from_string(
r#"
colors.outer = "green"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, true).unwrap();
formatter.push_label("outer").unwrap();
formatter.push_label("inner").unwrap();
write!(formatter, " inside ").unwrap();
formatter.pop_label().unwrap();
formatter.pop_label().unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"<<outer inner:: inside >>");
}
#[test]
fn test_heading_labeled_writer() {
let config = config_from_string(
r#"
colors.inner = "green"
colors."inner heading" = "red"
"#,
);
let mut output: Vec<u8> = vec![];
let mut formatter: Box<dyn Formatter> =
Box::new(ColorFormatter::for_config(&mut output, &config, false).unwrap());
formatter
.as_mut()
.labeled("inner")
.with_heading("Should be noop: ");
let mut writer = formatter
.as_mut()
.labeled("inner")
.with_heading("Heading: ");
write!(writer, "Message").unwrap();
writeln!(writer, " continues").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @r###"
Heading: Message continues
"###);
}
#[test]
fn test_heading_labeled_writer_empty_string() {
let mut output: Vec<u8> = vec![];
let mut formatter: Box<dyn Formatter> = Box::new(PlainTextFormatter::new(&mut output));
let mut writer = formatter
.as_mut()
.labeled("inner")
.with_heading("Heading: ");
// write_fmt() is called even if the format string is empty. I don't
// know if that's guaranteed, but let's record the current behavior.
write!(writer, "").unwrap();
write!(writer, "").unwrap();
drop(formatter);
insta::assert_snapshot!(String::from_utf8(output).unwrap(), @"Heading: ");
}
#[test]
fn test_format_recorder() {
let mut recorder = FormatRecorder::new();
write!(recorder, " outer1 ").unwrap();
recorder.push_label("inner").unwrap();
write!(recorder, " inner1 ").unwrap();
write!(recorder, " inner2 ").unwrap();
recorder.pop_label().unwrap();
write!(recorder, " outer2 ").unwrap();
insta::assert_snapshot!(
str::from_utf8(recorder.data()).unwrap(),
@" outer1 inner1 inner2 outer2 ");
// Replayed output should be labeled.
let config = config_from_string(r#" colors.inner = "red" "#);
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
recorder.replay(&mut formatter).unwrap();
drop(formatter);
insta::assert_snapshot!(
String::from_utf8(output).unwrap(),
@" outer1  inner1 inner2  outer2 ");
// Replayed output should be split at push/pop_label() call.
let mut output: Vec<u8> = vec![];
let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap();
recorder
.replay_with(&mut formatter, |formatter, range| {
let data = &recorder.data()[range];
write!(formatter, "<<{}>>", str::from_utf8(data).unwrap())
})
.unwrap();
drop(formatter);
insta::assert_snapshot!(
String::from_utf8(output).unwrap(),
@"<< outer1 >><< inner1 inner2 >><< outer2 >>");
}
}