feat: add --interactive option to prompt for each change

This commit is contained in:
Bnyro 2024-10-11 13:07:49 +02:00
parent 85ec8ca2c8
commit b435635f0e
5 changed files with 228 additions and 4 deletions

104
Cargo.lock generated
View file

@ -179,6 +179,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.18" version = "4.5.18"
@ -275,6 +281,19 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf0a07a401f374238ab8e2f11a104d2851bf9ce711ec69804834de8af45c7af" 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]] [[package]]
name = "content_inspector" name = "content_inspector"
version = "0.2.4" version = "0.2.4"
@ -338,6 +357,16 @@ dependencies = [
"memchr", "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]] [[package]]
name = "darling" name = "darling"
version = "0.20.3" version = "0.20.3"
@ -406,6 +435,19 @@ dependencies = [
"syn", "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]] [[package]]
name = "dictgen" name = "dictgen"
version = "0.2.9" version = "0.2.9"
@ -477,6 +519,12 @@ version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
[[package]]
name = "encode_unicode"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.34" version = "0.8.34"
@ -731,10 +779,16 @@ dependencies = [
] ]
[[package]] [[package]]
name = "libc" name = "lazy_static"
version = "0.2.149" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" 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]] [[package]]
name = "linux-raw-sys" name = "linux-raw-sys"
@ -790,6 +844,18 @@ dependencies = [
"unicase", "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]] [[package]]
name = "normalize-line-endings" name = "normalize-line-endings"
version = "0.3.0" version = "0.3.0"
@ -1117,6 +1183,12 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "shell-words"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde"
[[package]] [[package]]
name = "shlex" name = "shlex"
version = "1.2.0" version = "1.2.0"
@ -1348,9 +1420,12 @@ dependencies = [
"clap", "clap",
"clap-verbosity-flag", "clap-verbosity-flag",
"colorchoice-clap", "colorchoice-clap",
"console",
"content_inspector", "content_inspector",
"ctrlc",
"derive_more", "derive_more",
"derive_setters", "derive_setters",
"dialoguer",
"difflib", "difflib",
"divan", "divan",
"encoding_rs", "encoding_rs",
@ -1378,7 +1453,7 @@ dependencies = [
"unic-emoji-char", "unic-emoji-char",
"unicase", "unicase",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width 0.2.0",
"varcon-core", "varcon-core",
] ]
@ -1473,6 +1548,12 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.2.0" version = "0.2.0"
@ -1600,6 +1681,15 @@ dependencies = [
"windows-targets 0.48.5", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.59.0" version = "0.59.0"
@ -1758,3 +1848,9 @@ dependencies = [
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"

View file

@ -77,6 +77,9 @@ colorchoice-clap = "1.0.3"
serde_regex = "1.1.0" serde_regex = "1.1.0"
regex = "1.10.4" regex = "1.10.4"
encoding_rs = "0.8.34" encoding_rs = "0.8.34"
dialoguer = "0.11.0"
console = "0.15.8"
ctrlc = "3.4.5"
[dev-dependencies] [dev-dependencies]
assert_fs = "1.1" assert_fs = "1.1"

View file

@ -67,6 +67,10 @@ pub(crate) struct Args {
#[arg(long, short = 'w', group = "mode", help_heading = "Mode")] #[arg(long, short = 'w', group = "mode", help_heading = "Mode")]
pub(crate) write_changes: bool, 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. /// Debug: Print each file that would be spellchecked.
#[arg(long, group = "mode", help_heading = "Mode")] #[arg(long, group = "mode", help_heading = "Mode")]
pub(crate) files: bool, pub(crate) files: bool,

View file

@ -32,6 +32,14 @@ fn run() -> proc_exit::ExitResult {
init_logging(args.verbose.log_level()); 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() { if let Some(output_path) = args.dump_config.as_ref() {
run_dump_config(&args, output_path) run_dump_config(&args, output_path)
} else if args.type_list { } else if args.type_list {
@ -289,6 +297,8 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
&typos_cli::file::Identifiers &typos_cli::file::Identifiers
} else if args.words { } else if args.words {
&typos_cli::file::Words &typos_cli::file::Words
} else if args.interactive {
&typos_cli::file::Interactive
} else if args.write_changes { } else if args.write_changes {
&typos_cli::file::FixTypos &typos_cli::file::FixTypos
} else if args.diff { } else if args.diff {

View file

@ -1,4 +1,5 @@
use bstr::ByteSlice; use bstr::ByteSlice;
use dialoguer::{Confirm, Select};
use std::io::Read; use std::io::Read;
use std::io::Write; 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)] #[derive(Debug, Clone, Copy)]
pub struct DiffTypos; pub struct DiffTypos;
@ -675,6 +757,35 @@ fn fix_file_name<'a>(
Ok(new_path) Ok(new_path)
} }
fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
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( pub fn walk_path(
walk: ignore::Walk, walk: ignore::Walk,
checks: &dyn FileChecker, checks: &dyn FileChecker,