Merge pull request #244 from epage/color

feat(cli): Colored output
This commit is contained in:
Ed Page 2021-05-13 06:57:58 -05:00 committed by GitHub
commit 95d023452b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 479 additions and 264 deletions

35
Cargo.lock generated
View file

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

View file

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

View file

@ -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);

View file

@ -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<dyn typos_cli::report::Report> {
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,
}

100
src/bin/typos-cli/color.rs Normal file
View file

@ -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<bool> {
self.color.colored()
}
}
arg_enum! {
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ColorValue {
Always,
Never,
Auto,
}
}
impl ColorValue {
fn colored(self) -> Option<bool> {
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<bool> {
if atty::is(atty::Stream::Stdout) {
None
} else {
Some(false)
}
}
pub fn colored_stderr() -> Option<bool> {
if atty::is(atty::Stream::Stderr) {
None
} else {
Some(false)
}
}
pub fn colored_env() -> Option<bool> {
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<bool> {
// 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<bool> {
// 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
}

View file

@ -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<log::Level>) {
fn init_logging(level: Option<log::Level>, 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());

274
src/bin/typos-cli/report.rs Normal file
View file

@ -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<Context<'c>>) -> &'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(())
}
}

View file

@ -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<Context<'c>>) -> &'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(())
}
}