use std::io::Write; use itertools::Itertools; use syntect::easy::HighlightLines; use syntect::highlighting::Style as SyntectStyle; use syntect::parsing::{SyntaxReference, SyntaxSet}; use unicode_segmentation::UnicodeSegmentation; use crate::ansi; use crate::config::{self, delta_unreachable}; use crate::delta::State; use crate::edits; use crate::features::line_numbers; use crate::features::side_by_side; use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; pub struct Painter<'a> { pub minus_lines: Vec<(String, State)>, pub plus_lines: Vec<(String, State)>, pub writer: &'a mut dyn Write, pub syntax: &'a SyntaxReference, pub highlighter: HighlightLines<'a>, pub config: &'a config::Config, pub output_buffer: String, pub line_numbers_data: line_numbers::LineNumbersData<'a>, } impl<'a> Painter<'a> { pub fn new(writer: &'a mut dyn Write, config: &'a config::Config) -> Self { let default_syntax = Self::get_syntax(&config.syntax_set, None); // TODO: Avoid doing this. let dummy_highlighter = HighlightLines::new(default_syntax, &config.syntax_dummy_theme); let line_numbers_data = if config.line_numbers { line_numbers::LineNumbersData::from_format_strings( &config.line_numbers_left_format, &config.line_numbers_right_format, ) } else { line_numbers::LineNumbersData::default() }; Self { minus_lines: Vec::new(), plus_lines: Vec::new(), output_buffer: String::new(), syntax: default_syntax, highlighter: dummy_highlighter, writer, config, line_numbers_data, } } pub fn set_syntax(&mut self, extension: Option<&str>) { self.syntax = Painter::get_syntax(&self.config.syntax_set, extension); } fn get_syntax(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 = HighlightLines::new(self.syntax, &syntax_theme) }; } /// Replace initial -/+ character with ' ', expand tabs as spaces, and optionally terminate with /// newline. // Terminating with newline character is necessary for many of the sublime syntax definitions to // highlight correctly. // See https://docs.rs/syntect/3.2.0/syntect/parsing/struct.SyntaxSetBuilder.html#method.add_from_folder pub fn prepare(&self, line: &str) -> String { if !line.is_empty() { let mut line = line.graphemes(true); // The first column contains a -/+/space character, added by git. We substitute it for // a space now, so that it is not present during syntax highlighting. When emitting the // line in Painter::paint_line, we drop the space (unless --keep-plus-minus-markers is // in effect in which case we replace it with the appropriate marker). // TODO: Things should, but do not, work if this leading space is omitted at this stage. // See comment in align::Alignment::new. line.next(); format!(" {}\n", self.expand_tabs(line)) } else { "\n".to_string() } } /// 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 }, ) } /// Expand tabs as spaces. /// tab_width = 0 is documented to mean do not replace tabs. pub fn expand_tabs<'b, I>(&self, line: I) -> String where I: Iterator, { if self.config.tab_width > 0 { let tab_replacement = " ".repeat(self.config.tab_width); line.map(|s| if s == "\t" { &tab_replacement } else { s }) .collect::() } else { line.collect::() } } 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), &mut self.highlighter, self.config, ); let plus_line_syntax_style_sections = Self::get_syntax_style_sections_for_lines( &self.plus_lines, &State::HunkPlus(None), &mut self.highlighter, self.config, ); let (minus_line_diff_style_sections, plus_line_diff_style_sections, line_alignment) = Self::get_diff_style_sections(&self.minus_lines, &self.plus_lines, self.config); if self.config.side_by_side { side_by_side::paint_minus_and_plus_lines_side_by_side( minus_line_syntax_style_sections, minus_line_diff_style_sections, self.minus_lines.iter().map(|(_, state)| state).collect(), plus_line_syntax_style_sections, plus_line_diff_style_sections, self.plus_lines.iter().map(|(_, state)| state).collect(), line_alignment, &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), None, ); } else { if !self.minus_lines.is_empty() { Painter::paint_lines( minus_line_syntax_style_sections, minus_line_diff_style_sections, self.minus_lines.iter().map(|(_, state)| state), &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), if self.config.keep_plus_minus_markers { Some(self.config.minus_style.paint("-")) } else { None }, Some(self.config.minus_empty_line_marker_style), None, ); } if !self.plus_lines.is_empty() { Painter::paint_lines( plus_line_syntax_style_sections, plus_line_diff_style_sections, self.plus_lines.iter().map(|(_, state)| state), &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), if self.config.keep_plus_minus_markers { Some(self.config.plus_style.paint("+")) } else { None }, Some(self.config.plus_empty_line_marker_style), None, ); } } self.minus_lines.clear(); self.plus_lines.clear(); } pub fn paint_zero_line(&mut self, line: &str) { let state = State::HunkZero; let painted_prefix = if self.config.keep_plus_minus_markers && !line.is_empty() { Some(self.config.zero_style.paint(&line[..1])) } else { None }; let lines = vec![(self.prepare(line), state.clone())]; let syntax_style_sections = Painter::get_syntax_style_sections_for_lines( &lines, &state, &mut self.highlighter, &self.config, ); let diff_style_sections = vec![(self.config.zero_style, lines[0].0.as_str())]; // TODO: compute style from state if self.config.side_by_side { side_by_side::paint_zero_lines_side_by_side( syntax_style_sections, vec![diff_style_sections], &State::HunkZero, &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), painted_prefix, None, ); } else { Painter::paint_lines( syntax_style_sections, vec![diff_style_sections], [state].iter(), &mut self.output_buffer, self.config, &mut Some(&mut self.line_numbers_data), painted_prefix, None, None, ); } } /// Superimpose background styles and foreground syntax /// highlighting styles, and write colored lines to output buffer. #[allow(clippy::too_many_arguments)] pub fn paint_lines( syntax_style_sections: Vec>, diff_style_sections: Vec>, states: impl Iterator, output_buffer: &mut String, config: &config::Config, line_numbers_data: &mut Option<&mut line_numbers::LineNumbersData>, painted_prefix: Option, empty_line_style: Option