Skip to content

Commit ec626a1

Browse files
authored
Merge pull request #1257 from epage/highlight
feat(cli): Add '--highlight-<identifiers|words>' flags
2 parents 72f3776 + d06a1dd commit ec626a1

File tree

4 files changed

+241
-0
lines changed

4 files changed

+241
-0
lines changed

crates/typos-cli/src/bin/typos-cli/args.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,18 @@ pub(crate) struct Args {
7777
#[arg(long, group = "mode", help_heading = "Mode")]
7878
pub(crate) file_types: bool,
7979

80+
/// Debug: Print back out files, stylizing identifiers that would be spellchecked.
81+
#[arg(long, group = "mode", help_heading = "Mode")]
82+
pub(crate) highlight_identifiers: bool,
83+
8084
/// Debug: Print each identifier that would be spellchecked.
8185
#[arg(long, group = "mode", help_heading = "Mode")]
8286
pub(crate) identifiers: bool,
8387

88+
/// Debug: Print back out files, stylizing words that would be spellchecked.
89+
#[arg(long, group = "mode", help_heading = "Mode")]
90+
pub(crate) highlight_words: bool,
91+
8492
/// Debug: Print each word that would be spellchecked.
8593
#[arg(long, group = "mode", help_heading = "Mode")]
8694
pub(crate) words: bool,

crates/typos-cli/src/bin/typos-cli/main.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,12 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult {
288288
&typos_cli::file::FoundFiles
289289
} else if args.file_types {
290290
&typos_cli::file::FileTypes
291+
} else if args.highlight_identifiers {
292+
&typos_cli::file::HighlightIdentifiers
291293
} else if args.identifiers {
292294
&typos_cli::file::Identifiers
295+
} else if args.highlight_words {
296+
&typos_cli::file::HighlightWords
293297
} else if args.words {
294298
&typos_cli::file::Words
295299
} else if args.write_changes {

crates/typos-cli/src/file.rs

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,113 @@ impl FileChecker for DiffTypos {
245245
}
246246
}
247247

248+
#[derive(Debug, Clone, Copy)]
249+
pub struct HighlightIdentifiers;
250+
251+
impl FileChecker for HighlightIdentifiers {
252+
fn check_file(
253+
&self,
254+
path: &std::path::Path,
255+
explicit: bool,
256+
policy: &crate::policy::Policy<'_, '_, '_>,
257+
reporter: &dyn report::Report,
258+
) -> Result<(), std::io::Error> {
259+
use std::fmt::Write as _;
260+
261+
let stdout = std::io::stdout();
262+
let mut handle = stdout.lock();
263+
264+
let mut ignores: Option<Ignores> = None;
265+
if policy.check_filenames {
266+
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
267+
let mut styled = String::new();
268+
let mut prev_end = 0;
269+
for (word, highlight) in policy
270+
.tokenizer
271+
.parse_str(file_name)
272+
.filter(|word| {
273+
!ignores
274+
.get_or_insert_with(|| {
275+
Ignores::new(file_name.as_bytes(), policy.ignore)
276+
})
277+
.is_ignored(word.span())
278+
})
279+
.zip(HIGHLIGHTS.iter().cycle())
280+
{
281+
let start = word.offset();
282+
let end = word.offset() + word.token().len();
283+
if prev_end != start {
284+
let _ = write!(
285+
&mut styled,
286+
"{UNMATCHED}{}{UNMATCHED:#}",
287+
&file_name[prev_end..start]
288+
);
289+
}
290+
let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token());
291+
prev_end = end;
292+
}
293+
let _ = write!(
294+
&mut styled,
295+
"{UNMATCHED}{}{UNMATCHED:#}",
296+
&file_name[prev_end..file_name.len()]
297+
);
298+
299+
let parent_dir = path.parent().unwrap();
300+
if !parent_dir.as_os_str().is_empty() {
301+
let parent_dir = parent_dir.display();
302+
write!(handle, "{UNMATCHED}{parent_dir}/")?;
303+
}
304+
writeln!(handle, "{styled}{UNMATCHED}:{UNMATCHED:#}")?;
305+
} else {
306+
writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?;
307+
}
308+
} else {
309+
writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?;
310+
}
311+
312+
if policy.check_files {
313+
let (buffer, content_type) = read_file(path, reporter)?;
314+
if !explicit && !policy.binary && content_type.is_binary() {
315+
// nop
316+
} else if let Ok(buffer) = buffer.to_str() {
317+
let mut styled = String::new();
318+
let mut prev_end = 0;
319+
for (word, highlight) in policy
320+
.tokenizer
321+
.parse_bytes(buffer.as_bytes())
322+
.filter(|word| {
323+
!ignores
324+
.get_or_insert_with(|| Ignores::new(buffer.as_bytes(), policy.ignore))
325+
.is_ignored(word.span())
326+
})
327+
.zip(HIGHLIGHTS.iter().cycle())
328+
{
329+
let start = word.offset();
330+
let end = word.offset() + word.token().len();
331+
if prev_end != start {
332+
let _ = write!(
333+
&mut styled,
334+
"{UNMATCHED}{}{UNMATCHED:#}",
335+
&buffer[prev_end..start]
336+
);
337+
}
338+
let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token());
339+
prev_end = end;
340+
}
341+
let _ = write!(
342+
&mut styled,
343+
"{UNMATCHED}{}{UNMATCHED:#}",
344+
&buffer[prev_end..buffer.len()]
345+
);
346+
347+
write!(handle, "{styled}")?;
348+
}
349+
}
350+
351+
Ok(())
352+
}
353+
}
354+
248355
#[derive(Debug, Clone, Copy)]
249356
pub struct Identifiers;
250357

