1 | //! This module provides unified diff functionality. |
2 | //! |
3 | //! It is available for as long as the `text` feature is enabled which |
4 | //! is enabled by default: |
5 | //! |
6 | //! ```rust |
7 | //! use similar::TextDiff; |
8 | //! # let old_text = "" ; |
9 | //! # let new_text = "" ; |
10 | //! let text_diff = TextDiff::from_lines(old_text, new_text); |
11 | //! print!("{}" , text_diff |
12 | //! .unified_diff() |
13 | //! .context_radius(10) |
14 | //! .header("old_file" , "new_file" )); |
15 | //! ``` |
16 | //! |
17 | //! # Unicode vs Bytes |
18 | //! |
19 | //! The [`UnifiedDiff`] type supports both unicode and byte diffs for all |
20 | //! types compatible with [`DiffableStr`]. You can pick between the two |
21 | //! versions by using the [`Display`](std::fmt::Display) implementation or |
22 | //! [`UnifiedDiff`] or [`UnifiedDiff::to_writer`]. |
23 | //! |
24 | //! The former uses [`DiffableStr::to_string_lossy`], the latter uses |
25 | //! [`DiffableStr::as_bytes`] for each line. |
26 | #[cfg (feature = "text" )] |
27 | use std::{fmt, io}; |
28 | |
29 | use crate::iter::AllChangesIter; |
30 | use crate::text::{DiffableStr, TextDiff}; |
31 | use crate::types::{Algorithm, DiffOp}; |
32 | |
33 | struct MissingNewlineHint(bool); |
34 | |
35 | impl fmt::Display for MissingNewlineHint { |
36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
37 | if self.0 { |
38 | write!(f, " \n\\ No newline at end of file" )?; |
39 | } |
40 | Ok(()) |
41 | } |
42 | } |
43 | |
44 | #[derive (Copy, Clone, Debug)] |
45 | struct UnifiedDiffHunkRange(usize, usize); |
46 | |
47 | impl UnifiedDiffHunkRange { |
48 | fn start(&self) -> usize { |
49 | self.0 |
50 | } |
51 | |
52 | fn end(&self) -> usize { |
53 | self.1 |
54 | } |
55 | } |
56 | |
57 | impl fmt::Display for UnifiedDiffHunkRange { |
58 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
59 | let mut beginning: usize = self.start() + 1; |
60 | let len: usize = self.end().saturating_sub(self.start()); |
61 | if len == 1 { |
62 | write!(f, " {}" , beginning) |
63 | } else { |
64 | if len == 0 { |
65 | // empty ranges begin at line just before the range |
66 | beginning -= 1; |
67 | } |
68 | write!(f, " {}, {}" , beginning, len) |
69 | } |
70 | } |
71 | } |
72 | |
73 | /// Unified diff hunk header formatter. |
74 | pub struct UnifiedHunkHeader { |
75 | old_range: UnifiedDiffHunkRange, |
76 | new_range: UnifiedDiffHunkRange, |
77 | } |
78 | |
79 | impl UnifiedHunkHeader { |
80 | /// Creates a hunk header from a (non empty) slice of diff ops. |
81 | pub fn new(ops: &[DiffOp]) -> UnifiedHunkHeader { |
82 | let first: DiffOp = ops[0]; |
83 | let last: DiffOp = ops[ops.len() - 1]; |
84 | let old_start: usize = first.old_range().start; |
85 | let new_start: usize = first.new_range().start; |
86 | let old_end: usize = last.old_range().end; |
87 | let new_end: usize = last.new_range().end; |
88 | UnifiedHunkHeader { |
89 | old_range: UnifiedDiffHunkRange(old_start, old_end), |
90 | new_range: UnifiedDiffHunkRange(new_start, new_end), |
91 | } |
92 | } |
93 | } |
94 | |
95 | impl fmt::Display for UnifiedHunkHeader { |
96 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
97 | write!(f, "@@ - {} + {} @@" , &self.old_range, &self.new_range) |
98 | } |
99 | } |
100 | |
101 | /// Unified diff formatter. |
102 | /// |
103 | /// ```rust |
104 | /// use similar::TextDiff; |
105 | /// # let old_text = "" ; |
106 | /// # let new_text = "" ; |
107 | /// let text_diff = TextDiff::from_lines(old_text, new_text); |
108 | /// print!("{}" , text_diff |
109 | /// .unified_diff() |
110 | /// .context_radius(10) |
111 | /// .header("old_file" , "new_file" )); |
112 | /// ``` |
113 | /// |
114 | /// ## Unicode vs Bytes |
115 | /// |
116 | /// The [`UnifiedDiff`] type supports both unicode and byte diffs for all |
117 | /// types compatible with [`DiffableStr`]. You can pick between the two |
118 | /// versions by using [`UnifiedDiff.to_string`] or [`UnifiedDiff.to_writer`]. |
119 | /// The former uses [`DiffableStr::to_string_lossy`], the latter uses |
120 | /// [`DiffableStr::as_bytes`] for each line. |
121 | pub struct UnifiedDiff<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> { |
122 | diff: &'diff TextDiff<'old, 'new, 'bufs, T>, |
123 | context_radius: usize, |
124 | missing_newline_hint: bool, |
125 | header: Option<(String, String)>, |
126 | } |
127 | |
128 | impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> UnifiedDiff<'diff, 'old, 'new, 'bufs, T> { |
129 | /// Creates a formatter from a text diff object. |
130 | pub fn from_text_diff(diff: &'diff TextDiff<'old, 'new, 'bufs, T>) -> Self { |
131 | UnifiedDiff { |
132 | diff, |
133 | context_radius: 3, |
134 | missing_newline_hint: true, |
135 | header: None, |
136 | } |
137 | } |
138 | |
139 | /// Changes the context radius. |
140 | /// |
141 | /// The context radius is the number of lines between changes that should |
142 | /// be emitted. This defaults to `3`. |
143 | pub fn context_radius(&mut self, n: usize) -> &mut Self { |
144 | self.context_radius = n; |
145 | self |
146 | } |
147 | |
148 | /// Sets a header to the diff. |
149 | /// |
150 | /// `a` and `b` are the file names that are added to the top of the unified |
151 | /// file format. The names are accepted verbatim which lets you encode |
152 | /// a timestamp into it when separated by a tab (`\t`). For more information, |
153 | /// see [the unified diff format specification](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/diff.html#tag_20_34_10_07). |
154 | pub fn header(&mut self, a: &str, b: &str) -> &mut Self { |
155 | self.header = Some((a.to_string(), b.to_string())); |
156 | self |
157 | } |
158 | |
159 | /// Controls the missing newline hint. |
160 | /// |
161 | /// By default a special `\ No newline at end of file` marker is added to |
162 | /// the output when a file is not terminated with a final newline. This can |
163 | /// be disabled with this flag. |
164 | pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self { |
165 | self.missing_newline_hint = yes; |
166 | self |
167 | } |
168 | |
169 | /// Iterates over all hunks as configured. |
170 | pub fn iter_hunks(&self) -> impl Iterator<Item = UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>> { |
171 | let diff = self.diff; |
172 | let missing_newline_hint = self.missing_newline_hint; |
173 | self.diff |
174 | .grouped_ops(self.context_radius) |
175 | .into_iter() |
176 | .filter(|ops| !ops.is_empty()) |
177 | .map(move |ops| UnifiedDiffHunk::new(ops, diff, missing_newline_hint)) |
178 | } |
179 | |
180 | /// Write the unified diff as bytes to the output stream. |
181 | pub fn to_writer<W: io::Write>(&self, mut w: W) -> Result<(), io::Error> |
182 | where |
183 | 'diff: 'old + 'new + 'bufs, |
184 | { |
185 | let mut header = self.header.as_ref(); |
186 | for hunk in self.iter_hunks() { |
187 | if let Some((old_file, new_file)) = header.take() { |
188 | writeln!(w, "--- {}" , old_file)?; |
189 | writeln!(w, "+++ {}" , new_file)?; |
190 | } |
191 | write!(w, " {}" , hunk)?; |
192 | } |
193 | Ok(()) |
194 | } |
195 | |
196 | fn header_opt(&mut self, header: Option<(&str, &str)>) -> &mut Self { |
197 | if let Some((a, b)) = header { |
198 | self.header(a, b); |
199 | } |
200 | self |
201 | } |
202 | } |
203 | |
204 | /// Unified diff hunk formatter. |
205 | /// |
206 | /// The `Display` this renders out a single unified diff's hunk. |
207 | pub struct UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> { |
208 | diff: &'diff TextDiff<'old, 'new, 'bufs, T>, |
209 | ops: Vec<DiffOp>, |
210 | missing_newline_hint: bool, |
211 | } |
212 | |
213 | impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> |
214 | UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> |
215 | { |
216 | /// Creates a new hunk for some operations. |
217 | pub fn new( |
218 | ops: Vec<DiffOp>, |
219 | diff: &'diff TextDiff<'old, 'new, 'bufs, T>, |
220 | missing_newline_hint: bool, |
221 | ) -> UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> { |
222 | UnifiedDiffHunk { |
223 | diff, |
224 | ops, |
225 | missing_newline_hint, |
226 | } |
227 | } |
228 | |
229 | /// Returns the header for the hunk. |
230 | pub fn header(&self) -> UnifiedHunkHeader { |
231 | UnifiedHunkHeader::new(&self.ops) |
232 | } |
233 | |
234 | /// Returns all operations in the hunk. |
235 | pub fn ops(&self) -> &[DiffOp] { |
236 | &self.ops |
237 | } |
238 | |
239 | /// Returns the value of the `missing_newline_hint` flag. |
240 | pub fn missing_newline_hint(&self) -> bool { |
241 | self.missing_newline_hint |
242 | } |
243 | |
244 | /// Iterates over all changes in a hunk. |
245 | pub fn iter_changes<'x, 'slf>(&'slf self) -> AllChangesIter<'slf, 'x, T> |
246 | where |
247 | 'x: 'slf + 'old + 'new, |
248 | 'old: 'x, |
249 | 'new: 'x, |
250 | { |
251 | AllChangesIter::new(self.diff.old_slices(), self.diff.new_slices(), self.ops()) |
252 | } |
253 | |
254 | /// Write the hunk as bytes to the output stream. |
255 | pub fn to_writer<W: io::Write>(&self, mut w: W) -> Result<(), io::Error> |
256 | where |
257 | 'diff: 'old + 'new + 'bufs, |
258 | { |
259 | for (idx, change) in self.iter_changes().enumerate() { |
260 | if idx == 0 { |
261 | writeln!(w, " {}" , self.header())?; |
262 | } |
263 | write!(w, " {}" , change.tag())?; |
264 | w.write_all(change.value().as_bytes())?; |
265 | if !self.diff.newline_terminated() { |
266 | writeln!(w)?; |
267 | } |
268 | if self.diff.newline_terminated() && change.missing_newline() { |
269 | writeln!(w, " {}" , MissingNewlineHint(self.missing_newline_hint))?; |
270 | } |
271 | } |
272 | Ok(()) |
273 | } |
274 | } |
275 | |
276 | impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display |
277 | for UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T> |
278 | where |
279 | 'diff: 'old + 'new + 'bufs, |
280 | { |
281 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
282 | for (idx: usize, change: Change<&T>) in self.iter_changes().enumerate() { |
283 | if idx == 0 { |
284 | writeln!(f, " {}" , self.header())?; |
285 | } |
286 | write!(f, " {}{}" , change.tag(), change.to_string_lossy())?; |
287 | if !self.diff.newline_terminated() { |
288 | writeln!(f)?; |
289 | } |
290 | if self.diff.newline_terminated() && change.missing_newline() { |
291 | writeln!(f, " {}" , MissingNewlineHint(self.missing_newline_hint))?; |
292 | } |
293 | } |
294 | Ok(()) |
295 | } |
296 | } |
297 | |
298 | impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display |
299 | for UnifiedDiff<'diff, 'old, 'new, 'bufs, T> |
300 | where |
301 | 'diff: 'old + 'new + 'bufs, |
302 | { |
303 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
304 | let mut header: Option<&(String, String)> = self.header.as_ref(); |
305 | for hunk: UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, …> in self.iter_hunks() { |
306 | if let Some((old_file: &String, new_file: &String)) = header.take() { |
307 | writeln!(f, "--- {}" , old_file)?; |
308 | writeln!(f, "+++ {}" , new_file)?; |
309 | } |
310 | write!(f, " {}" , hunk)?; |
311 | } |
312 | Ok(()) |
313 | } |
314 | } |
315 | |
316 | /// Quick way to get a unified diff as string. |
317 | /// |
318 | /// `n` configures [`UnifiedDiff::context_radius`] and |
319 | /// `header` configures [`UnifiedDiff::header`] when not `None`. |
320 | pub fn unified_diff( |
321 | alg: Algorithm, |
322 | old: &str, |
323 | new: &str, |
324 | n: usize, |
325 | header: Option<(&str, &str)>, |
326 | ) -> String { |
327 | TextDiff&mut UnifiedDiff<'_, '_, '_, '_, …>::configure() |
328 | .algorithm(alg) |
329 | .diff_lines(old, new) |
330 | .unified_diff() |
331 | .context_radius(n) |
332 | .header_opt(header) |
333 | .to_string() |
334 | } |
335 | |
336 | #[test ] |
337 | fn test_unified_diff() { |
338 | let diff = TextDiff::from_lines( |
339 | "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" , |
340 | "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" , |
341 | ); |
342 | insta::assert_snapshot!(&diff.unified_diff().header("a.txt" , "b.txt" ).to_string()); |
343 | } |
344 | #[test ] |
345 | fn test_empty_unified_diff() { |
346 | let diff = TextDiff::from_lines("abc" , "abc" ); |
347 | assert_eq!(diff.unified_diff().header("a.txt" , "b.txt" ).to_string(), "" ); |
348 | } |
349 | |
350 | #[test ] |
351 | fn test_unified_diff_newline_hint() { |
352 | let diff = TextDiff::from_lines("a \n" , "b" ); |
353 | insta::assert_snapshot!(&diff.unified_diff().header("a.txt" , "b.txt" ).to_string()); |
354 | insta::assert_snapshot!(&diff |
355 | .unified_diff() |
356 | .missing_newline_hint(false) |
357 | .header("a.txt" , "b.txt" ) |
358 | .to_string()); |
359 | } |
360 | |