diff --git a/CHANGELOG.md b/CHANGELOG.md index bbf429ce..5d74b2f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ # Development version +- In the CLI, errors and warnings are now written to stderr. This allows you to + see issues that occur during `air format`, such as parse errors or file not + found errors (#155). + +- New global CLI option `--log-level` to control the log level. The default is + `warn` (#155). + +- New global CLI option `--no-color` to disable colored output (#155). + - Air now supports `.air.toml` files in addition to `air.toml` files. If both are in the same directory, `air.toml` is preferred, but we don't recommend doing that (#152). diff --git a/Cargo.lock b/Cargo.lock index 1030c744..c8a708b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -33,11 +33,11 @@ dependencies = [ "air_r_formatter", "air_r_parser", "anyhow", - "biome_console", - "biome_diagnostics", "biome_formatter", "biome_parser", "clap", + "colored", + "crates", "fs", "ignore", "itertools", @@ -47,6 +47,7 @@ dependencies = [ "thiserror 2.0.5", "tokio", "tracing", + "tracing-subscriber", "workspace", ] @@ -605,6 +606,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fde0e0ec90c9dfb3b4b1a0891a7dcd0e2bffde2f7efed5fe7c9bb00e5bfb915e" +dependencies = [ + "windows-sys 0.59.0", +] + [[package]] name = "console" version = "0.15.8" @@ -623,6 +633,14 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" +[[package]] +name = "crates" +version = "0.0.0" +dependencies = [ + "cargo_metadata", + "insta", +] + [[package]] name = "crc32fast" version = "1.4.2" @@ -1325,7 +1343,7 @@ dependencies = [ "biome_rowan", "biome_text_size", "bytes", - "cargo_metadata", + "crates", "crossbeam", "dissimilar", "futures", diff --git a/Cargo.toml b/Cargo.toml index 920440b0..885025d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ air_r_formatter = { path = "./crates/air_r_formatter" } air_r_parser = { path = "./crates/air_r_parser" } air_r_syntax = { path = "./crates/air_r_syntax" } biome_ungrammar = { path = "./crates/biome_ungrammar" } +crates = { path = "./crates/crates" } fs = { path = "./crates/fs" } line_ending = { path = "./crates/line_ending" } lsp = { path = "./crates/lsp" } @@ -49,6 +50,7 @@ biome_unicode_table = { git = "https://github.com/biomejs/biome", rev = "2648fa4 bytes = "1.8.0" cargo_metadata = "0.19.1" clap = { version = "4.5.20", features = ["derive"] } +colored = "3.0.0" crossbeam = "0.8.4" dissimilar = "1.0.9" futures = "0.3.31" diff --git a/crates/air/Cargo.toml b/crates/air/Cargo.toml index 9d547248..cfbfec9c 100644 --- a/crates/air/Cargo.toml +++ b/crates/air/Cargo.toml @@ -15,11 +15,11 @@ rust-version.workspace = true air_r_formatter = { workspace = true } air_r_parser = { workspace = true } anyhow = { workspace = true } -biome_console = { workspace = true } -biome_diagnostics = { workspace = true } biome_formatter = { workspace = true } biome_parser = { workspace = true } clap = { workspace = true, features = ["wrap_help"] } +colored = { workspace = true } +crates = { workspace = true } fs = { workspace = true } ignore = { workspace = true } itertools = { workspace = true } @@ -28,6 +28,7 @@ lsp = { workspace = true } thiserror = { workspace = true } tokio = "1.41.1" tracing = { workspace = true } +tracing-subscriber = { workspace = true } workspace = { workspace = true } [dev-dependencies] diff --git a/crates/air/src/args.rs b/crates/air/src/args.rs index f2be9cb8..a2d330f5 100644 --- a/crates/air/src/args.rs +++ b/crates/air/src/args.rs @@ -2,6 +2,8 @@ use clap::Parser; use clap::Subcommand; use std::path::PathBuf; +use crate::logging; + #[derive(Parser)] #[command( author, @@ -13,6 +15,9 @@ use std::path::PathBuf; pub struct Args { #[command(subcommand)] pub(crate) command: Command, + + #[clap(flatten)] + pub(crate) global_options: GlobalOptions, } #[derive(Subcommand)] @@ -39,3 +44,18 @@ pub(crate) struct FormatCommand { #[derive(Clone, Debug, Parser)] pub(crate) struct LanguageServerCommand {} + +/// All configuration options that can be passed "globally" +#[derive(Debug, Default, clap::Args)] +#[command(next_help_heading = "Global options")] +pub(crate) struct GlobalOptions { + /// The log level. One of: `error`, `warn`, `info`, `debug`, or `trace`. Defaults + /// to `warn`. + #[arg(long, global = true)] + pub(crate) log_level: Option, + + /// Disable colored output. To turn colored output off, either set this option or set + /// the environment variable `NO_COLOR` to any non-zero value. + #[arg(long, global = true)] + pub(crate) no_color: bool, +} diff --git a/crates/air/src/commands/format.rs b/crates/air/src/commands/format.rs index 26aff743..89d743b6 100644 --- a/crates/air/src/commands/format.rs +++ b/crates/air/src/commands/format.rs @@ -7,6 +7,7 @@ use std::path::PathBuf; use air_r_formatter::context::RFormatOptions; use air_r_parser::RParserOptions; +use colored::Colorize; use fs::relativize_path; use itertools::Either; use itertools::Itertools; @@ -215,7 +216,7 @@ impl Display for FormatCommandError { write!( f, "Failed to format {path}: {err}", - path = relativize_path(path), + path = relativize_path(path).underline(), err = err .io_error() .map_or_else(|| err.to_string(), std::string::ToString::to_string) @@ -234,21 +235,21 @@ impl Display for FormatCommandError { write!( f, "Failed to read {path}: {err}", - path = relativize_path(path), + path = relativize_path(path).underline(), ) } Self::Write(path, err) => { write!( f, "Failed to write {path}: {err}", - path = relativize_path(path), + path = relativize_path(path).underline(), ) } Self::Format(path, err) => { write!( f, "Failed to format {path}: {err}", - path = relativize_path(path), + path = relativize_path(path).underline(), ) } } diff --git a/crates/air/src/lib.rs b/crates/air/src/lib.rs index 254efd71..c4436d24 100644 --- a/crates/air/src/lib.rs +++ b/crates/air/src/lib.rs @@ -4,9 +4,15 @@ use crate::status::ExitStatus; pub mod args; mod commands; +mod logging; pub mod status; pub fn run(args: Args) -> anyhow::Result { + logging::init_logging( + args.global_options.log_level.unwrap_or_default(), + args.global_options.no_color, + ); + match args.command { Command::Format(command) => commands::format::format(command), Command::LanguageServer(command) => commands::language_server::language_server(command), diff --git a/crates/air/src/logging.rs b/crates/air/src/logging.rs new file mode 100644 index 00000000..4d19f8ea --- /dev/null +++ b/crates/air/src/logging.rs @@ -0,0 +1,108 @@ +use std::fmt::Display; +use std::str::FromStr; +use tracing_subscriber::filter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::Layer; +use tracing_subscriber::Registry; + +pub(crate) fn init_logging(log_level: LogLevel, no_color: bool) { + let log_level = log_level.tracing_level(); + + // Apply the log level to each air crate. + // We don't report any logs from non-air crates in the CLI. + let mut filter = filter::Targets::new(); + for target in crates::AIR_CRATE_NAMES { + filter = filter.with_target(*target, log_level); + } + + let mut layer = tracing_subscriber::fmt::layer(); + + if no_color { + layer = turn_off_colors(layer); + }; + + let layer = layer + // i.e. Displaying `ERROR` or `WARN` + .with_level(true) + // Don't show the module name, not useful in a cli + .with_target(false) + // Don't show the timestamp, not useful in a cli + .without_time() + .with_writer(std::io::stderr) + .with_filter(filter); + + let subscriber = tracing_subscriber::Registry::default().with(layer); + + tracing::subscriber::set_global_default(subscriber) + .expect("Should be able to set the global subscriber exactly once."); + + // Emit message after subscriber is set up, so we actually see it + tracing::trace!("Initialized logging"); +} + +/// Explicitly turn off ANSI colored / styled output +/// +/// We use colored output in two places: +/// - Level labels in tracing-subscriber, like `ERROR` , which shows up in red +/// - Usage of the `colored` crate +/// +/// Both respect the `NO_COLOR` environment variable if we don't specify anything. +/// +/// We explicitly call `set_override()` and `with_ansi()` ONLY when turning colors off. +/// `set_override(true)` and `with_ansi(true)` override the `NO_COLOR` environment +/// variable, and we don't want to do that. +fn turn_off_colors( + layer: tracing_subscriber::fmt::Layer, +) -> tracing_subscriber::fmt::Layer { + colored::control::set_override(false); + layer.with_ansi(false) +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum LogLevel { + Error, + #[default] + Warn, + Info, + Debug, + Trace, +} + +impl LogLevel { + fn tracing_level(self) -> tracing::Level { + match self { + Self::Error => tracing::Level::ERROR, + Self::Warn => tracing::Level::WARN, + Self::Info => tracing::Level::INFO, + Self::Debug => tracing::Level::DEBUG, + Self::Trace => tracing::Level::TRACE, + } + } +} + +impl Display for LogLevel { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Error => f.write_str("Error"), + Self::Warn => f.write_str("Warn"), + Self::Info => f.write_str("Info"), + Self::Debug => f.write_str("Debug"), + Self::Trace => f.write_str("Trace"), + } + } +} + +impl FromStr for LogLevel { + type Err = anyhow::Error; + + fn from_str(value: &str) -> Result { + match value { + "error" => Ok(LogLevel::Error), + "warn" => Ok(LogLevel::Warn), + "info" => Ok(LogLevel::Info), + "debug" => Ok(LogLevel::Debug), + "trace" => Ok(LogLevel::Trace), + value => Err(anyhow::anyhow!("Can't parse log level from '{value}'.")), + } + } +} diff --git a/crates/air/tests/format-check.rs b/crates/air/tests/format-check.rs new file mode 100644 index 00000000..3c9db1e8 --- /dev/null +++ b/crates/air/tests/format-check.rs @@ -0,0 +1,26 @@ +use std::path::Path; +use std::path::PathBuf; + +use air::args::Args; +use air::run; +use air::status::ExitStatus; +use clap::Parser; + +#[test] +fn test_check_returns_cleanly_for_multiline_strings_with_crlf_line_endings() -> anyhow::Result<()> { + let fixtures = path_fixtures(); + let path = fixtures.join("crlf").join("multiline_string_value.R"); + let path = path.to_str().unwrap(); + + let args = Args::parse_from(["", "format", path, "--check"]); + let err = run(args)?; + + assert_eq!(err, ExitStatus::Success); + Ok(()) +} + +fn path_fixtures() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .join("tests") + .join("fixtures") +} diff --git a/crates/air/tests/format.rs b/crates/air/tests/format.rs index f710a2fb..6e9eb285 100644 --- a/crates/air/tests/format.rs +++ b/crates/air/tests/format.rs @@ -1,6 +1,3 @@ -use std::path::Path; -use std::path::PathBuf; - use air::args::Args; use air::run; use air::status::ExitStatus; @@ -24,22 +21,3 @@ fn default_options() -> anyhow::Result<()> { assert_eq!(err, ExitStatus::Success); Ok(()) } - -#[test] -fn test_check_returns_cleanly_for_multiline_strings_with_crlf_line_endings() -> anyhow::Result<()> { - let fixtures = path_fixtures(); - let path = fixtures.join("crlf").join("multiline_string_value.R"); - let path = path.to_str().unwrap(); - - let args = Args::parse_from(["", "format", path, "--check"]); - let err = run(args)?; - - assert_eq!(err, ExitStatus::Success); - Ok(()) -} - -fn path_fixtures() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .join("tests") - .join("fixtures") -} diff --git a/crates/air_r_parser/src/parse.rs b/crates/air_r_parser/src/parse.rs index df668c43..75ce8c8e 100644 --- a/crates/air_r_parser/src/parse.rs +++ b/crates/air_r_parser/src/parse.rs @@ -178,7 +178,7 @@ fn parse_failure() -> (Vec>, Vec, Vec) { // Generate a single diagnostic, wrap it in our error type let span: Option = None; - let diagnostic = ParseDiagnostic::new("Failed to parse", span); + let diagnostic = ParseDiagnostic::new("Failed to parse due to syntax errors.", span); let error = ParseError::from(diagnostic); let errors = vec![error]; diff --git a/crates/crates/Cargo.toml b/crates/crates/Cargo.toml new file mode 100644 index 00000000..9418fcda --- /dev/null +++ b/crates/crates/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "crates" +version = "0.0.0" +authors.workspace = true +categories.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dev-dependencies] +insta.workspace = true + +[build-dependencies] +cargo_metadata.workspace = true + +[lints] +workspace = true diff --git a/crates/lsp/build.rs b/crates/crates/build.rs similarity index 87% rename from crates/lsp/build.rs rename to crates/crates/build.rs index 025d58a7..3ac5bbdb 100644 --- a/crates/lsp/build.rs +++ b/crates/crates/build.rs @@ -11,7 +11,7 @@ fn main() { /// Write out a constant array of air crate names as `AIR_CRATE_NAMES` at build time fn write_workspace_crate_names() { let dir = env::var_os("OUT_DIR").unwrap(); - let path = Path::new(&dir).join("crates.rs"); + let path = Path::new(&dir).join("lib.rs"); // Equivalent to `cargo metadata --no-deps` let mut cmd = cargo_metadata::MetadataCommand::new(); @@ -34,7 +34,7 @@ fn write_workspace_crate_names() { let packages = packages.join(" "); - let contents = format!("pub(crate) const AIR_CRATE_NAMES: &[&str] = &[{packages}];"); + let contents = format!("pub const AIR_CRATE_NAMES: &[&str] = &[{packages}];"); fs::write(&path, contents).unwrap(); println!("cargo::rerun-if-changed=build.rs"); diff --git a/crates/lsp/src/crates.rs b/crates/crates/src/lib.rs similarity index 65% rename from crates/lsp/src/crates.rs rename to crates/crates/src/lib.rs index b7996511..3b103c71 100644 --- a/crates/lsp/src/crates.rs +++ b/crates/crates/src/lib.rs @@ -1,10 +1,10 @@ // Generates `AIR_CRATE_NAMES`, a const array of the crate names in the air workspace, -// see `lsp/src/build.rs` -include!(concat!(env!("OUT_DIR"), "/crates.rs")); +// see `crates/build.rs` +include!(concat!(env!("OUT_DIR"), "/lib.rs")); #[cfg(test)] mod tests { - use crate::crates::AIR_CRATE_NAMES; + use crate::AIR_CRATE_NAMES; #[test] fn test_crate_names() { diff --git a/crates/lsp/src/snapshots/lsp__crates__tests__crate_names.snap b/crates/crates/src/snapshots/crates__tests__crate_names.snap similarity index 87% rename from crates/lsp/src/snapshots/lsp__crates__tests__crate_names.snap rename to crates/crates/src/snapshots/crates__tests__crate_names.snap index 1fc4411e..7f911ce4 100644 --- a/crates/lsp/src/snapshots/lsp__crates__tests__crate_names.snap +++ b/crates/crates/src/snapshots/crates__tests__crate_names.snap @@ -1,5 +1,5 @@ --- -source: crates/lsp/src/crates.rs +source: crates/crates/src/lib.rs expression: AIR_CRATE_NAMES --- [ @@ -10,6 +10,7 @@ expression: AIR_CRATE_NAMES "air_r_parser", "air_r_syntax", "biome_ungrammar", + "crates", "fs", "line_ending", "lsp", diff --git a/crates/lsp/Cargo.toml b/crates/lsp/Cargo.toml index 20eed737..ec98fb2c 100644 --- a/crates/lsp/Cargo.toml +++ b/crates/lsp/Cargo.toml @@ -22,6 +22,7 @@ biome_lsp_converters.workspace = true biome_parser.workspace = true biome_rowan.workspace = true biome_text_size.workspace = true +crates.workspace = true crossbeam.workspace = true dissimilar.workspace = true futures.workspace = true @@ -56,8 +57,5 @@ memchr.workspace = true tests_macros.workspace = true tokio-util.workspace = true -[build-dependencies] -cargo_metadata.workspace = true - [lints] workspace = true diff --git a/crates/lsp/src/lib.rs b/crates/lsp/src/lib.rs index 7e70ac5a..1c80953e 100644 --- a/crates/lsp/src/lib.rs +++ b/crates/lsp/src/lib.rs @@ -4,7 +4,6 @@ pub use tower_lsp::start_lsp; pub mod capabilities; -pub mod crates; pub mod documents; pub mod encoding; pub mod from_proto; diff --git a/crates/lsp/src/logging.rs b/crates/lsp/src/logging.rs index 70b34df5..90ee0c2b 100644 --- a/crates/lsp/src/logging.rs +++ b/crates/lsp/src/logging.rs @@ -64,8 +64,6 @@ use tracing_subscriber::{ layer::SubscriberExt, }; -use crate::crates; - const AIR_LOG_LEVEL: &str = "AIR_LOG_LEVEL"; const AIR_DEPENDENCY_LOG_LEVELS: &str = "AIR_DEPENDENCY_LOG_LEVELS";