summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDan Davison <dandavison7@gmail.com>2021-11-22 20:15:06 -0500
committerDan Davison <dandavison7@gmail.com>2021-11-23 19:30:36 -0500
commit5dc0d6ef7e37a565b06d794b50fcc763079f9ed7 (patch)
treefc660d958f4d6f341dfa7a8942cb75b489861f28
parent87f458ae6be9377c040afadecae0f7f81d5e72b3 (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
-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,
);