diff --git a/Cargo.lock b/Cargo.lock index 0375887..b73102e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -179,6 +179,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "clap" version = "4.5.18" @@ -275,6 +281,19 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" +[[package]] +name = "console" +version = "0.15.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" +dependencies = [ + "encode_unicode", + "lazy_static", + "libc", + "unicode-width 0.1.14", + "windows-sys 0.52.0", +] + [[package]] name = "content_inspector" version = "0.2.4" @@ -338,6 +357,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctrlc" +version = "3.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90eeab0aa92f3f9b4e87f258c72b139c207d251f9cbc1080a0086b86a8870dd3" +dependencies = [ + "nix", + "windows-sys 0.59.0", +] + [[package]] name = "darling" version = "0.20.3" @@ -406,6 +435,19 @@ dependencies = [ "syn", ] +[[package]] +name = "dialoguer" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" +dependencies = [ + "console", + "shell-words", + "tempfile", + "thiserror", + "zeroize", +] + [[package]] name = "dictgen" version = "0.2.9" @@ -477,6 +519,12 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + [[package]] name = "encoding_rs" version = "0.8.34" @@ -731,10 +779,16 @@ dependencies = [ ] [[package]] -name = "libc" -version = "0.2.149" +name = "lazy_static" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a08173bc88b7955d1b3145aa561539096c421ac8debde8cbc3612ec635fee29b" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.161" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "linux-raw-sys" @@ -790,6 +844,18 @@ dependencies = [ "unicase", ] +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.4.1", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -1117,6 +1183,12 @@ dependencies = [ "serde", ] +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + [[package]] name = "shlex" version = "1.2.0" @@ -1348,9 +1420,12 @@ dependencies = [ "clap", "clap-verbosity-flag", "colorchoice-clap", + "console", "content_inspector", + "ctrlc", "derive_more", "derive_setters", + "dialoguer", "difflib", "divan", "encoding_rs", @@ -1378,7 +1453,7 @@ dependencies = [ "unic-emoji-char", "unicase", "unicode-segmentation", - "unicode-width", + "unicode-width 0.2.0", "varcon-core", ] @@ -1473,6 +1548,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" @@ -1600,6 +1681,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.59.0" @@ -1758,3 +1848,9 @@ dependencies = [ "quote", "syn", ] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/crates/typos-cli/Cargo.toml b/crates/typos-cli/Cargo.toml index 65d58c3..937b956 100644 --- a/crates/typos-cli/Cargo.toml +++ b/crates/typos-cli/Cargo.toml @@ -77,6 +77,9 @@ colorchoice-clap = "1.0.3" serde_regex = "1.1.0" regex = "1.10.4" encoding_rs = "0.8.34" +dialoguer = "0.11.0" +console = "0.15.8" +ctrlc = "3.4.5" [dev-dependencies] assert_fs = "1.1" diff --git a/crates/typos-cli/src/bin/typos-cli/args.rs b/crates/typos-cli/src/bin/typos-cli/args.rs index 3541533..4d00d24 100644 --- a/crates/typos-cli/src/bin/typos-cli/args.rs +++ b/crates/typos-cli/src/bin/typos-cli/args.rs @@ -67,6 +67,10 @@ pub(crate) struct Args { #[arg(long, short = 'w', group = "mode", help_heading = "Mode")] pub(crate) write_changes: bool, + /// Prompt for each suggested correction whether to write the fix + #[arg(long, short = 'i', group = "mode", help_heading = "Mode")] + pub(crate) interactive: bool, + /// Debug: Print each file that would be spellchecked. #[arg(long, group = "mode", help_heading = "Mode")] pub(crate) files: bool, diff --git a/crates/typos-cli/src/bin/typos-cli/main.rs b/crates/typos-cli/src/bin/typos-cli/main.rs index 9dd8773..38e5037 100644 --- a/crates/typos-cli/src/bin/typos-cli/main.rs +++ b/crates/typos-cli/src/bin/typos-cli/main.rs @@ -32,6 +32,14 @@ fn run() -> proc_exit::ExitResult { init_logging(args.verbose.log_level()); + // HACK: Ensure the terminal gets reset to a good state if the user hits ctrl-c during a prompt + // https://github.com/console-rs/dialoguer/issues/294 + ctrlc::set_handler(move || { + let _ = console::Term::stdout().show_cursor(); + std::process::exit(130); + }) + .expect("Failed to set handler for Ctrl+C needed to restore terminal defaults after killing the process"); + if let Some(output_path) = args.dump_config.as_ref() { run_dump_config(&args, output_path) } else if args.type_list { @@ -289,6 +297,8 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { &typos_cli::file::Identifiers } else if args.words { &typos_cli::file::Words + } else if args.interactive { + &typos_cli::file::Interactive } else if args.write_changes { &typos_cli::file::FixTypos } else if args.diff { diff --git a/crates/typos-cli/src/file.rs b/crates/typos-cli/src/file.rs index 7955d14..81ad231 100644 --- a/crates/typos-cli/src/file.rs +++ b/crates/typos-cli/src/file.rs @@ -1,4 +1,5 @@ use bstr::ByteSlice; +use dialoguer::{Confirm, Select}; use std::io::Read; use std::io::Write; @@ -137,6 +138,87 @@ impl FileChecker for FixTypos { } } +#[derive(Debug, Clone, Copy)] +pub struct Interactive; + +impl FileChecker for Interactive { + fn check_file( + &self, + path: &std::path::Path, + explicit: bool, + policy: &crate::policy::Policy<'_, '_, '_>, + reporter: &dyn report::Report, + ) -> Result<(), std::io::Error> { + if policy.check_files { + let (buffer, content_type) = read_file(path, reporter)?; + let bc = buffer.clone(); + if !explicit && !policy.binary && content_type.is_binary() { + let msg = report::BinaryFile { path }; + reporter.report(msg.into())?; + } else { + let mut fixes = Vec::new(); + + let mut accum_line_num = AccumulateLineNum::new(); + for typo in check_bytes(&bc, policy) { + let line_num = accum_line_num.line_num(&buffer, typo.byte_offset); + let (line, line_offset) = extract_line(&buffer, typo.byte_offset); + let msg = report::Typo { + context: Some(report::FileContext { path, line_num }.into()), + buffer: std::borrow::Cow::Borrowed(line), + byte_offset: line_offset, + typo: typo.typo.as_ref(), + corrections: typo.corrections.clone(), + }; + // HACK: we use the reporter to display the possible corrections to the user + // this will be looking very ugly with the format set to anything else than json + // technically we should only report typos when not correcting + reporter.report(msg.into())?; + + if let Some(correction_index) = select_fix(&typo) { + fixes.push((typo, correction_index)); + } + } + + if !fixes.is_empty() || path == std::path::Path::new("-") { + let buffer = fix_buffer(buffer, fixes.into_iter()); + write_file(path, content_type, buffer, reporter)?; + } + } + } + + if policy.check_filenames { + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { + let mut fixes = Vec::new(); + + for typo in check_str(file_name, policy) { + let msg = report::Typo { + context: Some(report::PathContext { path }.into()), + buffer: std::borrow::Cow::Borrowed(file_name.as_bytes()), + byte_offset: typo.byte_offset, + typo: typo.typo.as_ref(), + corrections: typo.corrections.clone(), + }; + // HACK: we use the reporter to display the possible corrections to the user + // this will be looking very ugly with the format set to anything else than json + // technically we should only report typos when not correcting + reporter.report(msg.into())?; + + if let Some(correction_index) = select_fix(&typo) { + fixes.push((typo, correction_index)); + } + } + + if !fixes.is_empty() { + let new_path = fix_file_name(path, file_name, fixes.into_iter())?; + std::fs::rename(path, new_path)?; + } + } + } + + Ok(()) + } +} + #[derive(Debug, Clone, Copy)] pub struct DiffTypos; @@ -675,6 +757,35 @@ fn fix_file_name<'a>( Ok(new_path) } +fn select_fix(typo: &typos::Typo<'_>) -> Option { + let corrections = match &typo.corrections { + typos::Status::Corrections(c) => c, + _ => return None, + }; + + if corrections.len() == 1 { + Confirm::new() + .with_prompt("Do you want to apply the fix suggested above?") + .default(true) + .show_default(true) + .interact() + .ok()?; + + Some(0) + } else { + let mut items = corrections.clone(); + + items.insert(0, std::borrow::Cow::from("None (skip)")); + let selection = Select::new() + .with_prompt("Please choose one of the following suggestions") + .items(&items) + .default(0) + .interact() + .ok()?; + selection.checked_sub(1) + } +} + pub fn walk_path( walk: ignore::Walk, checks: &dyn FileChecker,