summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--etc/examples/72-color-moved-2.diff8
-rw-r--r--etc/examples/72-color-moved-3.diff14
-rw-r--r--src/cli.rs6
-rw-r--r--src/config.rs26
-rw-r--r--src/options/set.rs1
-rw-r--r--src/paint.rs151
-rw-r--r--src/style.rs104
-rw-r--r--src/tests/ansi_test_utils.rs1
8 files changed, 244 insertions, 67 deletions
diff --git a/etc/examples/72-color-moved-2.diff b/etc/examples/72-color-moved-2.diff
new file mode 100644
index 00000000..69ddc5c8
--- /dev/null
+++ b/etc/examples/72-color-moved-2.diff
@@ -0,0 +1,8 @@
+diff --git a/file.py b/file.py
+index f07db74..3cb162d 100644
+--- a/file.py
++++ b/file.py
+@@ -1,2 +1,2 @@
+-class X: pass
+ class Y: pass
++class X: pass
diff --git a/etc/examples/72-color-moved-3.diff b/etc/examples/72-color-moved-3.diff
new file mode 100644
index 00000000..a24d447a
--- /dev/null
+++ b/etc/examples/72-color-moved-3.diff
@@ -0,0 +1,14 @@
+commit fffb6bf94087c432b6e2e29cab97bf1f8987c641
+Author: Dan Davison <dandavison7@gmail.com>
+Date: Tue Nov 23 14:51:46 2021 -0500
+
+ DEBUG
+
+diff --git a/src/config.rs b/src/config.rs
+index efe6adb..9762222 100644
+--- a/src/config.rs
++++ b/src/config.rs
+@@ -400,6 +400,7 @@ fn make_blame_palette(blame_palette: Option<String>, is_light_mode: bool) -> Vec
++pub fn user_supplied_option(option: &str, arg_matches: &clap::ArgMatches) -> bool {
+@@ -416,29 +433,30 @@ fn make_styles_map(opt: &cli::Opt) -> Option<HashMap<style::AnsiTermStyleEqualit
+-pub fn user_supplied_option(option: &str, arg_matches: &clap::ArgMatches) -> bool {
diff --git a/src/cli.rs b/src/cli.rs
index bc928291..ec9b44cf 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -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,
);