termdiff/
lib.rs

1//! This library is for helping you create diff for displaying on the terminal
2//!
3//! # Examples
4//!
5//! ```
6//! use termdiff::{diff, ArrowsTheme};
7//! let old = "The quick brown fox and\njumps over the sleepy dog";
8//! let new = "The quick red fox and\njumps over the lazy dog";
9//! let mut buffer: Vec<u8> = Vec::new();
10//! let theme = ArrowsTheme::default();
11//! diff(&mut buffer, old, new, &theme).unwrap();
12//! let actual: String = String::from_utf8(buffer).expect("Not valid UTF-8");
13//!
14//! assert_eq!(
15//!     actual,
16//!     "< left / > right
17//! <The quick brown fox and
18//! <jumps over the sleepy dog
19//! >The quick red fox and
20//! >jumps over the lazy dog
21//! "
22//! );
23//! ```
24//!
25//! Alternatively if you are dropping this into a `format!` or similar, you
26//! might want to use the displayable instead
27//!
28//! ```
29//! use termdiff::{DrawDiff, SignsTheme};
30//! let old = "The quick brown fox and\njumps over the sleepy dog";
31//! let new = "The quick red fox and\njumps over the lazy dog";
32//! let theme = SignsTheme::default();
33//! let actual = format!("{}", DrawDiff::new(old, new, &theme));
34//!
35//! assert_eq!(
36//!     actual,
37//!     "--- remove | insert +++
38//! -The quick brown fox and
39//! -jumps over the sleepy dog
40//! +The quick red fox and
41//! +jumps over the lazy dog
42//! "
43//! );
44//! ```
45//!
46//! # Features
47//!
48//! This crate provides several features that can be enabled or disabled in your `Cargo.toml`:
49//!
50//! ## Diff Algorithms
51//!
52//! * `myers` - Implements the Myers diff algorithm, which is a widely used algorithm for computing
53//!   differences between sequences. It's efficient for most common use cases.
54//!
55//! * `similar` - Uses the "similar" crate to compute diffs. This is an alternative implementation
56//!   that may have different performance characteristics or output in some cases.
57//!
58//! ## Themes
59//!
60//! * `arrows` - A simple, colorless theme that uses arrow symbols (`<` and `>`) to indicate
61//!   deleted and inserted lines. The header shows "< left / > right".
62//!
63//! * `arrows_color` - A colored version of the arrows theme. Uses red for deleted content and
64//!   green for inserted content. Requires the "crossterm" crate for terminal color support.
65//!
66//! * `signs` - A simple, colorless theme that uses plus and minus signs (`-` and `+`) to indicate
67//!   deleted and inserted lines. The header shows "--- remove | insert +++". This style is
68//!   similar to traditional diff output.
69//!
70//! * `signs_color` - A colored version of the signs theme. Uses red for deleted content and
71//!   green for inserted content. Requires the "crossterm" crate for terminal color support.
72//!
73//! By default, all features are enabled. You can selectively disable features by specifying
74//! `default-features = false` and then listing the features you want to enable.
75//!
76//! You can define your own theme if you like
77//!
78//!
79//! ``` rust
80//! use std::borrow::Cow;
81//!
82//! use crossterm::style::Stylize;
83//! use termdiff::{DrawDiff, Theme};
84//!
85//! #[derive(Debug)]
86//! struct MyTheme {}
87//! impl Theme for MyTheme {
88//!     fn highlight_insert<'this>(&self, input: &'this str) -> Cow<'this, str> {
89//!         input.into()
90//!     }
91//!
92//!     fn highlight_delete<'this>(&self, input: &'this str) -> Cow<'this, str> {
93//!         input.into()
94//!     }
95//!
96//!     fn equal_content<'this>(&self, input: &'this str) -> Cow<'this, str> {
97//!         input.into()
98//!     }
99//!
100//!     fn delete_content<'this>(&self, input: &'this str) -> Cow<'this, str> {
101//!         input.into()
102//!     }
103//!
104//!     fn equal_prefix<'this>(&self) -> Cow<'this, str> {
105//!         "=".into()
106//!     }
107//!
108//!     fn delete_prefix<'this>(&self) -> Cow<'this, str> {
109//!         "!".into()
110//!     }
111//!
112//!     fn insert_line<'this>(&self, input: &'this str) -> Cow<'this, str> {
113//!         input.into()
114//!     }
115//!
116//!     fn insert_prefix<'this>(&self) -> Cow<'this, str> {
117//!         "|".into()
118//!     }
119//!
120//!     fn line_end<'this>(&self) -> Cow<'this, str> {
121//!         "\n".into()
122//!     }
123//!
124//!     fn header<'this>(&self) -> Cow<'this, str> {
125//!         format!("{}\n", "Header").into()
126//!     }
127//! }
128//! let my_theme = MyTheme {};
129//! let old = "The quick brown fox and\njumps over the sleepy dog";
130//! let new = "The quick red fox and\njumps over the lazy dog";
131//! let actual = format!("{}", DrawDiff::new(old, new, &my_theme));
132//!
133//! assert_eq!(
134//!     actual,
135//!     "Header
136//! !The quick brown fox and
137//! !jumps over the sleepy dog
138//! |The quick red fox and
139//! |jumps over the lazy dog
140//! "
141//! );
142//! ```
143
144#![warn(clippy::nursery)]
145#![deny(
146    unused,
147    nonstandard_style,
148    future_incompatible,
149    missing_copy_implementations,
150    missing_debug_implementations,
151    missing_docs,
152    clippy::pedantic,
153    clippy::cargo,
154    clippy::complexity,
155    clippy::correctness,
156    clippy::perf,
157    clippy::style,
158    clippy::suspicious,
159    non_fmt_panics
160)]
161#![allow(clippy::multiple_crate_versions)]
162
163pub use cmd::{diff, diff_with_algorithm};
164pub use diff_algorithm::Algorithm;
165pub use draw_diff::DrawDiff;
166
167// Re-export the Theme trait and theme implementations
168#[cfg(feature = "arrows_color")]
169pub use themes::ArrowsColorTheme;
170#[cfg(feature = "arrows")]
171pub use themes::ArrowsTheme;
172#[cfg(feature = "signs_color")]
173pub use themes::SignsColorTheme;
174#[cfg(feature = "signs")]
175pub use themes::SignsTheme;
176pub use themes::Theme;
177
178mod cmd;
179mod diff_algorithm;
180mod draw_diff;
181mod themes;
182
183#[cfg(doctest)]
184mod test_readme {
185    macro_rules! external_doc_test {
186        ($x:expr) => {
187            #[doc = $x]
188            extern "C" {}
189        };
190    }
191
192    external_doc_test!(include_str!("../README.md"));
193}
194
195#[cfg(test)]
196mod integration_tests {
197    use crate::{diff, ArrowsTheme, DrawDiff, SignsTheme, Theme};
198    use std::io::Cursor;
199
200    /// Test that the diff function produces the expected output with `ArrowsTheme`
201    #[test]
202    fn test_diff_with_arrows_theme() {
203        let old = "The quick brown fox";
204        let new = "The quick red fox";
205        let mut buffer = Cursor::new(Vec::new());
206        let theme = ArrowsTheme::default();
207
208        diff(&mut buffer, old, new, &theme).unwrap();
209
210        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
211        assert!(output.contains("<The quick brown fox"));
212        assert!(output.contains(">The quick red fox"));
213        assert!(output.contains(">The quick red fox"));
214        assert!(output.contains(">The quick red fox"));
215        assert!(output.contains("< left / > right"));
216    }
217
218    /// Test that the diff function produces the expected output with `SignsTheme`
219    #[test]
220    fn test_diff_with_signs_theme() {
221        let old = "The quick brown fox";
222        let new = "The quick red fox";
223        let mut buffer = Cursor::new(Vec::new());
224        let theme = SignsTheme::default();
225
226        diff(&mut buffer, old, new, &theme).unwrap();
227
228        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
229        assert!(output.contains("-The quick brown fox"));
230        assert!(output.contains("+The quick red fox"));
231        assert!(output.contains("--- remove | insert +++"));
232    }
233
234    /// Test that `DrawDiff` produces the expected output with `ArrowsTheme`
235    #[test]
236    fn test_draw_diff_with_arrows_theme() {
237        let old = "The quick brown fox";
238        let new = "The quick red fox";
239        let theme = ArrowsTheme::default();
240
241        let output = format!("{}", DrawDiff::new(old, new, &theme));
242
243        // Verify formatted output with proper spacing
244        assert!(
245            output.contains("<The quick brown fox"),
246            "Expected deleted line marker without space, got:\n{}",
247            output
248        );
249        assert!(
250            output.contains(">The quick red fox"),
251            "Expected inserted line marker with space, got:\n{}",
252            output
253        );
254        assert!(
255            output.contains("< left / > right"),
256            "Expected header with proper spacing, got:\n{}",
257            output
258        );
259    }
260
261    /// Test that `DrawDiff` produces the expected output with `SignsTheme`
262    #[test]
263    fn test_draw_diff_with_signs_theme() {
264        let old = "The quick brown fox";
265        let new = "The quick red fox";
266        let theme = SignsTheme::default();
267
268        let output = format!("{}", DrawDiff::new(old, new, &theme));
269
270        assert!(output.contains("-The quick brown fox"));
271        assert!(output.contains("+The quick red fox"));
272        assert!(output.contains("--- remove | insert +++"));
273    }
274
275    /// Test handling of empty strings
276    #[test]
277    fn test_empty_strings() {
278        let old = "";
279        let new = "";
280        let theme = ArrowsTheme::default();
281
282        let output = format!("{}", DrawDiff::new(old, new, &theme));
283
284        // Should just contain the header
285        assert_eq!(output, "< left / > right\n");
286    }
287
288    /// Test handling of strings with only newline differences
289    #[test]
290    fn test_newline_differences() {
291        let old = "line\n";
292        let new = "line";
293        let theme = ArrowsTheme::default();
294
295        let output = format!("{}", DrawDiff::new(old, new, &theme));
296
297        // Should show the newline difference
298        assert!(output.contains("line␊"));
299    }
300
301    /// Test with a custom theme implementation
302    #[test]
303    fn test_custom_theme() {
304        use std::borrow::Cow;
305
306        #[derive(Debug)]
307        struct CustomTheme;
308
309        impl Theme for CustomTheme {
310            fn equal_prefix<'this>(&self) -> Cow<'this, str> {
311                "=".into()
312            }
313
314            fn delete_prefix<'this>(&self) -> Cow<'this, str> {
315                "DEL>".into()
316            }
317
318            fn insert_prefix<'this>(&self) -> Cow<'this, str> {
319                "INS>".into()
320            }
321
322            fn header<'this>(&self) -> Cow<'this, str> {
323                "CUSTOM DIFF\n".into()
324            }
325        }
326
327        let old = "The quick brown fox";
328        let new = "The quick red fox";
329        let theme = CustomTheme;
330
331        let output = format!("{}", DrawDiff::new(old, new, &theme));
332
333        assert!(output.contains("DEL>The quick brown fox"));
334        assert!(output.contains("INS>The quick red fox"));
335        assert!(output.contains("CUSTOM DIFF"));
336    }
337
338    /// Test with multiline input containing both changes and unchanged lines
339    #[test]
340    fn test_multiline_with_unchanged_lines() {
341        let old = "Line 1\nLine 2\nLine 3\nLine 4";
342        let new = "Line 1\nModified Line 2\nLine 3\nModified Line 4";
343        let theme = SignsTheme::default();
344
345        let output = format!("{}", DrawDiff::new(old, new, &theme));
346
347        // Check that unchanged lines are preserved
348        assert!(output.contains(" Line 1"));
349        assert!(output.contains(" Line 3"));
350
351        // Check that changed lines are marked
352        assert!(output.contains("-Line 2"));
353        assert!(output.contains("+Modified Line 2"));
354        assert!(output.contains("-Line 4"));
355        assert!(output.contains("+Modified Line 4"));
356    }
357
358    /// Test conversion from `DrawDiff` to String
359    #[test]
360    fn test_draw_diff_to_string() {
361        let old = "The quick brown fox";
362        let new = "The quick red fox";
363        let theme = ArrowsTheme::default();
364
365        let diff = DrawDiff::new(old, new, &theme);
366        let output: String = diff.into();
367
368        assert!(output.contains("<The quick brown fox"));
369        assert!(output.contains(">The quick red fox"));
370        assert!(output.contains(">The quick red fox"));
371    }
372}