Skip to content

Commit

Permalink
feat: add --write-ask option to prompt for each change
Browse files Browse the repository at this point in the history
  • Loading branch information
Bnyro committed Oct 11, 2024
1 parent 6802cc6 commit 5c2fec3
Show file tree
Hide file tree
Showing 5 changed files with 228 additions and 15 deletions.
68 changes: 67 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/typos-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ colorchoice-clap = "1.0.3"
serde_regex = "1.1.0"
regex = "1.10.4"
encoding_rs = "0.8.34"
dialoguer = "0.11.0"

[dev-dependencies]
assert_fs = "1.1"
Expand Down
4 changes: 4 additions & 0 deletions crates/typos-cli/src/bin/typos-cli/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions crates/typos-cli/src/bin/typos-cli/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,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 {
Expand Down
168 changes: 154 additions & 14 deletions crates/typos-cli/src/file.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use anyhow::Result;
use bstr::ByteSlice;
use dialoguer::{Confirm, Select};
use std::io::Read;
use std::io::Write;

Expand Down Expand Up @@ -102,7 +104,7 @@ impl FileChecker for FixTypos {
}
}
if !fixes.is_empty() || path == std::path::Path::new("-") {
let buffer = fix_buffer(buffer, fixes.into_iter());
let buffer = fix_buffer(buffer, fixes, None);
write_file(path, content_type, buffer, reporter)?;
}
}
Expand All @@ -127,12 +129,85 @@ impl FileChecker for FixTypos {
}
}
if !fixes.is_empty() {
let file_name = file_name.to_owned().into_bytes();
let new_name = fix_buffer(file_name, fixes.into_iter());
let new_name =
String::from_utf8(new_name).expect("corrections are valid utf-8");
let new_path = path.with_file_name(new_name);
std::fs::rename(path, new_path)?;
fix_file_name(path, file_name, fixes, None)?;
}
}
}

Ok(())
}
}

#[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 correction_indices = 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(),
};
reporter.report(msg.into())?;

if let Some(correction_index) = select_fix(&typo) {
fixes.push(typo);
correction_indices.push(correction_index);
}
}

if !fixes.is_empty() || path == std::path::Path::new("-") {
let buffer = fix_buffer(buffer, fixes, Some(correction_indices));
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();
let mut correction_indices = 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(),
};
reporter.report(msg.into())?;

if let Some(correction_index) = select_fix(&typo) {
fixes.push(typo);
correction_indices.push(correction_index);
}
}

if !fixes.is_empty() {
fix_file_name(path, file_name, fixes, Some(correction_indices))?;
}
}
}
Expand Down Expand Up @@ -179,7 +254,7 @@ impl FileChecker for DiffTypos {
}
}
if !fixes.is_empty() {
new_content = fix_buffer(buffer.clone(), fixes.into_iter());
new_content = fix_buffer(buffer.clone(), fixes, None);
content = buffer;
}
}
Expand All @@ -206,7 +281,7 @@ impl FileChecker for DiffTypos {
}
if !fixes.is_empty() {
let file_name = file_name.to_owned().into_bytes();
let new_name = fix_buffer(file_name, fixes.into_iter());
let new_name = fix_buffer(file_name, fixes, None);
let new_name =
String::from_utf8(new_name).expect("corrections are valid utf-8");
new_path = Some(path.with_file_name(new_name));
Expand Down Expand Up @@ -650,10 +725,24 @@ fn is_fixable(typo: &typos::Typo<'_>) -> bool {
extract_fix(typo).is_some()
}

fn fix_buffer(mut buffer: Vec<u8>, typos: impl Iterator<Item = typos::Typo<'static>>) -> Vec<u8> {
fn fix_buffer(
mut buffer: Vec<u8>,
typos: Vec<typos::Typo<'_>>,
correction_indices: Option<Vec<usize>>,
) -> Vec<u8> {
let mut offset = 0isize;
for typo in typos {
let fix = extract_fix(&typo).expect("Caller only provides fixable typos");
for typo_index in 0..typos.len() - 1 {
let typo = typos[typo_index].clone();
let correction_index = match correction_indices.clone() {
Some(correction_indices) => correction_indices[typo_index],
None => 0,
};

let fix = match &typo.corrections {
typos::Status::Corrections(c) => Some(c[correction_index].as_ref()),
_ => None,
}
.expect("Caller provided invalid fix index");
let start = ((typo.byte_offset as isize) + offset) as usize;
let end = start + typo.typo.len();

Expand All @@ -664,6 +753,56 @@ fn fix_buffer(mut buffer: Vec<u8>, typos: impl Iterator<Item = typos::Typo<'stat
buffer
}

fn fix_file_name<'a>(
path: &std::path::Path,
file_name: &'a str,
fixes: Vec<typos::Typo<'a>>,
correction_indices: Option<Vec<usize>>,
) -> Result<(), std::io::Error> {
let file_name = file_name.to_owned().into_bytes();
let new_name = fix_buffer(file_name, fixes, correction_indices);
let new_name = String::from_utf8(new_name).expect("corrections are valid utf-8");
let new_path = path.with_file_name(new_name);
std::fs::rename(path, new_path)?;
Ok(())
}

fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
let corrections = match &typo.corrections {
typos::Status::Corrections(c) => c,
_ => return None,
}
.clone();

if corrections.len() == 1 {
let confirmation = Confirm::new()
.with_prompt("Do you want to apply the fix suggested above?")
.default(true)
.show_default(true)
.interact()
.unwrap();

if confirmation {
return 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()
.unwrap();
if selection != 0 {
return Some(selection - 1);
}
}

None
}

pub fn walk_path(
walk: ignore::Walk,
checks: &dyn FileChecker,
Expand Down Expand Up @@ -777,8 +916,9 @@ mod test {
byte_offset,
typo: typo.into(),
corrections: typos::Status::Corrections(vec![correction.into()]),
});
let actual = fix_buffer(line, corrections);
})
.collect();
let actual = fix_buffer(line, corrections, None);
String::from_utf8(actual).unwrap()
}

Expand Down

0 comments on commit 5c2fec3

Please sign in to comment.