diff options
author | Dan Davison <dandavison7@gmail.com> | 2021-11-22 20:15:06 -0500 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2021-11-23 19:30:36 -0500 |
commit | 5dc0d6ef7e37a565b06d794b50fcc763079f9ed7 (patch) | |
tree | fc660d958f4d6f341dfa7a8942cb75b489861f28 /src | |
parent | 87f458ae6be9377c040afadecae0f7f81d5e72b3 (diff) |
New option to map raw styles encountered in input
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
Diffstat (limited to 'src')
-rw-r--r-- | src/cli.rs | 6 | ||||
-rw-r--r-- | src/config.rs | 26 | ||||
-rw-r--r-- | src/options/set.rs | 1 | ||||
-rw-r--r-- | src/paint.rs | 151 | ||||
-rw-r--r-- | src/style.rs | 104 | ||||
-rw-r--r-- | src/tests/ansi_test_utils.rs | 1 |
6 files changed, 222 insertions, 67 deletions
@@ -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='bold purple => red "#eeeeee", bold cyan => syntax "#eeeeee"' + pub map_styles: Option<String>, + /// Format string for git blame commit metadata. Available placeholders are /// "{timestamp}", "{author}", and "{commit}". #[structopt( diff --git a/src/config.rs b/src/config.rs index 1c28d060..efe6adb0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -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; @@ -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, @@ -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") @@ -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) { @@ -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 diff --git a/src/options/set.rs b/src/options/set.rs index b1dd49d0..4cc2395e 100644 --- a/src/options/set.rs +++ b/src/options/set.rs @@ -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 diff --git a/src/paint.rs b/src/paint.rs index 03b484db..fbd0ee15 100644 --- a/src/paint.rs +++ b/src/paint.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; use std::io::Write; use itertools::Itertools; @@ -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; @@ -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)>, @@ -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. @@ -165,13 +158,11 @@ impl<'p> Painter<'p> { pub fn paint_buffered_minus_and_plus_lines(&mut self) { let minus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines( &self.minus_lines, - &State::HunkMinus(None), self.highlighter.as_mut(), self.config, ); let plus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines( &self.plus_lines, - &State::HunkPlus(None), self.highlighter.as_mut(), self.config, ); @@ -312,7 +303,6 @@ impl<'p> Painter<'p> { let lines = vec![(self.prepare(line), state.clone())]; let syntax_style_sections = Painter::get_syntax_style_sections_for_lines( &lines, - &state, self.highlighter.as_mut(), self.config, ); @@ -426,7 +416,6 @@ impl<'p> Painter<'p> { let lines = vec![(self.expand_tabs(line.graphemes(true)), state.clone())]; let syntax_style_sections = Painter::get_syntax_style_sections_for_lines( &lines, - &state, self.highlighter.as_mut(), self.config, ); @@ -461,33 +450,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.is_empty() { + 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), @@ -573,18 +546,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, @@ -636,7 +597,12 @@ impl<'p> Painter<'p> { || config.plus_non_emph_style.is_syntax_highlighted } State::HunkHeader(_, _) => true, - State::HunkMinus(Some(_)) | State::HunkPlus(Some(_)) => false, + State::HunkMinus(Some(_raw_line)) | State::HunkPlus(Some(_raw_line)) => { + // It is possible that the captured raw line contains an ANSI + // style that has been mapped (via map-styles) to a delta Style + // with syntax-highlighting. + true + } State::Blame(_, _) => true, State::GitShowFile => true, State::Grep => true, @@ -658,14 +624,15 @@ impl<'p> Painter<'p> { pub fn get_syntax_style_sections_for_lines<'a>( lines: &'a [(String, State)], - state: &State, highlighter: Option<&mut HighlightLines>, config: &config::Config, ) -> Vec<LineSegments<'a, SyntectStyle>> { let mut line_sections = Vec::new(); match ( highlighter, - Painter::should_compute_syntax_highlighting(state, config), + lines + .iter() + .any(|(_, state)| Painter::should_compute_syntax_highlighting(state, config)), ) { (Some(highlighter), true) => { for (line, _) in lines.iter() { @@ -684,19 +651,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(); @@ -711,23 +678,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 } @@ -740,12 +714,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 = @@ -768,6 +755,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>, @@ -861,17 +872,25 @@ mod superimpose_style_sections { use crate::style::Style; use crate::utils::bat::terminal::to_ansi_color; + // We have two different annotations of the same line: + // `syntax_style_sections` contains foreground styles computed by syntect, + // and `diff_style_sections` contains styles computed by delta reflecting + // within-line edits. The delta styles may assign a foreground color, or + // they may indicate that the foreground color comes from syntax + // highlighting (the is_syntax_highlighting attribute on style::Style). This + // function takes in the two input streams and outputs one stream with a + // single style assigned to each character. pub fn superimpose_style_sections( - sections_1: &[(SyntectStyle, &str)], - sections_2: &[(Style, &str)], + syntax_style_sections: &[(SyntectStyle, &str)], + diff_style_sections: &[(Style, &str)], true_color: bool, null_syntect_style: SyntectStyle, ) -> Vec<(Style, String)> { coalesce( superimpose( - explode(sections_1) + explode(syntax_style_sections) .iter() - .zip(explode(sections_2)) + .zip(explode(diff_style_sections)) .collect::<Vec<(&(SyntectStyle, char), (Style, char))>>(), ), true_color, diff --git a/src/style.rs b/src/style.rs index b745cf33..047b9cc9 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; use std::fmt; +use std::hash::{Hash, Hasher}; use lazy_static::lazy_static; @@ -210,6 +211,89 @@ 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), + } +} + +impl fmt::Debug for AnsiTermStyleEqualityKey { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let is_set = |c: char, set: bool| -> String { + if set { + c.to_uppercase().to_string() + } else { + c.to_lowercase().to_string() + } + }; + + let (bold, dimmed, italic, underline, blink, reverse, hidden, strikethrough) = + self.attrs_key; + write!( + f, + "ansi_term::Style {{ {:?} {:?} {}{}{}{}{}{}{}{} }}", + self.foreground_key, + self.background_key, + is_set('b', bold), + is_set('d', dimmed), + is_set('i', italic), + is_set('u', underline), + is_set('l', blink), + is_set('r', reverse), + is_set('h', hidden), + is_set('s', strikethrough), + ) + } +} + fn ansi_term_color_equality(a: Option<ansi_term::Color>, b: Option<ansi_term::Color>) -> bool { match (a, b) { (None, None) => true, @@ -239,6 +323,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(), diff --git a/src/tests/ansi_test_utils.rs b/src/tests/ansi_test_utils.rs index 7d8c5641..c4968ade 100644 --- a/src/tests/ansi_test_utils.rs +++ b/src/tests/ansi_test_utils.rs @@ -132,7 +132,6 @@ pub mod ansi_test_utils { let lines = vec![(line.to_string(), state.clone())]; let syntax_style_sections = paint::Painter::get_syntax_style_sections_for_lines( &lines, - &state, painter.highlighter.as_mut(), config, ); |