diff --git a/crates/typos-cli/src/bin/typos-cli/args.rs b/crates/typos-cli/src/bin/typos-cli/args.rs index a1a7f02..2ca24ad 100644 --- a/crates/typos-cli/src/bin/typos-cli/args.rs +++ b/crates/typos-cli/src/bin/typos-cli/args.rs @@ -77,10 +77,18 @@ pub(crate) struct Args { #[arg(long, group = "mode", help_heading = "Mode")] pub(crate) file_types: bool, + /// Debug: Print back out files, stylizing identifiers that would be spellchecked. + #[arg(long, group = "mode", help_heading = "Mode")] + pub(crate) highlight_identifiers: bool, + /// Debug: Print each identifier that would be spellchecked. #[arg(long, group = "mode", help_heading = "Mode")] pub(crate) identifiers: bool, + /// Debug: Print back out files, stylizing words that would be spellchecked. + #[arg(long, group = "mode", help_heading = "Mode")] + pub(crate) highlight_words: bool, + /// Debug: Print each word that would be spellchecked. #[arg(long, group = "mode", help_heading = "Mode")] pub(crate) words: bool, diff --git a/crates/typos-cli/src/bin/typos-cli/main.rs b/crates/typos-cli/src/bin/typos-cli/main.rs index f250630..f793777 100644 --- a/crates/typos-cli/src/bin/typos-cli/main.rs +++ b/crates/typos-cli/src/bin/typos-cli/main.rs @@ -288,8 +288,12 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { &typos_cli::file::FoundFiles } else if args.file_types { &typos_cli::file::FileTypes + } else if args.highlight_identifiers { + &typos_cli::file::HighlightIdentifiers } else if args.identifiers { &typos_cli::file::Identifiers + } else if args.highlight_words { + &typos_cli::file::HighlightWords } else if args.words { &typos_cli::file::Words } else if args.write_changes { diff --git a/crates/typos-cli/src/file.rs b/crates/typos-cli/src/file.rs index b12037a..fd57e2a 100644 --- a/crates/typos-cli/src/file.rs +++ b/crates/typos-cli/src/file.rs @@ -245,6 +245,113 @@ impl FileChecker for DiffTypos { } } +#[derive(Debug, Clone, Copy)] +pub struct HighlightIdentifiers; + +impl FileChecker for HighlightIdentifiers { + fn check_file( + &self, + path: &std::path::Path, + explicit: bool, + policy: &crate::policy::Policy<'_, '_, '_>, + reporter: &dyn report::Report, + ) -> Result<(), std::io::Error> { + use std::fmt::Write as _; + + let stdout = std::io::stdout(); + let mut handle = stdout.lock(); + + let mut ignores: Option = None; + if policy.check_filenames { + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { + let mut styled = String::new(); + let mut prev_end = 0; + for (word, highlight) in policy + .tokenizer + .parse_str(file_name) + .filter(|word| { + !ignores + .get_or_insert_with(|| { + Ignores::new(file_name.as_bytes(), policy.ignore) + }) + .is_ignored(word.span()) + }) + .zip(HIGHLIGHTS.iter().cycle()) + { + let start = word.offset(); + let end = word.offset() + word.token().len(); + if prev_end != start { + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &file_name[prev_end..start] + ); + } + let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token()); + prev_end = end; + } + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &file_name[prev_end..file_name.len()] + ); + + let parent_dir = path.parent().unwrap(); + if !parent_dir.as_os_str().is_empty() { + let parent_dir = parent_dir.display(); + write!(handle, "{UNMATCHED}{parent_dir}/")?; + } + writeln!(handle, "{styled}{UNMATCHED}:{UNMATCHED:#}")?; + } else { + writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?; + } + } else { + writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?; + } + + if policy.check_files { + let (buffer, content_type) = read_file(path, reporter)?; + if !explicit && !policy.binary && content_type.is_binary() { + // nop + } else if let Ok(buffer) = buffer.to_str() { + let mut styled = String::new(); + let mut prev_end = 0; + for (word, highlight) in policy + .tokenizer + .parse_bytes(buffer.as_bytes()) + .filter(|word| { + !ignores + .get_or_insert_with(|| Ignores::new(buffer.as_bytes(), policy.ignore)) + .is_ignored(word.span()) + }) + .zip(HIGHLIGHTS.iter().cycle()) + { + let start = word.offset(); + let end = word.offset() + word.token().len(); + if prev_end != start { + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &buffer[prev_end..start] + ); + } + let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token()); + prev_end = end; + } + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &buffer[prev_end..buffer.len()] + ); + + write!(handle, "{styled}")?; + } + } + + Ok(()) + } +} + #[derive(Debug, Clone, Copy)] pub struct Identifiers; @@ -307,6 +414,124 @@ impl FileChecker for Identifiers { } } +#[derive(Debug, Clone, Copy)] +pub struct HighlightWords; + +impl FileChecker for HighlightWords { + fn check_file( + &self, + path: &std::path::Path, + explicit: bool, + policy: &crate::policy::Policy<'_, '_, '_>, + reporter: &dyn report::Report, + ) -> Result<(), std::io::Error> { + use std::fmt::Write as _; + + let stdout = std::io::stdout(); + let mut handle = stdout.lock(); + + let mut ignores: Option = None; + if policy.check_filenames { + if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) { + let mut styled = String::new(); + let mut prev_end = 0; + for (word, highlight) in policy + .tokenizer + .parse_str(file_name) + .flat_map(|i| i.split()) + .filter(|word| { + !ignores + .get_or_insert_with(|| { + Ignores::new(file_name.as_bytes(), policy.ignore) + }) + .is_ignored(word.span()) + }) + .zip(HIGHLIGHTS.iter().cycle()) + { + let start = word.offset(); + let end = word.offset() + word.token().len(); + if prev_end != start { + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &file_name[prev_end..start] + ); + } + let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token()); + prev_end = end; + } + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &file_name[prev_end..file_name.len()] + ); + + let parent_dir = path.parent().unwrap(); + if !parent_dir.as_os_str().is_empty() { + let parent_dir = parent_dir.display(); + write!(handle, "{UNMATCHED}{parent_dir}/")?; + } + writeln!(handle, "{styled}{UNMATCHED}:{UNMATCHED:#}")?; + } else { + writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?; + } + } else { + writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?; + } + + if policy.check_files { + let (buffer, content_type) = read_file(path, reporter)?; + if !explicit && !policy.binary && content_type.is_binary() { + // nop + } else if let Ok(buffer) = buffer.to_str() { + let mut styled = String::new(); + let mut prev_end = 0; + for (word, highlight) in policy + .tokenizer + .parse_bytes(buffer.as_bytes()) + .flat_map(|i| i.split()) + .filter(|word| { + !ignores + .get_or_insert_with(|| Ignores::new(buffer.as_bytes(), policy.ignore)) + .is_ignored(word.span()) + }) + .zip(HIGHLIGHTS.iter().cycle()) + { + let start = word.offset(); + let end = word.offset() + word.token().len(); + if prev_end != start { + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &buffer[prev_end..start] + ); + } + let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token()); + prev_end = end; + } + let _ = write!( + &mut styled, + "{UNMATCHED}{}{UNMATCHED:#}", + &buffer[prev_end..buffer.len()] + ); + + write!(handle, "{styled}")?; + } + } + + Ok(()) + } +} + +static HIGHLIGHTS: &[anstyle::Style] = &[ + anstyle::AnsiColor::Cyan.on_default(), + anstyle::AnsiColor::Cyan + .on_default() + .effects(anstyle::Effects::BOLD), +]; + +static UNMATCHED: anstyle::Style = anstyle::Style::new().effects(anstyle::Effects::DIMMED); + #[derive(Debug, Clone, Copy)] pub struct Words; diff --git a/crates/typos-cli/tests/cmd/help.toml b/crates/typos-cli/tests/cmd/help.toml index c02facd..72f87d8 100644 --- a/crates/typos-cli/tests/cmd/help.toml +++ b/crates/typos-cli/tests/cmd/help.toml @@ -38,7 +38,11 @@ Mode: -w, --write-changes Write fixes out --files Debug: Print each file that would be spellchecked --file-types Debug: Print each file's type + --highlight-identifiers Debug: Print back out files, stylizing identifiers that would be + spellchecked --identifiers Debug: Print each identifier that would be spellchecked + --highlight-words Debug: Print back out files, stylizing words that would be + spellchecked --words Debug: Print each word that would be spellchecked --dump-config Write the current configuration to file with `-` for stdout --type-list Show all supported file types