Implement user level air.toml support - #513
Conversation
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(); |
There was a problem hiding this comment.
Really the only new thing in the CLI is that Settings::default() is replaced with this, where we first attempt to find user settings
| 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/` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
I find this a bit confusing. Path separators are allowed in the pattern right? Which contradicts "unless it is at the very end"
There was a problem hiding this comment.
Now I understand. Can we link to the doc here? Or clarify a bit by mentioning the notion of "rooted".
There was a problem hiding this comment.
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)
| 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(); |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
We've renamed:
FilePatterns->RootedFilePatternsDefaultFilePatterns->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"
| pub struct PathResolver<T> { | ||
| /// Fallback value to be used when a `path` isn't associated with any `items` | ||
| fallback: T, | ||
|
|
There was a problem hiding this comment.
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
| 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/` |
There was a problem hiding this comment.
I find this a bit confusing. Path separators are allowed in the pattern right? Which contradicts "unless it is at the very end"
| 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/` |
There was a problem hiding this comment.
Now I understand. Can we link to the doc here? Or clarify a bit by mentioning the notion of "rooted".
Closes #309
This PR adds user level
air.tomlsupport. It uses the etcetera crate, same as ark and ruff, to locate a config directory that is OS specific:~/.config/air/air.tomlon Linux and macOS. The environment variableXDG_CONFIG_HOMEcan be set as an alternative to~/.config, i.e. if set, Air will look in{XDG_CONFIG_HOME}/air/air.toml.%APPDATA%\air\air.tomlon Windows. The environment variableAPPDATAcan 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 levelair.tomlbefore falling back to the default Air settings. This makes the user levelair.tomla useful place to put personal settings for scratch files or projects that don't use Air. We still highly encourage using a project levelair.tomlfor collaborative projects.IDE Synchronization
A user level
air.tomlcounts as a "real"air.toml, and takes precedence over IDE settings.Watched files notifications
We use
GlobPattern::Relativeto watch a directory outside of the workspace, i.e.:where
directoryhere is the user config directory of~/.config/air/(on mac). Note how we update~/.config/air/air.tomloutside of the IDE and still get a change notification!Screen.Recording.2026-07-06.at.11.12.47.PM.mov
excludesThe most complicated part of this PR is
excludehandling.A user level
air.tomllives outside of your project's directory tree, so you can't specify rooted (or relative) patterns likeR/foo.Rbecause this resolves to{root}/R/foo.R.Instead, you must supply unrooted (or global) patterns like
**/foo.Ror the simplerfoo.R, which also resolves to**/foo.R. You can also dofolder/, 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 levelair.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).