From d0a29a831d285438e7d1218c838accbffef3ebe2 Mon Sep 17 00:00:00 2001 From: tinger Date: Mon, 6 May 2024 18:23:36 +0200 Subject: [PATCH] cli: add `ui.color = "debug"` When using `ui.color = "debug"`, changes in the output style additionally include delimiters << and >>, as well as all active labels at this point separated by ::. The output is otherwise unformatted and the delimiters and labels inherit the style of the content they apply to. --- CHANGELOG.md | 2 + cli/src/cli_util.rs | 2 +- cli/src/config-schema.json | 1 + cli/src/formatter.rs | 109 ++++++++++++++++++++++-------- cli/src/template_builder.rs | 3 +- cli/src/text_util.rs | 2 +- cli/src/ui.rs | 20 ++++-- cli/tests/cli-reference@.md.snap | 2 +- cli/tests/test_commit_template.rs | 69 +++++++++++++++++++ cli/tests/test_global_opts.rs | 10 ++- cli/tests/test_log_command.rs | 8 +++ docs/config.md | 5 +- 12 files changed, 194 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c660fde78..a2187b67da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * Conflict markers now include an explanation of what each part of the conflict represents. +* `ui.color = "debug"` prints active labels alongside the regular colored output. + ### Fixed bugs ## [0.17.1] - 2024-05-07 diff --git a/cli/src/cli_util.rs b/cli/src/cli_util.rs index 133a889ea2..059af819d3 100644 --- a/cli/src/cli_util.rs +++ b/cli/src/cli_util.rs @@ -2411,7 +2411,7 @@ pub struct GlobalArgs { #[derive(clap::Args, Clone, Debug)] pub struct EarlyArgs { - /// When to colorize output (always, never, auto) + /// When to colorize output (always, never, debug, auto) #[arg(long, value_name = "WHEN", global = true)] pub color: Option, /// Silence non-primary command output diff --git a/cli/src/config-schema.json b/cli/src/config-schema.json index 6aad73449e..7ccb03bd9e 100644 --- a/cli/src/config-schema.json +++ b/cli/src/config-schema.json @@ -77,6 +77,7 @@ "enum": [ "always", "never", + "debug", "auto" ], "default": "auto" diff --git a/cli/src/formatter.rs b/cli/src/formatter.rs index 965b3d48e4..ecc0e2581c 100644 --- a/cli/src/formatter.rs +++ b/cli/src/formatter.rs @@ -132,18 +132,19 @@ pub struct FormatterFactory { enum FormatterFactoryKind { PlainText, Sanitized, - Color { rules: Arc }, + Color { rules: Arc, debug: bool }, } impl FormatterFactory { pub fn prepare( config: &config::Config, + debug: bool, color: bool, sanitized: bool, ) -> Result { let kind = if color { let rules = Arc::new(rules_from_config(config)?); - FormatterFactoryKind::Color { rules } + FormatterFactoryKind::Color { rules, debug } } else if sanitized { FormatterFactoryKind::Sanitized } else { @@ -159,8 +160,8 @@ impl FormatterFactory { match &self.kind { FormatterFactoryKind::PlainText => Box::new(PlainTextFormatter::new(output)), FormatterFactoryKind::Sanitized => Box::new(SanitizingFormatter::new(output)), - FormatterFactoryKind::Color { rules } => { - Box::new(ColorFormatter::new(output, rules.clone())) + FormatterFactoryKind::Color { rules, debug } => { + Box::new(ColorFormatter::new(output, rules.clone(), *debug)) } } } @@ -255,6 +256,7 @@ impl Style { #[derive(Clone, Debug)] pub struct ColorFormatter { output: W, + debug: bool, rules: Arc, /// The stack of currently applied labels. These determine the desired /// style. @@ -265,19 +267,24 @@ pub struct ColorFormatter { } impl ColorFormatter { - pub fn new(output: W, rules: Arc) -> ColorFormatter { + pub fn new(output: W, rules: Arc, debug: bool) -> ColorFormatter { ColorFormatter { output, rules, + debug, labels: vec![], cached_styles: HashMap::new(), current_style: Style::default(), } } - pub fn for_config(output: W, config: &config::Config) -> Result { + pub fn for_config( + output: W, + config: &config::Config, + debug: bool, + ) -> Result { let rules = rules_from_config(config)?; - Ok(Self::new(output, Arc::new(rules))) + Ok(Self::new(output, Arc::new(rules), debug)) } fn requested_style(&mut self) -> Style { @@ -473,19 +480,43 @@ impl Write for ColorFormatter { * 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])?; + write_line_exclusive( + &mut self.output, + &line[..line.len() - 1], + &self.labels, + self.debug, + )?; 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)?; + write_line_exclusive(&mut self.output, line, &self.labels, self.debug)?; } } + + fn write_line_exclusive( + output: &mut W, + buf: &[u8], + labels: &[String], + debug: bool, + ) -> io::Result<()> { + if debug { + write!(output, "<<{}::", labels.join(" "))?; + write_sanitized(output, buf)?; + write!(output, ">>")?; + } else { + write_sanitized(output, buf)?; + } + + Ok(()) + } + Ok(data.len()) } @@ -699,7 +730,8 @@ mod tests { } let mut output: Vec = vec![]; let mut formatter = - ColorFormatter::for_config(&mut output, &config_builder.build().unwrap()).unwrap(); + 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(); @@ -744,7 +776,8 @@ mod tests { } let mut output: Vec = vec![]; let mut formatter = - ColorFormatter::for_config(&mut output, &config_builder.build().unwrap()).unwrap(); + 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(); @@ -769,7 +802,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap(); write!(formatter, " before ").unwrap(); formatter.push_label("inside").unwrap(); write!(formatter, " inside ").unwrap(); @@ -793,7 +826,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -841,7 +874,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -863,7 +896,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap(); write!(formatter, "before").unwrap(); formatter.push_label("red").unwrap(); write!(formatter, "first").unwrap(); @@ -885,7 +918,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -906,7 +939,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -930,7 +963,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -953,7 +986,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let err = ColorFormatter::for_config(&mut output, &config) + let err = ColorFormatter::for_config(&mut output, &config, false) .unwrap_err() .to_string(); insta::assert_snapshot!(err, @@ -970,7 +1003,7 @@ mod tests { "##, ); let mut output: Vec = vec![]; - let err = ColorFormatter::for_config(&mut output, &config) + let err = ColorFormatter::for_config(&mut output, &config, false) .unwrap_err() .to_string(); insta::assert_snapshot!(err, @@ -989,7 +1022,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -1018,7 +1051,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -1038,7 +1071,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -1060,7 +1093,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -1087,7 +1120,7 @@ mod tests { "#, ); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + 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(); @@ -1095,6 +1128,26 @@ mod tests { 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 = 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(), @"<>"); + } + #[test] fn test_heading_labeled_writer() { let config = config_from_string( @@ -1105,7 +1158,7 @@ mod tests { ); let mut output: Vec = vec![]; let mut formatter: Box = - Box::new(ColorFormatter::for_config(&mut output, &config).unwrap()); + Box::new(ColorFormatter::for_config(&mut output, &config, false).unwrap()); HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Should be noop: "); let mut writer = HeadingLabeledWriter::new(formatter.as_mut(), "inner", "Heading: "); write!(writer, "Message").unwrap(); @@ -1146,7 +1199,7 @@ mod tests { // Replayed output should be labeled. let config = config_from_string(r#" colors.inner = "red" "#); let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap(); recorder.replay(&mut formatter).unwrap(); drop(formatter); insta::assert_snapshot!( @@ -1155,7 +1208,7 @@ mod tests { // Replayed output should be split at push/pop_label() call. let mut output: Vec = vec![]; - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap(); recorder .replay_with(&mut formatter, |formatter, range| { let data = &recorder.data()[range]; diff --git a/cli/src/template_builder.rs b/cli/src/template_builder.rs index 31ae846756..a023513caa 100644 --- a/cli/src/template_builder.rs +++ b/cli/src/template_builder.rs @@ -1292,7 +1292,8 @@ mod tests { fn render_ok(&self, template: &str) -> String { let template = self.parse(template).unwrap(); let mut output = Vec::new(); - let mut formatter = ColorFormatter::new(&mut output, self.color_rules.clone().into()); + let mut formatter = + ColorFormatter::new(&mut output, self.color_rules.clone().into(), false); template.format(&(), &mut formatter).unwrap(); drop(formatter); String::from_utf8(output).unwrap() diff --git a/cli/src/text_util.rs b/cli/src/text_util.rs index 6ce99df0c2..59874a0a47 100644 --- a/cli/src/text_util.rs +++ b/cli/src/text_util.rs @@ -275,7 +275,7 @@ mod tests { .build() .unwrap(); let mut output = Vec::new(); - let mut formatter = ColorFormatter::for_config(&mut output, &config).unwrap(); + let mut formatter = ColorFormatter::for_config(&mut output, &config, false).unwrap(); write(&mut formatter).unwrap(); drop(formatter); String::from_utf8(output).unwrap() diff --git a/cli/src/ui.rs b/cli/src/ui.rs index 4907b4940e..0762ba2b62 100644 --- a/cli/src/ui.rs +++ b/cli/src/ui.rs @@ -179,6 +179,7 @@ fn progress_indicator_setting(config: &config::Config) -> bool { pub enum ColorChoice { Always, Never, + Debug, #[default] Auto, } @@ -190,6 +191,7 @@ impl FromStr for ColorChoice { match s { "always" => Ok(ColorChoice::Always), "never" => Ok(ColorChoice::Never), + "debug" => Ok(ColorChoice::Debug), "auto" => Ok(ColorChoice::Auto), _ => Err("must be one of always, never, or auto"), } @@ -201,6 +203,7 @@ impl fmt::Display for ColorChoice { let s = match self { ColorChoice::Always => "always", ColorChoice::Never => "never", + ColorChoice::Debug => "debug", ColorChoice::Auto => "auto", }; write!(f, "{s}") @@ -215,10 +218,15 @@ fn color_setting(config: &config::Config) -> ColorChoice { .unwrap_or_default() } +fn debug_color(choice: ColorChoice) -> bool { + matches!(choice, ColorChoice::Debug) +} + fn use_color(choice: ColorChoice) -> bool { match choice { ColorChoice::Always => true, ColorChoice::Never => false, + ColorChoice::Debug => true, ColorChoice::Auto => io::stdout().is_terminal(), } } @@ -249,12 +257,14 @@ fn pager_setting(config: &config::Config) -> Result Result { - let color = use_color(color_setting(config)); + let color = color_setting(config); + let debug = debug_color(color); + let color = use_color(color); let quiet = be_quiet(config); // Sanitize ANSI escape codes if we're printing to a terminal. Doesn't affect // ANSI escape codes that originate from the formatter itself. let sanitize = io::stdout().is_terminal(); - let formatter_factory = FormatterFactory::prepare(config, color, sanitize)?; + let formatter_factory = FormatterFactory::prepare(config, debug, color, sanitize)?; let progress_indicator = progress_indicator_setting(config); Ok(Ui { color, @@ -268,13 +278,15 @@ impl Ui { } pub fn reset(&mut self, config: &config::Config) -> Result<(), CommandError> { - self.color = use_color(color_setting(config)); + let color = color_setting(config); + let debug = debug_color(color); + self.color = use_color(color); self.quiet = be_quiet(config); self.paginate = pagination_setting(config)?; self.pager_cmd = pager_setting(config)?; self.progress_indicator = progress_indicator_setting(config); let sanitize = io::stdout().is_terminal(); - self.formatter_factory = FormatterFactory::prepare(config, self.color, sanitize)?; + self.formatter_factory = FormatterFactory::prepare(config, debug, self.color, sanitize)?; Ok(()) } diff --git a/cli/tests/cli-reference@.md.snap b/cli/tests/cli-reference@.md.snap index 04557b8836..c74cbbfa50 100644 --- a/cli/tests/cli-reference@.md.snap +++ b/cli/tests/cli-reference@.md.snap @@ -164,7 +164,7 @@ To get started, see the tutorial at https://github.com/martinvonz/jj/blob/main/d Possible values: `true`, `false` -* `--color ` — When to colorize output (always, never, auto) +* `--color ` — When to colorize output (always, never, debug, auto) * `--quiet` — Silence non-primary command output Possible values: `true`, `false` diff --git a/cli/tests/test_commit_template.rs b/cli/tests/test_commit_template.rs index 1c97ddc525..f188d9be3a 100644 --- a/cli/tests/test_commit_template.rs +++ b/cli/tests/test_commit_template.rs @@ -360,6 +360,75 @@ fn test_log_builtin_templates_colored() { "###); } +#[test] +fn test_log_builtin_templates_colored_debug() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_ok(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + let render = + |template| test_env.jj_cmd_success(&repo_path, &["--color=debug", "log", "-T", template]); + + test_env.jj_cmd_ok( + &repo_path, + &[ + "--config-toml=user.email=''", + "--config-toml=user.name=''", + "new", + ], + ); + test_env.jj_cmd_ok(&repo_path, &["branch", "create", "my-branch"]); + + insta::assert_snapshot!(render(r#"builtin_log_oneline"#), @r###" + <> <><><><><><><><><><><><><><><><> + <> <><><><><><><><><><><><><><> + <> <><><><><><><><> + "###); + + insta::assert_snapshot!(render(r#"builtin_log_compact"#), @r###" + <> <><><><><><><><><><><><> + │ <><><><> + <> <><><><><><><><><><> + │ <><><><> + <> <><><><><><><><> + "###); + + insta::assert_snapshot!(render(r#"builtin_log_comfortable"#), @r###" + <> <><><><><><><><><><><><> + │ <><><><> + │ <> + <> <><><><><><><><><><> + │ <><><><> + │ <> + <> <><><><><><><><> + <> + "###); + + insta::assert_snapshot!(render(r#"builtin_log_detailed"#), @r###" + <> <><><> + │ <><><> + │ <><><> + │ <><><><><>><><><><> + │ <><><><><>><><><><> + │ <> + │ <><><> + │ <> + <> <><><> + │ <><><> + │ <><><><><>><><><><> + │ <><><><><>><><><><> + │ <> + │ <><><> + │ <> + <> <><><> + <><><> + <><><><><>><><><><> + <><><><><>><><><><> + <> + <><><> + <> + "###); +} + #[test] fn test_log_obslog_divergence() { let test_env = TestEnvironment::default(); diff --git a/cli/tests/test_global_opts.rs b/cli/tests/test_global_opts.rs index fe4567896f..cdc3a24a1f 100644 --- a/cli/tests/test_global_opts.rs +++ b/cli/tests/test_global_opts.rs @@ -486,6 +486,14 @@ fn test_color_ui_messages() {  qpvuntsm 230dd059 (empty) (no description set) Hint: Prefix the expression with 'all:' to allow any number of revisions (i.e. 'all:..'). "###); + + // debugging colors + let (stdout, _stderr) = test_env.jj_cmd_ok(&repo_path, &["st", "--color", "debug"]); + insta::assert_snapshot!(stdout, @r###" + <<::The working copy is clean>> + <<::Working copy : >><><><><><><><><><><<::>> + <<::Parent commit: >><><><<:: >><><><<:: >><><<:: >><><<::>> + "###); } #[test] @@ -595,7 +603,7 @@ fn test_help() { --ignore-immutable Allow rewriting immutable commits --at-operation Operation to load the repo at [default: @] [aliases: at-op] --debug Enable debug logging - --color When to colorize output (always, never, auto) + --color When to colorize output (always, never, debug, auto) --quiet Silence non-primary command output --no-pager Disable the pager --config-toml Additional configuration options (can be repeated) diff --git a/cli/tests/test_log_command.rs b/cli/tests/test_log_command.rs index ae96eec6ec..3e8f125439 100644 --- a/cli/tests/test_log_command.rs +++ b/cli/tests/test_log_command.rs @@ -1161,6 +1161,14 @@ fn test_graph_template_color() { │ third line ◉ "###); + let stdout = test_env.jj_cmd_success(&repo_path, &["--color=debug", "log", "-T", template]); + insta::assert_snapshot!(stdout, @r###" + <> <> + <> <> + │ <> + │ <> + <> + "###); } #[test] diff --git a/docs/config.md b/docs/config.md index d8aef09249..569634fc86 100644 --- a/docs/config.md +++ b/docs/config.md @@ -80,8 +80,9 @@ Don't forget to change these to your own details! ### Colorizing output -Possible values are `always`, `never` and `auto` (default: `auto`). -`auto` will use color only when writing to a terminal. +Possible values are `always`, `never`, `debug` and `auto` (default: `auto`). +`auto` will use color only when writing to a terminal. `debug` will print the +active labels alongside the regular colorized output. This setting overrides the `NO_COLOR` environment variable (if set).