├── .github ├── example-cmd.png └── example-ps.png ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── UNLICENSE ├── examples ├── poem.rs └── pretty_assertions.rs └── src └── lib.rs /.github/example-cmd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CAD97/colored-diff/23fe67ae7eebc5acb1e3c76d0759679208ae1394/.github/example-cmd.png -------------------------------------------------------------------------------- /.github/example-ps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CAD97/colored-diff/23fe67ae7eebc5acb1e3c76d0759679208ae1394/.github/example-ps.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Rust template 3 | # Generated by Cargo 4 | # will have compiled files and executables 5 | /target/ 6 | 7 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 8 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 9 | Cargo.lock 10 | 11 | # These are backup files generated by rustfmt 12 | **/*.rs.bk 13 | ### JetBrains template 14 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 15 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 16 | 17 | # User-specific stuff 18 | .idea/**/workspace.xml 19 | .idea/**/tasks.xml 20 | .idea/**/dictionaries 21 | .idea/**/shelf 22 | 23 | # Sensitive or high-churn files 24 | .idea/**/dataSources/ 25 | .idea/**/dataSources.ids 26 | .idea/**/dataSources.local.xml 27 | .idea/**/sqlDataSources.xml 28 | .idea/**/dynamic.xml 29 | .idea/**/uiDesigner.xml 30 | .idea/**/dbnavigator.xml 31 | 32 | # Gradle 33 | .idea/**/gradle.xml 34 | .idea/**/libraries 35 | 36 | # CMake 37 | cmake-build-debug/ 38 | cmake-build-release/ 39 | 40 | # Mongo Explorer plugin 41 | .idea/**/mongoSettings.xml 42 | 43 | # File-based project format 44 | *.iws 45 | 46 | # IntelliJ 47 | out/ 48 | 49 | # mpeltonen/sbt-idea plugin 50 | .idea_modules/ 51 | 52 | # JIRA plugin 53 | atlassian-ide-plugin.xml 54 | 55 | # Cursive Clojure plugin 56 | .idea/replstate.xml 57 | 58 | # Crashlytics plugin (for Android Studio and IntelliJ) 59 | com_crashlytics_export_strings.xml 60 | crashlytics.properties 61 | crashlytics-build.properties 62 | fabric.properties 63 | 64 | # Editor-based Rest Client 65 | .idea/httpRequests 66 | 67 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 5 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v0.2.2] - 2019-05-16 10 | 11 | - Fix overflow panic when the first change is an addition. 12 | 13 | ## v0.2.1 - 2018-09-24 14 | 15 | - Fix build for non-Windows platforms 16 | 17 | ## [v0.2.0] - 2018-06-05 18 | Removed `init()`. Instead, ANSI is automatically enabled the first time an ANSI string is crated. 19 | 20 | ## v0.1.2 - 2018-06-02 21 | Initial release! 22 | 23 | [Unreleased]: https://github.com/cad97/colored-diff/compare/v0.2.2...HEAD 24 | [v0.2.0]: https://github.com/cad97/colored-diff/compare/v0.1.2...v0.2.0 25 | [v0.2.2]: https://github.com/cad97/colored-diff/compare/v0.2.0...v0.2.2 26 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "colored-diff" 3 | version = "0.2.3" 4 | 5 | license = "MIT OR Unlicense" 6 | readme = "README.md" 7 | description = "Format the difference between two strings with ANSI colors" 8 | repository = "https://github.com/CAD97/colored-diff" 9 | keywords = ["pretty-assertions", "diff", "difference"] 10 | categories = ["development-tools::testing"] 11 | 12 | exclude = ["/.github/**/*"] 13 | 14 | [badges] 15 | maintenance = { status = "passively-maintained" } 16 | 17 | [dependencies] 18 | dissimilar = "1.0" 19 | ansi_term = "0.12" 20 | itertools = { version = "0.10", default-features = false } 21 | 22 | [dev-dependencies] 23 | regex = "1.5" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Christopher Durham (aka CAD97) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # colored-diff 2 | 3 | Show colored differences between source strings! 4 | 5 | Inspired by / extracted from [pretty-assertions](https://github.com/colin-kiegel/rust-pretty-assertions) 6 | and [difference's github-style example](https://github.com/johannhof/difference.rs/blob/master/examples/github-style.rs) 7 | 8 | Powershell: 9 | ![Powershell Example](.github/example-ps.png) 10 | 11 | Command Prompt: 12 | ![Command Prompt Example](.github/example-cmd.png) 13 | 14 | (Now accepting PRs for a macOS Terminal and/or Ubuntu (whatever console window) examples!) 15 | 16 | [Poem Example](examples/poem.rs): 17 | 18 | ```rust 19 | let expected = "\ 20 | Roses are red, violets are blue,\n\ 21 | I wrote this library here,\n\ 22 | just for you.\n\ 23 | (It's true).\n\ 24 | "; 25 | let actual = "\ 26 | Roses are red, violets are blue,\n\ 27 | I wrote this documentation here,\n\ 28 | just for you.\n\ 29 | (It's quite true).\n\ 30 | "; 31 | 32 | println!("{}", colored_diff::PrettyDifference { expected, actual }) 33 | ``` 34 | 35 | [Pretty-Assertions Example](examples/pretty_assertions.rs): 36 | 37 | ```rust 38 | #[derive(Debug, PartialEq)] 39 | struct Foo { 40 | lorem: &'static str, 41 | ipsum: u32, 42 | dolor: Result, 43 | } 44 | 45 | let x = Some(Foo { lorem: "Hello World!", ipsum: 42, dolor: Ok("hey".to_string())}); 46 | let y = Some(Foo { lorem: "Hello Wrold!", ipsum: 42, dolor: Ok("hey ho!".to_string())}); 47 | 48 | let x = format!("{:#?}", x); 49 | let y = format!("{:#?}", y); 50 | 51 | println!("{}", colored_diff::PrettyDifference { expected: &x, actual: &y }) 52 | ``` 53 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /examples/poem.rs: -------------------------------------------------------------------------------- 1 | extern crate colored_diff; 2 | 3 | fn main() { 4 | let expected = "\ 5 | Roses are red, violets are blue,\n\ 6 | I wrote this library here,\n\ 7 | just for you.\n\ 8 | (It's true).\n\ 9 | "; 10 | let actual = "\ 11 | Roses are red, violets are blue,\n\ 12 | I wrote this documentation here,\n\ 13 | just for you.\n\ 14 | (It's quite true).\n\ 15 | "; 16 | 17 | println!("{}", colored_diff::PrettyDifference { expected, actual }) 18 | } 19 | -------------------------------------------------------------------------------- /examples/pretty_assertions.rs: -------------------------------------------------------------------------------- 1 | extern crate colored_diff; 2 | 3 | fn main() { 4 | #[derive(Debug, PartialEq)] 5 | struct Foo { 6 | lorem: &'static str, 7 | ipsum: u32, 8 | dolor: Result, 9 | } 10 | 11 | let x = Some(Foo { lorem: "Hello World!", ipsum: 42, dolor: Ok("hey".to_string())}); 12 | let y = Some(Foo { lorem: "Hello Wrold!", ipsum: 42, dolor: Ok("hey ho!".to_string())}); 13 | 14 | let x = format!("{:#?}", x); 15 | let y = format!("{:#?}", y); 16 | 17 | println!("{}", colored_diff::PrettyDifference { expected: &x, actual: &y }) 18 | } 19 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate ansi_term; 2 | extern crate dissimilar; 3 | extern crate itertools; 4 | #[cfg(test)] 5 | extern crate regex; 6 | 7 | use ansi_term::{ANSIGenericString, Colour}; 8 | use dissimilar::Chunk; 9 | use itertools::Itertools; 10 | use std::fmt; 11 | 12 | fn red(s: &str) -> ANSIGenericString { 13 | Colour::Red.paint(s) 14 | } 15 | fn on_red(s: &str) -> ANSIGenericString { 16 | Colour::White.on(Colour::Red).bold().paint(s) 17 | } 18 | fn green(s: &str) -> ANSIGenericString { 19 | Colour::Green.paint(s) 20 | } 21 | fn on_green(s: &str) -> ANSIGenericString { 22 | Colour::White.on(Colour::Green).bold().paint(s) 23 | } 24 | 25 | static LEFT: &str = "<"; 26 | static NL_LEFT: &str = "\n<"; 27 | static RIGHT: &str = ">"; 28 | static NL_RIGHT: &str = "\n>"; 29 | 30 | #[cfg(windows)] 31 | #[inline(always)] 32 | fn enable_ansi() { 33 | use std::sync::Once; 34 | 35 | static ONCE: Once = Once::new(); 36 | ONCE.call_once(|| {ansi_term::enable_ansi_support().ok();}); 37 | } 38 | 39 | #[cfg(not(windows))] 40 | #[inline(always)] 41 | fn enable_ansi() { 42 | } 43 | 44 | #[derive(Copy, Clone, Debug)] 45 | pub struct PrettyDifference<'a> { 46 | pub expected: &'a str, 47 | pub actual: &'a str, 48 | } 49 | 50 | impl<'a> fmt::Display for PrettyDifference<'a> { 51 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 52 | diff(f, self.expected, self.actual) 53 | } 54 | } 55 | 56 | /// Format the difference between strings using GitHub-like formatting with ANSI coloring. 57 | pub fn diff(f: &mut fmt::Formatter, expected: &str, actual: &str) -> fmt::Result { 58 | let changeset = dissimilar::diff(expected, actual); 59 | fmt_changeset(f, &changeset) 60 | } 61 | 62 | fn fmt_changeset(f: &mut fmt::Formatter, changeset: &Vec) -> fmt::Result { 63 | enable_ansi(); 64 | 65 | writeln!(f, "{} {} / {} {}", 66 | red(LEFT), red("left"), 67 | green(RIGHT), green("right"), 68 | )?; 69 | 70 | for (i, diff) in changeset.iter().enumerate() { 71 | match diff { 72 | Chunk::Equal(text) => { 73 | format_same(f, text)?; 74 | } 75 | Chunk::Insert(added) => { 76 | if let Some(Chunk::Delete(removed)) = i.checked_sub(1).map(|i| &changeset[i]) { 77 | format_add_rem(f, added, removed)?; 78 | } else { 79 | format_add(f, added)?; 80 | } 81 | } 82 | Chunk::Delete(removed) => { 83 | if let Some(Chunk::Insert(_)) = changeset.get(i + 1) { 84 | continue; 85 | } else { 86 | format_rem(f, removed)?; 87 | } 88 | } 89 | } 90 | } 91 | 92 | Ok(()) 93 | } 94 | 95 | fn format_add_rem(f: &mut fmt::Formatter, added: &str, removed: &str) -> fmt::Result { 96 | let diffs = dissimilar::diff(removed, added); 97 | 98 | // LEFT (removed) 99 | write!(f, "{}", red(LEFT))?; 100 | for diff in &diffs { 101 | match diff { 102 | Chunk::Equal(text) => { 103 | for blob in Itertools::intersperse(text.split('\n'), NL_LEFT) { 104 | write!(f, "{}", red(blob))?; 105 | } 106 | } 107 | Chunk::Delete(text) => { 108 | for blob in Itertools::intersperse(text.split('\n'), NL_LEFT) { 109 | write!(f, "{}", on_red(blob))?; 110 | } 111 | } 112 | Chunk::Insert(_) => continue, 113 | } 114 | } 115 | writeln!(f)?; 116 | 117 | // RIGHT (added) 118 | write!(f, "{}", green(RIGHT))?; 119 | for diff in &diffs { 120 | match diff { 121 | Chunk::Equal(text) => { 122 | for blob in Itertools::intersperse(text.split('\n'), NL_RIGHT) { 123 | write!(f, "{}", green(blob))?; 124 | } 125 | } 126 | Chunk::Insert(text) => { 127 | for blob in Itertools::intersperse(text.split('\n'), NL_RIGHT) { 128 | write!(f, "{}", on_green(blob))?; 129 | } 130 | } 131 | Chunk::Delete(_) => continue, 132 | } 133 | } 134 | writeln!(f)?; 135 | 136 | Ok(()) 137 | } 138 | 139 | fn format_same(f: &mut fmt::Formatter, text: &str) -> fmt::Result { 140 | for line in text.split('\n') { 141 | writeln!(f, " {}", line)?; 142 | } 143 | Ok(()) 144 | } 145 | 146 | fn format_add(f: &mut fmt::Formatter, text: &str) -> fmt::Result { 147 | for line in text.split('\n') { 148 | writeln!(f, "{}{}", green(RIGHT), green(line))?; 149 | } 150 | Ok(()) 151 | } 152 | 153 | fn format_rem(f: &mut fmt::Formatter, text: &str) -> fmt::Result { 154 | for line in text.split('\n') { 155 | writeln!(f, "{}{}", red(LEFT), red(line))?; 156 | } 157 | Ok(()) 158 | } 159 | 160 | #[cfg(test)] 161 | mod tests { 162 | use regex::Regex; 163 | 164 | use super::*; 165 | 166 | #[test] 167 | fn single_add() { 168 | PrettyDifference { 169 | expected: "", 170 | actual: "foo", 171 | } 172 | .to_string(); 173 | } 174 | 175 | #[test] 176 | fn color_free_diff() { 177 | let diff: String = PrettyDifference { 178 | expected: "a\nb\nc", 179 | actual: "\nb\ncc", 180 | } 181 | .to_string(); 182 | 183 | let re = Regex::new(r"\u{1b}\[[0-9;]*m").unwrap(); 184 | assert_eq!( 185 | re.replace_all(&diff, ""), 186 | "< left / > right\nc\n" 187 | ); 188 | } 189 | 190 | #[test] 191 | fn color_diff() { 192 | let diff: String = PrettyDifference { 193 | expected: "a\nb\nc", 194 | actual: "\nb\ncc", 195 | } 196 | .to_string(); 197 | 198 | assert_eq!(diff, "\u{1b}[31m<\u{1b}[0m \u{1b}[31mleft\u{1b}[0m / \u{1b}[32m>\u{1b}[0m \u{1b}[32mright\u{1b}[0m\n\u{1b}[31m<\u{1b}[0m\u{1b}[31ma\u{1b}[0m\n \n b\n c\n\u{1b}[32m>\u{1b}[0m\u{1b}[32mc\u{1b}[0m\n"); 199 | } 200 | } 201 | --------------------------------------------------------------------------------