diff --git a/Cargo.lock b/Cargo.lock index 27f7a71..696db77 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -291,6 +291,12 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "doc-comment" version = "0.3.3" @@ -1078,6 +1084,7 @@ dependencies = [ "bstr", "clap", "clap-verbosity-flag", + "difflib", "env_logger 0.8.1", "ignore", "log", diff --git a/Cargo.toml b/Cargo.toml index 0ffd1af..f87e14c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,6 +47,7 @@ log = "0.4" env_logger = "0.8" bstr = "0.2" ahash = "0.5.8" +difflib = "0.4" [dev-dependencies] assert_fs = "1.0" diff --git a/src/args.rs b/src/args.rs index 2bab458..f9d2b0f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -54,6 +54,10 @@ pub(crate) struct Args { /// Ignore implicit configuration files. pub(crate) isolated: bool, + #[structopt(long)] + /// Print a diff of what would change + pub(crate) diff: bool, + #[structopt(long, short = "w")] /// Write corrections out pub(crate) write_changes: bool, diff --git a/src/diff.rs b/src/diff.rs new file mode 100644 index 0000000..a49ae13 --- /dev/null +++ b/src/diff.rs @@ -0,0 +1,93 @@ +use std::collections::BTreeMap; +use std::sync; + +use bstr::ByteSlice; + +pub struct Diff<'r> { + reporter: &'r dyn typos::report::Report, + deferred: sync::Mutex, +} + +impl<'r> Diff<'r> { + pub(crate) fn new(reporter: &'r dyn typos::report::Report) -> Self { + Self { + reporter, + deferred: sync::Mutex::new(crate::replace::Deferred::default()), + } + } + + pub fn show(&self) -> Result<(), std::io::Error> { + let deferred = self.deferred.lock().unwrap(); + + for (path, corrections) in deferred.content.iter() { + let buffer = std::fs::read(path)?; + + let mut original = Vec::new(); + let mut corrected = Vec::new(); + for (line_idx, line) in buffer.lines_with_terminator().enumerate() { + original.push(String::from_utf8_lossy(line).into_owned()); + + let line_num = line_idx + 1; + let line = if let Some(corrections) = corrections.get(&line_num) { + let line = line.to_vec(); + crate::replace::correct(line, &corrections) + } else { + line.to_owned() + }; + corrected.push(String::from_utf8_lossy(&line).into_owned()) + } + + let display_path = path.display().to_string(); + let diff = difflib::unified_diff( + &original, + &corrected, + display_path.as_str(), + display_path.as_str(), + "original", + "corrected", + 0, + ); + for line in diff { + print!("{}", line); + } + } + + Ok(()) + } +} + +impl<'r> typos::report::Report for Diff<'r> { + fn report(&self, msg: typos::report::Message<'_>) -> bool { + let typo = match &msg { + typos::report::Message::Typo(typo) => typo, + _ => return self.reporter.report(msg), + }; + + let corrections = match &typo.corrections { + typos::Status::Corrections(corrections) if corrections.len() == 1 => corrections, + _ => return self.reporter.report(msg), + }; + + match &typo.context { + Some(typos::report::Context::File(file)) => { + let path = file.path.to_owned(); + let line_num = file.line_num; + let correction = crate::replace::Correction::new( + typo.byte_offset, + typo.typo, + corrections[0].as_ref(), + ); + let mut deferred = self.deferred.lock().unwrap(); + let content = deferred + .content + .entry(path) + .or_insert_with(BTreeMap::new) + .entry(line_num) + .or_insert_with(Vec::new); + content.push(correction); + false + } + _ => msg.is_correction(), + } + } +} diff --git a/src/main.rs b/src/main.rs index 5206c26..00ce2e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,6 +10,7 @@ mod args; mod checks; mod config; mod dict; +mod diff; mod replace; fn main() { @@ -86,7 +87,10 @@ fn run() -> Result { let mut reporter = args.format.reporter(); let replace_reporter = replace::Replace::new(reporter); - if args.write_changes { + let diff_reporter = diff::Diff::new(reporter); + if args.diff { + reporter = &diff_reporter; + } else if args.write_changes { reporter = &replace_reporter; } @@ -129,7 +133,9 @@ fn run() -> Result { errors_found = true; } - if args.write_changes { + if args.diff { + diff_reporter.show()?; + } else if args.write_changes { replace_reporter.write()?; } } diff --git a/src/replace.rs b/src/replace.rs index ae1f101..aef418a 100644 --- a/src/replace.rs +++ b/src/replace.rs @@ -97,20 +97,20 @@ impl<'r> typos::report::Report for Replace<'r> { } #[derive(Clone, Debug, Default)] -struct Deferred { - content: BTreeMap>>, - paths: BTreeMap>, +pub(crate) struct Deferred { + pub(crate) content: BTreeMap>>, + pub(crate) paths: BTreeMap>, } #[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] -struct Correction { +pub(crate) struct Correction { pub byte_offset: usize, pub typo: Vec, pub correction: Vec, } impl Correction { - fn new(byte_offset: usize, typo: &str, correction: &str) -> Self { + pub(crate) fn new(byte_offset: usize, typo: &str, correction: &str) -> Self { Self { byte_offset, typo: typo.as_bytes().to_vec(), @@ -119,7 +119,7 @@ impl Correction { } } -fn correct(mut line: Vec, corrections: &[Correction]) -> Vec { +pub(crate) fn correct(mut line: Vec, corrections: &[Correction]) -> Vec { let mut corrections: Vec<_> = corrections.iter().collect(); corrections.sort_unstable(); corrections.reverse();