diff --git a/CHANGELOG.md b/CHANGELOG.md index 88941dea..bbf429ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ # Development version +- 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). + # 0.1.2 diff --git a/crates/lsp/src/handlers.rs b/crates/lsp/src/handlers.rs index 1e45a7dd..67113ecc 100644 --- a/crates/lsp/src/handlers.rs +++ b/crates/lsp/src/handlers.rs @@ -57,16 +57,22 @@ pub(crate) async fn handle_initialized(lsp_state: &LspState) -> anyhow::Result<( .capabilities .dynamic_registration_for_did_change_watched_files { - // Watch for changes in `air.toml` files so we can react dynamically + // Watch for changes in configuration files so we can react dynamically let watch_air_toml_registration = lsp_types::Registration { id: String::from("air-toml-watcher"), method: "workspace/didChangeWatchedFiles".into(), register_options: Some( serde_json::to_value(DidChangeWatchedFilesRegistrationOptions { - watchers: vec![FileSystemWatcher { - glob_pattern: lsp_types::GlobPattern::String("**/air.toml".into()), - kind: None, - }], + watchers: vec![ + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/air.toml".into()), + kind: None, + }, + FileSystemWatcher { + glob_pattern: lsp_types::GlobPattern::String("**/.air.toml".into()), + kind: None, + }, + ], }) .unwrap(), ), diff --git a/crates/lsp/src/workspaces.rs b/crates/lsp/src/workspaces.rs index c36c7f9c..7685d4a9 100644 --- a/crates/lsp/src/workspaces.rs +++ b/crates/lsp/src/workspaces.rs @@ -14,6 +14,7 @@ use workspace::discovery::discover_settings; use workspace::discovery::DiscoveredSettings; use workspace::resolve::PathResolver; use workspace::settings::Settings; +use workspace::toml::is_air_toml; /// Convenience type for the inner resolver of path -> [`Settings`] type SettingsResolver = PathResolver; @@ -164,7 +165,7 @@ impl WorkspaceSettingsResolver { } }; - if !path.ends_with("air.toml") { + if !is_air_toml(&path) { // We could get called with a changed file that isn't an `air.toml` if we are // watching more than `air.toml` files tracing::trace!("Ignoring non-`air.toml` changed URL: {url}"); diff --git a/crates/workspace/src/toml.rs b/crates/workspace/src/toml.rs index 0a627322..98c63d49 100644 --- a/crates/workspace/src/toml.rs +++ b/crates/workspace/src/toml.rs @@ -13,7 +13,7 @@ use std::fmt::Formatter; use std::io; use std::path::{Path, PathBuf}; -/// Parse an `air.toml` file. +/// Parse an air configuration file. pub fn parse_air_toml>(path: P) -> Result { let contents = std::fs::read_to_string(path.as_ref()) .map_err(|err| ParseTomlError::Read(path.as_ref().to_path_buf(), err))?; @@ -50,19 +50,25 @@ impl Display for ParseTomlError { } } -/// Return the path to the `air.toml` file in a given directory. +/// Return the path to the `air.toml` or `.air.toml` file in a given directory. pub fn find_air_toml_in_directory>(path: P) -> Option { - // Check for `air.toml`. + // Check for `air.toml` first, as we prioritize the "visible" one. let toml = path.as_ref().join("air.toml"); + if toml.is_file() { + return Some(toml); + } + // Now check for `.air.toml` as well + let toml = path.as_ref().join(".air.toml"); if toml.is_file() { - Some(toml) - } else { - None + return Some(toml); } + + // Didn't find a configuration file + None } -/// Find the path to the closest `air.toml` if one exists, walking up the filesystem +/// Find the path to the closest `air.toml` or `.air.toml` if one exists, walking up the filesystem pub fn find_air_toml>(path: P) -> Option { for directory in path.as_ref().ancestors() { if let Some(toml) = find_air_toml_in_directory(directory) { @@ -72,6 +78,14 @@ pub fn find_air_toml>(path: P) -> Option { None } +/// Check if a path is named like an `air.toml` or `.air.toml` file +/// +/// Does not check if the path is an existing file on disk +pub fn is_air_toml>(path: P) -> bool { + let path = path.as_ref(); + path.ends_with("air.toml") || path.ends_with(".air.toml") +} + #[cfg(test)] mod tests { use anyhow::{Context, Result}; @@ -128,4 +142,60 @@ line-ending = "auto" Ok(()) } + + #[test] + fn find_and_parse_dot_air_toml() -> Result<()> { + let tempdir = TempDir::new()?; + let toml = tempdir.path().join(".air.toml"); + fs::write( + toml, + r#" +[format] +ignore-magic-line-break = true +"#, + )?; + + let toml = find_air_toml(tempdir.path()).context("Failed to find air.toml")?; + let options = parse_air_toml(toml)?; + + let ignore_magic_line_break = options + .format + .as_ref() + .context("Expected to find [format] table")? + .ignore_magic_line_break + .context("Expected to find `ignore-magic-line-break` field")?; + + assert!(ignore_magic_line_break); + + Ok(()) + } + + #[test] + fn test_air_toml_priority() -> Result<()> { + let tempdir = TempDir::new()?; + + let toml = tempdir.path().join("air.toml"); + fs::write( + toml.clone(), + r#" +[format] +indent-width = 3 +"#, + )?; + + let dot_toml = tempdir.path().join(".air.toml"); + fs::write( + dot_toml, + r#" +[format] +indent-width = 4 +"#, + )?; + + // Finds `air.toml` over `.air.toml` + let found_toml = find_air_toml(tempdir.path()).context("Failed to find air.toml")?; + assert_eq!(found_toml, toml); + + Ok(()) + } } diff --git a/docs/configuration.qmd b/docs/configuration.qmd index fe8cf656..14c4f8e6 100644 --- a/docs/configuration.qmd +++ b/docs/configuration.qmd @@ -44,6 +44,11 @@ If you run `air format` with a working directory of `~/files/dplyr` or open your Air also supports walking up the directory tree from the project root. For example, if you ran `air format` from within `~/files/dplyr/R`, then Air would look "up" one directory and would find and use `~/files/dplyr/air.toml`. +## Dotfiles + +Air supports both `air.toml` and `.air.toml`. +If both are present in the same directory, then `air.toml` is preferred (but we don't recommend this). + ## Format options All formatting options are specified under the `[format]` table.