From 904e9c5520f6c05488104b16a64299a9f3d63739 Mon Sep 17 00:00:00 2001 From: Yuya Nishihara Date: Sun, 5 Mar 2023 13:10:02 +0900 Subject: [PATCH] cli: add ui.log-word-wrap option Unlike Mercurial, this isn't a template keyword/function, but a config knob. Exposing graph_width to templater wouldn't be easy, and I don't think it's better to handle terminal wrapping in template. I'm not sure if patch content should be wrapped, so this option only applies to the template output for now. Closes #1043 --- CHANGELOG.md | 3 + docs/config.md | 9 +++ src/cli_util.rs | 49 +++++++++++++- src/commands/mod.rs | 27 +++++--- src/commands/operation.rs | 10 ++- src/config-schema.json | 5 ++ src/config/misc.toml | 3 + tests/test_log_command.rs | 123 +++++++++++++++++++++++++++++++++++ tests/test_obslog_command.rs | 59 ++++++++++++++++- tests/test_operations.rs | 42 +++++++++++- 10 files changed, 315 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35e3290bc..ee236c41e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 * `jj config set` command allows simple config edits like `jj config set --repo user.email "somebody@example.com"` +* Added `ui.log-word-wrap` option to wrap `jj log`/`obslog`/`op log` content + based on terminal width. [#1043](https://github.com/martinvonz/jj/issues/1043) + ### Fixed bugs * Modify/delete conflicts now include context lines diff --git a/docs/config.md b/docs/config.md index addc1af4a..43afa82d9 100644 --- a/docs/config.md +++ b/docs/config.md @@ -136,6 +136,15 @@ ui.diff.format = "git" ui.graph.style = "square" ``` +### Wrap log content + +If enabled, `log`/`obslog`/`op log` content will be wrapped based on +the terminal width. + +```toml +ui.log-word-wrap = true +``` + ### Display of commit and change ids Can be customized by the `format_short_id()` template alias. diff --git a/src/cli_util.rs b/src/cli_util.rs index ed5bb9d0f..9be06d680 100644 --- a/src/cli_util.rs +++ b/src/cli_util.rs @@ -62,7 +62,7 @@ use tracing_subscriber::prelude::*; use crate::config::{ config_path, AnnotatedValue, CommandNameAndArgs, ConfigSource, LayeredConfigs, }; -use crate::formatter::{Formatter, PlainTextFormatter}; +use crate::formatter::{FormatRecorder, Formatter, PlainTextFormatter}; use crate::merge_tools::{ConflictResolveError, DiffEditError}; use crate::template_parser::{TemplateAliasesMap, TemplateParseError}; use crate::templater::Template; @@ -1617,6 +1617,53 @@ fn parse_commit_summary_template<'a>( )?) } +/// Helper to reformat content of log-like commands. +#[derive(Clone, Debug)] +pub enum LogContentFormat { + NoWrap, + Wrap { term_width: usize }, +} + +impl LogContentFormat { + pub fn new(ui: &Ui, settings: &UserSettings) -> Result { + if settings.config().get_bool("ui.log-word-wrap")? { + let term_width = usize::from(ui.term_width().unwrap_or(80)); + Ok(LogContentFormat::Wrap { term_width }) + } else { + Ok(LogContentFormat::NoWrap) + } + } + + pub fn write( + &self, + formatter: &mut dyn Formatter, + content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>, + ) -> std::io::Result<()> { + self.write_graph_text(formatter, content_fn, || 0) + } + + pub fn write_graph_text( + &self, + formatter: &mut dyn Formatter, + content_fn: impl FnOnce(&mut dyn Formatter) -> std::io::Result<()>, + graph_width_fn: impl FnOnce() -> usize, + ) -> std::io::Result<()> { + match self { + LogContentFormat::NoWrap => content_fn(formatter), + LogContentFormat::Wrap { term_width } => { + let mut recorder = FormatRecorder::new(); + content_fn(&mut recorder)?; + text_util::write_wrapped( + formatter, + &recorder, + term_width.saturating_sub(graph_width_fn()), + )?; + Ok(()) + } + } + } +} + // TODO: Use a proper TOML library to serialize instead. pub fn serialize_config_value(value: &config::Value) -> String { match &value.kind { diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 0ad381e91..c3773fadc 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -52,8 +52,8 @@ use crate::cli_util::{ check_stale_working_copy, get_config_file_path, print_checkout_stats, resolve_multiple_nonempty_revsets, resolve_mutliple_nonempty_revsets_flag_guarded, run_ui_editor, serialize_config_value, short_commit_hash, user_error, user_error_with_hint, - write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg, RevisionArg, - WorkspaceCommandHelper, + write_config_value_to_file, Args, CommandError, CommandHelper, DescriptionArg, + LogContentFormat, RevisionArg, WorkspaceCommandHelper, }; use crate::config::{AnnotatedValue, ConfigSource}; use crate::diff_util::{self, DiffFormat, DiffFormatArgs}; @@ -1475,6 +1475,7 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C None => command.settings().config().get_string("templates.log")?, }; let template = workspace_command.parse_commit_template(&template_string)?; + let with_content_format = LogContentFormat::new(ui, command.settings())?; { ui.request_pager(); @@ -1516,7 +1517,11 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C let mut buffer = vec![]; let commit_id = index_entry.commit_id(); let commit = store.get_commit(&commit_id)?; - template.format(&commit, ui.new_formatter(&mut buffer).as_mut())?; + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| template.format(&commit, formatter), + || graph.width(&index_entry.position(), &graphlog_edges), + )?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } @@ -1551,7 +1556,8 @@ fn cmd_log(ui: &mut Ui, command: &CommandHelper, args: &LogArgs) -> Result<(), C }; for index_entry in iter { let commit = store.get_commit(&index_entry.commit_id())?; - template.format(&commit, formatter)?; + with_content_format + .write(formatter, |formatter| template.format(&commit, formatter))?; if !diff_formats.is_empty() { diff_util::show_patch( formatter, @@ -1604,6 +1610,7 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result None => command.settings().config().get_string("templates.log")?, }; let template = workspace_command.parse_commit_template(&template_string)?; + let with_content_format = LogContentFormat::new(ui, command.settings())?; ui.request_pager(); let mut formatter = ui.stdout_formatter(); @@ -1623,10 +1630,11 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result edges.push(Edge::direct(predecessor.id().clone())); } let mut buffer = vec![]; - { - let mut formatter = ui.new_formatter(&mut buffer); - template.format(&commit, formatter.as_mut())?; - } + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| template.format(&commit, formatter), + || graph.width(commit.id(), &edges), + )?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } @@ -1653,7 +1661,8 @@ fn cmd_obslog(ui: &mut Ui, command: &CommandHelper, args: &ObslogArgs) -> Result } } else { for commit in commits { - template.format(&commit, formatter)?; + with_content_format + .write(formatter, |formatter| template.format(&commit, formatter))?; if !diff_formats.is_empty() { show_predecessor_patch(formatter, &workspace_command, &commit, &diff_formats)?; } diff --git a/src/commands/operation.rs b/src/commands/operation.rs index 07edf3414..992c8abd3 100644 --- a/src/commands/operation.rs +++ b/src/commands/operation.rs @@ -2,7 +2,7 @@ use clap::Subcommand; use jujutsu_lib::dag_walk::topo_order_reverse; use jujutsu_lib::operation::Operation; -use crate::cli_util::{user_error, CommandError, CommandHelper}; +use crate::cli_util::{user_error, CommandError, CommandHelper, LogContentFormat}; use crate::graphlog::{get_graphlog, Edge}; use crate::operation_templater; use crate::templater::Template as _; @@ -63,6 +63,7 @@ fn cmd_op_log( &template_string, workspace_command.template_aliases_map(), )?; + let with_content_format = LogContentFormat::new(ui, command.settings())?; ui.request_pager(); let mut formatter = ui.stdout_formatter(); @@ -79,8 +80,11 @@ fn cmd_op_log( } let is_head_op = op.id() == &head_op_id; let mut buffer = vec![]; - ui.new_formatter(&mut buffer) - .with_label("op_log", |formatter| template.format(&op, formatter))?; + with_content_format.write_graph_text( + ui.new_formatter(&mut buffer).as_mut(), + |formatter| formatter.with_label("op_log", |formatter| template.format(&op, formatter)), + || graph.width(op.id(), &edges), + )?; if !buffer.ends_with(b"\n") { buffer.push(b'\n'); } diff --git a/src/config-schema.json b/src/config-schema.json index 56ed4f166..4884de3c9 100644 --- a/src/config-schema.json +++ b/src/config-schema.json @@ -102,6 +102,11 @@ } } }, + "log-word-wrap": { + "type": "boolean", + "description": "Whether to wrap log template output", + "default": false + }, "editor": { "type": "string", "description": "Editor to use for commands that involve editing text" diff --git a/src/config/misc.toml b/src/config/misc.toml index 74f19aa0c..7157d672f 100644 --- a/src/config/misc.toml +++ b/src/config/misc.toml @@ -7,3 +7,6 @@ fetch = "origin" [revset-aliases] # Placeholder: added by user + +[ui] +log-word-wrap = false diff --git a/tests/test_log_command.rs b/tests/test_log_command.rs index 25875eef6..dfb7b1149 100644 --- a/tests/test_log_command.rs +++ b/tests/test_log_command.rs @@ -989,3 +989,126 @@ fn test_graph_styles() { o "###); } + +#[test] +fn test_log_word_wrap() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + let render = |args: &[&str], columns: u32, word_wrap: bool| { + let mut args = args.to_vec(); + if word_wrap { + args.push("--config-toml=ui.log-word-wrap=true"); + } + let assert = test_env + .jj_cmd(&repo_path, &args) + .env("COLUMNS", columns.to_string()) + .assert() + .success() + .stderr(""); + get_stdout_string(&assert) + }; + + test_env.jj_cmd_success(&repo_path, &["commit", "-m", "main branch 1"]); + test_env.jj_cmd_success(&repo_path, &["describe", "-m", "main branch 2"]); + test_env.jj_cmd_success(&repo_path, &["new", "-m", "side"]); + test_env.jj_cmd_success(&repo_path, &["new", "-m", "merge", "@--", "@"]); + + // ui.log-word-wrap option applies to both graph/no-graph outputs + insta::assert_snapshot!(render(&["log", "-r@"], 40, false), @r###" + @ mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e + │ (empty) merge + ~ + "###); + insta::assert_snapshot!(render(&["log", "-r@"], 40, true), @r###" + @ mzvwutvlkqwt test.user@example.com + │ 2001-02-03 04:05:11.000 +07:00 + ~ 68518a7e6c9e + (empty) merge + "###); + insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, false), @r###" + mzvwutvlkqwt test.user@example.com 2001-02-03 04:05:11.000 +07:00 68518a7e6c9e + (empty) merge + "###); + insta::assert_snapshot!(render(&["log", "--no-graph", "-r@"], 40, true), @r###" + mzvwutvlkqwt test.user@example.com + 2001-02-03 04:05:11.000 +07:00 + 68518a7e6c9e + (empty) merge + "###); + + // Color labels should be preserved + insta::assert_snapshot!(render(&["log", "-r@", "--color=always"], 40, true), @r###" + @ mzvwutvlkqwt test.user@example.com + │ 2001-02-03 04:05:11.000 +07:00 + ~ 68518a7e6c9e + (empty) merge + "###); + + // Graph width should be subtracted from the term width + let template = r#""0 1 2 3 4 5 6 7 8 9""#; + insta::assert_snapshot!(render(&["log", "-T", template], 10, true), @r###" + @ 0 1 2 + ├─╮ 3 4 5 + │ │ 6 7 8 + │ │ 9 + o │ 0 1 2 + │ │ 3 4 5 + │ │ 6 7 8 + │ │ 9 + o │ 0 1 2 + ├─╯ 3 4 5 + │ 6 7 8 + │ 9 + o 0 1 2 3 + │ 4 5 6 7 + │ 8 9 + o 0 1 2 3 + 4 5 6 7 + 8 9 + "###); + insta::assert_snapshot!( + render(&["log", "-T", template, "--config-toml=ui.graph.style='legacy'"], 9, true), + @r###" + @ 0 1 2 + |\ 3 4 5 + | | 6 7 8 + | | 9 + o | 0 1 2 + | | 3 4 5 + | | 6 7 8 + | | 9 + o | 0 1 2 + |/ 3 4 5 + | 6 7 8 + | 9 + o 0 1 2 3 + | 4 5 6 7 + | 8 9 + o 0 1 2 3 + 4 5 6 7 + 8 9 + "###); + + // Shouldn't panic with $COLUMNS < graph_width + insta::assert_snapshot!(render(&["log", "-r@"], 0, true), @r###" + @ mzvwutvlkqwt + │ test.user@example.com + ~ 2001-02-03 + 04:05:11.000 + +07:00 + 68518a7e6c9e + (empty) + merge + "###); + insta::assert_snapshot!(render(&["log", "-r@"], 1, true), @r###" + @ mzvwutvlkqwt + │ test.user@example.com + ~ 2001-02-03 + 04:05:11.000 + +07:00 + 68518a7e6c9e + (empty) + merge + "###); +} diff --git a/tests/test_obslog_command.rs b/tests/test_obslog_command.rs index b1fbbea71..878756225 100644 --- a/tests/test_obslog_command.rs +++ b/tests/test_obslog_command.rs @@ -11,7 +11,8 @@ // 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 common::TestEnvironment; + +use common::{get_stdout_string, TestEnvironment}; pub mod common; @@ -119,6 +120,62 @@ fn test_obslog_with_or_without_diff() { "###); } +#[test] +fn test_obslog_word_wrap() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + let render = |args: &[&str], columns: u32, word_wrap: bool| { + let mut args = args.to_vec(); + if word_wrap { + args.push("--config-toml=ui.log-word-wrap=true"); + } + let assert = test_env + .jj_cmd(&repo_path, &args) + .env("COLUMNS", columns.to_string()) + .assert() + .success() + .stderr(""); + get_stdout_string(&assert) + }; + + test_env.jj_cmd_success(&repo_path, &["describe", "-m", "first"]); + + // ui.log-word-wrap option applies to both graph/no-graph outputs + insta::assert_snapshot!(render(&["obslog"], 40, false), @r###" + @ qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:08.000 +07:00 69542c1984c1 + │ (empty) first + o qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0 + (empty) (no description set) + "###); + insta::assert_snapshot!(render(&["obslog"], 40, true), @r###" + @ qpvuntsmwlqt test.user@example.com + │ 2001-02-03 04:05:08.000 +07:00 + │ 69542c1984c1 + │ (empty) first + o qpvuntsmwlqt test.user@example.com + 2001-02-03 04:05:07.000 +07:00 + 230dd059e1b0 + (empty) (no description set) + "###); + insta::assert_snapshot!(render(&["obslog", "--no-graph"], 40, false), @r###" + qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:08.000 +07:00 69542c1984c1 + (empty) first + qpvuntsmwlqt test.user@example.com 2001-02-03 04:05:07.000 +07:00 230dd059e1b0 + (empty) (no description set) + "###); + insta::assert_snapshot!(render(&["obslog", "--no-graph"], 40, true), @r###" + qpvuntsmwlqt test.user@example.com + 2001-02-03 04:05:08.000 +07:00 + 69542c1984c1 + (empty) first + qpvuntsmwlqt test.user@example.com + 2001-02-03 04:05:07.000 +07:00 + 230dd059e1b0 + (empty) (no description set) + "###); +} + #[test] fn test_obslog_squash() { let mut test_env = TestEnvironment::default(); diff --git a/tests/test_operations.rs b/tests/test_operations.rs index c6e691cf9..7c42c101f 100644 --- a/tests/test_operations.rs +++ b/tests/test_operations.rs @@ -16,7 +16,7 @@ use std::path::Path; use regex::Regex; -use crate::common::TestEnvironment; +use crate::common::{get_stdout_string, TestEnvironment}; pub mod common; @@ -147,6 +147,46 @@ fn test_op_log_template() { "###); } +#[test] +fn test_op_log_word_wrap() { + let test_env = TestEnvironment::default(); + test_env.jj_cmd_success(test_env.env_root(), &["init", "repo", "--git"]); + let repo_path = test_env.env_root().join("repo"); + let render = |args: &[&str], columns: u32, word_wrap: bool| { + let mut args = args.to_vec(); + if word_wrap { + args.push("--config-toml=ui.log-word-wrap=true"); + } + let assert = test_env + .jj_cmd(&repo_path, &args) + .env("COLUMNS", columns.to_string()) + .assert() + .success() + .stderr(""); + get_stdout_string(&assert) + }; + + // ui.log-word-wrap option works + insta::assert_snapshot!(render(&["op", "log"], 40, false), @r###" + @ a99a3fd5c51e test-username@host.example.com 22 years ago, lasted less than a microsecond + │ add workspace 'default' + o 56b94dfc38e7 test-username@host.example.com 22 years ago, lasted less than a microsecond + initialize repo + "###); + insta::assert_snapshot!(render(&["op", "log"], 40, true), @r###" + @ a99a3fd5c51e + │ test-username@host.example.com 22 + │ years ago, lasted less than a + │ microsecond + │ add workspace 'default' + o 56b94dfc38e7 + test-username@host.example.com 22 + years ago, lasted less than a + microsecond + initialize repo + "###); +} + #[test] fn test_op_log_configurable() { let test_env = TestEnvironment::default();