Skip to content

Commit 13b0a28

Browse files
committed
feat: add --interactive option to prompt for each change
1 parent 7222d7c commit 13b0a28

File tree

5 files changed

+191
-1
lines changed

5 files changed

+191
-1
lines changed

Cargo.lock

Lines changed: 67 additions & 1 deletion
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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ 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"
8081

8182
[dev-dependencies]
8283
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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
289289
&typos_cli::file::Identifiers
290290
} else if args.words {
291291
&typos_cli::file::Words
292+
} else if args.interactive {
293+
&typos_cli::file::Interactive
292294
} else if args.write_changes {
293295
&typos_cli::file::FixTypos
294296
} else if args.diff {

crates/typos-cli/src/file.rs

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use anyhow::Result;
12
use bstr::ByteSlice;
3+
use dialoguer::{Confirm, Select};
24
use std::io::Read;
35
use std::io::Write;
46

@@ -137,6 +139,85 @@ impl FileChecker for FixTypos {
137139
}
138140
}
139141

142+
#[derive(Debug, Clone, Copy)]
143+
pub struct Interactive;
144+
145+
impl FileChecker for Interactive {
146+
fn check_file(
147+
&self,
148+
path: &std::path::Path,
149+
explicit: bool,
150+
policy: &crate::policy::Policy<'_, '_, '_>,
151+
reporter: &dyn report::Report,
152+
) -> Result<(), std::io::Error> {
153+
if policy.check_files {
154+
let (buffer, content_type) = read_file(path, reporter)?;
155+
let bc = buffer.clone();
156+
if !explicit && !policy.binary && content_type.is_binary() {
157+
let msg = report::BinaryFile { path };
158+
reporter.report(msg.into())?;
159+
} else {
160+
let mut fixes = Vec::new();
161+
let mut correction_indices = Vec::new();
162+
163+
let mut accum_line_num = AccumulateLineNum::new();
164+
for typo in check_bytes(&bc, policy) {
165+
let line_num = accum_line_num.line_num(&buffer, typo.byte_offset);
166+
let (line, line_offset) = extract_line(&buffer, typo.byte_offset);
167+
let msg = report::Typo {
168+
context: Some(report::FileContext { path, line_num }.into()),
169+
buffer: std::borrow::Cow::Borrowed(line),
170+
byte_offset: line_offset,
171+
typo: typo.typo.as_ref(),
172+
corrections: typo.corrections.clone(),
173+
};
174+
reporter.report(msg.into())?;
175+
176+
if let Some(correction_index) = select_fix(&typo) {
177+
fixes.push(typo);
178+
correction_indices.push(correction_index);
179+
}
180+
}
181+
182+
if !fixes.is_empty() || path == std::path::Path::new("-") {
183+
let buffer = fix_buffer(buffer, fixes, Some(correction_indices));
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+
let mut correction_indices = Vec::new();
193+
194+
for typo in check_str(file_name, policy) {
195+
let msg = report::Typo {
196+
context: Some(report::PathContext { path }.into()),
197+
buffer: std::borrow::Cow::Borrowed(file_name.as_bytes()),
198+
byte_offset: typo.byte_offset,
199+
typo: typo.typo.as_ref(),
200+
corrections: typo.corrections.clone(),
201+
};
202+
reporter.report(msg.into())?;
203+
204+
if let Some(correction_index) = select_fix(&typo) {
205+
fixes.push(typo);
206+
correction_indices.push(correction_index);
207+
}
208+
}
209+
210+
if !fixes.is_empty() {
211+
let new_path = fix_file_name(path, file_name, fixes, Some(correction_indices))?;
212+
std::fs::rename(path, new_path)?;
213+
}
214+
}
215+
}
216+
217+
Ok(())
218+
}
219+
}
220+
140221
#[derive(Debug, Clone, Copy)]
141222
pub struct DiffTypos;
142223

@@ -683,6 +764,42 @@ fn fix_file_name<'a>(
683764
Ok(new_path)
684765
}
685766

767+
fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
768+
let corrections = match &typo.corrections {
769+
typos::Status::Corrections(c) => c,
770+
_ => return None,
771+
}
772+
.clone();
773+
774+
if corrections.len() == 1 {
775+
let confirmation = Confirm::new()
776+
.with_prompt("Do you want to apply the fix suggested above?")
777+
.default(true)
778+
.show_default(true)
779+
.interact()
780+
.unwrap();
781+
782+
if confirmation {
783+
return Some(0);
784+
}
785+
} else {
786+
let mut items = corrections.clone();
787+
788+
items.insert(0, std::borrow::Cow::from("None (skip)"));
789+
let selection = Select::new()
790+
.with_prompt("Please choose one of the following suggestions")
791+
.items(&items)
792+
.default(0)
793+
.interact()
794+
.unwrap();
795+
if selection != 0 {
796+
return Some(selection - 1);
797+
}
798+
}
799+
800+
None
801+
}
802+
686803
pub fn walk_path(
687804
walk: ignore::Walk,
688805
checks: &dyn FileChecker,

0 commit comments

Comments
 (0)