1#[cfg(feature = "text")]
27use std::{fmt, io};
28
29use crate::iter::AllChangesIter;
30use crate::text::{DiffableStr, TextDiff};
31use crate::types::{Algorithm, DiffOp};
32
33struct MissingNewlineHint(bool);
34
35impl 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)]
45struct UnifiedDiffHunkRange(usize, usize);
46
47impl UnifiedDiffHunkRange {
48 fn start(&self) -> usize {
49 self.0
50 }
51
52 fn end(&self) -> usize {
53 self.1
54 }
55}
56
57impl fmt::Display for UnifiedDiffHunkRange {
58 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
59 let mut beginning = self.start() + 1;
60 let len = self.end().saturating_sub(self.start());
61 if len == 1 {
62 write!(f, "{}", beginning)
63 } else {
64 if len == 0 {
65 beginning -= 1;
67 }
68 write!(f, "{},{}", beginning, len)
69 }
70 }
71}
72
73pub struct UnifiedHunkHeader {
75 old_range: UnifiedDiffHunkRange,
76 new_range: UnifiedDiffHunkRange,
77}
78
79impl UnifiedHunkHeader {
80 pub fn new(ops: &[DiffOp]) -> UnifiedHunkHeader {
82 let first = ops[0];
83 let last = ops[ops.len() - 1];
84 let old_start = first.old_range().start;
85 let new_start = first.new_range().start;
86 let old_end = last.old_range().end;
87 let new_end = 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
95impl 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
101pub 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
128impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> UnifiedDiff<'diff, 'old, 'new, 'bufs, T> {
129 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 pub fn context_radius(&mut self, n: usize) -> &mut Self {
144 self.context_radius = n;
145 self
146 }
147
148 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 pub fn missing_newline_hint(&mut self, yes: bool) -> &mut Self {
165 self.missing_newline_hint = yes;
166 self
167 }
168
169 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 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
204pub 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
213impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized>
214 UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>
215{
216 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 pub fn header(&self) -> UnifiedHunkHeader {
231 UnifiedHunkHeader::new(&self.ops)
232 }
233
234 pub fn ops(&self) -> &[DiffOp] {
236 &self.ops
237 }
238
239 pub fn missing_newline_hint(&self) -> bool {
241 self.missing_newline_hint
242 }
243
244 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 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
276impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display
277 for UnifiedDiffHunk<'diff, 'old, 'new, 'bufs, T>
278where
279 'diff: 'old + 'new + 'bufs,
280{
281 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282 for (idx, change) 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
298impl<'diff, 'old, 'new, 'bufs, T: DiffableStr + ?Sized> fmt::Display
299 for UnifiedDiff<'diff, 'old, 'new, 'bufs, T>
300where
301 'diff: 'old + 'new + 'bufs,
302{
303 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
304 let mut header = self.header.as_ref();
305 for hunk in self.iter_hunks() {
306 if let Some((old_file, new_file)) = header.take() {
307 writeln!(f, "--- {}", old_file)?;
308 writeln!(f, "+++ {}", new_file)?;
309 }
310 write!(f, "{}", hunk)?;
311 }
312 Ok(())
313 }
314}
315
316pub fn unified_diff(
321 alg: Algorithm,
322 old: &str,
323 new: &str,
324 n: usize,
325 header: Option<(&str, &str)>,
326) -> String {
327 TextDiff::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]
337fn 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]
345fn 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]
351fn 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}