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::pedantic,
157    clippy::perf,
158    clippy::style,
159    clippy::suspicious,
160    non_fmt_panics
161)]
162#![allow(clippy::multiple_crate_versions)]
163
164pub use cmd::{diff, diff_with_algorithm};
165pub use diff_algorithm::Algorithm;
166pub use draw_diff::DrawDiff;
167
168// Re-export the Theme trait and theme implementations
169#[cfg(feature = "arrows_color")]
170pub use themes::ArrowsColorTheme;
171#[cfg(feature = "arrows")]
172pub use themes::ArrowsTheme;
173#[cfg(feature = "signs_color")]
174pub use themes::SignsColorTheme;
175#[cfg(feature = "signs")]
176pub use themes::SignsTheme;
177pub use themes::Theme;
178
179mod cmd;
180mod diff_algorithm;
181mod draw_diff;
182mod themes;
183
184#[cfg(doctest)]
185mod test_readme {
186    macro_rules! external_doc_test {
187        ($x:expr) => {
188            #[doc = $x]
189            extern "C" {}
190        };
191    }
192
193    external_doc_test!(include_str!("../README.md"));
194}
195
196#[cfg(test)]
197mod integration_tests {
198    use crate::{diff, ArrowsTheme, DrawDiff, SignsTheme, Theme};
199    use std::io::Cursor;
200
201    /// Test that the diff function produces the expected output with `ArrowsTheme`
202    #[test]
203    fn test_diff_with_arrows_theme() {
204        let old = "The quick brown fox";
205        let new = "The quick red fox";
206        let mut buffer = Cursor::new(Vec::new());
207        let theme = ArrowsTheme::default();
208
209        diff(&mut buffer, old, new, &theme).unwrap();
210
211        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
212        assert!(output.contains("<The quick brown fox"));
213        assert!(output.contains(">The quick red fox"));
214        assert!(output.contains(">The quick red fox"));
215        assert!(output.contains(">The quick red fox"));
216        assert!(output.contains("< left / > right"));
217    }
218
219    /// Test that the diff function produces the expected output with `SignsTheme`
220    #[test]
221    fn test_diff_with_signs_theme() {
222        let old = "The quick brown fox";
223        let new = "The quick red fox";
224        let mut buffer = Cursor::new(Vec::new());
225        let theme = SignsTheme::default();
226
227        diff(&mut buffer, old, new, &theme).unwrap();
228
229        let output = String::from_utf8(buffer.into_inner()).expect("Not valid UTF-8");
230        assert!(output.contains("-The quick brown fox"));
231        assert!(output.contains("+The quick red fox"));
232        assert!(output.contains("--- remove | insert +++"));
233    }
234
235    /// Test that `DrawDiff` produces the expected output with `ArrowsTheme`
236    #[test]
237    fn test_draw_diff_with_arrows_theme() {
238        let old = "The quick brown fox";
239        let new = "The quick red fox";
240        let theme = ArrowsTheme::default();
241
242        let output = format!("{}", DrawDiff::new(old, new, &theme));
243
244        // Verify formatted output with proper spacing
245        assert!(
246            output.contains("<The quick brown fox"),
247            "Expected deleted line marker without space, got:\n{}",
248            output
249        );
250        assert!(
251            output.contains(">The quick red fox"),
252            "Expected inserted line marker with space, got:\n{}",
253            output
254        );
255        assert!(
256            output.contains("< left / > right"),
257            "Expected header with proper spacing, got:\n{}",
258            output
259        );
260    }
261
262    /// Test that `DrawDiff` produces the expected output with `SignsTheme`
263    #[test]
264    fn test_draw_diff_with_signs_theme() {
265        let old = "The quick brown fox";
266        let new = "The quick red fox";
267        let theme = SignsTheme::default();
268
269        let output = format!("{}", DrawDiff::new(old, new, &theme));
270
271        assert!(output.contains("-The quick brown fox"));
272        assert!(output.contains("+The quick red fox"));
273        assert!(output.contains("--- remove | insert +++"));
274    }
275
276    /// Test handling of empty strings
277    #[test]
278    fn test_empty_strings() {
279        let old = "";
280        let new = "";
281        let theme = ArrowsTheme::default();
282
283        let output = format!("{}", DrawDiff::new(old, new, &theme));
284
285        // Should just contain the header
286        assert_eq!(output, "< left / > right\n");
287    }
288
289    /// Test handling of strings with only newline differences
290    #[test]
291    fn test_newline_differences() {
292        let old = "line\n";
293        let new = "line";
294        let theme = ArrowsTheme::default();
295
296        let output = format!("{}", DrawDiff::new(old, new, &theme));
297
298        // Should show the newline difference
299        assert!(output.contains("line␊"));
300    }
301
302    /// Test with a custom theme implementation
303    #[test]
304    fn test_custom_theme() {
305        use std::borrow::Cow;
306
307        #[derive(Debug)]
308        struct CustomTheme;
309
310        impl Theme for CustomTheme {
311            fn equal_prefix<'this>(&self) -> Cow<'this, str> {
312                "=".into()
313            }
314
315            fn delete_prefix<'this>(&self) -> Cow<'this, str> {
316                "DEL>".into()
317            }
318
319            fn insert_prefix<'this>(&self) -> Cow<'this, str> {
320                "INS>".into()
321            }
322
323            fn header<'this>(&self) -> Cow<'this, str> {
324                "CUSTOM DIFF\n".into()
325            }
326        }
327
328        let old = "The quick brown fox";
329        let new = "The quick red fox";
330        let theme = CustomTheme;
331
332        let output = format!("{}", DrawDiff::new(old, new, &theme));
333
334        assert!(output.contains("DEL>The quick brown fox"));
335        assert!(output.contains("INS>The quick red fox"));
336        assert!(output.contains("CUSTOM DIFF"));
337    }
338
339    /// Test with multiline input containing both changes and unchanged lines
340    #[test]
341    fn test_multiline_with_unchanged_lines() {
342        let old = "Line 1\nLine 2\nLine 3\nLine 4";
343        let new = "Line 1\nModified Line 2\nLine 3\nModified Line 4";
344        let theme = SignsTheme::default();
345
346        let output = format!("{}", DrawDiff::new(old, new, &theme));
347
348        // Check that unchanged lines are preserved
349        assert!(output.contains(" Line 1"));
350        assert!(output.contains(" Line 3"));
351
352        // Check that changed lines are marked
353        assert!(output.contains("-Line 2"));
354        assert!(output.contains("+Modified Line 2"));
355        assert!(output.contains("-Line 4"));
356        assert!(output.contains("+Modified Line 4"));
357    }
358
359    /// Test conversion from `DrawDiff` to String
360    #[test]
361    fn test_draw_diff_to_string() {
362        let old = "The quick brown fox";
363        let new = "The quick red fox";
364        let theme = ArrowsTheme::default();
365
366        let diff = DrawDiff::new(old, new, &theme);
367        let output: String = diff.into();
368
369        assert!(output.contains("<The quick brown fox"));
370        assert!(output.contains(">The quick red fox"));
371        assert!(output.contains(">The quick red fox"));
372    }
373}