use std::fmt;
use std::io::Write;
use std::path::PathBuf;
use similar::{ChangeTag, TextDiff};
use crate::alloc::prelude::*;
use crate::alloc::BTreeSet;
use crate::cli::{AssetKind, CommandBase, Config, Entry, EntryPoint, ExitCode, Io, SharedFlags};
use crate::support::{Context, Result};
use crate::termcolor::{Color, ColorSpec, WriteColor};
use crate::{Diagnostics, Options, Source, Sources};
mod cli {
use std::path::PathBuf;
use std::vec::Vec;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(rename_all = "kebab-case")]
pub(crate) struct Flags {
#[arg(long)]
pub(super) warnings_are_errors: bool,
#[arg(long)]
pub(super) check: bool,
pub(super) fmt_path: Vec<PathBuf>,
}
}
pub(super) use cli::Flags;
impl CommandBase for Flags {
#[inline]
fn is_workspace(&self, _: AssetKind) -> bool {
true
}
#[inline]
fn describe(&self) -> &str {
"Formatting"
}
#[inline]
fn paths(&self) -> &[PathBuf] {
&self.fmt_path
}
}
pub(super) fn run<'m, I>(
io: &mut Io<'_>,
entry: &mut Entry<'_>,
c: &Config,
entrys: I,
flags: &Flags,
shared: &SharedFlags,
options: &Options,
) -> Result<ExitCode>
where
I: IntoIterator<Item = EntryPoint<'m>>,
{
let col = Colors::new();
let mut changed = 0u32;
let mut failed = 0u32;
let mut unchanged = 0u32;
let mut failed_builds = 0u32;
let context = shared.context(entry, c, None)?;
let mut paths = BTreeSet::new();
for e in entrys {
if e.is_argument() {
paths.try_insert(e.path().try_to_owned()?)?;
continue;
}
let mut diagnostics = if shared.warnings || flags.warnings_are_errors {
Diagnostics::new()
} else {
Diagnostics::without_warnings()
};
let mut sources = Sources::new();
sources.insert(match Source::from_path(e.path()) {
Ok(source) => source,
Err(error) => return Err(error).context(e.path().display().try_to_string()?),
})?;
let _ = crate::prepare(&mut sources)
.with_context(&context)
.with_diagnostics(&mut diagnostics)
.with_options(options)
.build();
diagnostics.emit(&mut io.stdout.lock(), &sources)?;
if diagnostics.has_error() || flags.warnings_are_errors && diagnostics.has_warning() {
failed_builds += 1;
}
for source in sources.iter() {
if let Some(path) = source.path() {
paths.try_insert(path.try_to_owned()?)?;
}
}
}
for path in paths {
let mut sources = Sources::new();
sources.insert(match Source::from_path(&path) {
Ok(source) => source,
Err(error) => return Err(error).context(path.display().try_to_string()?),
})?;
let mut diagnostics = Diagnostics::new();
let build = crate::fmt::prepare(&sources)
.with_options(options)
.with_diagnostics(&mut diagnostics);
let result = build.format();
if !diagnostics.is_empty() {
diagnostics.emit(io.stdout, &sources)?;
}
let Ok(formatted) = result else {
failed += 1;
continue;
};
for (id, formatted) in formatted {
let Some(source) = sources.get(id) else {
continue;
};
let same = source.as_str() == formatted;
if same {
unchanged += 1;
if shared.verbose {
io.stdout.set_color(&col.green)?;
write!(io.stdout, "== ")?;
io.stdout.reset()?;
writeln!(io.stdout, "{}", source.name())?;
}
continue;
}
changed += 1;
if shared.verbose || flags.check {
io.stdout.set_color(&col.yellow)?;
write!(io.stdout, "++ ")?;
io.stdout.reset()?;
writeln!(io.stdout, "{}", source.name())?;
diff(io, source.as_str(), &formatted, &col)?;
}
if !flags.check {
if let Some(path) = source.path() {
std::fs::write(path, &formatted)?;
}
}
}
}
if shared.verbose && unchanged > 0 {
io.stdout.set_color(&col.green)?;
write!(io.stdout, "{}", unchanged)?;
io.stdout.reset()?;
writeln!(io.stdout, " unchanged")?;
}
if shared.verbose && changed > 0 {
io.stdout.set_color(&col.yellow)?;
write!(io.stdout, "{}", changed)?;
io.stdout.reset()?;
writeln!(io.stdout, " changed")?;
}
if shared.verbose || failed > 0 {
io.stdout.set_color(&col.red)?;
write!(io.stdout, "{}", failed)?;
io.stdout.reset()?;
writeln!(io.stdout, " failed")?;
}
if shared.verbose || failed_builds > 0 {
io.stdout.set_color(&col.red)?;
write!(io.stdout, "{}", failed_builds)?;
io.stdout.reset()?;
writeln!(io.stdout, " failed builds")?;
}
if flags.check && changed > 0 {
io.stdout.set_color(&col.red)?;
writeln!(
io.stdout,
"Failure due to `--check` flag and unformatted files."
)?;
io.stdout.reset()?;
return Ok(ExitCode::Failure);
}
if failed > 0 || failed_builds > 0 {
return Ok(ExitCode::Failure);
}
Ok(ExitCode::Success)
}
fn diff(io: &mut Io, source: &str, val: &str, col: &Colors) -> Result<(), anyhow::Error> {
let diff = TextDiff::from_lines(source, val);
for (idx, group) in diff.grouped_ops(3).iter().enumerate() {
if idx > 0 {
println!("{:-^1$}", "-", 80);
}
for op in group {
for change in diff.iter_inline_changes(op) {
let (sign, color) = match change.tag() {
ChangeTag::Delete => ("-", &col.red),
ChangeTag::Insert => ("+", &col.green),
ChangeTag::Equal => (" ", &col.dim),
};
io.stdout.set_color(color)?;
write!(io.stdout, "{}", Line(change.old_index()))?;
write!(io.stdout, "{sign}")?;
for (_, value) in change.iter_strings_lossy() {
write!(io.stdout, "{value}")?;
}
io.stdout.reset()?;
if change.missing_newline() {
writeln!(io.stdout)?;
}
}
}
}
Ok(())
}
struct Line(Option<usize>);
impl fmt::Display for Line {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self.0 {
None => write!(f, " "),
Some(idx) => write!(f, "{:<4}", idx + 1),
}
}
}
struct Colors {
red: ColorSpec,
green: ColorSpec,
yellow: ColorSpec,
dim: ColorSpec,
}
impl Colors {
fn new() -> Self {
let mut this = Self {
red: ColorSpec::new(),
green: ColorSpec::new(),
yellow: ColorSpec::new(),
dim: ColorSpec::new(),
};
this.red.set_fg(Some(Color::Red));
this.green.set_fg(Some(Color::Green));
this.yellow.set_fg(Some(Color::Yellow));
this
}
}