Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add side-by-side line wrapping mode #515

Merged
merged 10 commits into from
Oct 16, 2021
1 change: 1 addition & 0 deletions src/ansi/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use iterator::{AnsiElementIterator, Element};
pub const ANSI_CSI_CLEAR_TO_EOL: &str = "\x1b[0K";
pub const ANSI_CSI_CLEAR_TO_BOL: &str = "\x1b[1K";
pub const ANSI_SGR_RESET: &str = "\x1b[0m";
pub const ANSI_SGR_REVERSE: &str = "\x1b[7m";

pub fn strip_ansi_codes(s: &str) -> String {
strip_ansi_codes_from_strings_iterator(ansi_strings_iterator(s))
Expand Down
36 changes: 35 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ use crate::bat_utils::output::PagingMode;
use crate::git_config::{GitConfig, GitConfigEntry};
use crate::options;

#[derive(StructOpt, Default)]
// No Default trait as this ignores `default_value = ..`
#[derive(StructOpt)]
#[structopt(
name = "delta",
about = "A viewer for git and diff output",
Expand Down Expand Up @@ -446,6 +447,12 @@ pub struct Opt {
#[structopt(long = "default-language")]
pub default_language: Option<String>,

#[structopt(long = "inline-hint-style", default_value = "blue")]
/// Style (foreground, background, attributes) for content added by delta to
/// the original diff such as special characters to highlight tabs, and the
/// symbols used to indicate wrapped lines. See STYLES section.
pub inline_hint_style: String,

/// The regular expression used to decide what a word is for the within-line highlight
/// algorithm. For less fine-grained matching than the default try --word-diff-regex="\S+"
/// --max-line-distance=1.0 (this is more similar to `git --word-diff`).
Expand Down Expand Up @@ -497,6 +504,32 @@ pub struct Opt {
#[structopt(long = "line-numbers-right-style", default_value = "auto")]
pub line_numbers_right_style: String,

/// How often a line should be wrapped if it does not fit. Zero means to never wrap. Any content
/// which does not fit will be truncated. A value of "unlimited" means a line will be wrapped
/// as many times as required.
#[structopt(long = "wrap-max-lines", default_value = "2")]
pub wrap_max_lines: String,

/// Symbol added to the end of a line indicating that the content has been wrapped
/// onto the next line and continues left-aligned.
#[structopt(long = "wrap-left-symbol", default_value = "↵")]
pub wrap_left_symbol: String,

/// Symbol added to the end of a line indicating that the content has been wrapped
/// onto the next line and continues right-aligned.
#[structopt(long = "wrap-right-symbol", default_value = "↴")]
pub wrap_right_symbol: String,

/// Threshold for right-aligning wrapped content. If the length of the remaining wrapped
/// content, as a percentage of width, is less than this quantity it will be right-aligned.
/// Otherwise it will be left-aligned.
#[structopt(long = "wrap-right-percent", default_value = "37.0")]
pub wrap_right_percent: String,

/// Symbol displayed in front of right-aligned wrapped content.
#[structopt(long = "wrap-right-prefix-symbol", default_value = "…")]
pub wrap_right_prefix_symbol: String,

#[structopt(long = "file-modified-label", default_value = "")]
/// Text to display in front of a modified file path.
pub file_modified_label: String,
Expand Down Expand Up @@ -524,6 +557,7 @@ pub struct Opt {
#[structopt(long = "max-line-length", default_value = "512")]
/// Truncate lines longer than this. To prevent any truncation, set to zero. Note that
/// delta will be slow on very long lines (e.g. minified .js) if truncation is disabled.
/// When wrapping lines it is automatically set to fit at least all visible characters.
pub max_line_length: usize,

/// How to extend the background color to the end of the line in side-by-side mode. Can
Expand Down
6 changes: 3 additions & 3 deletions src/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use lazy_static::lazy_static;
use syntect::highlighting::Color as SyntectColor;

use crate::bat_utils::terminal::to_ansi_color;
use crate::syntect_color;
use crate::syntect_utils;

pub fn parse_color(s: &str, true_color: bool) -> Option<Color> {
if s == "normal" {
Expand All @@ -22,8 +22,8 @@ pub fn parse_color(s: &str, true_color: bool) -> Option<Color> {
} else {
s.parse::<u8>()
.ok()
.and_then(syntect_color::syntect_color_from_ansi_number)
.or_else(|| syntect_color::syntect_color_from_ansi_name(s))
.and_then(syntect_utils::syntect_color_from_ansi_number)
.or_else(|| syntect_utils::syntect_color_from_ansi_name(s))
.unwrap_or_else(die)
};
to_ansi_color(syntect_color, true_color)
Expand Down
111 changes: 104 additions & 7 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,51 @@ use structopt::clap;
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::bat_utils::output::PagingMode;
use crate::cli;
use crate::color;
use crate::delta::State;
use crate::env;
use crate::fatal;
use crate::features::navigate;
use crate::features::side_by_side;
use crate::git_config::{GitConfig, GitConfigEntry};
use crate::paint::BgFillMethod;
use crate::style::{self, Style};
use crate::syntect_utils::FromDeltaStyle;
use crate::wrapping::WrapConfig;

pub const INLINE_SYMBOL_WIDTH_1: usize = 1;

fn remove_percent_suffix(arg: &str) -> &str {
match &arg.strip_suffix('%') {
Some(s) => s,
None => arg,
}
}

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 {}, display width of \"{}\" must be {} but is {}",
what, arg, INLINE_SYMBOL_WIDTH_1, width
)),
}
}

fn adapt_wrap_max_lines_argument(arg: String) -> usize {
if arg == "∞" || arg == "unlimited" || arg.starts_with("inf") {
0
} else {
arg.parse::<usize>()
.unwrap_or_else(|err| fatal(format!("Invalid wrap-max-lines argument: {}", err)))
+ 1
}
}

pub struct Config {
pub available_terminal_width: usize,
Expand Down Expand Up @@ -50,6 +84,7 @@ pub struct Config {
pub hyperlinks: bool,
pub hyperlinks_commit_link_format: Option<String>,
pub hyperlinks_file_link_format: String,
pub inline_hint_style: Style,
pub inspect_raw_lines: cli::InspectRawLines,
pub keep_plus_minus_markers: bool,
pub line_fill_method: BgFillMethod,
Expand Down Expand Up @@ -95,6 +130,7 @@ pub struct Config {
pub true_color: bool,
pub truncation_symbol: String,
pub whitespace_error_style: Style,
pub wrap_config: WrapConfig,
pub zero_style: Style,
}

Expand Down Expand Up @@ -173,6 +209,13 @@ impl From<cli::Opt> for Config {
&opt.computed.available_terminal_width,
);

let inline_hint_style = Style::from_str(
&opt.inline_hint_style,
None,
None,
opt.computed.true_color,
false,
);
let git_minus_style = match opt.git_config_entries.get("color.diff.old") {
Some(GitConfigEntry::Style(s)) => Style::from_git_str(s),
_ => *style::GIT_DEFAULT_MINUS_STYLE,
Expand All @@ -193,10 +236,7 @@ impl From<cli::Opt> for Config {
// Note that "default" is not documented
Some("ansi") | Some("default") | None => BgFillMethod::TryAnsiSequence,
Some("spaces") => BgFillMethod::Spaces,
_ => {
eprintln!("Invalid option for line-fill-method: Expected \"ansi\" or \"spaces\".");
process::exit(1);
}
_ => fatal("Invalid option for line-fill-method: Expected \"ansi\" or \"spaces\"."),
};

let navigate_regexp = if opt.navigate || opt.show_themes {
Expand All @@ -212,6 +252,8 @@ impl From<cli::Opt> for Config {
None
};

let wrap_max_lines_plus1 = adapt_wrap_max_lines_argument(opt.wrap_max_lines);

Self {
available_terminal_width: opt.computed.available_terminal_width,
background_color_extends_to_terminal_width: opt
Expand Down Expand Up @@ -256,8 +298,17 @@ impl From<cli::Opt> for Config {
hyperlinks_commit_link_format: opt.hyperlinks_commit_link_format,
hyperlinks_file_link_format: opt.hyperlinks_file_link_format,
inspect_raw_lines: opt.computed.inspect_raw_lines,
inline_hint_style,
keep_plus_minus_markers: opt.keep_plus_minus_markers,
line_fill_method,
line_fill_method: if opt.side_by_side {
// Panels in side-by-side always sum up to an even number, if the terminal has
// an odd width then extending the background color with an ANSI sequence
// would indicate the wrong width and extend beyond truncated or wrapped content,
// thus spaces are used here by default.
BgFillMethod::Spaces
} else {
line_fill_method
},
line_numbers: opt.line_numbers,
line_numbers_left_format: opt.line_numbers_left_format,
line_numbers_left_style,
Expand All @@ -269,7 +320,23 @@ impl From<cli::Opt> for Config {
line_buffer_size: opt.line_buffer_size,
max_line_distance: opt.max_line_distance,
max_line_distance_for_naively_paired_lines,
max_line_length: opt.max_line_length,
max_line_length: match (opt.side_by_side, wrap_max_lines_plus1) {
(false, _) | (true, 1) => opt.max_line_length,
// Ensure there is enough text to wrap, either don't truncate the input at all (0)
// or ensure there is enough for the requested number of lines.
// The input can contain ANSI sequences, so round up a bit. This is enough for
// normal `git diff`, but might not be with ANSI heavy input.
(true, 0) => 0,
(true, wrap_max_lines) => {
let single_pane_width = opt.computed.available_terminal_width / 2;
let add_25_percent_or_term_width =
|x| x + std::cmp::max((x * 250) / 1000, single_pane_width) as usize;
std::cmp::max(
opt.max_line_length,
add_25_percent_or_term_width(single_pane_width * wrap_max_lines),
)
}
},
minus_emph_style,
minus_empty_line_marker_style,
minus_file: opt.minus_file,
Expand Down Expand Up @@ -298,7 +365,33 @@ impl From<cli::Opt> for Config {
tab_width: opt.tab_width,
tokenization_regex,
true_color: opt.computed.true_color,
truncation_symbol: "→".to_string(),
truncation_symbol: format!("{}→{}", ansi::ANSI_SGR_REVERSE, ansi::ANSI_SGR_RESET),
wrap_config: WrapConfig {
left_symbol: ensure_display_width_1("wrap-left-symbol", opt.wrap_left_symbol),
right_symbol: ensure_display_width_1("wrap-right-symbol", opt.wrap_right_symbol),
right_prefix_symbol: ensure_display_width_1(
"wrap-right-prefix-symbol",
opt.wrap_right_prefix_symbol,
),
use_wrap_right_permille: {
let arg = &opt.wrap_right_percent;
let percent = remove_percent_suffix(arg)
.parse::<f64>()
.unwrap_or_else(|err| {
fatal(format!(
"Could not parse wrap-right-percent argument {}: {}.",
&arg, err
))
});
if percent.is_finite() && percent > 0.0 && percent < 100.0 {
(percent * 10.0).round() as usize
} else {
fatal("Invalid value for wrap-right-percent, not between 0 and 100.")
}
},
max_lines: wrap_max_lines_plus1,
inline_hint_syntect_style: SyntectStyle::from_delta_style(inline_hint_style),
},
whitespace_error_style,
zero_style,
}
Expand Down Expand Up @@ -529,6 +622,10 @@ pub fn delta_unreachable(message: &str) -> ! {
process::exit(error_exit_code);
}

#[cfg(test)]
// Usual length of the header returned by `run_delta()`, often `skip()`-ed.
pub const HEADER_LEN: usize = 7;

#[cfg(test)]
pub mod tests {
use crate::bat_utils::output::PagingMode;
Expand Down
4 changes: 4 additions & 0 deletions src/delta.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ pub enum State {
SubmoduleShort(String), // In a submodule section, with gitconfig diff.submodule = short
Blame(String), // In a line of `git blame` output.
Unknown,
// The following elements are created when a line is wrapped to display it:
HunkZeroWrapped, // Wrapped unchanged line
HunkMinusWrapped, // Wrapped removed line
HunkPlusWrapped, // Wrapped added line
}

#[derive(Debug, PartialEq)]
Expand Down
Loading