diff options
Diffstat (limited to 'vendor/similar/src/udiff.rs')
-rw-r--r-- | vendor/similar/src/udiff.rs | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/vendor/similar/src/udiff.rs b/vendor/similar/src/udiff.rs new file mode 100644 index 0000000..26701da --- /dev/null +++ b/vendor/similar/src/udiff.rs @@ -0,0 +1,359 @@ +//! This module provides unified diff functionality. +//! +//! It is available for as long as the `text` feature is enabled which +//! is enabled by default: +//! +//! ```rust +//! use similar::TextDiff; +//! # let old_text = ""; +//! # let new_text = ""; +//! let text_diff = TextDiff::from_lines(old_text, new_text); +//! print!("{}", text_diff +//! .unified_diff() +//! .context_radius(10) +//! .header("old_file", "new_file")); +//! ``` +//! +//! # Unicode vs Bytes +//! +//! The [`UnifiedDiff`] type supports both unicode and byte diffs for all +//! types compatible with [`DiffableStr`]. You can pick between the two +//! versions by using the [`Display`](std::fmt::Display) implementation or +//! [`UnifiedDiff`] or [`UnifiedDiff::to_writer`]. +//! +//! The former uses [`DiffableStr::to_string_lossy`], the latter uses +//! [`DiffableStr::as_bytes`] for each line. +#[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 { + // empty ranges begin at line just before the range + beginning -= 1; + } + write!(f, "{},{}", beginning, len) + } + } +} + +/// Unified diff hunk header formatter. +pub struct UnifiedHunkHeader { + old_range: UnifiedDiffHunkRange, + new_range: UnifiedDiffHunkRange, +} + +impl UnifiedHunkHeader { + /// Creates a hunk header from a (non empty) slice of diff ops. + 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) + } +} + +/// Unified diff formatter. +/// +/// ```rust +/// use similar::TextDiff; +/// # let old_text = ""; +/// # let new_text = ""; +/// let text_diff = TextDiff::from_lines(old_text, new_text); +/// print!("{}", text_diff +/// .unified_diff() +/// .context_radius(10) +/// .header("old_file", "new_file")); +/// ``` +/// +/// ## Unicode vs Bytes +/// +/// The [`UnifiedDiff`] type supports both unicode and byte diffs for all +/// types compatible with [`DiffableStr`]. You can pick between the two +/// versions by using [`UnifiedDiff.to_string`] or [`UnifiedDiff.to_writer`]. +/// The former uses [`DiffableStr::to_string_lossy`], the latter uses +/// [`DiffableStr::as_bytes`] for each line. +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> { + /// Creates a formatter from a text diff object. + pub fn from_text_diff(diff: &'diff TextDiff<'old, 'new, 'bufs, T>) -> Self { + UnifiedDiff { + diff, + context_radius: 3, + missing_newline_hint: true, + header: None, + } + } + + /// Changes the context radius. + /// + /// The context radius is the number of lines between changes that should + /// be emitted. This defaults to `3`. + pub fn context_radius(&mut self, n: usize) -> &mut Self { + self.context_radius = n; + self + } + + /// Sets a header to the diff. + /// + /// `a` and `b` are the file names that are added to the top of the unified + /// file format. The names are accepted verbatim which lets you encode + /// a timestamp into it when separated by a tab (`\t`). For more information, + /// see [the unified diff format specification](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/diff.html#tag_20_34_10_07). + pub fn header(&mut self, a: &str, b: &str) -> &mut Self { + self.header = Some((a.to_string(), b.to_string())); + self + } + + /// Controls the missing newline hint. + /// + /// By default a special `\ No newline at end of file` marker is added to + /// the output when a file is not terminated with a final newline. This can + /// be disabled with this flag. + pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self { + self.missing_newline_hint = yes; + self + } + + /// Iterates over all hunks as configured. + 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)) + } + + /// Write the unified diff as bytes to the output stream. + 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 + } +} + +/// Unified diff hunk formatter. +/// +/// The `Display` this renders out a single unified diff's hunk. +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> +{ + /// Creates a new hunk for some operations. + 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, + } + } + + /// Returns the header for the hunk. + pub fn header(&self) -> UnifiedHunkHeader { + UnifiedHunkHeader::new(&self.ops) + } + + /// Returns all operations in the hunk. + pub fn ops(&self) -> &[DiffOp] { + &self.ops + } + + /// Returns the value of the `missing_newline_hint` flag. + pub fn missing_newline_hint(&self) -> bool { + self.missing_newline_hint + } + + /// Iterates over all changes in a hunk. + 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()) + } + + /// Write the hunk as bytes to the output stream. + 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(()) + } +} + +/// Quick way to get a unified diff as string. +/// +/// `n` configures [`UnifiedDiff::context_radius`] and +/// `header` configures [`UnifiedDiff::header`] when not `None`. +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()); +} |