mirror of https://github.com/Wilfred/difftastic/
Initial proof of concept
commit
958033924a
@ -0,0 +1,2 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
@ -0,0 +1,245 @@
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.6.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"memchr 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ansi_term"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "2.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colored"
|
||||
version = "1.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "difftastic"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.1.44"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "redox_termios"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"redox_syscall 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"memchr 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex-syntax 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "termion"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_syscall 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ucd-util"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "utf8-ranges"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "vec_map"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[metadata]
|
||||
"checksum aho-corasick 0.6.9 (registry+https://github.com/rust-lang/crates.io-index)" = "1e9a933f4e58658d7b12defcf96dc5c720f20832deebe3e0a19efd3b6aaeeb9e"
|
||||
"checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
|
||||
"checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
|
||||
"checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
|
||||
"checksum cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "082bb9b28e00d3c9d39cc03e64ce4cea0f1bb9b3fde493f0cbc008472d22bdf4"
|
||||
"checksum clap 2.32.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b957d88f4b6a63b9d70d5f454ac8011819c6efa7727858f458ab71c756ce2d3e"
|
||||
"checksum colored 1.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "dc0a60679001b62fb628c4da80e574b9645ab4646056d7c9018885efffe45533"
|
||||
"checksum diff 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "3c2b69f912779fbb121ceb775d74d51e915af17aaebc38d28a592843a2dd0a3a"
|
||||
"checksum either 1.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "3be565ca5c557d7f59e7cfcf1844f9e3033650c929c6566f511e8005f205c1d0"
|
||||
"checksum itertools 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5b8467d9c1cebe26feb08c640139247fac215782d35371ade9a2136ed6085358"
|
||||
"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1"
|
||||
"checksum libc 0.2.45 (registry+https://github.com/rust-lang/crates.io-index)" = "2d2857ec59fadc0773853c664d2d18e7198e83883e7060b63c924cb077bd5c74"
|
||||
"checksum memchr 2.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "db4c41318937f6e76648f42826b1d9ade5c09cafb5aef7e351240a70f39206e9"
|
||||
"checksum redox_syscall 0.1.44 (registry+https://github.com/rust-lang/crates.io-index)" = "a84bcd297b87a545980a2d25a0beb72a1f490c31f0a9fde52fca35bfbb1ceb70"
|
||||
"checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76"
|
||||
"checksum regex 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "37e7cbbd370869ce2e8dff25c7018702d10b21a20ef7135316f8daecd6c25b7f"
|
||||
"checksum regex-syntax 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "4e47a2ed29da7a9e1960e1639e7a982e6edc6d49be308a3b02daf511504a16d1"
|
||||
"checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550"
|
||||
"checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096"
|
||||
"checksum textwrap 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)" = "307686869c93e71f94da64286f9a9524c0f308a9e1c87a583de8e9c9039ad3f6"
|
||||
"checksum thread_local 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b"
|
||||
"checksum ucd-util 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "535c204ee4d8434478593480b8f86ab45ec9aae0e83c568ca81abf0fd0e88f86"
|
||||
"checksum unicode-width 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "882386231c45df4700b275c7ff55b6f3698780a650026380e72dabe76fa46526"
|
||||
"checksum utf8-ranges 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "796f7e48bef87609f7ade7e06495a87d5cd06c7866e6a5cbfceffc558a243737"
|
||||
"checksum vec_map 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
|
||||
"checksum version_check 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
|
||||
"checksum winapi 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "92c1eb33641e276cfa214a0522acad57be5c56b10cb348b3c5117db75f3ac4b0"
|
||||
"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "difftastic"
|
||||
version = "0.1.0"
|
||||
authors = ["Wilfred Hughes <me@wilfred.me.uk>"]
|
||||
|
||||
[dependencies]
|
||||
regex = "1"
|
||||
colored = "1.6"
|
||||
diff = "0.1.11"
|
||||
clap = "2.32"
|
||||
itertools = "0.8.0"
|
||||
@ -0,0 +1,8 @@
|
||||
# Difftastic
|
||||
|
||||
A language-aware word-level diff.
|
||||
|
||||
## Related Projects
|
||||
|
||||
* http://fazzone.github.io/autochrome.html
|
||||
* https://github.com/janestreet/patdiff
|
||||
@ -0,0 +1,185 @@
|
||||
extern crate clap;
|
||||
extern crate colored;
|
||||
extern crate diff;
|
||||
extern crate itertools;
|
||||
extern crate regex;
|
||||
|
||||
use clap::{App, Arg};
|
||||
use colored::*;
|
||||
use itertools::EitherOrBoth::{Both, Left, Right};
|
||||
use itertools::Itertools;
|
||||
use regex::{Match, Regex};
|
||||
use std::cmp::max;
|
||||
use std::fs;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct Token {
|
||||
start: usize,
|
||||
// The actual token, e.g. "var"
|
||||
text: String,
|
||||
// Any trailing whitespace or comment before the next token.
|
||||
trivia: String,
|
||||
}
|
||||
|
||||
impl PartialEq for Token {
|
||||
fn eq(&self, other: &Token) -> bool {
|
||||
self.text == other.text
|
||||
}
|
||||
}
|
||||
|
||||
fn lex(src: &str, language: Option<&str>) -> Vec<Token> {
|
||||
let mut result = vec![];
|
||||
|
||||
let js_re = Regex::new(r#"//.+|[a-zA-Z0-9_]+|"(\\.|[^"\\])*"|[^ \t\n]"#).unwrap();
|
||||
let lisp_re = Regex::new(r#";.+|[a-zA-Z0-9_*!.-]+|"(\\.|[^"\\])*"|[^ \t\n]"#).unwrap();
|
||||
let css_re = Regex::new(r#"(?s)/\*.*?\*/|[a-zA-Z0-9_*!.-]+|"(\\.|[^"\\])*"|[^ \t\n]"#).unwrap();
|
||||
|
||||
let re = if language == Some("lisp") {
|
||||
lisp_re
|
||||
} else if language == Some("css") {
|
||||
css_re
|
||||
} else {
|
||||
js_re
|
||||
};
|
||||
|
||||
let mut prev: Option<Match> = None;
|
||||
for mat in re.find_iter(src) {
|
||||
if let Some(prev_match) = prev {
|
||||
let trivia = &src[prev_match.end()..mat.start()];
|
||||
result.push(Token {
|
||||
start: prev_match.end(),
|
||||
text: prev_match.as_str().to_string(),
|
||||
trivia: trivia.to_string(),
|
||||
});
|
||||
}
|
||||
prev = Some(mat);
|
||||
}
|
||||
if let Some(prev_match) = prev {
|
||||
let trivia = &src[prev_match.end()..src.len()];
|
||||
result.push(Token {
|
||||
start: prev_match.end(),
|
||||
text: prev_match.as_str().to_string(),
|
||||
trivia: trivia.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn highlight_changes(
|
||||
before_src: &str,
|
||||
after_src: &str,
|
||||
language: Option<&str>,
|
||||
) -> (String, String) {
|
||||
let before_tokens = lex(&before_src, language);
|
||||
let after_tokens = lex(&after_src, language);
|
||||
|
||||
let mut before_colored = String::with_capacity(before_src.len());
|
||||
let mut after_colored = String::with_capacity(after_src.len());
|
||||
for d in diff::slice(&before_tokens, &after_tokens) {
|
||||
match d {
|
||||
// Only present in the before, so has been removed.
|
||||
diff::Result::Left(l) => {
|
||||
before_colored.push_str(&l.text.red().to_string());
|
||||
before_colored.push_str(&l.trivia);
|
||||
}
|
||||
// Present in both.
|
||||
diff::Result::Both(l, r) => {
|
||||
before_colored.push_str(&l.text);
|
||||
before_colored.push_str(&l.trivia);
|
||||
after_colored.push_str(&r.text);
|
||||
after_colored.push_str(&r.trivia);
|
||||
}
|
||||
// Only present in the after.
|
||||
diff::Result::Right(r) => {
|
||||
after_colored.push_str(&r.text.green().to_string());
|
||||
after_colored.push_str(&r.trivia);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(before_colored, after_colored)
|
||||
}
|
||||
|
||||
fn max_line_length(s: &str) -> usize {
|
||||
let mut max_length = 0;
|
||||
for line in s.lines() {
|
||||
max_length = max(max_length, line.len());
|
||||
}
|
||||
|
||||
max_length
|
||||
}
|
||||
|
||||
// Ensure that every line in S in the same length, by appending spaces as necessary.
|
||||
fn pad_string(s: &str, min_length: usize) -> String {
|
||||
let mut result = String::with_capacity(s.len());
|
||||
for line in s.lines() {
|
||||
result.push_str(&format!("{:width$}\n", line, width = min_length));
|
||||
}
|
||||
result.push_str("\n");
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn vertical_concat(left: &str, right: &str, max_left_length: usize) -> String {
|
||||
let mut result = String::with_capacity(left.len() + right.len());
|
||||
|
||||
let spacer = " ";
|
||||
for item in left.lines().zip_longest(right.lines()) {
|
||||
match item {
|
||||
Both(left_line, right_line) if left_line != "" => {
|
||||
result.push_str(left_line);
|
||||
result.push_str(spacer);
|
||||
result.push_str(right_line);
|
||||
}
|
||||
Both(_, right_line) | Right(right_line) => {
|
||||
result.push_str(&" ".repeat(max_left_length));
|
||||
result.push_str(spacer);
|
||||
result.push_str(right_line);
|
||||
}
|
||||
Left(left_line) => {
|
||||
result.push_str(left_line);
|
||||
}
|
||||
}
|
||||
result.push_str("\n");
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let matches = App::new("Difftastic")
|
||||
.version("0.1")
|
||||
.about("A word level diff tool that understands syntax!")
|
||||
.author("Wilfred Hughes")
|
||||
.arg(
|
||||
Arg::with_name("language")
|
||||
.long("lang")
|
||||
.takes_value(true)
|
||||
.help("Override the language parser"),
|
||||
)
|
||||
.arg(Arg::with_name("first").index(1).required(true))
|
||||
.arg(Arg::with_name("second").index(2).required(true))
|
||||
.get_matches();
|
||||
|
||||
let before_path = matches.value_of("first").unwrap();
|
||||
let mut before_src = fs::read_to_string(before_path).expect("Unable to read PATH 1");
|
||||
let after_path = matches.value_of("second").unwrap();
|
||||
let mut after_src = fs::read_to_string(after_path).expect("Unable to read PATH 2");
|
||||
|
||||
let pad_to_length = max_line_length(&before_src);
|
||||
// Pad the left column, so the right column aligns. Do this before
|
||||
// diffing, so we can calculate the visible length correctly.
|
||||
before_src = pad_string(&before_src, pad_to_length);
|
||||
// Pad after too, so unchanged comments don't have differing
|
||||
// whitespace.
|
||||
after_src = pad_string(&after_src, pad_to_length);
|
||||
|
||||
let language = matches.value_of("language");
|
||||
let (before_colored, after_colored) = highlight_changes(&before_src, &after_src, language);
|
||||
|
||||
print!(
|
||||
"{}",
|
||||
vertical_concat(&before_colored, &after_colored, pad_to_length)
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue