feat(cli): Add '--highlight-<identifiers|words>' flags

Fixes #1254
This commit is contained in:
Ed Page 2025-03-10 10:52:14 -05:00
parent 72f3776b6e
commit d06a1dd728
4 changed files with 241 additions and 0 deletions

View file

@ -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,

View file

@ -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 {

View file

@ -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<Ignores> = 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<Ignores> = 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;

View file

@ -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 <DUMP_CONFIG> Write the current configuration to file with `-` for stdout
--type-list Show all supported file types