use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use badness::formatter::{FormatStyle, WrapMode, check_paths_with_style, format_with_style};
use badness::linter::{Diagnostic, OutputMode, render_findings};
use badness::parser::parse;
use clap::{Parser, Subcommand, ValueEnum};
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
enum WrapArg {
Reflow,
Sentence,
Semantic,
Preserve,
}
impl From<WrapArg> for WrapMode {
fn from(arg: WrapArg) -> Self {
match arg {
WrapArg::Reflow => WrapMode::Reflow,
WrapArg::Sentence => WrapMode::Sentence,
WrapArg::Semantic => WrapMode::Semantic,
WrapArg::Preserve => WrapMode::Preserve,
}
}
}
#[derive(Parser)]
#[command(
name = "badness",
version,
about = "A formatter, linter, and language server for LaTeX"
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
Format {
paths: Vec<PathBuf>,
#[arg(long)]
check: bool,
#[arg(long)]
line_width: Option<usize>,
#[arg(long)]
indent_width: Option<usize>,
#[arg(long, value_enum)]
wrap: Option<WrapArg>,
},
Lint {
paths: Vec<PathBuf>,
},
Lsp,
}
fn main() -> ExitCode {
let cli = Cli::parse();
match cli.command {
Command::Format {
paths,
check,
line_width,
indent_width,
wrap,
} => {
let mut style = FormatStyle::default();
if let Some(w) = line_width {
style.line_width = w;
}
if let Some(w) = indent_width {
style.indent_width = w;
}
if let Some(w) = wrap {
style.wrap = w.into();
}
run_format(&paths, check, style)
}
Command::Lint { paths } => run_lint(&paths),
Command::Lsp => run_lsp(),
}
}
fn run_lsp() -> ExitCode {
match badness::lsp::run() {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("badness: language server error: {err}");
ExitCode::FAILURE
}
}
}
fn run_lint(paths: &[PathBuf]) -> ExitCode {
let mut sources: Vec<(PathBuf, String)> = Vec::new();
let mut failed = false;
if paths.is_empty() {
let mut input = String::new();
if let Err(err) = std::io::stdin().read_to_string(&mut input) {
eprintln!("badness: cannot read stdin: {err}");
return ExitCode::FAILURE;
}
sources.push((PathBuf::from("<stdin>"), input));
} else {
for path in paths {
match std::fs::read_to_string(path) {
Ok(content) => sources.push((path.clone(), content)),
Err(err) => {
eprintln!("badness: cannot read {}: {err}", path.display());
failed = true;
}
}
}
}
let mut diagnostics: Vec<Diagnostic> = Vec::new();
for (path, content) in &sources {
let parsed = parse(content);
diagnostics.extend(
parsed
.errors
.iter()
.map(|err| Diagnostic::from_parse(path.clone(), err)),
);
}
if !diagnostics.is_empty() {
let source_for = |path: &Path| {
sources
.iter()
.find(|(p, _)| p == path)
.map(|(_, text)| text.clone())
};
eprint!(
"{}",
render_findings(&diagnostics, OutputMode::Pretty, &source_for)
);
}
if failed || !diagnostics.is_empty() {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}
fn run_format(paths: &[PathBuf], check: bool, style: FormatStyle) -> ExitCode {
if check {
return run_check(paths, style);
}
if paths.is_empty() {
run_format_stdin(style)
} else {
run_format_paths(paths, style)
}
}
fn run_check(paths: &[PathBuf], style: FormatStyle) -> ExitCode {
match check_paths_with_style(paths, style) {
Ok(result) => {
if result.changed_files.is_empty() {
ExitCode::SUCCESS
} else {
for path in &result.changed_files {
eprintln!("would reformat {}", path.display());
}
eprintln!(
"{} of {} file(s) would be reformatted",
result.changed_files.len(),
result.checked_files
);
ExitCode::FAILURE
}
}
Err(err) => {
eprintln!("badness: {err}");
ExitCode::FAILURE
}
}
}
fn run_format_stdin(style: FormatStyle) -> ExitCode {
let mut input = String::new();
if let Err(err) = std::io::stdin().read_to_string(&mut input) {
eprintln!("badness: cannot read stdin: {err}");
return ExitCode::FAILURE;
}
match format_with_style(&input, style) {
Ok(formatted) => {
if let Err(err) = std::io::stdout().write_all(formatted.as_bytes()) {
eprintln!("badness: cannot write stdout: {err}");
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
Err(err) => {
eprintln!("badness: {err}");
ExitCode::FAILURE
}
}
}
fn run_format_paths(paths: &[PathBuf], style: FormatStyle) -> ExitCode {
let mut failed = false;
for path in paths {
let content = match std::fs::read_to_string(path) {
Ok(content) => content,
Err(err) => {
eprintln!("badness: cannot read {}: {err}", path.display());
failed = true;
continue;
}
};
match format_with_style(&content, style) {
Ok(formatted) => {
if formatted != content
&& let Err(err) = std::fs::write(path, formatted)
{
eprintln!("badness: cannot write {}: {err}", path.display());
failed = true;
}
}
Err(err) => {
eprintln!("badness: cannot format {}: {err}", path.display());
failed = true;
}
}
}
if failed {
ExitCode::FAILURE
} else {
ExitCode::SUCCESS
}
}