From 8b33bb7c460e15018dd2b980cfd90c489a0091c3 Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Tue, 9 Dec 2025 21:09:18 +0100 Subject: [PATCH 1/3] Set computed.color_mode before line numbers feature. This feature inspects on opt.computed.color_mode, which is set by this function. Otherwise --light or --dark have no effect. --- src/options/set.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/options/set.rs b/src/options/set.rs index 6d26d15df..66a427c6b 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -125,6 +125,10 @@ pub fn set_options( .unwrap_or_else(|| "magenta reverse".to_string()) } + // Sets opt.computed.color_mode to the computed or provided value (by --light or --dark), + // then used e.g. by the line numbers feature. + theme::set__color_mode__syntax_theme__syntax_set(opt, assets); + set_options!( [ blame_code_style, @@ -237,7 +241,6 @@ pub fn set_options( // Setting ComputedValues set_widths_and_isatty(opt); set_true_color(opt); - theme::set__color_mode__syntax_theme__syntax_set(opt, assets); opt.computed.inspect_raw_lines = cli::InspectRawLines::from_str(&opt.inspect_raw_lines).unwrap(); opt.computed.paging_mode = parse_paging_mode(&opt.paging_mode); From f471e2c5bee9cbeb2e51eb61a02992812608e502 Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Tue, 9 Dec 2025 21:11:42 +0100 Subject: [PATCH 2/3] Move ensure_display_width_1 to config --- src/config.rs | 10 ++++++++++ src/wrapping.rs | 12 +----------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/config.rs b/src/config.rs index 1c01530a6..11192290a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,7 @@ use regex::Regex; use syntect::highlighting::Style as SyntectStyle; use syntect::highlighting::Theme as SyntaxTheme; use syntect::parsing::SyntaxSet; +use unicode_segmentation::UnicodeSegmentation; use crate::ansi; use crate::cli; @@ -462,6 +463,15 @@ pub fn user_supplied_option(option: &str, arg_matches: &clap::ArgMatches) -> boo arg_matches.value_source(option) == Some(ValueSource::CommandLine) } +pub fn ensure_display_width_1(what: &str, arg: String) -> String { + match arg.grapheme_indices(true).count() { + INLINE_SYMBOL_WIDTH_1 => arg, + width => fatal(format!( + "Invalid value for {what}, display width of \"{arg}\" must be {INLINE_SYMBOL_WIDTH_1} but is {width}", + )), + } +} + pub fn delta_unreachable(message: &str) -> ! { fatal(format!( "{message} This should not be possible. \ diff --git a/src/wrapping.rs b/src/wrapping.rs index 261094a3b..f4edaf2cd 100644 --- a/src/wrapping.rs +++ b/src/wrapping.rs @@ -3,10 +3,9 @@ use unicode_segmentation::UnicodeSegmentation; use unicode_width::UnicodeWidthStr; use crate::cli; -use crate::config::INLINE_SYMBOL_WIDTH_1; +use crate::config::{ensure_display_width_1, Config, INLINE_SYMBOL_WIDTH_1}; use crate::fatal; -use crate::config::Config; use crate::delta::DiffType; use crate::delta::State; use crate::features::line_numbers::{self, SideBySideLineWidth}; @@ -97,15 +96,6 @@ fn remove_percent_suffix(arg: &str) -> &str { } } -fn ensure_display_width_1(what: &str, arg: String) -> String { - match arg.grapheme_indices(true).count() { - INLINE_SYMBOL_WIDTH_1 => arg, - width => fatal(format!( - "Invalid value for {what}, display width of \"{arg}\" must be {INLINE_SYMBOL_WIDTH_1} but is {width}", - )), - } -} - fn adapt_wrap_max_lines_argument(arg: String) -> usize { if arg == "∞" || arg == "unlimited" || arg.starts_with("inf") { 0 From 73a727b115d1f259ee9c53bdf7a732b73fdaf45e Mon Sep 17 00:00:00 2001 From: Thomas Otto Date: Tue, 9 Dec 2025 21:29:58 +0100 Subject: [PATCH 3/3] Add --side-by-side-fill-empty to fill up unmatched lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This changes │ 10 │ deleted line │ │ │ │ │ │ 11 │ added line │ to the following. The fill char and style can be configured. │ 10 │ deleted line │ │ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ │ │ │ ╱╱╱╱╱╱╱╱╱╱╱╱╱╱ │ 11 │ added line │ Thanks to @guanghechen for the idea. --- src/cli.rs | 8 ++++ src/config.rs | 9 +++++ src/features/side_by_side.rs | 75 +++++++++++++++++++++++++++++++++--- src/options/set.rs | 1 + 4 files changed, 87 insertions(+), 6 deletions(-) diff --git a/src/cli.rs b/src/cli.rs index 544b25ceb..e81a0437b 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -860,6 +860,14 @@ pub struct Opt { /// Display diffs in side-by-side layout. pub side_by_side: bool, + #[arg(long = "side-by-side-fill-empty", value_name = "CHAR_AND_STYLE")] + /// Fill empty side. Can be "true", or a fill character, or a character plus a style. + /// + /// On "true" the unmatched side will be filled with "/" in the style of + /// --line-numbers-zero-style, if a single character is given this is used instead. + /// If the character is followed by a style, that style is used. + pub side_by_side_fill_empty: Option, + #[arg(long = "syntax-theme", value_name = "SYNTAX_THEME")] /// The syntax-highlighting theme to use. /// diff --git a/src/config.rs b/src/config.rs index 11192290a..ef40595a2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -128,6 +128,7 @@ pub struct Config { pub relative_paths: bool, pub show_themes: bool, pub side_by_side_data: side_by_side::SideBySideData, + pub side_by_side_fill_empty: Option, pub side_by_side: bool, pub syntax_set: SyntaxSet, pub syntax_theme: Option, @@ -248,6 +249,13 @@ impl From for Config { side_by_side_data, ); + let side_by_side_fill_empty = side_by_side::SideBySideFillEmpty::from_str( + opt.side_by_side_fill_empty, + &opt.line_numbers_zero_style, + opt.computed.true_color, + opt.git_config.as_ref(), + ); + let navigate_regex = if (opt.navigate || opt.show_themes) && (opt.navigate_regex.is_none() || opt.navigate_regex == Some("".to_string())) { @@ -427,6 +435,7 @@ impl From for Config { show_themes: opt.show_themes, side_by_side: opt.side_by_side && !handlers::hunk::is_word_diff(), side_by_side_data, + side_by_side_fill_empty, styles_map, syntax_set: opt.computed.syntax_set, syntax_theme: opt.computed.syntax_theme, diff --git a/src/features/side_by_side.rs b/src/features/side_by_side.rs index 6e0d032cc..08f2135ca 100644 --- a/src/features/side_by_side.rs +++ b/src/features/side_by_side.rs @@ -4,6 +4,7 @@ use unicode_width::UnicodeWidthStr; use crate::ansi; use crate::cli; +use crate::config::ensure_display_width_1; use crate::config::{self, delta_unreachable, Config}; use crate::delta::DiffType; use crate::delta::State; @@ -56,6 +57,59 @@ impl SideBySideData { } } +#[derive(Clone, Debug)] +pub struct SideBySideFillEmpty { + pub filler: smol_str::SmolStr, + pub style: Style, +} + +impl SideBySideFillEmpty { + fn new(filler: smol_str::SmolStr, style: Style) -> Self { + ensure_display_width_1( + "first argument of side-by-side-fill-empty", + filler.to_string(), + ); + Self { filler, style } + } + + fn default_filler(style: Style) -> Self { + Self { + filler: "/".into(), + style, + } + } + + pub fn from_str( + s: Option, + default_style: &str, + true_color: bool, + git_config: Option<&crate::git_config::GitConfig>, + ) -> Option { + match s?.split(char::is_whitespace).collect::>().as_slice() { + [] | [""] => None, + [single_arg] => match single_arg.to_lowercase().as_str() { + "false" => None, + "true" => Some(SideBySideFillEmpty::default_filler(Style::from_str( + default_style, + None, + None, + true_color, + git_config, + ))), + _ => Some(SideBySideFillEmpty::new( + single_arg.into(), + Style::from_str(default_style, None, None, true_color, git_config), + )), + }, + args => { + let style = + Style::from_str(&args[1..].join(" "), None, None, true_color, git_config); + Some(SideBySideFillEmpty::new(args[0].into(), style)) + } + } + } +} + pub fn available_line_width( config: &Config, data: &line_numbers::LineNumbersData, @@ -518,18 +572,27 @@ fn pad_panel_line_to_width( config, ); - match bg_fill_mode { - Some(BgFillMethod::TryAnsiSequence) => { + match ( + bg_fill_mode, + config.side_by_side_fill_empty.as_ref(), + panel_line_is_empty, + ) { + (_, Some(fill), true) => panel_line.push_str( + &fill + .style + .paint(fill.filler.repeat(panel_width - text_width)) + .to_string(), + ), + (Some(BgFillMethod::TryAnsiSequence), _, _) => { Painter::right_fill_background_color(panel_line, fill_style) } - Some(BgFillMethod::Spaces) if text_width >= panel_width => (), - Some(BgFillMethod::Spaces) => panel_line.push_str( - #[allow(clippy::unnecessary_to_owned)] + (Some(BgFillMethod::Spaces), _, _) if text_width >= panel_width => (), + (Some(BgFillMethod::Spaces), _, _) => panel_line.push_str( &fill_style .paint(" ".repeat(panel_width - text_width)) .to_string(), ), - None => (), + (None, _, _) => (), } } diff --git a/src/options/set.rs b/src/options/set.rs index 66a427c6b..0e4dad514 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -218,6 +218,7 @@ pub fn set_options( show_colors, show_themes, side_by_side, + side_by_side_fill_empty, wrap_max_lines, wrap_right_prefix_symbol, wrap_right_percent,