diff --git a/Cargo.lock b/Cargo.lock index 2b1d83b..7f71a17 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -122,9 +122,9 @@ dependencies = [ [[package]] name = "bstr" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d" +checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" dependencies = [ "lazy_static", "memchr", @@ -521,7 +521,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" dependencies = [ "atty", - "humantime 1.3.0", + "humantime", "log", "regex", "termcolor", @@ -533,10 +533,7 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" dependencies = [ - "atty", - "humantime 2.1.0", "log", - "regex", "termcolor", ] @@ -661,12 +658,6 @@ dependencies = [ "quick-error", ] -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "ident_case" version = "1.0.1" @@ -1174,9 +1165,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efb2352a0f4d4b128f734b5c44c79ff80117351138733f12f982fe3e2b13343" +checksum = "ce5f1ceb7f74abbce32601642fcf8e8508a8a8991e0621c7d750295b9095702b" dependencies = [ "aho-corasick", "memchr", @@ -1194,9 +1185,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.24" +version = "0.6.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00efb87459ba4f6fb2169d20f68565555688e1250ee6825cdf6254f8b48fafb2" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" [[package]] name = "remove_dir_all" @@ -1209,9 +1200,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e3bad0ee36814ca07d7968269dd4b7ec89ec2da10c4bb613928d3077083c232" +checksum = "410f7acf3cb3a44527c5d9546bad4bf4e6c460915d5f9f2fc524498bfe8f70ce" [[package]] name = "rustc_version" @@ -1485,6 +1476,7 @@ dependencies = [ "ahash", "anyhow", "assert_fs", + "atty", "bstr", "clap", "clap-verbosity-flag", @@ -1516,6 +1508,7 @@ dependencies = [ "unicase", "unicode-segmentation", "varcon-core", + "yansi", ] [[package]] @@ -1797,3 +1790,9 @@ name = "wyz" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "yansi" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc79f4a1e39857fc00c3f662cbf2651c771f00e9c15fe2abc341806bd46bd71" diff --git a/Cargo.toml b/Cargo.toml index 02125a3..bc31c89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ pre-release-replacements = [ [[bin]] name = "typos" -path = "src/main.rs" +path = "src/bin/typos-cli/main.rs" doc = false [badges] @@ -62,7 +62,9 @@ ignore = "0.4" serde = { version = "1.0", features = ["derive"] } toml = "0.5" log = "0.4" -env_logger = "0.8" +env_logger = { version = "0.8", default-features = false, features = ["termcolor"] } +atty = "0.2.14" +yansi = "0.5.0" bstr = "0.2" once_cell = "1.2.0" ahash = "0.7" diff --git a/benches/checks.rs b/benches/checks.rs index 810f339..c55c93d 100644 --- a/benches/checks.rs +++ b/benches/checks.rs @@ -26,7 +26,7 @@ fn bench_checks(c: &mut Criterion) { sample_path.path(), true, &policy, - &typos_cli::report::PrintSilent, + &PrintSilent, ) }); }); @@ -36,28 +36,18 @@ fn bench_checks(c: &mut Criterion) { sample_path.path(), true, &policy, - &typos_cli::report::PrintSilent, + &PrintSilent, ) }); }); group.bench_with_input(BenchmarkId::new("Words", name), &len, |b, _| { b.iter(|| { - typos_cli::file::Words.check_file( - sample_path.path(), - true, - &policy, - &typos_cli::report::PrintSilent, - ) + typos_cli::file::Words.check_file(sample_path.path(), true, &policy, &PrintSilent) }); }); group.bench_with_input(BenchmarkId::new("Typos", name), &len, |b, _| { b.iter(|| { - typos_cli::file::Typos.check_file( - sample_path.path(), - true, - &policy, - &typos_cli::report::PrintSilent, - ) + typos_cli::file::Typos.check_file(sample_path.path(), true, &policy, &PrintSilent) }); }); } @@ -66,5 +56,14 @@ fn bench_checks(c: &mut Criterion) { temp.close().unwrap(); } +#[derive(Debug, Default)] +pub struct PrintSilent; + +impl typos_cli::report::Report for PrintSilent { + fn report(&self, _msg: typos_cli::report::Message) -> Result<(), std::io::Error> { + Ok(()) + } +} + criterion_group!(benches, bench_checks,); criterion_main!(benches); diff --git a/src/args.rs b/src/bin/typos-cli/args.rs similarity index 91% rename from src/args.rs rename to src/bin/typos-cli/args.rs index 53f96ac..5f170d9 100644 --- a/src/args.rs +++ b/src/bin/typos-cli/args.rs @@ -1,6 +1,6 @@ use structopt::StructOpt; -use crate::config; +use typos_cli::config; arg_enum! { #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -12,18 +12,23 @@ arg_enum! { } } -pub const PRINT_SILENT: typos_cli::report::PrintSilent = typos_cli::report::PrintSilent; -pub const PRINT_BRIEF: typos_cli::report::PrintBrief = typos_cli::report::PrintBrief; -pub const PRINT_LONG: typos_cli::report::PrintLong = typos_cli::report::PrintLong; -pub const PRINT_JSON: typos_cli::report::PrintJson = typos_cli::report::PrintJson; - impl Format { - pub(crate) fn reporter(self) -> &'static dyn typos_cli::report::Report { + pub(crate) fn reporter( + self, + stdout_palette: crate::report::Palette, + stderr_palette: crate::report::Palette, + ) -> Box { match self { - Format::Silent => &PRINT_SILENT, - Format::Brief => &PRINT_BRIEF, - Format::Long => &PRINT_LONG, - Format::Json => &PRINT_JSON, + Format::Silent => Box::new(crate::report::PrintSilent), + Format::Brief => Box::new(crate::report::PrintBrief { + stdout_palette, + stderr_palette, + }), + Format::Long => Box::new(crate::report::PrintLong { + stdout_palette, + stderr_palette, + }), + Format::Json => Box::new(crate::report::PrintJson), } } } @@ -98,6 +103,9 @@ pub(crate) struct Args { #[structopt(flatten)] pub(crate) config: ConfigArgs, + #[structopt(flatten)] + pub(crate) color: crate::color::ColorArgs, + #[structopt(flatten)] pub(crate) verbose: clap_verbosity_flag::Verbosity, } diff --git a/src/bin/typos-cli/color.rs b/src/bin/typos-cli/color.rs new file mode 100644 index 0000000..2a3aa97 --- /dev/null +++ b/src/bin/typos-cli/color.rs @@ -0,0 +1,100 @@ +use structopt::StructOpt; + +#[derive(Debug, StructOpt)] +#[structopt(rename_all = "kebab-case")] +pub struct ColorArgs { + /// "Specify when to use colored output. The automatic mode + /// only enables colors if an interactive terminal is detected - + /// colors are automatically disabled if the output goes to a pipe. + /// + /// Possible values: *auto*, never, always. + #[structopt( + long, + value_name="when", + possible_values(&ColorValue::variants()), + case_insensitive(true), + default_value("auto"), + hide_possible_values(true), + hide_default_value(true), + help="When to use colors (*auto*, never, always).")] + color: ColorValue, +} + +impl ColorArgs { + pub fn colored(&self) -> Option { + self.color.colored() + } +} + +arg_enum! { + #[derive(Debug, Copy, Clone, PartialEq, Eq)] + pub enum ColorValue { + Always, + Never, + Auto, + } +} + +impl ColorValue { + fn colored(self) -> Option { + match self { + ColorValue::Always => Some(true), + ColorValue::Never => Some(false), + ColorValue::Auto => None, + } + } +} + +impl Default for ColorValue { + fn default() -> Self { + ColorValue::Auto + } +} + +pub fn colored_stdout() -> Option { + if atty::is(atty::Stream::Stdout) { + None + } else { + Some(false) + } +} + +pub fn colored_stderr() -> Option { + if atty::is(atty::Stream::Stderr) { + None + } else { + Some(false) + } +} + +pub fn colored_env() -> Option { + match std::env::var_os("TERM") { + None => noterm_colored(), + Some(k) => { + if k == "dumb" { + Some(false) + } else { + None + } + } + } + .or_else(|| { + // See https://no-color.org/ + std::env::var_os("NO_COLOR").map(|_| true) + }) +} + +#[cfg(not(windows))] +fn noterm_colored() -> Option { + // If TERM isn't set, then we are in a weird environment that + // probably doesn't support colors. + Some(false) +} + +#[cfg(windows)] +fn noterm_colored() -> Option { + // On Windows, if TERM isn't set, then we shouldn't automatically + // assume that colors aren't allowed. This is unlike Unix environments + // where TERM is more rigorously set. + None +} diff --git a/src/main.rs b/src/bin/typos-cli/main.rs similarity index 78% rename from src/main.rs rename to src/bin/typos-cli/main.rs index 0d9139d..336c26f 100644 --- a/src/main.rs +++ b/src/bin/typos-cli/main.rs @@ -7,8 +7,8 @@ use std::io::Write; use structopt::StructOpt; mod args; -use typos_cli::config; -use typos_cli::report; +mod color; +mod report; use proc_exit::WithCodeResultExt; @@ -31,14 +31,33 @@ fn run() -> proc_exit::ExitResult { } }; - init_logging(args.verbose.log_level()); + let colored = args.color.colored().or_else(color::colored_env); + let mut colored_stdout = colored.or_else(color::colored_stdout).unwrap_or(true); + let mut colored_stderr = colored.or_else(color::colored_stderr).unwrap_or(true); + if (colored_stdout || colored_stderr) && !yansi::Paint::enable_windows_ascii() { + colored_stdout = false; + colored_stderr = false; + } + + init_logging(args.verbose.log_level(), colored_stderr); + + let stdout_palette = if colored_stdout { + report::Palette::colored() + } else { + report::Palette::plain() + }; + let stderr_palette = if colored_stderr { + report::Palette::colored() + } else { + report::Palette::plain() + }; if let Some(output_path) = args.dump_config.as_ref() { run_dump_config(&args, output_path) } else if args.type_list { run_type_list(&args) } else { - run_checks(&args) + run_checks(&args, stdout_palette, stderr_palette) } } @@ -63,9 +82,10 @@ fn run_dump_config(args: &args::Args, output_path: &std::path::Path) -> proc_exi let mut engine = typos_cli::policy::ConfigEngine::new(&storage); engine.set_isolated(args.isolated); - let mut overrides = config::Config::default(); + let mut overrides = typos_cli::config::Config::default(); if let Some(path) = args.custom_config.as_ref() { - let custom = config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; + let custom = + typos_cli::config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; overrides.update(&custom); } overrides.update(&args.config.to_config()); @@ -75,7 +95,7 @@ fn run_dump_config(args: &args::Args, output_path: &std::path::Path) -> proc_exi .load_config(cwd) .with_code(proc_exit::Code::CONFIG_ERR)?; - let mut defaulted_config = config::Config::from_defaults(); + let mut defaulted_config = typos_cli::config::Config::from_defaults(); defaulted_config.update(&config); let output = toml::to_string_pretty(&defaulted_config).with_code(proc_exit::Code::FAILURE)?; if output_path == std::path::Path::new("-") { @@ -108,9 +128,10 @@ fn run_type_list(args: &args::Args) -> proc_exit::ExitResult { let mut engine = typos_cli::policy::ConfigEngine::new(&storage); engine.set_isolated(args.isolated); - let mut overrides = config::Config::default(); + let mut overrides = typos_cli::config::Config::default(); if let Some(path) = args.custom_config.as_ref() { - let custom = config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; + let custom = + typos_cli::config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; overrides.update(&custom); } overrides.update(&args.config.to_config()); @@ -135,16 +156,21 @@ fn run_type_list(args: &args::Args) -> proc_exit::ExitResult { Ok(()) } -fn run_checks(args: &args::Args) -> proc_exit::ExitResult { +fn run_checks( + args: &args::Args, + stdout_palette: report::Palette, + stderr_palette: report::Palette, +) -> proc_exit::ExitResult { let global_cwd = std::env::current_dir()?; let storage = typos_cli::policy::ConfigStorage::new(); let mut engine = typos_cli::policy::ConfigEngine::new(&storage); engine.set_isolated(args.isolated); - let mut overrides = config::Config::default(); + let mut overrides = typos_cli::config::Config::default(); if let Some(path) = args.custom_config.as_ref() { - let custom = config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; + let custom = + typos_cli::config::Config::from_file(path).with_code(proc_exit::Code::CONFIG_ERR)?; overrides.update(&custom); } overrides.update(&args.config.to_config()); @@ -185,12 +211,12 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { // HACK: Diff doesn't handle mixing content let output_reporter = if args.diff { - &args::PRINT_SILENT + Box::new(crate::report::PrintSilent) } else { - args.format.reporter() + args.format.reporter(stdout_palette, stderr_palette) }; - let status_reporter = report::MessageStatus::new(output_reporter); - let reporter: &dyn report::Report = &status_reporter; + let status_reporter = report::MessageStatus::new(output_reporter.as_ref()); + let reporter: &dyn typos_cli::report::Report = &status_reporter; let selected_checks: &dyn typos_cli::file::FileChecker = if args.files { &typos_cli::file::FoundFiles @@ -243,9 +269,14 @@ fn run_checks(args: &args::Args) -> proc_exit::ExitResult { } } -fn init_logging(level: Option) { +fn init_logging(level: Option, colored: bool) { if let Some(level) = level { let mut builder = env_logger::Builder::new(); + builder.write_style(if colored { + env_logger::WriteStyle::Always + } else { + env_logger::WriteStyle::Never + }); builder.filter(None, level.to_level_filter()); diff --git a/src/bin/typos-cli/report.rs b/src/bin/typos-cli/report.rs new file mode 100644 index 0000000..53b337b --- /dev/null +++ b/src/bin/typos-cli/report.rs @@ -0,0 +1,274 @@ +#![allow(clippy::needless_update)] + +use std::io::{self, Write}; +use std::sync::atomic; + +use typos_cli::report::{Context, Message, Report, Typo}; + +#[derive(Copy, Clone, Debug)] +pub struct Palette { + error: yansi::Style, + warn: yansi::Style, + info: yansi::Style, + strong: yansi::Style, +} + +impl Palette { + pub fn colored() -> Self { + Self { + error: yansi::Style::new(yansi::Color::Red), + warn: yansi::Style::new(yansi::Color::Yellow), + info: yansi::Style::new(yansi::Color::Blue), + strong: yansi::Style::default().bold(), + } + } + + pub fn plain() -> Self { + Self { + error: yansi::Style::default(), + warn: yansi::Style::default(), + info: yansi::Style::default(), + strong: yansi::Style::default(), + } + } +} + +pub struct MessageStatus<'r> { + typos_found: atomic::AtomicBool, + errors_found: atomic::AtomicBool, + reporter: &'r dyn Report, +} + +impl<'r> MessageStatus<'r> { + pub fn new(reporter: &'r dyn Report) -> Self { + Self { + typos_found: atomic::AtomicBool::new(false), + errors_found: atomic::AtomicBool::new(false), + reporter, + } + } + + pub fn typos_found(&self) -> bool { + self.typos_found.load(atomic::Ordering::Relaxed) + } + + pub fn errors_found(&self) -> bool { + self.errors_found.load(atomic::Ordering::Relaxed) + } +} + +impl<'r> Report for MessageStatus<'r> { + fn report(&self, msg: Message) -> Result<(), std::io::Error> { + let _ = self.typos_found.compare_exchange( + false, + msg.is_correction(), + atomic::Ordering::Relaxed, + atomic::Ordering::Relaxed, + ); + let _ = self + .errors_found + .compare_exchange( + false, + msg.is_error(), + atomic::Ordering::Relaxed, + atomic::Ordering::Relaxed, + ) + .unwrap(); + self.reporter.report(msg) + } +} + +#[derive(Debug, Default)] +pub struct PrintSilent; + +impl Report for PrintSilent { + fn report(&self, _msg: Message) -> Result<(), std::io::Error> { + Ok(()) + } +} + +pub struct PrintBrief { + pub stdout_palette: Palette, + pub stderr_palette: Palette, +} + +impl Report for PrintBrief { + fn report(&self, msg: Message) -> Result<(), std::io::Error> { + match &msg { + Message::BinaryFile(msg) => { + log::info!("{}", msg); + } + Message::Typo(msg) => print_brief_correction(msg, self.stdout_palette)?, + Message::File(msg) => { + writeln!(io::stdout(), "{}", msg.path.display())?; + } + Message::Parse(msg) => { + writeln!(io::stdout(), "{}", msg.data)?; + } + Message::Error(msg) => { + log::error!("{}: {}", context_display(&msg.context), msg.msg); + } + _ => unimplemented!("New message {:?}", msg), + } + Ok(()) + } +} + +pub struct PrintLong { + pub stdout_palette: Palette, + pub stderr_palette: Palette, +} + +impl Report for PrintLong { + fn report(&self, msg: Message) -> Result<(), std::io::Error> { + match &msg { + Message::BinaryFile(msg) => { + log::info!("{}", msg); + } + Message::Typo(msg) => print_long_correction(msg, self.stdout_palette)?, + Message::File(msg) => { + writeln!(io::stdout(), "{}", msg.path.display())?; + } + Message::Parse(msg) => { + writeln!(io::stdout(), "{}", msg.data)?; + } + Message::Error(msg) => { + log::error!("{}: {}", context_display(&msg.context), msg.msg); + } + _ => unimplemented!("New message {:?}", msg), + } + Ok(()) + } +} + +fn print_brief_correction(msg: &Typo, palette: Palette) -> Result<(), std::io::Error> { + let line = String::from_utf8_lossy(msg.buffer.as_ref()); + let line = line.replace("\t", " "); + let column = unicode_segmentation::UnicodeSegmentation::graphemes( + line.get(0..msg.byte_offset).unwrap(), + true, + ) + .count(); + match &msg.corrections { + typos::Status::Valid => {} + typos::Status::Invalid => { + let divider = ":"; + writeln!( + io::stdout(), + "{}{}{}: {}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column), + palette + .strong + .paint(format_args!("`{}` is disallowed:", msg.typo)), + )?; + } + typos::Status::Corrections(corrections) => { + let divider = ":"; + writeln!( + io::stdout(), + "{}{}{}: {}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column), + palette.strong.paint(format_args!( + "`{}` -> {}", + msg.typo, + itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + )), + )?; + } + } + + Ok(()) +} + +fn print_long_correction(msg: &Typo, palette: Palette) -> Result<(), std::io::Error> { + let stdout = io::stdout(); + let mut handle = stdout.lock(); + + let line = String::from_utf8_lossy(msg.buffer.as_ref()); + let line = line.replace("\t", " "); + let column = unicode_segmentation::UnicodeSegmentation::graphemes( + line.get(0..msg.byte_offset).unwrap(), + true, + ) + .count(); + match &msg.corrections { + typos::Status::Valid => {} + typos::Status::Invalid => { + writeln!( + handle, + "{}: {}", + palette.error.paint("error"), + palette + .strong + .paint(format_args!("`{}` is disallowed`", msg.typo)) + )?; + } + typos::Status::Corrections(corrections) => { + writeln!( + handle, + "{}: {}", + palette.error.paint("error"), + palette.strong.paint(format_args!( + "`{}` should be {}", + msg.typo, + itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") + )) + )?; + } + } + let divider = ":"; + writeln!( + handle, + " --> {}{}{}", + palette.info.paint(context_display(&msg.context)), + palette.info.paint(divider), + palette.info.paint(column) + )?; + + if let Some(Context::File(context)) = &msg.context { + let line_num = context.line_num.to_string(); + let line_indent: String = itertools::repeat_n(" ", line_num.len()).collect(); + + let hl_indent: String = itertools::repeat_n(" ", column).collect(); + let hl: String = itertools::repeat_n("^", msg.typo.len()).collect(); + + writeln!(handle, "{} |", line_indent)?; + writeln!( + handle, + "{} | {}", + palette.info.paint(line_num), + line.trim_end() + )?; + writeln!( + handle, + "{} | {}{}", + line_indent, + hl_indent, + palette.error.paint(hl) + )?; + writeln!(handle, "{} |", line_indent)?; + } + + Ok(()) +} + +fn context_display<'c>(context: &'c Option>) -> &'c dyn std::fmt::Display { + context + .as_ref() + .map(|c| c as &dyn std::fmt::Display) + .unwrap_or(&"") +} + +#[derive(Copy, Clone, Debug)] +pub struct PrintJson; + +impl Report for PrintJson { + fn report(&self, msg: Message) -> Result<(), std::io::Error> { + writeln!(io::stdout(), "{}", serde_json::to_string(&msg).unwrap())?; + Ok(()) + } +} diff --git a/src/report.rs b/src/report.rs index 08b2f28..871accb 100644 --- a/src/report.rs +++ b/src/report.rs @@ -1,8 +1,10 @@ #![allow(clippy::needless_update)] use std::borrow::Cow; -use std::io::{self, Write}; -use std::sync::atomic; + +pub trait Report: Send + Sync { + fn report(&self, msg: Message) -> Result<(), std::io::Error>; +} #[derive(Clone, Debug, serde::Serialize, derive_more::From)] #[serde(rename_all = "snake_case")] @@ -203,203 +205,3 @@ impl<'m> Default for Error<'m> { } } } - -pub trait Report: Send + Sync { - fn report(&self, msg: Message) -> Result<(), std::io::Error>; -} - -pub struct MessageStatus<'r> { - typos_found: atomic::AtomicBool, - errors_found: atomic::AtomicBool, - reporter: &'r dyn Report, -} - -impl<'r> MessageStatus<'r> { - pub fn new(reporter: &'r dyn Report) -> Self { - Self { - typos_found: atomic::AtomicBool::new(false), - errors_found: atomic::AtomicBool::new(false), - reporter, - } - } - - pub fn typos_found(&self) -> bool { - self.typos_found.load(atomic::Ordering::Relaxed) - } - - pub fn errors_found(&self) -> bool { - self.errors_found.load(atomic::Ordering::Relaxed) - } -} - -impl<'r> Report for MessageStatus<'r> { - fn report(&self, msg: Message) -> Result<(), std::io::Error> { - let _ = self.typos_found.compare_exchange( - false, - msg.is_correction(), - atomic::Ordering::Relaxed, - atomic::Ordering::Relaxed, - ); - let _ = self - .errors_found - .compare_exchange( - false, - msg.is_error(), - atomic::Ordering::Relaxed, - atomic::Ordering::Relaxed, - ) - .unwrap(); - self.reporter.report(msg) - } -} - -#[derive(Debug, Default)] -pub struct PrintSilent; - -impl Report for PrintSilent { - fn report(&self, _msg: Message) -> Result<(), std::io::Error> { - Ok(()) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct PrintBrief; - -impl Report for PrintBrief { - fn report(&self, msg: Message) -> Result<(), std::io::Error> { - match &msg { - Message::BinaryFile(msg) => { - log::info!("{}", msg); - } - Message::Typo(msg) => print_brief_correction(msg)?, - Message::File(msg) => { - writeln!(io::stdout(), "{}", msg.path.display())?; - } - Message::Parse(msg) => { - writeln!(io::stdout(), "{}", msg.data)?; - } - Message::Error(msg) => { - log::error!("{}: {}", context_display(&msg.context), msg.msg); - } - } - Ok(()) - } -} - -#[derive(Copy, Clone, Debug)] -pub struct PrintLong; - -impl Report for PrintLong { - fn report(&self, msg: Message) -> Result<(), std::io::Error> { - match &msg { - Message::BinaryFile(msg) => { - log::info!("{}", msg); - } - Message::Typo(msg) => print_long_correction(msg)?, - Message::File(msg) => { - writeln!(io::stdout(), "{}", msg.path.display())?; - } - Message::Parse(msg) => { - writeln!(io::stdout(), "{}", msg.data)?; - } - Message::Error(msg) => { - log::error!("{}: {}", context_display(&msg.context), msg.msg); - } - } - Ok(()) - } -} - -fn print_brief_correction(msg: &Typo) -> Result<(), std::io::Error> { - let line = String::from_utf8_lossy(msg.buffer.as_ref()); - let line = line.replace("\t", " "); - let column = unicode_segmentation::UnicodeSegmentation::graphemes( - line.get(0..msg.byte_offset).unwrap(), - true, - ) - .count(); - match &msg.corrections { - typos::Status::Valid => {} - typos::Status::Invalid => { - writeln!( - io::stdout(), - "{}:{}: `{}` is disallowed", - context_display(&msg.context), - column, - msg.typo, - )?; - } - typos::Status::Corrections(corrections) => { - writeln!( - io::stdout(), - "{}:{}: `{}` -> {}", - context_display(&msg.context), - column, - msg.typo, - itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") - )?; - } - } - - Ok(()) -} - -fn print_long_correction(msg: &Typo) -> Result<(), std::io::Error> { - let stdout = io::stdout(); - let mut handle = stdout.lock(); - - let line = String::from_utf8_lossy(msg.buffer.as_ref()); - let line = line.replace("\t", " "); - let column = unicode_segmentation::UnicodeSegmentation::graphemes( - line.get(0..msg.byte_offset).unwrap(), - true, - ) - .count(); - match &msg.corrections { - typos::Status::Valid => {} - typos::Status::Invalid => { - writeln!(handle, "error: `{}` is disallowed`", msg.typo,)?; - } - typos::Status::Corrections(corrections) => { - writeln!( - handle, - "error: `{}` should be {}", - msg.typo, - itertools::join(corrections.iter().map(|s| format!("`{}`", s)), ", ") - )?; - } - } - writeln!(handle, " --> {}:{}", context_display(&msg.context), column)?; - - if let Some(Context::File(context)) = &msg.context { - let line_num = context.line_num.to_string(); - let line_indent: String = itertools::repeat_n(" ", line_num.len()).collect(); - - let hl_indent: String = itertools::repeat_n(" ", column).collect(); - let hl: String = itertools::repeat_n("^", msg.typo.len()).collect(); - - writeln!(handle, "{} |", line_indent)?; - writeln!(handle, "{} | {}", line_num, line.trim_end())?; - writeln!(handle, "{} | {}{}", line_indent, hl_indent, hl)?; - writeln!(handle, "{} |", line_indent)?; - } - - Ok(()) -} - -fn context_display<'c>(context: &'c Option>) -> &'c dyn std::fmt::Display { - context - .as_ref() - .map(|c| c as &dyn std::fmt::Display) - .unwrap_or(&"") -} - -#[derive(Copy, Clone, Debug)] -pub struct PrintJson; - -impl Report for PrintJson { - fn report(&self, msg: Message) -> Result<(), std::io::Error> { - writeln!(io::stdout(), "{}", serde_json::to_string(&msg).unwrap())?; - Ok(()) - } -}