Skip to content

Implement user level air.toml support - #513

Merged
DavisVaughan merged 4 commits into
mainfrom
feature/global-config
Jul 14, 2026
Merged

Implement user level air.toml support#513
DavisVaughan merged 4 commits into
mainfrom
feature/global-config

Conversation

@DavisVaughan

Copy link
Copy Markdown
Collaborator

Closes #309

This PR adds user level air.toml support. It uses the etcetera crate, same as ark and ruff, to locate a config directory that is OS specific:

  • ~/.config/air/air.toml on Linux and macOS. The environment variable XDG_CONFIG_HOME can be set as an alternative to ~/.config, i.e. if set, Air will look in {XDG_CONFIG_HOME}/air/air.toml.

  • %APPDATA%\air\air.toml on Windows. The environment variable APPDATA can be set as an alternative to %APPDATA%, i.e. if set, Air will look in {APPDATA}\air\air.toml.

When there isn't a project level air.toml, Air will now look for a user level air.toml before falling back to the default Air settings. This makes the user level air.toml a useful place to put personal settings for scratch files or projects that don't use Air. We still highly encourage using a project level air.toml for collaborative projects.

IDE Synchronization

A user level air.toml counts as a "real" air.toml, and takes precedence over IDE settings.

Watched files notifications

We use GlobPattern::Relative to watch a directory outside of the workspace, i.e.:

        watchers.push(FileSystemWatcher {
            glob_pattern: GlobPattern::Relative(RelativePattern {
                base_uri: OneOf::Right(directory.clone()),
                pattern: String::from("air.toml"),
            }),
            kind: None,
        });

where directory here is the user config directory of ~/.config/air/ (on mac). Note how we update ~/.config/air/air.toml outside of the IDE and still get a change notification!

Screen.Recording.2026-07-06.at.11.12.47.PM.mov

excludes

The most complicated part of this PR is exclude handling.

A user level air.toml lives outside of your project's directory tree, so you can't specify rooted (or relative) patterns like R/foo.R because this resolves to {root}/R/foo.R.

Instead, you must supply unrooted (or global) patterns like **/foo.R or the simpler foo.R, which also resolves to **/foo.R. You can also do folder/, which resolves to **/folder/.

The simplest way to think about it is that you can't supply anything with leading or interior /, as that is relative to a {root}, which isn't applicable for user level air.tomls. I don't anticipate this being restrictive at all, and we give a pretty good error message about it. (These are also the same rules that git follows, as implemented by the ignore crate).

By reworking file patterns into `RootedFilePatterns` and `UnrootedFilePatterns`, so that `ExcludePatterns` can be an enum over both, depending on whether or not a `root` was provided.

This is required for a global config `air.toml`, which won't have a `root` directory to be relative to, but we still want people to be able to provide unrooted global `exclude`s like `foo.R`, but not `/folder/bar.R`.
resolver.add(&directory, settings);
}

let default_settings = discover_user_settings()?.unwrap_or_default();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really the only new thing in the CLI is that Settings::default() is replaced with this, where we first attempt to find user settings

Comment on lines +11 to +15
air failed
Cause: Failed to parse [AIR_TOML]:
Pattern `src/foo.R` must meet one of the following conditions:
- Start with `**/`, i.e. `**/foo.R`
- Contain no `/` characters unless it is at the very end, i.e. `foo.R` or `folder/`

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the error you get when you have a bad exclude. Unfortunately I can't easily get exclude mentioned in there due to how the types are set up.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit confusing. Path separators are allowed in the pattern right? Which contradicts "unless it is at the very end"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I understand. Can we link to the doc here? Or clarify a bit by mentioning the notion of "rooted".

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hopefully 5b78a17 makes this all clearer with:

  • consistent rooted vs unrooted terminology everywhere
  • explicit "Unrooted patterns" section in the docs with bulleted comparison
  • expanded error message information, which mirrors the "Unrooted patterns" docs (decided not to link to it, a lengthy error message describing the exact issue felt more useful in this case, and I'd hate to accidentally break the link one day in the future if we rework the site)

Comment on lines +45 to +47
let relative_pattern_support_for_did_change_watched_files = did_change_watched_files
.and_then(|watched_files| watched_files.relative_pattern_support)
.unwrap_or_default();

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the Client side setting that determines if out-of-workspace watching is supported

/// also `{root}/subdir/special.R`.
#[derive(Clone, Debug)]
pub struct FilePatterns {
pub struct RootedFilePatterns {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We've renamed:

  • FilePatterns -> RootedFilePatterns
  • DefaultFilePatterns -> UnrootedFilePatterns

The "default" name made sense at the time because these were used in DefaultExcludePatterns and DefaultIncludePatterns, but it turns out this is the exact data structure we need for the general case of "I don't have a root path to resolve patterns relative to"

Comment on lines 18 to -21
pub struct PathResolver<T> {
/// Fallback value to be used when a `path` isn't associated with any `items`
fallback: T,

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crates/lsp/src/workspaces.rs got much simpler to implement after removing the fallback handling from PathResolver and instead relying on the caller to handle caller specific fallback details when resolve() returns None.

I was able to remove a few // How can we do better? style TODOs over there after clearing this up

@DavisVaughan
DavisVaughan requested a review from lionel- July 7, 2026 17:05
Comment on lines +11 to +15
air failed
Cause: Failed to parse [AIR_TOML]:
Pattern `src/foo.R` must meet one of the following conditions:
- Start with `**/`, i.e. `**/foo.R`
- Contain no `/` characters unless it is at the very end, i.e. `foo.R` or `folder/`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this a bit confusing. Path separators are allowed in the pattern right? Which contradicts "unless it is at the very end"

Comment thread crates/air/tests/integration/config.rs Outdated
Comment thread crates/lsp/src/handlers.rs
Comment thread crates/workspace/src/file_patterns.rs
Comment thread crates/workspace/src/file_patterns.rs Outdated
Comment thread crates/workspace/src/file_patterns.rs Outdated
Comment thread docs/configuration.qmd Outdated
Comment thread docs/configuration.qmd Outdated
Comment thread docs/configuration.qmd Outdated
Comment on lines +11 to +15
air failed
Cause: Failed to parse [AIR_TOML]:
Pattern `src/foo.R` must meet one of the following conditions:
- Start with `**/`, i.e. `**/foo.R`
- Contain no `/` characters unless it is at the very end, i.e. `foo.R` or `folder/`

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now I understand. Can we link to the doc here? Or clarify a bit by mentioning the notion of "rooted".

@DavisVaughan
DavisVaughan merged commit 3f0d006 into main Jul 14, 2026
4 checks passed
@DavisVaughan
DavisVaughan deleted the feature/global-config branch July 14, 2026 16:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Consider a global fallback air.toml location

2 participants