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, Config}; use crate::delta::State; use crate::edits; use crate::features::hyperlinks; use crate::features::line_numbers; use crate::features::side_by_side::ansifill; use crate::features::side_by_side::{self, available_line_width, LineSegments, PanelSide}; use crate::minusplus::*; use crate::paint::superimpose_style_sections::superimpose_style_sections; use crate::style::Style; use crate::wrapping::wrap_minusplus_block; 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>, pub config: &'p config::Config, pub output_buffer: String, // If config.line_numbers is true, then the following is always Some(). // In side-by-side mode it is always Some (but possibly an empty one), even // if config.line_numbers is false. See `UseFullPanelWidth` as well. pub line_numbers_data: Option>, } // How the background of a line is filled up to the end #[derive(Debug, PartialEq, Clone, Copy)] pub enum BgFillMethod { // Fill the background with ANSI spaces if possible, // but might fallback to Spaces (e.g. in the left side-by-side panel), // also see `UseFullPanelWidth` TryAnsiSequence, Spaces, } // If the background of a line extends to the end, and if configured to do so, how. #[derive(Debug, PartialEq, 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(LineSegments<'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 { // If line numbers are disabled in side-by-side then the data is still used // for width calculaction and to pad odd width to even, see `UseFullPanelWidth` // for details. 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, } } 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)) }; } /// Remove initial -/+ character, expand tabs as spaces, and 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 remove it now so that // it is not present during syntax highlighting or wrapping. If --keep-plus-minus-markers is // in effect this character is re-inserted in Painter::paint_line. 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<'a, 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), 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, ); 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 { let syntax_left_right = MinusPlus::new( minus_line_syntax_style_sections, plus_line_syntax_style_sections, ); let diff_left_right = MinusPlus::new( minus_line_diff_style_sections, plus_line_diff_style_sections, ); let states_left_right = MinusPlus::new( self.minus_lines .iter() .map(|(_, state)| state.clone()) .collect(), self.plus_lines .iter() .map(|(_, state)| state.clone()) .collect(), ); let line_numbers_data = self.line_numbers_data.as_mut().unwrap_or_else(|| { delta_unreachable("side-by-side requires Some(line_numbers_data)") }); let bg_fill_left_right = MinusPlus::new( // Using an ANSI sequence to fill the left panel would not work. BgShouldFill::With(BgFillMethod::Spaces), // Use what is configured for the right side. BgShouldFill::With(self.config.line_fill_method), ); // Only set `should_wrap` to true if wrapping is wanted and lines which are // too long are found. // If so, remember the calculated line width and which of the lines are too // long for later re-use. let (should_wrap, line_width, long_lines) = { if self.config.wrap_config.max_lines == 1 { (false, MinusPlus::default(), MinusPlus::default()) } else { let line_width = available_line_width(self.config, line_numbers_data); let lines = MinusPlus::new(&self.minus_lines, &self.plus_lines); let (should_wrap, long_lines) = side_by_side::has_long_lines(&lines, &line_width); (should_wrap, line_width, long_lines) } }; let (line_alignment, line_states, syntax_left_right, diff_left_right) = if should_wrap { // Calculated for syntect::highlighting::style::Style and delta::Style wrap_minusplus_block( self.config, syntax_left_right, diff_left_right, &line_alignment, &line_width, &long_lines, ) } else { ( line_alignment, states_left_right, syntax_left_right, diff_left_right, ) }; side_by_side::paint_minus_and_plus_lines_side_by_side( syntax_left_right, diff_left_right, line_states, line_alignment, &mut self.output_buffer, self.config, &mut Some(line_numbers_data), bg_fill_left_right, ); } else { // Unified mode: 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 self.line_numbers_data.as_mut(), if self.config.keep_plus_minus_markers { Some(self.config.minus_style.paint("-")) } else { None }, Some(self.config.minus_empty_line_marker_style), BgShouldFill::default(), ); } 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 self.line_numbers_data.as_mut(), if self.config.keep_plus_minus_markers { Some(self.config.plus_style.paint("+")) } else { None }, Some(self.config.plus_empty_line_marker_style), BgShouldFill::default(), ); } } 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() { // A zero line here still contains the " " prefix, so use it. 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, self.highlighter.as_mut(), 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 { // `lines[0].0` so the line has the '\n' already added (as in the +- case) side_by_side::paint_zero_lines_side_by_side( &lines[0].0, syntax_style_sections, vec![diff_style_sections], &mut self.output_buffer, self.config, &mut self.line_numbers_data.as_mut(), painted_prefix, BgShouldFill::With(BgFillMethod::Spaces), ); } else { Painter::paint_lines( syntax_style_sections, vec![diff_style_sections], [state].iter(), &mut self.output_buffer, self.config, &mut self.line_numbers_data.as_mut(), painted_prefix, None, BgShouldFill::With(BgFillMethod::Spaces), ); } } /// Superimpose background styles and foreground syntax /// highlighting styles, and write colored lines to output buffer. #[allow(clippy::too_many_arguments)] pub fn paint_lines<'a>( 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