use crate::diff::{Hunk, Line, RichHunk, RichHunks};
use anyhow::Result;
use console::{Color, Style, Term};
use log::{debug, info};
use logging_timer::time;
use serde::{Deserialize, Serialize};
use std::{
cmp::max,
io::{BufWriter, Write},
};
use strum_macros::EnumString;
const TITLE_SEPARATOR: &str = "=";
const HUNK_TITLE_SEPARATOR: &str = "-";
#[derive(Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
#[serde(remote = "Color", rename_all = "snake_case")]
enum ColorDef {
Color256(u8),
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl From<ColorDef> for Color {
fn from(c: ColorDef) -> Self {
match c {
ColorDef::Black => Color::Black,
ColorDef::White => Color::White,
ColorDef::Red => Color::Red,
ColorDef::Green => Color::Green,
ColorDef::Yellow => Color::Yellow,
ColorDef::Blue => Color::Blue,
ColorDef::Magenta => Color::Magenta,
ColorDef::Cyan => Color::Cyan,
ColorDef::Color256(c) => Color::Color256(c),
}
}
}
impl Default for ColorDef {
fn default() -> Self {
ColorDef::Black
}
}
#[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
#[serde(rename_all = "kebab-case")]
pub struct Config {
#[serde(with = "opt_color_def", default = "default_option")]
pub highlight: Option<Color>,
#[serde(with = "ColorDef")]
pub regular_foreground: Color,
#[serde(with = "ColorDef")]
pub emphasized_foreground: Color,
pub bold: bool,
pub underline: bool,
pub prefix: String,
}
fn default_option<T>() -> Option<T> {
None
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct RegularStyle(Style);
#[derive(Clone, Debug, PartialEq, Eq)]
struct EmphasizedStyle(Style);
impl From<&Config> for RegularStyle {
fn from(fmt: &Config) -> Self {
let mut style = Style::default();
style = style.fg(fmt.regular_foreground);
RegularStyle(style)
}
}
impl From<&Config> for EmphasizedStyle {
fn from(fmt: &Config) -> Self {
let mut style = Style::default();
style = style.fg(fmt.emphasized_foreground);
if fmt.bold {
style = style.bold();
}
if fmt.underline {
style = style.underlined();
}
if let Some(color) = fmt.highlight {
style = style.bg(color);
}
EmphasizedStyle(style)
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
#[serde(default, rename_all = "kebab-case")]
pub struct DiffWriter {
pub addition: Config,
pub deletion: Config,
}
impl Default for DiffWriter {
fn default() -> Self {
DiffWriter {
addition: Config {
regular_foreground: Color::Green,
emphasized_foreground: Color::Green,
highlight: None,
bold: true,
underline: false,
prefix: "+ ".into(),
},
deletion: Config {
regular_foreground: Color::Red,
emphasized_foreground: Color::Red,
highlight: None,
bold: true,
underline: false,
prefix: "- ".into(),
},
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DisplayParameters<'a> {
pub hunks: RichHunks<'a>,
pub old: DocumentDiffData<'a>,
pub new: DocumentDiffData<'a>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentDiffData<'a> {
pub filename: &'a str,
pub text: &'a str,
}
struct FormattingDirectives<'a> {
pub regular: RegularStyle,
pub emphasis: EmphasizedStyle,
pub prefix: &'a dyn AsRef<str>,
}
impl<'a> From<&'a Config> for FormattingDirectives<'a> {
fn from(fmt_opts: &'a Config) -> Self {
Self {
regular: fmt_opts.into(),
emphasis: fmt_opts.into(),
prefix: &fmt_opts.prefix,
}
}
}
impl DiffWriter {
#[time("info", "formatting::{}")]
pub fn print(&self, term: &mut BufWriter<Term>, params: &DisplayParameters) -> Result<()> {
let DisplayParameters { hunks, old, new } = ¶ms;
let old_fmt = FormattingDirectives::from(&self.deletion);
let new_fmt = FormattingDirectives::from(&self.addition);
let old_lines: Vec<_> = old.text.lines().collect();
let new_lines: Vec<_> = new.text.lines().collect();
self.print_title(term, old.filename, new.filename, &old_fmt, &new_fmt)?;
for hunk_wrapper in &hunks.0 {
match hunk_wrapper {
RichHunk::Old(hunk) => {
self.print_hunk(term, &old_lines, hunk, &old_fmt)?;
}
RichHunk::New(hunk) => {
self.print_hunk(term, &new_lines, hunk, &new_fmt)?;
}
}
}
Ok(())
}
fn print_title(
&self,
term: &mut BufWriter<Term>,
old_fname: &str,
new_fname: &str,
old_fmt: &FormattingDirectives,
new_fmt: &FormattingDirectives,
) -> std::io::Result<()> {
#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, strum_macros::Display)]
#[strum(serialize_all = "snake_case")]
enum TitleStack {
Vertical,
Horizontal,
}
let divider = " -> ";
let title_len = format!("{}{}{}", old_fname, divider, new_fname).len();
let stack_style = if let Some((_, term_width)) = term.get_ref().size_checked() {
info!("Detected terminal width: {} columns", term_width);
if title_len <= term_width as usize {
TitleStack::Horizontal
} else {
TitleStack::Vertical
}
} else {
TitleStack::Vertical
};
info!("Using stack style {} for title", stack_style);
let (styled_title_str, title_sep) = match stack_style {
TitleStack::Horizontal => {
let title_len = old_fname.len() + divider.len() + new_fname.len();
let styled_title_str = format!(
"{}{}{}",
old_fmt.regular.0.apply_to(old_fname),
divider,
new_fmt.regular.0.apply_to(new_fname)
);
let title_sep = TITLE_SEPARATOR.repeat(title_len);
(styled_title_str, title_sep)
}
TitleStack::Vertical => {
let title_len = max(old_fname.len(), new_fname.len());
let styled_title_str = format!(
"{}\n{}",
old_fmt.regular.0.apply_to(old_fname),
new_fmt.regular.0.apply_to(new_fname)
);
let title_sep = TITLE_SEPARATOR.repeat(title_len);
(styled_title_str, title_sep)
}
};
writeln!(term, "{}", styled_title_str)?;
writeln!(term, "{}", title_sep)?;
Ok(())
}
fn print_hunk_title(
&self,
term: &mut dyn Write,
hunk: &Hunk,
fmt: &FormattingDirectives,
) -> Result<()> {
let first_line = hunk.first_line().unwrap();
let last_line = hunk.last_line().unwrap();
let title_str = if last_line - first_line == 0 {
format!("\n{}:", first_line)
} else {
format!("\n{} - {}:", first_line, last_line)
};
debug!("Title string has length of {}", title_str.len());
let separator = HUNK_TITLE_SEPARATOR.repeat(title_str.trim().len());
writeln!(term, "{}", fmt.regular.0.apply_to(title_str))?;
writeln!(term, "{}", separator)?;
Ok(())
}
fn print_hunk(
&self,
term: &mut dyn Write,
lines: &[&str],
hunk: &Hunk,
fmt: &FormattingDirectives,
) -> Result<()> {
debug!(
"Printing hunk (lines {} - {})",
hunk.first_line().unwrap(),
hunk.last_line().unwrap()
);
self.print_hunk_title(term, hunk, fmt)?;
for line in &hunk.0 {
let text = lines[line.line_index];
debug!("Printing line {}", line.line_index);
self.print_line(term, text, line, fmt)?;
debug!("End line {}", line.line_index);
}
debug!(
"End hunk (lines {} - {})",
hunk.first_line().unwrap(),
hunk.last_line().unwrap()
);
Ok(())
}
fn print_line(
&self,
term: &mut dyn Write,
text: &str,
line: &Line,
fmt: &FormattingDirectives,
) -> Result<()> {
let regular = &fmt.regular.0;
let emphasis = &fmt.emphasis.0;
write!(term, "{}", regular.apply_to(fmt.prefix.as_ref()))?;
let mut printed_chars = 0;
for entry in &line.entries {
let emphasis_range = entry.start_position().column..entry.end_position().column;
let regular_range = printed_chars..emphasis_range.start;
let regular_text: String = text[regular_range].into();
write!(term, "{}", regular.apply_to(®ular_text))?;
printed_chars = emphasis_range.end;
let emphasized_text: String = text[emphasis_range].into();
write!(term, "{}", emphasis.apply_to(emphasized_text))?;
}
let remaining_range = printed_chars..text.len();
let remaining_text: String = text[remaining_range].into();
writeln!(term, "{}", regular.apply_to(remaining_text))?;
Ok(())
}
}
#[derive(Debug, PartialEq, Eq, EnumString, Serialize, Deserialize)]
#[strum(serialize_all = "snake_case")]
pub enum Emphasis {
None,
Bold,
Underline,
Highlight(HighlightColors),
}
impl Default for Emphasis {
fn default() -> Self {
Emphasis::Bold
}
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HighlightColors {
#[serde(with = "ColorDef")]
pub addition: Color,
#[serde(with = "ColorDef")]
pub deletion: Color,
}
impl Default for HighlightColors {
fn default() -> Self {
HighlightColors {
addition: Color::Color256(0),
deletion: Color::Color256(0),
}
}
}
mod opt_color_def {
use super::{Color, ColorDef};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn serialize<S>(value: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
#[derive(Serialize)]
struct Helper<'a>(#[serde(with = "ColorDef")] &'a Color);
value.as_ref().map(Helper).serialize(serializer)
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Color>, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Helper(#[serde(with = "ColorDef")] Color);
let helper = Option::deserialize(deserializer)?;
Ok(helper.map(|Helper(external)| external))
}
}