@@ -307,6 +414,124 @@ impl FileChecker for Identifiers {
307414
}
308415
}
309416

417+
#[derive(Debug, Clone, Copy)]
418+
pub struct HighlightWords;
419+
420+
impl FileChecker for HighlightWords {
421+
fn check_file(
422+
&self,
423+
path: &std::path::Path,
424+
explicit: bool,
425+
policy: &crate::policy::Policy<'_, '_, '_>,
426+
reporter: &dyn report::Report,
427+
) -> Result<(), std::io::Error> {
428+
use std::fmt::Write as _;
429+
430+
let stdout = std::io::stdout();
431+
let mut handle = stdout.lock();
432+
433+
let mut ignores: Option<Ignores> = None;
434+
if policy.check_filenames {
435+
if let Some(file_name) = path.file_name().and_then(|s| s.to_str()) {
436+
let mut styled = String::new();
437+
let mut prev_end = 0;
438+
for (word, highlight) in policy
439+
.tokenizer
440+
.parse_str(file_name)
441+
.flat_map(|i| i.split())
442+
.filter(|word| {
443+
!ignores
444+
.get_or_insert_with(|| {
445+
Ignores::new(file_name.as_bytes(), policy.ignore)
446+
})
447+
.is_ignored(word.span())
448+
})
449+
.zip(HIGHLIGHTS.iter().cycle())
450+
{
451+
let start = word.offset();
452+
let end = word.offset() + word.token().len();
453+
if prev_end != start {
454+
let _ = write!(
455+
&mut styled,
456+
"{UNMATCHED}{}{UNMATCHED:#}",
457+
&file_name[prev_end..start]
458+
);
459+
}
460+
let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token());
461+
prev_end = end;
462+
}
463+
let _ = write!(
464+
&mut styled,
465+
"{UNMATCHED}{}{UNMATCHED:#}",
466+
&file_name[prev_end..file_name.len()]
467+
);
468+
469+
let parent_dir = path.parent().unwrap();
470+
if !parent_dir.as_os_str().is_empty() {
471+
let parent_dir = parent_dir.display();
472+
write!(handle, "{UNMATCHED}{parent_dir}/")?;
473+
}
474+
writeln!(handle, "{styled}{UNMATCHED}:{UNMATCHED:#}")?;
475+
} else {
476+
writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?;
477+
}
478+
} else {
479+
writeln!(handle, "{UNMATCHED}{}:{UNMATCHED:#}", path.display())?;
480+
}
481+
482+
if policy.check_files {
483+
let (buffer, content_type) = read_file(path, reporter)?;
484+
if !explicit && !policy.binary && content_type.is_binary() {
485+
// nop
486+
} else if let Ok(buffer) = buffer.to_str() {
487+
let mut styled = String::new();
488+
let mut prev_end = 0;
489+
for (word, highlight) in policy
490+
.tokenizer
491+
.parse_bytes(buffer.as_bytes())
492+
.flat_map(|i| i.split())
493+
.filter(|word| {
494+
!ignores
495+
.get_or_insert_with(|| Ignores::new(buffer.as_bytes(), policy.ignore))
496+
.is_ignored(word.span())
497+
})
498+
.zip(HIGHLIGHTS.iter().cycle())
499+
{
500+
let start = word.offset();
501+
let end = word.offset() + word.token().len();
502+
if prev_end != start {
503+
let _ = write!(
504+
&mut styled,
505+
"{UNMATCHED}{}{UNMATCHED:#}",
506+
&buffer[prev_end..start]
507+
);
508+
}
509+
let _ = write!(&mut styled, "{highlight}{}{highlight:#}", word.token());
510+
prev_end = end;
511+
}
512+
let _ = write!(
513+
&mut styled,
514+
"{UNMATCHED}{}{UNMATCHED:#}",
515+
&buffer[prev_end..buffer.len()]
516+
);
517+
518+
write!(handle, "{styled}")?;
519+
}
520+
}
521+
522+
Ok(())
523+
}
524+
}
525+
526+
static HIGHLIGHTS: &[anstyle::Style] = &[
527+
anstyle::AnsiColor::Cyan.on_default(),
528+
anstyle::AnsiColor::Cyan
529+
.on_default()
530+
.effects(anstyle::Effects::BOLD),
531+
];
532+
533+
static UNMATCHED: anstyle::Style = anstyle::Style::new().effects(anstyle::Effects::DIMMED);
534+
310535
#[derive(Debug, Clone, Copy)]
311536
pub struct Words;
312537

crates/typos-cli/tests/cmd/help.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ Mode:
3838
-w, --write-changes Write fixes out
3939
--files Debug: Print each file that would be spellchecked
4040
--file-types Debug: Print each file's type
41+
--highlight-identifiers Debug: Print back out files, stylizing identifiers that would be
42+
spellchecked
4143
--identifiers Debug: Print each identifier that would be spellchecked
44+
--highlight-words Debug: Print back out files, stylizing words that would be
45+
spellchecked
4246
--words Debug: Print each word that would be spellchecked
4347
--dump-config <DUMP_CONFIG> Write the current configuration to file with `-` for stdout
4448
--type-list Show all supported file types

0 commit comments

Comments
 (0)