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 4d7bc44
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 59 deletions.
8 changes: 8 additions & 0 deletions etc/examples/72-color-moved-2.diff
Original file line number Diff line number Diff line change
@@ -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
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
135 changes: 76 additions & 59 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 Expand Up @@ -859,17 +868,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,
Expand Down
Loading

0 comments on commit 4d7bc44

Please sign in to comment.