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 10, 2024
1 parent 6802cc6 commit c83b305
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 7 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 = 'a', group = "mode", help_heading = "Mode")]
pub(crate) write_ask: 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.write_ask {
&typos_cli::file::AskFixTypos
} else if args.write_changes {
&typos_cli::file::FixTypos
} else if args.diff {
Expand Down
171 changes: 165 additions & 6 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 @@ -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.to_owned(), fixes)?;
}
}
}

Ok(())
}
}

#[derive(Debug, Clone, Copy)]
pub struct AskFixTypos;

impl FileChecker for AskFixTypos {
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(),
};
reporter.report(msg.into())?;

match select_fix(&typo) {
Some(correction_index) => fixes.push((typo, correction_index)),
None => (),
}

println!("\n");
}

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

match select_fix(&typo) {
Some(correction_index) => fixes.push((typo, correction_index)),
None => (),
}
}

if !fixes.is_empty() {
fix_file_name_with_correction_index(path, &file_name, fixes)?;
}
}
}
Expand Down Expand Up @@ -650,6 +725,40 @@ fn is_fixable(typo: &typos::Typo<'_>) -> bool {
extract_fix(typo).is_some()
}

fn fix_buffer_with_correction_index<'a>(
mut buffer: Vec<u8>,
typos: impl Iterator<Item = (typos::Typo<'a>, usize)>,
) -> Vec<u8> {
let mut offset = 0isize;
for (typo, correction_index) in typos {
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();

buffer.splice(start..end, fix.as_bytes().iter().copied());

offset += (fix.len() as isize) - (typo.typo.len() as isize);
}
buffer
}

fn fix_file_name_with_correction_index<'a>(
path: &std::path::Path,
file_name: &'a str,
fixes: Vec<(typos::Typo<'a>, usize)>,
) -> Result<(), std::io::Error> {
let file_name = file_name.to_owned().into_bytes();
let new_name = fix_buffer_with_correction_index(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)?;
Ok(())
}

fn fix_buffer(mut buffer: Vec<u8>, typos: impl Iterator<Item = typos::Typo<'static>>) -> Vec<u8> {
let mut offset = 0isize;
for typo in typos {
Expand All @@ -664,6 +773,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<'static>>,
) -> Result<(), std::io::Error> {
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)?;
Ok(())
}

fn select_fix(typo: &typos::Typo<'_>) -> Option<usize> {
if is_fixable(&typo) {
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 = match &typo.corrections {
typos::Status::Corrections(c) => c,
_ => return None,
}
.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 None;
}

return Some(selection - 1);
}

None
}

pub fn walk_path(
walk: ignore::Walk,
checks: &dyn FileChecker,
Expand Down

0 comments on commit c83b305

Please sign in to comment.