Skip to content

Commit

Permalink
New option to map raw styles encountered in input
Browse files Browse the repository at this point in the history
Unify handling of styles parsed from raw line and computed diff
styles. This enables syntax highlighting to be used in color-moved
sections.

Fixes #72
  • Loading branch information
dandavison committed Nov 23, 2021
1 parent 0c0043b commit 596d3dc
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 55 deletions.
6 changes: 6 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,12 @@ pub struct Opt {
/// (underline), 'ol' (overline), or the combination 'ul ol'.
pub hunk_header_decoration_style: String,

#[structopt(long = "map-styles")]
/// A string specifying a mapping styles encountered in raw input to desired
/// output styles. An example is --map-styles='black cyan => white magenta,
/// red cyan => white blue'
pub map_styles: Option<String>,

/// Format string for git blame commit metadata. Available placeholders are
/// "{timestamp}", "{author}", and "{commit}".
#[structopt(
Expand Down
26 changes: 26 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use crate::git_config::{GitConfig, GitConfigEntry};
use crate::minusplus::MinusPlus;
use crate::paint::BgFillMethod;
use crate::parse_styles;
use crate::style;
use crate::style::Style;
use crate::tests::TESTING;
use crate::utils::bat::output::PagingMode;
Expand Down Expand Up @@ -105,6 +106,7 @@ pub struct Config {
pub line_numbers_style_minusplus: MinusPlus<Style>,
pub line_numbers_zero_style: Style,
pub line_numbers: bool,
pub styles_map: Option<HashMap<style::AnsiTermStyleEqualityKey, Style>>,
pub max_line_distance_for_naively_paired_lines: f64,
pub max_line_distance: f64,
pub max_line_length: usize,
Expand Down Expand Up @@ -157,6 +159,7 @@ impl Config {
impl From<cli::Opt> for Config {
fn from(opt: cli::Opt) -> Self {
let styles = parse_styles::parse_styles(&opt);
let styles_map = make_styles_map(&opt);

let max_line_distance_for_naively_paired_lines =
env::get_env_var("DELTA_EXPERIMENTAL_MAX_LINE_DISTANCE_FOR_NAIVELY_PAIRED_LINES")
Expand Down Expand Up @@ -297,6 +300,7 @@ impl From<cli::Opt> for Config {
),
line_numbers_zero_style: styles["line-numbers-zero-style"],
line_buffer_size: opt.line_buffer_size,
styles_map,
max_line_distance: opt.max_line_distance,
max_line_distance_for_naively_paired_lines,
max_line_length: match (opt.side_by_side, wrap_max_lines_plus1) {
Expand Down Expand Up @@ -396,6 +400,28 @@ fn make_blame_palette(blame_palette: Option<String>, is_light_mode: bool) -> Vec
}
}

fn make_styles_map(opt: &cli::Opt) -> Option<HashMap<style::AnsiTermStyleEqualityKey, Style>> {
if let Some(styles_map_str) = &opt.map_styles {
let mut styles_map = HashMap::new();
for pair_str in styles_map_str.split(",") {
let mut style_strs = pair_str.split("=>").map(|s| s.trim());
if let (Some(from_str), Some(to_str)) = (style_strs.next(), style_strs.next()) {
let key = style::ansi_term_style_equality_key(
Style::from_str(from_str, None, None, true, opt.git_config.as_ref())
.ansi_term_style,
);
styles_map.insert(
key,
Style::from_str(to_str, None, None, true, opt.git_config.as_ref()),
);
}
}
Some(styles_map)
} else {
None
}
}

/// Did the user supply `option` on the command line?
pub fn user_supplied_option(option: &str, arg_matches: &clap::ArgMatches) -> bool {
arg_matches.occurrences_of(option) > 0
Expand Down
1 change: 1 addition & 0 deletions src/options/set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ pub fn set_options(
inspect_raw_lines,
keep_plus_minus_markers,
line_buffer_size,
map_styles,
max_line_distance,
max_line_length,
// Hack: minus-style must come before minus-*emph-style because the latter default
Expand Down
119 changes: 64 additions & 55 deletions src/paint.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::HashMap;
use std::io::Write;

use itertools::Itertools;
Expand All @@ -6,7 +7,6 @@ use syntect::highlighting::Style as SyntectStyle;
use syntect::parsing::{SyntaxReference, SyntaxSet};
use unicode_segmentation::UnicodeSegmentation;

use crate::ansi;
use crate::config::{self, delta_unreachable, Config};
use crate::delta::State;
use crate::edits;
Expand All @@ -18,6 +18,7 @@ use crate::minusplus::*;
use crate::paint::superimpose_style_sections::superimpose_style_sections;
use crate::style::Style;
use crate::wrapping::wrap_minusplus_block;
use crate::{ansi, style};

pub struct Painter<'p> {
pub minus_lines: Vec<(String, State)>,
Expand Down Expand Up @@ -134,17 +135,9 @@ impl<'p> Painter<'p> {
}
}

/// Remove the initial +/- character of a line that will be emitted unchanged, including any
/// ANSI escape sequences.
pub fn prepare_raw_line(&self, line: &str) -> String {
ansi::ansi_preserving_slice(
&self.expand_tabs(line.graphemes(true)),
if self.config.keep_plus_minus_markers {
0
} else {
1
},
)
// Remove initial -/+ character, and expand tabs as spaces, retaining ANSI sequences.
pub fn prepare_raw_line(&self, raw_line: &str) -> String {
ansi::ansi_preserving_slice(&self.expand_tabs(raw_line.graphemes(true)), 1)
}

/// Expand tabs as spaces.
Expand Down Expand Up @@ -461,33 +454,17 @@ impl<'p> Painter<'p> {
State::HunkMinus(None) | State::HunkMinusWrapped => {
(config.minus_style, config.minus_non_emph_style)
}
State::HunkMinus(Some(raw_line)) => {
// TODO: This is the second time we are parsing the ANSI sequences
if let Some(ansi_term_style) = ansi::parse_first_style(raw_line) {
let style = Style {
ansi_term_style,
..Style::new()
};
(style, style)
} else {
(config.minus_style, config.minus_non_emph_style)
}
}
State::HunkZero | State::HunkZeroWrapped => (config.zero_style, config.zero_style),
State::HunkPlus(None) | State::HunkPlusWrapped => {
(config.plus_style, config.plus_non_emph_style)
}
State::HunkPlus(Some(raw_line)) => {
// TODO: This is the second time we are parsing the ANSI sequences
if let Some(ansi_term_style) = ansi::parse_first_style(raw_line) {
let style = Style {
ansi_term_style,
..Style::new()
};
(style, style)
State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => {
let style = if diff_sections.len() > 0 {
diff_sections[diff_sections.len() - 1].0
} else {
(config.plus_style, config.plus_non_emph_style)
}
config.null_style
};
(style, style)
}
State::Blame(_, _) => (diff_sections[0].0, diff_sections[0].0),
_ => (config.null_style, config.null_style),
Expand Down Expand Up @@ -573,18 +550,6 @@ impl<'p> Painter<'p> {
))
}
}
match state {
State::HunkMinus(Some(raw_line)) | State::HunkPlus(Some(raw_line)) => {
// This line has been identified as one which should be emitted unchanged,
// including any ANSI escape sequences that it has.
return (
format!("{}{}", ansi_term::ANSIStrings(&ansi_strings), raw_line),
false,
);
}
_ => {}
}

let superimposed = superimpose_style_sections(
syntax_sections,
diff_sections,
Expand Down Expand Up @@ -682,19 +647,19 @@ impl<'p> Painter<'p> {
/// Set background styles to represent diff for minus and plus lines in buffer.
#[allow(clippy::type_complexity)]
fn get_diff_style_sections<'a>(
minus_lines: &'a [(String, State)],
plus_lines: &'a [(String, State)],
minus_lines_and_states: &'a [(String, State)],
plus_lines_and_states: &'a [(String, State)],
config: &config::Config,
) -> (
Vec<LineSegments<'a, Style>>,
Vec<LineSegments<'a, Style>>,
Vec<(Option<usize>, Option<usize>)>,
) {
let (minus_lines, minus_styles): (Vec<&str>, Vec<Style>) = minus_lines
let (minus_lines, minus_styles): (Vec<&str>, Vec<Style>) = minus_lines_and_states
.iter()
.map(|(s, t)| (s.as_str(), *config.get_style(t)))
.unzip();
let (plus_lines, plus_styles): (Vec<&str>, Vec<Style>) = plus_lines
let (plus_lines, plus_styles): (Vec<&str>, Vec<Style>) = plus_lines_and_states
.iter()
.map(|(s, t)| (s.as_str(), *config.get_style(t)))
.unzip();
Expand All @@ -709,23 +674,30 @@ impl<'p> Painter<'p> {
config.max_line_distance,
config.max_line_distance_for_naively_paired_lines,
);

let minus_non_emph_style = if config.minus_non_emph_style != config.minus_emph_style {
Some(config.minus_non_emph_style)
} else {
None
};
let mut lines_style_sections = MinusPlus::new(&mut diff_sections.0, &mut diff_sections.1);
Self::update_styles(lines_style_sections[Minus], None, minus_non_emph_style);
Self::update_styles(
&minus_lines_and_states,
lines_style_sections[Minus],
None,
minus_non_emph_style,
config,
);
let plus_non_emph_style = if config.plus_non_emph_style != config.plus_emph_style {
Some(config.plus_non_emph_style)
} else {
None
};
Self::update_styles(
&plus_lines_and_states,
lines_style_sections[Plus],
Some(config.whitespace_error_style),
plus_non_emph_style,
config,
);
diff_sections
}
Expand All @@ -738,12 +710,25 @@ impl<'p> Painter<'p> {
/// sections.
/// 2. If the line constitutes a whitespace error, then the whitespace error style
/// should be applied to the added material.
fn update_styles(
lines_style_sections: &mut Vec<LineSegments<'_, Style>>,
/// 3. If delta recognized the raw line as one containing ANSI colors that
/// are going to be preserved in the output, then replace delta's
/// computed diff styles with these styles from the raw line. (This is
/// how support for git's --color-moved is implemented.)
fn update_styles<'a>(
lines_and_states: &'a [(String, State)],
lines_style_sections: &mut Vec<LineSegments<'a, Style>>,
whitespace_error_style: Option<Style>,
non_emph_style: Option<Style>,
config: &config::Config,
) {
for style_sections in lines_style_sections {
for ((_, state), style_sections) in lines_and_states.iter().zip(lines_style_sections) {
match state {
State::HunkMinus(Some(raw_line)) | State::HunkPlus(Some(raw_line)) => {
*style_sections = parse_style_sections(raw_line, config);
continue;
}
_ => {}
};
let line_has_emph_and_non_emph_sections =
style_sections_contain_more_than_one_style(style_sections);
let should_update_non_emph_styles =
Expand All @@ -766,6 +751,30 @@ impl<'p> Painter<'p> {
}
}

// Parse ANSI styles encountered in `raw_line` and apply `styles_map`.
pub fn parse_style_sections<'a>(
raw_line: &'a str,
config: &config::Config,
) -> LineSegments<'a, Style> {
let empty_map = HashMap::new();
let styles_map = config.styles_map.as_ref().unwrap_or(&empty_map);
ansi::parse_style_sections(raw_line)
.iter()
.map(|(original_style, s)| {
match styles_map.get(&style::ansi_term_style_equality_key(*original_style)) {
Some(mapped_style) => (*mapped_style, *s),
None => (
Style {
ansi_term_style: *original_style,
..Style::default()
},
*s,
),
}
})
.collect()
}

#[allow(clippy::too_many_arguments)]
pub fn paint_file_path_with_line_number(
line_number: Option<usize>,
Expand Down
75 changes: 75 additions & 0 deletions src/style.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::borrow::Cow;
use std::fmt;
use std::hash::{Hash, Hasher};

use lazy_static::lazy_static;

Expand Down Expand Up @@ -210,6 +211,60 @@ pub fn ansi_term_style_equality(a: ansi_term::Style, b: ansi_term::Style) -> boo
}
}

// TODO: The equality methods were implemented first, and the equality_key
// methods later. The former should be re-implemented in terms of the latter.
// But why did the former not address equality of ansi_term::Color::RGB values?
pub struct AnsiTermStyleEqualityKey {
attrs_key: (bool, bool, bool, bool, bool, bool, bool, bool),
foreground_key: Option<(u8, u8, u8, u8)>,
background_key: Option<(u8, u8, u8, u8)>,
}

impl PartialEq for AnsiTermStyleEqualityKey {
fn eq(&self, other: &Self) -> bool {
let option_eq = |opt_a, opt_b| match (opt_a, opt_b) {
(Some(a), Some(b)) => a == b,
(None, None) => true,
_ => false,
};

if self.attrs_key != other.attrs_key {
false
} else {
option_eq(self.foreground_key, other.foreground_key)
&& option_eq(self.background_key, other.background_key)
}
}
}

impl Eq for AnsiTermStyleEqualityKey {}

impl Hash for AnsiTermStyleEqualityKey {
fn hash<H: Hasher>(&self, state: &mut H) {
self.attrs_key.hash(state);
self.foreground_key.hash(state);
self.background_key.hash(state);
}
}

pub fn ansi_term_style_equality_key(style: ansi_term::Style) -> AnsiTermStyleEqualityKey {
let attrs_key = (
style.is_bold,
style.is_dimmed,
style.is_italic,
style.is_underline,
style.is_blink,
style.is_reverse,
style.is_hidden,
style.is_strikethrough,
);
AnsiTermStyleEqualityKey {
attrs_key,
foreground_key: style.foreground.map(ansi_term_color_equality_key),
background_key: style.background.map(ansi_term_color_equality_key),
}
}

fn ansi_term_color_equality(a: Option<ansi_term::Color>, b: Option<ansi_term::Color>) -> bool {
match (a, b) {
(None, None) => true,
Expand Down Expand Up @@ -239,6 +294,26 @@ fn ansi_term_16_color_equality(a: ansi_term::Color, b: ansi_term::Color) -> bool
)
}

fn ansi_term_color_equality_key(color: ansi_term::Color) -> (u8, u8, u8, u8) {
// Same (r, g, b, a) encoding as in utils::bat::terminal::to_ansi_color.
// When a = 0xFF, then a 256-color number is stored in the red channel, and
// the green and blue channels are meaningless. But a=0 signifies an RGB
// color.
let default = 0xFF;
match color {
ansi_term::Color::Fixed(0) | ansi_term::Color::Black => (0, default, default, default),
ansi_term::Color::Fixed(1) | ansi_term::Color::Red => (1, default, default, default),
ansi_term::Color::Fixed(2) | ansi_term::Color::Green => (2, default, default, default),
ansi_term::Color::Fixed(3) | ansi_term::Color::Yellow => (3, default, default, default),
ansi_term::Color::Fixed(4) | ansi_term::Color::Blue => (4, default, default, default),
ansi_term::Color::Fixed(5) | ansi_term::Color::Purple => (5, default, default, default),
ansi_term::Color::Fixed(6) | ansi_term::Color::Cyan => (6, default, default, default),
ansi_term::Color::Fixed(7) | ansi_term::Color::White => (7, default, default, default),
ansi_term::Color::Fixed(n) => (n, default, default, default),
ansi_term::Color::RGB(r, g, b) => (r, g, b, 0),
}
}

lazy_static! {
pub static ref GIT_DEFAULT_MINUS_STYLE: Style = Style {
ansi_term_style: ansi_term::Color::Red.normal(),
Expand Down

0 comments on commit 596d3dc

Please sign in to comment.