use std::borrow::Cow;
use std::collections::HashMap;
use std::io::Write;
use ansi_term::ANSIString;
use itertools::Itertools;
use syntect::easy::HighlightLines;
use syntect::highlighting::Style as SyntectStyle;
use syntect::parsing::{SyntaxReference, SyntaxSet};
use crate::config::{self, delta_unreachable, Config};
use crate::delta::{DiffType, InMergeConflict, MergeParents, State};
use crate::features::hyperlinks;
use crate::features::line_numbers::{self, LineNumbersData};
use crate::features::side_by_side::ansifill;
use crate::features::side_by_side::{self, PanelSide};
use crate::handlers::merge_conflict;
use crate::minusplus::*;
use crate::paint::superimpose_style_sections::superimpose_style_sections;
use crate::style::Style;
use crate::{ansi, style};
use crate::{edits, utils, utils::tabs};
pub type LineSections<'a, S> = Vec<(S, &'a str)>;
pub struct Painter<'p> {
pub minus_lines: Vec<(String, State)>,
pub plus_lines: Vec<(String, State)>,
pub writer: &'p mut dyn Write,
pub syntax: &'p SyntaxReference,
pub highlighter: Option<HighlightLines<'p>>,
pub config: &'p config::Config,
pub output_buffer: String,
pub line_numbers_data: Option<line_numbers::LineNumbersData<'p>>,
pub merge_conflict_lines: merge_conflict::MergeConflictLines,
pub merge_conflict_commit_names: merge_conflict::MergeConflictCommitNames,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum BgFillMethod {
TryAnsiSequence,
Spaces,
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum BgShouldFill {
With(BgFillMethod),
No,
}
impl Default for BgShouldFill {
fn default() -> Self {
BgShouldFill::With(BgFillMethod::TryAnsiSequence)
}
}
#[derive(PartialEq, Debug)]
pub enum StyleSectionSpecifier<'l> {
Style(Style),
StyleSections(LineSections<'l, Style>),
}
impl<'p> Painter<'p> {
pub fn new(writer: &'p mut dyn Write, config: &'p config::Config) -> Self {
let default_syntax = Self::get_syntax(&config.syntax_set, None);
let panel_width_fix = ansifill::UseFullPanelWidth::new(config);
let line_numbers_data = if config.line_numbers {
Some(line_numbers::LineNumbersData::from_format_strings(
&config.line_numbers_format,
panel_width_fix,
))
} else if config.side_by_side {
Some(line_numbers::LineNumbersData::empty_for_sbs(
panel_width_fix,
))
} else {
None
};
Self {
minus_lines: Vec::new(),
plus_lines: Vec::new(),
output_buffer: String::new(),
syntax: default_syntax,
highlighter: None,
writer,
config,
line_numbers_data,
merge_conflict_lines: merge_conflict::MergeConflictLines::new(),
merge_conflict_commit_names: merge_conflict::MergeConflictCommitNames::new(),
}
}
pub fn set_syntax(&mut self, extension: Option<&str>) {
self.syntax = Painter::get_syntax(&self.config.syntax_set, extension);
}
fn get_syntax<'a>(syntax_set: &'a SyntaxSet, extension: Option<&str>) -> &'a SyntaxReference {
if let Some(extension) = extension {
if let Some(syntax) = syntax_set.find_syntax_by_extension(extension) {
return syntax;
}
}
syntax_set
.find_syntax_by_extension("txt")
.unwrap_or_else(|| delta_unreachable("Failed to find any language syntax definitions."))
}
pub fn set_highlighter(&mut self) {
if let Some(ref syntax_theme) = self.config.syntax_theme {
self.highlighter = Some(HighlightLines::new(self.syntax, syntax_theme))
};
}
pub fn paint_buffered_minus_and_plus_lines(&mut self) {
if self.minus_lines.is_empty() && self.plus_lines.is_empty() {
return;
}
paint_minus_and_plus_lines(
MinusPlus::new(&self.minus_lines, &self.plus_lines),
&mut self.line_numbers_data,
&mut self.highlighter,
&mut self.output_buffer,
self.config,
);
self.minus_lines.clear();
self.plus_lines.clear();
}
pub fn paint_zero_line(&mut self, line: &str, state: State) {
let lines = &[(line.to_string(), state.clone())];
let syntax_style_sections =
get_syntax_style_sections_for_lines(lines, self.highlighter.as_mut(), self.config);
let mut diff_style_sections = vec![vec![(self.config.zero_style, lines[0].0.as_str())]]; Painter::update_diff_style_sections(
lines,
&mut diff_style_sections,
None,
None,
&[false],
self.config,
);
if self.config.side_by_side {
side_by_side::paint_zero_lines_side_by_side(
&lines[0].0,
syntax_style_sections,
diff_style_sections,
&mut self.output_buffer,
self.config,
&mut self.line_numbers_data.as_mut(),
painted_prefix(state, self.config),
BgShouldFill::With(BgFillMethod::Spaces),
);
} else {
Painter::paint_lines(
lines,
&syntax_style_sections,
diff_style_sections.as_slice(),
&[false],
&mut self.output_buffer,
self.config,
&mut self.line_numbers_data.as_mut(),
None,
BgShouldFill::With(BgFillMethod::Spaces),
);
}
}
#[allow(clippy::too_many_arguments)]
pub fn paint_lines<'a>(
lines: &'a [(String, State)],
syntax_style_sections: &[LineSections<'a, SyntectStyle>],
diff_style_sections: &[LineSections<'a, Style>],
lines_have_homolog: &[bool],
output_buffer: &mut String,
config: &config::Config,
line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
empty_line_style: Option<Style>, background_color_extends_to_terminal_width: BgShouldFill,
) {
for ((((_, state), syntax_sections), diff_sections), &line_has_homolog) in lines
.iter()
.zip_eq(syntax_style_sections)
.zip_eq(diff_style_sections)
.zip_eq(lines_have_homolog)
{
let (mut line, line_is_empty) = Painter::paint_line(
syntax_sections,
diff_sections,
state,
line_numbers_data,
None,
painted_prefix(state.clone(), config),
config,
);
let (bg_fill_mode, fill_style) =
Painter::get_should_right_fill_background_color_and_fill_style(
diff_sections,
Some(line_has_homolog),
state,
background_color_extends_to_terminal_width,
config,
);
if let Some(BgFillMethod::TryAnsiSequence) = bg_fill_mode {
Painter::right_fill_background_color(&mut line, fill_style);
} else if let Some(BgFillMethod::Spaces) = bg_fill_mode {
let text_width = ansi::measure_text_width(&line);
line.push_str(
#[allow(clippy::unnecessary_to_owned)]
&fill_style
.paint(" ".repeat(config.available_terminal_width - text_width))
.to_string(),
);
} else if line_is_empty {
if let Some(empty_line_style) = empty_line_style {
Painter::mark_empty_line(
&empty_line_style,
&mut line,
if config.line_numbers { Some(" ") } else { None },
);
}
};
output_buffer.push_str(&line);
output_buffer.push('\n');
}
}
pub fn syntax_highlight_and_paint_line(
&mut self,
line: &str,
style_sections: StyleSectionSpecifier,
state: State,
background_color_extends_to_terminal_width: BgShouldFill,
) {
let lines = vec![(tabs::expand(line, &self.config.tab_cfg), state)];
let syntax_style_sections =
get_syntax_style_sections_for_lines(&lines, self.highlighter.as_mut(), self.config);
let diff_style_sections = match style_sections {
StyleSectionSpecifier::Style(style) => vec![vec![(style, lines[0].0.as_str())]],
StyleSectionSpecifier::StyleSections(style_sections) => vec![style_sections],
};
Painter::paint_lines(
&lines,
&syntax_style_sections,
&diff_style_sections,
&[false],
&mut self.output_buffer,
self.config,
&mut None,
None,
background_color_extends_to_terminal_width,
);
}
pub fn get_should_right_fill_background_color_and_fill_style(
diff_sections: &[(Style, &str)],
line_has_homolog: Option<bool>,
state: &State,
background_color_extends_to_terminal_width: BgShouldFill,
config: &config::Config,
) -> (Option<BgFillMethod>, Style) {
let fill_style = match state {
State::HunkMinus(_, None) | State::HunkMinusWrapped => {
if let Some(true) = line_has_homolog {
config.minus_non_emph_style
} else {
config.minus_style
}
}
State::HunkZero(_, None) | State::HunkZeroWrapped => config.zero_style,
State::HunkPlus(_, None) | State::HunkPlusWrapped => {
if let Some(true) = line_has_homolog {
config.plus_non_emph_style
} else {
config.plus_style
}
}
State::HunkMinus(_, Some(_))
| State::HunkZero(_, Some(_))
| State::HunkPlus(_, Some(_)) => {
diff_sections
.iter()
.rev()
.filter(|(_, s)| s != &"\n")
.map(|(style, _)| *style)
.next()
.unwrap_or(config.null_style)
}
State::Blame(_) => diff_sections[0].0,
_ => config.null_style,
};
match (
fill_style.get_background_color().is_some(),
background_color_extends_to_terminal_width,
) {
(false, _) | (_, BgShouldFill::No) => (None, fill_style),
(_, BgShouldFill::With(bgmode)) => {
if config.background_color_extends_to_terminal_width {
(Some(bgmode), fill_style)
} else {
(None, fill_style)
}
}
}
}
pub fn right_fill_background_color(line: &mut String, fill_style: Style) {
line.push_str(&ansi_term::ANSIStrings(&[fill_style.paint("")]).to_string());
if line
.to_lowercase()
.ends_with(&ansi::ANSI_SGR_RESET.to_lowercase())
{
line.truncate(line.len() - ansi::ANSI_SGR_RESET.len());
}
line.push_str(ansi::ANSI_CSI_CLEAR_TO_EOL);
line.push_str(ansi::ANSI_SGR_RESET);
}
pub fn mark_empty_line(empty_line_style: &Style, line: &mut String, marker: Option<&str>) {
line.push_str(
#[allow(clippy::unnecessary_to_owned)]
&empty_line_style
.paint(marker.unwrap_or(ansi::ANSI_CSI_CLEAR_TO_BOL))
.to_string(),
);
}
pub fn paint_line(
syntax_sections: &[(SyntectStyle, &str)],
diff_sections: &[(Style, &str)],
state: &State,
line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>,
side_by_side_panel: Option<PanelSide>,
mut painted_prefix: Option<ansi_term::ANSIString>,
config: &config::Config,
) -> (String, bool) {
let mut ansi_strings = Vec::new();
let output_line_numbers = line_numbers_data.is_some();
if output_line_numbers {
let increment = !matches!(side_by_side_panel, Some(side_by_side::Left));
if let Some((line_numbers, styles)) = line_numbers::linenumbers_and_styles(
line_numbers_data.as_mut().unwrap(),
state,
config,
increment,
) {
ansi_strings.extend(line_numbers::format_and_paint_line_numbers(
line_numbers_data.as_ref().unwrap(),
side_by_side_panel,
styles,
line_numbers,
config,
))
}
}
let superimposed = superimpose_style_sections(
syntax_sections,
diff_sections,
config.true_color,
config.null_syntect_style,
);
let mut handled_prefix = false;
for (section_style, text) in &superimposed {
if !handled_prefix {
if let Some(painted_prefix) = painted_prefix.take() {
ansi_strings.push(painted_prefix)
}
}
if !text.is_empty() {
ansi_strings.push(section_style.paint(text.as_str()));
}
handled_prefix = true;
}
let is_empty = syntax_sections.is_empty();
(ansi_term::ANSIStrings(&ansi_strings).to_string(), is_empty)
}
pub fn emit(&mut self) -> std::io::Result<()> {
write!(self.writer, "{}", self.output_buffer)?;
self.output_buffer.clear();
Ok(())
}
pub fn should_compute_syntax_highlighting(state: &State, config: &config::Config) -> bool {
if config.syntax_theme.is_none() {
return false;
}
match state {
State::HunkMinus(_, None) => {
config.minus_style.is_syntax_highlighted
|| config.minus_emph_style.is_syntax_highlighted
|| config.minus_non_emph_style.is_syntax_highlighted
}
State::HunkZero(_, None) => config.zero_style.is_syntax_highlighted,
State::HunkPlus(_, None) => {
config.plus_style.is_syntax_highlighted
|| config.plus_emph_style.is_syntax_highlighted
|| config.plus_non_emph_style.is_syntax_highlighted
}
State::HunkHeader(_, _, _, _) => true,
State::HunkMinus(_, Some(_raw_line))
| State::HunkZero(_, Some(_raw_line))
| State::HunkPlus(_, Some(_raw_line)) => {
true
}
State::Blame(_) => true,
State::GitShowFile => true,
State::Grep(_, _, _, _) => true,
State::Unknown
| State::CommitMeta
| State::DiffHeader(_)
| State::HunkMinusWrapped
| State::HunkZeroWrapped
| State::HunkPlusWrapped
| State::MergeConflict(_, _)
| State::SubmoduleLog
| State::SubmoduleShort(_) => {
panic!(
"should_compute_syntax_highlighting is undefined for state {:?}",
state
)
}
}
}
fn update_diff_style_sections<'a>(
lines: &'a [(String, State)],
diff_style_sections: &mut Vec<LineSections<'a, Style>>,
whitespace_error_style: Option<Style>,
non_emph_style: Option<Style>,
lines_have_homolog: &[bool],
config: &config::Config,
) {
for (((_, state), style_sections), line_has_homolog) in lines
.iter()
.zip_eq(diff_style_sections)
.zip_eq(lines_have_homolog)
{
if let State::HunkMinus(_, Some(raw_line))
| State::HunkZero(_, Some(raw_line))
| State::HunkPlus(_, Some(raw_line)) = state
{
*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 = non_emph_style.is_some() && *line_has_homolog;
let mut is_whitespace_error = whitespace_error_style.is_some();
for (style, s) in style_sections.iter_mut().rev() {
if is_whitespace_error && !s.trim().is_empty() {
is_whitespace_error = false;
}
if is_whitespace_error && (style.is_emph || !line_has_emph_and_non_emph_sections) {
*style = whitespace_error_style.unwrap();
}
else if should_update_non_emph_styles && !style.is_emph {
*style = non_emph_style.unwrap();
if is_whitespace_error {
*style = whitespace_error_style.unwrap();
}
}
}
}
}
}
pub fn prepare(line: &str, prefix_length: usize, config: &config::Config) -> String {
if !line.is_empty() {
let mut line = tabs::remove_prefix_and_expand(prefix_length, line, &config.tab_cfg);
line.push('\n');
line
} else {
"\n".to_string()
}
}
pub fn prepare_raw_line(raw_line: &str, prefix_length: usize, config: &config::Config) -> String {
let mut line = tabs::expand(raw_line, &config.tab_cfg);
line.push('\n');
ansi::ansi_preserving_slice(&line, prefix_length)
}
pub fn paint_minus_and_plus_lines(
lines: MinusPlus<&Vec<(String, State)>>,
line_numbers_data: &mut Option<LineNumbersData>,
highlighter: &mut Option<HighlightLines>,
output_buffer: &mut String,
config: &config::Config,
) {
let syntax_style_sections = MinusPlus::new(
get_syntax_style_sections_for_lines(lines[Minus], highlighter.as_mut(), config),
get_syntax_style_sections_for_lines(lines[Plus], highlighter.as_mut(), config),
);
let (mut diff_style_sections, line_alignment) = get_diff_style_sections(&lines, config);
let lines_have_homolog = edits::make_lines_have_homolog(&line_alignment);
Painter::update_diff_style_sections(
lines[Minus],
&mut diff_style_sections[Minus],
None,
if config.minus_non_emph_style != config.minus_emph_style {
Some(config.minus_non_emph_style)
} else {
None
},
&lines_have_homolog[Minus],
config,
);
Painter::update_diff_style_sections(
lines[Plus],
&mut diff_style_sections[Plus],
Some(config.whitespace_error_style),
if config.plus_non_emph_style != config.plus_emph_style {
Some(config.plus_non_emph_style)
} else {
None
},
&lines_have_homolog[Plus],
config,
);
if config.side_by_side {
side_by_side::paint_minus_and_plus_lines_side_by_side(
lines,
syntax_style_sections,
diff_style_sections,
lines_have_homolog,
line_alignment,
line_numbers_data,
output_buffer,
config,
)
} else {
if !lines[Minus].is_empty() {
Painter::paint_lines(
lines[Minus],
&syntax_style_sections[Minus],
&diff_style_sections[Minus],
&lines_have_homolog[Minus],
output_buffer,
config,
&mut line_numbers_data.as_mut(),
Some(config.minus_empty_line_marker_style),
BgShouldFill::default(),
);
}
if !lines[Plus].is_empty() {
Painter::paint_lines(
lines[Plus],
&syntax_style_sections[Plus],
&diff_style_sections[Plus],
&lines_have_homolog[Plus],
output_buffer,
config,
&mut line_numbers_data.as_mut(),
Some(config.plus_empty_line_marker_style),
BgShouldFill::default(),
);
}
}
}
pub fn get_syntax_style_sections_for_lines<'a>(
lines: &'a [(String, State)],
highlighter: Option<&mut HighlightLines>,
config: &config::Config,
) -> Vec<LineSections<'a, SyntectStyle>> {
let mut line_sections = Vec::new();
match (
highlighter,
lines
.iter()
.any(|(_, state)| Painter::should_compute_syntax_highlighting(state, config)),
) {
(Some(highlighter), true) => {
for (line, _) in lines.iter() {
line_sections.push(
highlighter
.highlight_line(line, &config.syntax_set)
.unwrap(),
);
}
}
_ => {
for (line, _) in lines.iter() {
line_sections.push(vec![(config.null_syntect_style, line.as_str())])
}
}
}
line_sections
}
#[allow(clippy::type_complexity)]
fn get_diff_style_sections<'a>(
lines: &MinusPlus<&'a Vec<(String, State)>>,
config: &config::Config,
) -> (
MinusPlus<Vec<LineSections<'a, Style>>>,
Vec<(Option<usize>, Option<usize>)>,
) {
let (minus_lines, minus_styles): (Vec<&str>, Vec<Style>) = lines[Minus]
.iter()
.map(|(s, state)| (s.as_str(), *config.get_style(state)))
.unzip();
let (plus_lines, plus_styles): (Vec<&str>, Vec<Style>) = lines[Plus]
.iter()
.map(|(s, state)| (s.as_str(), *config.get_style(state)))
.unzip();
let (minus_line_diff_style_sections, plus_line_diff_style_sections, line_alignment) =
edits::infer_edits(
minus_lines,
plus_lines,
minus_styles,
config.minus_emph_style, plus_styles,
config.plus_emph_style, &config.tokenization_regex,
config.max_line_distance,
config.max_line_distance_for_naively_paired_lines,
);
let diff_sections = MinusPlus::new(
minus_line_diff_style_sections,
plus_line_diff_style_sections,
);
(diff_sections, line_alignment)
}
fn painted_prefix(state: State, config: &config::Config) -> Option<ANSIString> {
use DiffType::*;
use State::*;
match (state, config.keep_plus_minus_markers) {
(HunkMinus(Combined(MergeParents::Prefix(prefix), InMergeConflict::No), _), _) => {
Some(config.minus_style.paint(prefix))
}
(HunkZero(Combined(MergeParents::Prefix(prefix), InMergeConflict::No), _), _) => {
Some(config.zero_style.paint(prefix))
}
(HunkPlus(Combined(MergeParents::Prefix(prefix), InMergeConflict::No), _), _) => {
Some(config.plus_style.paint(prefix))
}
(HunkMinus(_, _), true) => Some(config.minus_style.paint("-".to_string())),
(HunkZero(_, _), true) => Some(config.zero_style.paint(" ".to_string())),
(HunkPlus(_, _), true) => Some(config.plus_style.paint("+".to_string())),
_ => None,
}
}
pub fn parse_style_sections<'a>(
raw_line: &'a str,
config: &config::Config,
) -> LineSections<'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>,
file_path: &str,
pad_line_number: bool,
separator: &str,
terminate_with_separator: bool,
file_style: Option<Style>, line_number_style: Option<Style>, config: &Config,
) -> String {
let mut file_with_line_number = Vec::new();
if let Some(file_style) = file_style {
let file_path = if let Some(regex_replacement) = &config.file_regex_replacement {
regex_replacement.execute(file_path)
} else {
Cow::from(file_path)
};
file_with_line_number.push(file_style.paint(file_path))
};
if let Some(line_number) = line_number {
if let Some(line_number_style) = line_number_style {
if !file_with_line_number.is_empty() {
file_with_line_number.push(ansi_term::ANSIString::from(separator));
}
file_with_line_number.push(line_number_style.paint(format!("{line_number}")))
}
}
if terminate_with_separator {
file_with_line_number.push(ansi_term::ANSIGenericString::from(separator));
}
if pad_line_number {
match line_number {
Some(n) if n < 10 => {
file_with_line_number.push(ansi_term::ANSIGenericString::from(" "))
}
Some(n) if n < 100 => {
file_with_line_number.push(ansi_term::ANSIGenericString::from(" "))
}
_ => {}
}
}
let file_with_line_number = ansi_term::ANSIStrings(&file_with_line_number).to_string();
match if config.hyperlinks && !file_with_line_number.is_empty() {
utils::path::absolute_path(file_path, config)
} else {
None
} {
Some(absolute_path) => hyperlinks::format_osc8_file_hyperlink(
absolute_path,
line_number,
&file_with_line_number,
config,
)
.into(),
_ => file_with_line_number,
}
}
fn style_sections_contain_more_than_one_style(sections: &[(Style, &str)]) -> bool {
if sections.len() > 1 {
let (first_style, _) = sections[0];
sections.iter().any(|(style, _)| *style != first_style)
} else {
false
}
}
mod superimpose_style_sections {
use syntect::highlighting::Style as SyntectStyle;
use crate::style::Style;
use crate::utils::bat::terminal::to_ansi_color;
pub fn superimpose_style_sections(
syntax_style_sections: &[(SyntectStyle, &str)],
diff_style_sections: &[(Style, &str)],
true_color: bool,
null_syntect_style: SyntectStyle,
) -> Vec<(Style, String)> {
coalesce(
superimpose(
explode(syntax_style_sections)
.iter()
.zip(explode(diff_style_sections))
.collect::<Vec<(&(SyntectStyle, char), (Style, char))>>(),
),
true_color,
null_syntect_style,
)
}
fn explode<T>(style_sections: &[(T, &str)]) -> Vec<(T, char)>
where
T: Copy,
{
let mut exploded: Vec<(T, char)> = Vec::new();
for (style, s) in style_sections {
for c in s.chars() {
exploded.push((*style, c));
}
}
exploded
}
#[allow(clippy::type_complexity)]
fn superimpose(
style_section_pairs: Vec<(&(SyntectStyle, char), (Style, char))>,
) -> Vec<((SyntectStyle, Style), char)> {
let mut superimposed: Vec<((SyntectStyle, Style), char)> = Vec::new();
for ((syntax_style, char_1), (style, char_2)) in style_section_pairs {
if *char_1 != char_2 {
panic!(
"String mismatch encountered while superimposing style sections: '{}' vs '{}'",
*char_1, char_2
)
}
superimposed.push(((*syntax_style, style), *char_1));
}
superimposed
}
fn coalesce(
style_sections: Vec<((SyntectStyle, Style), char)>,
true_color: bool,
null_syntect_style: SyntectStyle,
) -> Vec<(Style, String)> {
let make_superimposed_style = |(syntect_style, style): (SyntectStyle, Style)| {
if style.is_syntax_highlighted && syntect_style != null_syntect_style {
Style {
ansi_term_style: ansi_term::Style {
foreground: to_ansi_color(syntect_style.foreground, true_color),
..style.ansi_term_style
},
..style
}
} else {
style
}
};
let mut coalesced: Vec<(Style, String)> = Vec::new();
let mut style_sections = style_sections.iter();
if let Some((style_pair, c)) = style_sections.next() {
let mut current_string = c.to_string();
let mut current_style_pair = style_pair;
for (style_pair, c) in style_sections {
if style_pair != current_style_pair {
let style = make_superimposed_style(*current_style_pair);
coalesced.push((style, current_string));
current_string = String::new();
current_style_pair = style_pair;
}
current_string.push(*c);
}
if current_string.ends_with('\n') {
current_string.truncate(current_string.len() - 1);
}
let style = make_superimposed_style(*current_style_pair);
coalesced.push((style, current_string));
}
coalesced
}
#[cfg(test)]
mod tests {
use lazy_static::lazy_static;
use super::*;
use ansi_term::{self, Color};
use syntect::highlighting::Color as SyntectColor;
use syntect::highlighting::FontStyle as SyntectFontStyle;
use syntect::highlighting::Style as SyntectStyle;
use crate::style::{DecorationStyle, Style};
lazy_static! {
static ref SYNTAX_STYLE: SyntectStyle = SyntectStyle {
foreground: SyntectColor::BLACK,
background: SyntectColor::BLACK,
font_style: SyntectFontStyle::BOLD,
};
}
lazy_static! {
static ref SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
ansi_term_style: ansi_term::Style {
foreground: Some(Color::White),
background: Some(Color::White),
is_underline: true,
..ansi_term::Style::new()
},
is_emph: false,
is_omitted: false,
is_raw: false,
is_syntax_highlighted: true,
decoration_style: DecorationStyle::NoDecoration,
};
}
lazy_static! {
static ref NON_SYNTAX_HIGHLIGHTED_STYLE: Style = Style {
ansi_term_style: ansi_term::Style {
foreground: Some(Color::White),
background: Some(Color::White),
is_underline: true,
..ansi_term::Style::new()
},
is_emph: false,
is_omitted: false,
is_raw: false,
is_syntax_highlighted: false,
decoration_style: DecorationStyle::NoDecoration,
};
}
lazy_static! {
static ref SUPERIMPOSED_STYLE: Style = Style {
ansi_term_style: ansi_term::Style {
foreground: to_ansi_color(SyntectColor::BLACK, true),
background: Some(Color::White),
is_underline: true,
..ansi_term::Style::new()
},
is_emph: false,
is_omitted: false,
is_raw: false,
is_syntax_highlighted: true,
decoration_style: DecorationStyle::NoDecoration,
};
}
#[test]
fn test_superimpose_style_sections_1() {
let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
let sections_2 = vec![(*SYNTAX_HIGHLIGHTED_STYLE, "ab")];
let superimposed = vec![(*SUPERIMPOSED_STYLE, "ab".to_string())];
assert_eq!(
superimpose_style_sections(§ions_1, §ions_2, true, SyntectStyle::default()),
superimposed
);
}
#[test]
fn test_superimpose_style_sections_2() {
let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
let sections_2 = vec![
(*SYNTAX_HIGHLIGHTED_STYLE, "a"),
(*SYNTAX_HIGHLIGHTED_STYLE, "b"),
];
let superimposed = vec![(*SUPERIMPOSED_STYLE, String::from("ab"))];
assert_eq!(
superimpose_style_sections(§ions_1, §ions_2, true, SyntectStyle::default()),
superimposed
);
}
#[test]
fn test_superimpose_style_sections_3() {
let sections_1 = vec![(*SYNTAX_STYLE, "ab")];
let sections_2 = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab")];
let superimposed = vec![(*NON_SYNTAX_HIGHLIGHTED_STYLE, "ab".to_string())];
assert_eq!(
superimpose_style_sections(§ions_1, §ions_2, true, SyntectStyle::default()),
superimposed
);
}
#[test]
fn test_explode() {
let arbitrary = 0;
assert_eq!(
explode(&[(arbitrary, "ab")]),
vec![(arbitrary, 'a'), (arbitrary, 'b')]
)
}
#[test]
fn test_superimpose() {
let x = (*SYNTAX_STYLE, 'a');
let pairs = vec![(&x, (*SYNTAX_HIGHLIGHTED_STYLE, 'a'))];
assert_eq!(
superimpose(pairs),
vec![((*SYNTAX_STYLE, *SYNTAX_HIGHLIGHTED_STYLE), 'a')]
);
}
}
}