-
-
Notifications
You must be signed in to change notification settings - Fork 538
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(oxc_language_server): Support nested configs #9724
Conversation
How to use the Graphite Merge QueueAdd either label to this PR to merge it via the merge queue:
You must have a Graphite account in order to use the merge queue. Sign up using this link. An organization admin has enabled the Graphite Merge Queue in this repository. Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue. |
CodSpeed Performance ReportMerging #9724 will create unknown performance changesComparing Summary
Benchmarks breakdown
|
struct Backend { | ||
client: Client, | ||
root_uri: OnceCell<Option<Url>>, | ||
server_linter: RwLock<ServerLinter>, | ||
diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>, | ||
options: Mutex<Options>, | ||
gitignore_glob: Mutex<Vec<Gitignore>>, | ||
nested_configs: RwLock<FxHashMap<PathBuf, ConfigStore>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Store a map of directory to config for all nested configs in the workspace.
@@ -54,11 +56,17 @@ struct Options { | |||
run: Run, | |||
enable: bool, | |||
config_path: String, | |||
use_nested_configs: bool, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow opting into nested configs. If false, there shouldn't be any behavior change.
debug!("watched file did change"); | ||
if self.options.lock().await.use_nested_configs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If using nested configs, check the watched files to determine if they are .oxlintrc.json
files (which are the only nested config files that are supported to my knowledge).
This just keeps the nested config map up to date with any workspace changes.
); | ||
} | ||
|
||
if self.options.lock().await.use_nested_configs { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If using nested configs, use Linter::new_with_nested_configs
and pass in the nested config files. Otherwise, should be no behavior change. However, I did do some refactoring here to make this easier for me to read through.
workspace.createFileSystemWatcher(oxlintConfigFileGlob), | ||
...workspaceConfigPatterns.map((workspaceConfigPattern) => { | ||
return workspace.createFileSystemWatcher(workspaceConfigPattern); | ||
}), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Watch any .oxlintrc.json file in the workspace.
Watch the file that the user has configured within their configuration.
This stops watching all variations of oxlint config files that were previously watched. Which, I think is safe.
changes: initialConfigFiles.map(file => { | ||
return { uri: file.toString(), type: FileChangeType.Created }; | ||
}), | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
On startup, send all found config files to the language server as "created", so that they get added to the nested config map.
struct Backend { | ||
client: Client, | ||
root_uri: OnceCell<Option<Url>>, | ||
server_linter: RwLock<ServerLinter>, | ||
diagnostics_report_map: ConcurrentHashMap<String, Vec<DiagnosticReport>>, | ||
options: Mutex<Options>, | ||
gitignore_glob: Mutex<Vec<Gitignore>>, | ||
nested_configs: RwLock<FxHashMap<PathBuf, ConfigStore>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know if this is the correct type. RwLock<FxHashMap<PathBuf, ConfigStore>>
. Maybe something else should be used, but this is the only one I could figure out how to make work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems ok to me, but I'm not an expert in the async data structures area 😄 Might be worth looking at https://docs.rs/papaya/latest/papaya/, as I know we have been using that recently?
let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
for (key, value) in existing_nested_configs { | ||
config_files.insert(key.clone(), value.clone()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am copying the map here, so I can mutate it later, before setting the value back into self.nested_configs
. There might be a better way to do this that I don't know about.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth looking at https://doc.rust-lang.org/std/mem/fn.take.html. It still requires allocating an empty hash map, but that is much cheaper compared to cloning a full hash map. Then after you've taken the hash map, you have full ownership over it and can insert into as needed.
let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
for (key, value) in existing_nested_configs { | ||
config_files.insert(key.clone(), value.clone()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copying the map so that I can pass it into Linter::new_with_nested_configs
. Might be a better way to do the copy that I'm not aware of.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe std::mem::take? https://doc.rust-lang.org/std/mem/fn.take.html but not sure if that works in async context like this
@@ -19,7 +19,7 @@ impl Clone for ResolvedLinterState { | |||
} | |||
} | |||
|
|||
#[derive(Debug)] | |||
#[derive(Debug, Clone)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed so that I can clone my map that contains this struct.
@@ -29,7 +29,7 @@ struct Config { | |||
} | |||
|
|||
/// Resolves a lint configuration for a given file, by applying overrides based on the file's path. | |||
#[derive(Debug)] | |||
#[derive(Debug, Clone)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needed so that I can clone my map that contains this struct.
@camchenry can you review this to make sure the logic lines up with how the CLI works? (And any assistance with the code that looks odd would be helpful to, like if I should be doing something different with the maps instead of cloning.) |
@Sysix can you make sure this functionality makes sense to you from the language server perspective? (And any feedback with how the code is actually written would be appreciated too.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this all looks right so far. I guess one advantage of having the actual config resolution in just oxc_linter is that it should "just work". As long as the config files are found correctly and passed in, the config files should just get resolved as expected, same as in the CLI (hopefully).
let mut config_files = FxHashMap::default(); | ||
let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
for (key, value) in existing_nested_configs { | ||
config_files.insert(key.clone(), value.clone()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
instead of inserting one-by-one, maybe we can try extending the hash map all at once? this should be more efficient because it allocates the needed memory upfront: https://doc.rust-lang.org/nightly/std/collections/struct.HashMap.html#impl-Extend%3C(K,+V)%3E-for-HashMap%3CK,+V,+S%3E
let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
for (key, value) in existing_nested_configs { | ||
config_files.insert(key.clone(), value.clone()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be worth looking at https://doc.rust-lang.org/std/mem/fn.take.html. It still requires allocating an empty hash map, but that is much cheaper compared to cloning a full hash map. Then after you've taken the hash map, you have full ownership over it and can insert into as needed.
if x.uri.to_file_path().unwrap().file_name().unwrap() != OXC_CONFIG_FILE { | ||
return; | ||
} | ||
// spellchecker:off -- "typ" is accurate | ||
if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | ||
// spellchecker:on | ||
let file_path = x.uri.to_file_path().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we could avoid a duplicate unwrap here and some work by saving to_file_path()
:
if x.uri.to_file_path().unwrap().file_name().unwrap() != OXC_CONFIG_FILE { | |
return; | |
} | |
// spellchecker:off -- "typ" is accurate | |
if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | |
// spellchecker:on | |
let file_path = x.uri.to_file_path().unwrap(); | |
let Some(file_path) = x.uri.to_file_path() else { | |
return; | |
}; | |
if file_path.file_name().unwrap() != OXC_CONFIG_FILE { | |
return; | |
} | |
// spellchecker:off -- "typ" is accurate | |
if x.typ == FileChangeType::CREATED || x.typ == FileChangeType::CHANGED { | |
// spellchecker:on |
let existing_nested_configs = (*self.nested_configs.read().await).clone(); | ||
for (key, value) in existing_nested_configs { | ||
config_files.insert(key.clone(), value.clone()); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe std::mem::take? https://doc.rust-lang.org/std/mem/fn.take.html but not sure if that works in async context like this
Just putting this up to have the idea validated before I clean it up and separate it out into different PRs.
Left comments inline to sort of communicate what I'm doing or why. I don't do much in Rust, so I think it might be helpful to communicate what I'm trying to do in case there are better alternatives.
.unwrap
that I want to review and fix.