use std::borrow::Cow;
use std::{path::Path, time::Duration};
use similar::{Algorithm, ChangeTag, TextDiff};
use crate::content::yaml;
use crate::snapshot::{MetaData, Snapshot};
use crate::utils::{format_rust_expression, style, term_width};
pub struct SnapshotPrinter<'a> {
workspace_root: &'a Path,
old_snapshot: Option<&'a Snapshot>,
new_snapshot: &'a Snapshot,
old_snapshot_hint: &'a str,
new_snapshot_hint: &'a str,
show_info: bool,
show_diff: bool,
title: Option<&'a str>,
line: Option<u32>,
snapshot_file: Option<&'a Path>,
}
impl<'a> SnapshotPrinter<'a> {
pub fn new(
workspace_root: &'a Path,
old_snapshot: Option<&'a Snapshot>,
new_snapshot: &'a Snapshot,
) -> SnapshotPrinter<'a> {
SnapshotPrinter {
workspace_root,
old_snapshot,
new_snapshot,
old_snapshot_hint: "old snapshot",
new_snapshot_hint: "new results",
show_info: false,
show_diff: false,
title: None,
line: None,
snapshot_file: None,
}
}
pub fn set_snapshot_hints(&mut self, old: &'a str, new: &'a str) {
self.old_snapshot_hint = old;
self.new_snapshot_hint = new;
}
pub fn set_show_info(&mut self, yes: bool) {
self.show_info = yes;
}
pub fn set_show_diff(&mut self, yes: bool) {
self.show_diff = yes;
}
pub fn set_title(&mut self, title: Option<&'a str>) {
self.title = title;
}
pub fn set_line(&mut self, line: Option<u32>) {
self.line = line;
}
pub fn set_snapshot_file(&mut self, file: Option<&'a Path>) {
self.snapshot_file = file;
}
pub fn print(&self) {
if let Some(title) = self.title {
let width = term_width();
println!(
"{title:━^width$}",
title = style(format!(" {} ", title)).bold(),
width = width
);
}
self.print_snapshot_diff();
}
fn print_snapshot_diff(&self) {
self.print_snapshot_summary();
if self.show_diff {
self.print_changeset();
} else {
self.print_snapshot();
}
}
fn print_snapshot_summary(&self) {
print_snapshot_summary(
self.workspace_root,
self.new_snapshot,
self.snapshot_file,
self.line,
);
}
fn print_info(&self) {
print_info(self.new_snapshot.metadata());
}
fn print_snapshot(&self) {
print_line(term_width());
let new_contents = self.new_snapshot.contents_str();
let width = term_width();
if self.show_info {
self.print_info();
}
println!("Snapshot Contents:");
println!("──────┬{:─^1$}", "", width.saturating_sub(7));
for (idx, line) in new_contents.lines().enumerate() {
println!("{:>5} │ {}", style(idx + 1).cyan().dim().bold(), line);
}
println!("──────┴{:─^1$}", "", width.saturating_sub(7));
}
fn print_changeset(&self) {
let old = self.old_snapshot.as_ref().map_or("", |x| x.contents_str());
let new = self.new_snapshot.contents_str();
let newlines_matter = newlines_matter(old, new);
let width = term_width();
let diff = TextDiff::configure()
.algorithm(Algorithm::Patience)
.timeout(Duration::from_millis(500))
.diff_lines(old, new);
print_line(width);
if self.show_info {
self.print_info();
}
if !old.is_empty() {
println!(
"{}",
style(format_args!("-{}", self.old_snapshot_hint)).red()
);
}
println!(
"{}",
style(format_args!("+{}", self.new_snapshot_hint)).green()
);
println!("────────────┬{:─^1$}", "", width.saturating_sub(13));
let mut has_changes = false;
for (idx, group) in diff.grouped_ops(4).iter().enumerate() {
if idx > 0 {
println!("┈┈┈┈┈┈┈┈┈┈┈┈┼{:┈^1$}", "", width.saturating_sub(13));
}
for op in group {
for change in diff.iter_inline_changes(op) {
match change.tag() {
ChangeTag::Insert => {
has_changes = true;
print!(
"{:>5} {:>5} │{}",
"",
style(change.new_index().unwrap()).cyan().dim().bold(),
style("+").green(),
);
for &(emphasized, change) in change.values() {
let change = render_invisible(change, newlines_matter);
if emphasized {
print!("{}", style(change).green().underlined());
} else {
print!("{}", style(change).green());
}
}
}
ChangeTag::Delete => {
has_changes = true;
print!(
"{:>5} {:>5} │{}",
style(change.old_index().unwrap()).cyan().dim(),
"",
style("-").red(),
);
for &(emphasized, change) in change.values() {
let change = render_invisible(change, newlines_matter);
if emphasized {
print!("{}", style(change).red().underlined());
} else {
print!("{}", style(change).red());
}
}
}
ChangeTag::Equal => {
print!(
"{:>5} {:>5} │ ",
style(change.old_index().unwrap()).cyan().dim(),
style(change.new_index().unwrap()).cyan().dim().bold(),
);
for &(_, change) in change.values() {
let change = render_invisible(change, newlines_matter);
print!("{}", style(change).dim());
}
}
}
if change.missing_newline() {
println!();
}
}
}
}
if !has_changes {
println!(
"{:>5} {:>5} │{}",
"",
style("-").dim(),
style(" snapshots are matching").cyan(),
);
}
println!("────────────┴{:─^1$}", "", width.saturating_sub(13));
}
}
pub fn print_snapshot_summary(
workspace_root: &Path,
snapshot: &Snapshot,
snapshot_file: Option<&Path>,
mut line: Option<u32>,
) {
if line.is_none() {
line = snapshot.metadata().assertion_line();
}
if let Some(snapshot_file) = snapshot_file {
let snapshot_file = workspace_root
.join(snapshot_file)
.strip_prefix(workspace_root)
.ok()
.map(|x| x.to_path_buf())
.unwrap_or_else(|| snapshot_file.to_path_buf());
println!(
"Snapshot file: {}",
style(snapshot_file.display()).cyan().underlined()
);
}
if let Some(name) = snapshot.snapshot_name() {
println!("Snapshot: {}", style(name).yellow());
} else {
println!("Snapshot: {}", style("<inline>").dim());
}
if let Some(ref value) = snapshot.metadata().get_relative_source(workspace_root) {
println!(
"Source: {}{}",
style(value.display()).cyan(),
if let Some(line) = line {
format!(":{}", style(line).bold())
} else {
"".to_string()
}
);
}
if let Some(ref value) = snapshot.metadata().input_file() {
println!("Input file: {}", style(value).cyan());
}
}
fn print_line(width: usize) {
println!("{:─^1$}", "", width);
}
fn trailing_newline(s: &str) -> &str {
if s.ends_with("\r\n") {
"\r\n"
} else if s.ends_with('\r') {
"\r"
} else if s.ends_with('\n') {
"\n"
} else {
""
}
}
fn detect_newlines(s: &str) -> (bool, bool, bool) {
let mut last_char = None;
let mut detected_crlf = false;
let mut detected_cr = false;
let mut detected_lf = false;
for c in s.chars() {
if c == '\n' {
if last_char.take() == Some('\r') {
detected_crlf = true;
} else {
detected_lf = true;
}
}
if last_char == Some('\r') {
detected_cr = true;
}
last_char = Some(c);
}
if last_char == Some('\r') {
detected_cr = true;
}
(detected_cr, detected_crlf, detected_lf)
}
fn newlines_matter(left: &str, right: &str) -> bool {
if trailing_newline(left) != trailing_newline(right) {
return true;
}
let (cr1, crlf1, lf1) = detect_newlines(left);
let (cr2, crlf2, lf2) = detect_newlines(right);
!matches!(
(cr1 || cr2, crlf1 || crlf2, lf1 || lf2),
(false, false, false) | (true, false, false) | (false, true, false) | (false, false, true)
)
}
fn render_invisible(s: &str, newlines_matter: bool) -> Cow<'_, str> {
if newlines_matter || s.find(&['\x1b', '\x07', '\x08', '\x7f'][..]).is_some() {
Cow::Owned(
s.replace('\r', "␍\r")
.replace('\n', "␊\n")
.replace("␍\r␊\n", "␍␊\r\n")
.replace('\x07', "␇")
.replace('\x08', "␈")
.replace('\x1b', "␛")
.replace('\x7f', "␡"),
)
} else {
Cow::Borrowed(s)
}
}
fn print_info(metadata: &MetaData) {
let width = term_width();
if let Some(expr) = metadata.expression() {
println!("Expression: {}", style(format_rust_expression(expr)));
print_line(width);
}
if let Some(descr) = metadata.description() {
println!("{}", descr);
print_line(width);
}
if let Some(info) = metadata.private_info() {
let out = yaml::to_string(info);
println!("{}", out.trim().strip_prefix("---").unwrap().trim_start());
print_line(width);
}
}
#[test]
fn test_invisible() {
assert_eq!(
render_invisible("\r\n\x1b\r\x07\x08\x7f\n", true),
"␍␊\r\n␛␍\r␇␈␡␊\n"
);
}