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}