Skip to content

Commit c83b305

Browse files
committed
feat: add --write-ask option to prompt for each change
1 parent 6802cc6 commit c83b305

File tree

5 files changed

+239
-7
lines changed

5 files changed

+239
-7
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 = 'a', group = "mode", help_heading = "Mode")]
72+
pub(crate) write_ask: 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.write_ask {
293+
&typos_cli::file::AskFixTypos
292294
} else if args.write_changes {
293295
&typos_cli::file::FixTypos
294296
} else if args.diff {

crates/typos-cli/src/file.rs

Lines changed: 165 additions & 6 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

@@ -127,12 +129,85 @@ impl FileChecker for FixTypos {
127129
}
128130
}
129131
if !fixes.is_empty() {
130-
let file_name = file_name.to_owned().into_bytes();
131-
let new_name = fix_buffer(file_name, fixes.into_iter());
132-
let new_name =
133-
String::from_utf8(new_name).expect("corrections are valid utf-8");
134-
let new_path = path.with_file_name(new_name);
135-
std::fs::rename(path, new_path)?;
132+
fix_file_name(path, &file_name.to_owned(), fixes)?;
133+
}
134+
}
135+
}
136+
137+
Ok(())
138+
}
139+
}
140+
141+
#[derive(Debug, Clone, Copy)]
142+
pub struct AskFixTypos;
143+
144+
impl FileChecker for AskFixTypos {
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+
reporter.report(msg.into())?;
173+
174+
match select_fix(&typo) {
175+
Some(correction_index) => fixes.push((typo, correction_index)),
176+
None => (),
177+
}
178+
179+
println!("\n");
180+
}
181+
182+
if !fixes.is_empty() || path == std::path::Path::new("-") {
183+
let buffer = fix_buffer_with_correction_index(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+
reporter.report(msg.into())?;
202+
203+
match select_fix(&typo) {
204+
Some(correction_index) => fixes.push((typo, correction_index)),
205+
None => (),
206+
}
207+
}
208+
209+
if !fixes.is_empty() {
210+
fix_file_name_with_correction_index(path, &file_name, fixes)?;
136211
}
137212
}
138213
}
@@ -650,6 +725,40 @@ fn is_fixable(typo: &typos::Typo<'_>) -> bool {
650725
extract_fix(typo).is_some()
651726
}
652727

728+
fn fix_buffer_with_correction_index<'a>(
729+
mut buffer: Vec<u8>,
730+
typos: impl Iterator<Item = (typos::Typo<'a>, usize)>,
731+
) -> Vec<u8> {
732+
let mut offset = 0isize;
733+
for (typo, correction_index) in typos {
734+
let fix = match &typo.corrections {
735+
typos::Status::Corrections(c) => Some(c[correction_index].as_ref()),
736+
_ => None,
737+
}
738+
.expect("Caller provided invalid fix index");
739+
let start = ((typo.byte_offset as isize) + offset) as usize;
740+
let end = start + typo.typo.len();
741+
742+
buffer.splice(start..end, fix.as_bytes().iter().copied());
743+
744+
offset += (fix.len() as isize) - (typo.typo.len() as isize);
745+
}
746+
buffer
747+
}
748+
749+
fn fix_file_name_with_correction_index<'a>(
750+
path: &std::path::Path,
751+
file_name: &'a str,
752+
fixes: Vec<(typos::Typo<'a>, usize)>,
753+
) -> Result<(), std::io::Error> {
754+
let file_name = file_name.to_owned().into_bytes();
755+
let new_name = fix_buffer_with_correction_index(file_name, fixes.into_iter());
756+
let new_name = String::from_utf8(new_name).expect("corrections are valid utf-8");
757+
let new_path = path.with_file_name(new_name);
758+
std::fs::rename(path, new_path)?;
759+
Ok(())
760+
}
761+
653762
fn fix_buffer(mut buffer: Vec<u8>, typos: impl Iterator<Item = typos::Typo<'static>>) -> Vec<u8> {
654763
let mut offset = 0isize;
655764
for typo in typos {
@@ -664,6 +773,56 @@ fn fix_buffer(mut buffer: Vec<u8>, typos: impl Iterator<Item = typos::Typo<'stat
664773
buffer
665774
}
666775

776+
fn fix_file_name<'a>(
777+
path: &std::path::Path,
778+
file_name: &'a str,
779+
fixes: Vec<typos::Typo<'static>>,
780+
) -> Result<(), std::io::Error> {
781+
let file_name = file_name.to_owned().into_bytes();
782+
let new_name = fix_buffer(file_name, fixes.into_iter());
783+
let new_name = String::from_utf8(new_name).expect("corrections are valid utf-8");
784+
let new_path = path.with_file_name(new_name);
785+
std::fs::rename(path, new_path)?;
786+
Ok(())
787+
}
788+
789+
fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
790+
if is_fixable(&typo) {
791+
let confirmation = Confirm::new()
792+
.with_prompt("Do you want to apply the fix suggested above?")
793+
.default(true)
794+
.show_default(true)
795+
.interact()
796+
.unwrap();
797+
798+
if confirmation {
799+
return Some(0);
800+
}
801+
} else {
802+
let mut items = match &typo.corrections {
803+
typos::Status::Corrections(c) => c,
804+
_ => return None,
805+
}
806+
.clone();
807+
items.insert(0, std::borrow::Cow::from("None (skip)"));
808+
809+
let selection = Select::new()
810+
.with_prompt("Please choose one of the following suggestions")
811+
.items(&items)
812+
.default(0)
813+
.interact()
814+
.unwrap();
815+
816+
if selection == 0 {
817+
return None;
818+
}
819+
820+
return Some(selection - 1);
821+
}
822+
823+
None
824+
}
825+
667826
pub fn walk_path(
668827
walk: ignore::Walk,
669828
checks: &dyn FileChecker,

0 commit comments

Comments
 (0)