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}