#[cfg(feature = "text")]
use std::{fmt, io};
use crate::iter::AllChangesIter;
use crate::text::{DiffableStr, TextDiff};
use crate::types::{Algorithm, DiffOp};
struct MissingNewlineHint(bool);
impl fmt::Display for MissingNewlineHint {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0 {
write!(f, "\n\\ No newline at end of file")?;
}
Ok(())
}
}
#[derive(Copy, Clone, Debug)]
struct UnifiedDiffHunkRange(usize, usize);
impl UnifiedDiffHunkRange {
fn start(&self) -> usize {
self.0
}
fn end(&self) -> usize {
self.1
}
}
impl fmt::Display for UnifiedDiffHunkRange {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut beginning = self.start() + 1;
let len = self.end().saturating_sub(self.start());
if len == 1 {
write!(f, "{}", beginning)
} else {
if len == 0 {
beginning -= 1;
}
write!(f, "{},{}", beginning, len)
}
}
}
pub struct UnifiedHunkHeader {
old_range: UnifiedDiffHunkRange,
new_range: UnifiedDiffHunkRange,
}
impl UnifiedHunkHeader {
pub fn new(ops: &[DiffOp]) -> UnifiedHunkHeader {
let first = ops[0];
let last = ops[ops.len() - 1];
let old_start = first.old_range().start;
let new_start = first.new_range().start;
let old_end = last.old_range().end;
let new_end = last.new_range().end;
UnifiedHunkHeader {
old_range: UnifiedDiffHunkRange(old_start, old_end),
new_range: UnifiedDiffHunkRange(new_start, new_end),
}
}
}
impl fmt::Display for UnifiedHunkHeader {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "@@ -{} +{} @@", &self.old_range, &self.new_range)
}
}
pub struct UnifiedDiff<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> {
diff: &'diff TextDiff<'old, 'new, 'bufs, T>,
context_radius: usize,
missing_newline_hint: bool,
header: Option<(String, String)>,
}
impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> UnifiedDiff<'diff, 'old, 'new, 'bufs, T> {
pub fn from_text_diff(diff: &'diff TextDiff<'old, 'new, 'bufs, T>) -> Self {
UnifiedDiff {
diff,
context_radius: 3,
missing_newline_hint: true,
header: None,
}
}
pub fn context_radius(&mut self, n: usize) -> &mut Self {
self.context_radius = n;
self
}
pub fn header(&mut self, a: &str, b: &str) -> &mut Self {
self.header = Some((a.to_string(), b.to_string()));
self
}
pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self {
self.missing_newline_hint = yes;
self
}
pub fn iter_hunks(&self) -> impl Iterator<Item = UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>> {
let diff = self.diff;
let missing_newline_hint = self.missing_newline_hint;
self.diff
.grouped_ops(self.context_radius)
.into_iter()
.filter(|ops| !ops.is_empty())
.map(move |ops| UnifiedDiffHunk::new(ops, diff, missing_newline_hint))
}
pub fn to_writer<W: io::Write>(&self, mut w: W) -> Result<(), io::Error>
where
'diff: 'old + 'new + 'bufs,
{
let mut header = self.header.as_ref();
for hunk in self.iter_hunks() {
if let Some((old_file, new_file)) = header.take() {
writeln!(w, "--- {}", old_file)?;
writeln!(w, "+++ {}", new_file)?;
}
write!(w, "{}", hunk)?;
}
Ok(())
}
fn header_opt(&mut self, header: Option<(&str, &str)>) -> &mut Self {
if let Some((a, b)) = header {
self.header(a, b);
}
self
}
}
pub struct UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> {
diff: &'diff TextDiff<'old, 'new, 'bufs, T>,
ops: Vec<DiffOp>,
missing_newline_hint: bool,
}
impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized>
UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>
{
pub fn new(
ops: Vec<DiffOp>,
diff: &'diff TextDiff<'old, 'new, 'bufs, T>,
missing_newline_hint: bool,
) -> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> {
UnifiedDiffHunk {
diff,
ops,
missing_newline_hint,
}
}
pub fn header(&self) -> UnifiedHunkHeader {
UnifiedHunkHeader::new(&self.ops)
}
pub fn ops(&self) -> &[DiffOp] {
&self.ops
}
pub fn missing_newline_hint(&self) -> bool {
self.missing_newline_hint
}
pub fn iter_changes<'x, 'slf>(&'slf self) -> AllChangesIter<'slf, 'x, T>
where
'x: 'slf + 'old + 'new,
'old: 'x,
'new: 'x,
{
AllChangesIter::new(self.diff.old_slices(), self.diff.new_slices(), self.ops())
}
pub fn to_writer<W: io::Write>(&self, mut w: W) -> Result<(), io::Error>
where
'diff: 'old + 'new + 'bufs,
{
for (idx, change) in self.iter_changes().enumerate() {
if idx == 0 {
writeln!(w, "{}", self.header())?;
}
write!(w, "{}", change.tag())?;
w.write_all(change.value().as_bytes())?;
if !self.diff.newline_terminated() {
writeln!(w)?;
}
if self.diff.newline_terminated() && change.missing_newline() {
writeln!(w, "{}", MissingNewlineHint(self.missing_newline_hint))?;
}
}
Ok(())
}
}
impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display
for UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>
where
'diff: 'old + 'new + 'bufs,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for (idx, change) in self.iter_changes().enumerate() {
if idx == 0 {
writeln!(f, "{}", self.header())?;
}
write!(f, "{}{}", change.tag(), change.to_string_lossy())?;
if !self.diff.newline_terminated() {
writeln!(f)?;
}
if self.diff.newline_terminated() && change.missing_newline() {
writeln!(f, "{}", MissingNewlineHint(self.missing_newline_hint))?;
}
}
Ok(())
}
}
impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display
for UnifiedDiff<'diff, 'old, 'new, 'bufs, T>
where
'diff: 'old + 'new + 'bufs,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut header = self.header.as_ref();
for hunk in self.iter_hunks() {
if let Some((old_file, new_file)) = header.take() {
writeln!(f, "--- {}", old_file)?;
writeln!(f, "+++ {}", new_file)?;
}
write!(f, "{}", hunk)?;
}
Ok(())
}
}
pub fn unified_diff(
alg: Algorithm,
old: &str,
new: &str,
n: usize,
header: Option<(&str, &str)>,
) -> String {
TextDiff::configure()
.algorithm(alg)
.diff_lines(old, new)
.unified_diff()
.context_radius(n)
.header_opt(header)
.to_string()
}
#[test]
fn test_unified_diff() {
let diff = TextDiff::from_lines(
"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\ns\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\nO\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ",
"a\nb\nc\nd\ne\nf\ng\nh\ni\nj\nk\nl\nm\nn\no\np\nq\nr\nS\nt\nu\nv\nw\nx\ny\nz\nA\nB\nC\nD\nE\nF\nG\nH\nI\nJ\nK\nL\nM\nN\no\nP\nQ\nR\nS\nT\nU\nV\nW\nX\nY\nZ",
);
insta::assert_snapshot!(&diff.unified_diff().header("a.txt", "b.txt").to_string());
}
#[test]
fn test_empty_unified_diff() {
let diff = TextDiff::from_lines("abc", "abc");
assert_eq!(diff.unified_diff().header("a.txt", "b.txt").to_string(), "");
}
#[test]
fn test_unified_diff_newline_hint() {
let diff = TextDiff::from_lines("a\n", "b");
insta::assert_snapshot!(&diff.unified_diff().header("a.txt", "b.txt").to_string());
insta::assert_snapshot!(&diff
.unified_diff()
.missing_newline_hint(false)
.header("a.txt", "b.txt")
.to_string());
}