Skip to content

Commit a843d91

Browse files
committed
feat: add --interactive option to prompt for each change
1 parent 85ec8ca commit a843d91

File tree

5 files changed

+227
-3
lines changed

5 files changed

+227
-3
lines changed

Cargo.lock

Lines changed: 99 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/typos-cli/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ colorchoice-clap = "1.0.3"
7777
serde_regex = "1.1.0"
7878
regex = "1.10.4"
7979
encoding_rs = "0.8.34"
80+
dialoguer = "0.11.0"
81+
console = "0.15.8"
82+
ctrlc = "3.4.5"
8083

8184
[dev-dependencies]
8285
assert_fs = "1.1"

crates/typos-cli/src/bin/typos-cli/args.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ pub(crate) struct Args {
6767
#[arg(long, short = 'w', group = "mode", help_heading = "Mode")]
6868
pub(crate) write_changes: bool,
6969

70+
/// Prompt for each suggested correction whether to write the fix
71+
#[arg(long, short = 'i', group = "mode", help_heading = "Mode")]
72+
pub(crate) interactive: bool,
73+
7074
/// Debug: Print each file that would be spellchecked.
7175
#[arg(long, group = "mode", help_heading = "Mode")]
7276
pub(crate) files: bool,

crates/typos-cli/src/bin/typos-cli/main.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ fn run() -> proc_exit::ExitResult {
3232

3333
init_logging(args.verbose.log_level());
3434

35+
// HACK: Ensure the terminal gets reset to a good state if the user hits ctrl-c during a prompt
36+
// https://github.com/console-rs/dialoguer/issues/294
37+
ctrlc::set_handler(move || {
38+
let _ = console::Term::stdout().show_cursor();
39+
std::process::exit(0);
40+
})
41+
.expect("Failed to set handler for Ctrl+C needed to restore terminal defaults after killing the process");
42+
3543
if let Some(output_path) = args.dump_config.as_ref() {
3644
run_dump_config(&args, output_path)
3745
} else if args.type_list {
@@ -289,6 +297,8 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
289297
&typos_cli::file::Identifiers
290298
} else if args.words {
291299
&typos_cli::file::Words
300+
} else if args.interactive {
301+
&typos_cli::file::Interactive
292302
} else if args.write_changes {
293303
&typos_cli::file::FixTypos
294304
} else if args.diff {

crates/typos-cli/src/file.rs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use bstr::ByteSlice;
2+
use dialoguer::{Confirm, Select};
23
use std::io::Read;
34
use std::io::Write;
45

@@ -137,6 +138,87 @@ impl FileChecker for FixTypos {
137138
}
138139
}
139140

141+
#[derive(Debug, Clone, Copy)]
142+
pub struct Interactive;
143+
144+
impl FileChecker for Interactive {
145+
fn check_file(
146+
&self,
147+
path: &std::path::Path,
148+
explicit: bool,
149+
policy: &crate::policy::Policy<'_, '_, '_>,
150+
reporter: &dyn report::Report,
151+
) -> Result<(), std::io::Error> {
152+
if policy.check_files {
153+
let (buffer, content_type) = read_file(path, reporter)?;
154+
let bc = buffer.clone();
155+
if !explicit && !policy.binary && content_type.is_binary() {
156+
let msg = report::BinaryFile { path };
157+
reporter.report(msg.into())?;
158+
} else {
159+
let mut fixes = Vec::new();
160+
161+
let mut accum_line_num = AccumulateLineNum::new();
162+
for typo in check_bytes(&bc, policy) {
163+
let line_num = accum_line_num.line_num(&buffer, typo.byte_offset);
164+
let (line, line_offset) = extract_line(&buffer, typo.byte_offset);
165+
let msg = report::Typo {
166+
context: Some(report::FileContext { path, line_num }.into()),
167+
buffer: std::borrow::Cow::Borrowed(line),
168+
byte_offset: line_offset,
169+
typo: typo.typo.as_ref(),
170+
corrections: typo.corrections.clone(),
171+
};
172+
// HACK: we use the reporter to display the possible corrections to the user
173+
// this will be looking very ugly with the format set to anything else than json
174+
// technically we should only report typos when not correcting
175+
reporter.report(msg.into())?;
176+
177+
if let Some(correction_index) = select_fix(&typo) {
178+
fixes.push((typo, correction_index));
179+
}
180+
}
181+
182+
if !fixes.is_empty() || path == std::path::Path::new("-") {
183+
let buffer = fix_buffer(buffer, fixes.into_iter());
184+
write_file(path, content_type, buffer, reporter)?;
185+
}
186+
}
187+
}
188+
189+
if policy.check_filenames {
190+
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
191+
let mut fixes = Vec::new();
192+
193+
for typo in check_str(file_name, policy) {
194+
let msg = report::Typo {
195+
context: Some(report::PathContext { path }.into()),
196+
buffer: std::borrow::Cow::Borrowed(file_name.as_bytes()),
197+
byte_offset: typo.byte_offset,
198+
typo: typo.typo.as_ref(),
199+
corrections: typo.corrections.clone(),
200+
};
201+
// HACK: we use the reporter to display the possible corrections to the user
202+
// this will be looking very ugly with the format set to anything else than json
203+
// technically we should only report typos when not correcting
204+
reporter.report(msg.into())?;
205+
206+
if let Some(correction_index) = select_fix(&typo) {
207+
fixes.push((typo, correction_index));
208+
}
209+
}
210+
211+
if !fixes.is_empty() {
212+
let new_path = fix_file_name(path, file_name, fixes.into_iter())?;
213+
std::fs::rename(path, new_path)?;
214+
}
215+
}
216+
}
217+
218+
Ok(())
219+
}
220+
}
221+
140222
#[derive(Debug, Clone, Copy)]
141223
pub struct DiffTypos;
142224

@@ -675,6 +757,35 @@ fn fix_file_name<'a>(
675757
Ok(new_path)
676758
}
677759

760+
fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
761+
let corrections = match &typo.corrections {
762+
typos::Status::Corrections(c) => c,
763+
_ => return None,
764+
};
765+
766+
if corrections.len() == 1 {
767+
Confirm::new()
768+
.with_prompt("Do you want to apply the fix suggested above?")
769+
.default(true)
770+
.show_default(true)
771+
.interact()
772+
.ok()?;
773+
774+
Some(0)
775+
} else {
776+
let mut items = corrections.clone();
777+
778+
items.insert(0, std::borrow::Cow::from("None (skip)"));
779+
let selection = Select::new()
780+
.with_prompt("Please choose one of the following suggestions")
781+
.items(&items)
782+
.default(0)
783+
.interact()
784+
.ok()?;
785+
selection.checked_sub(1)
786+
}
787+
}
788+
678789
pub fn walk_path(
679790
walk: ignore::Walk,
680791
checks: &dyn FileChecker,

0 commit comments

Comments
 (0)