-
-
Notifications
You must be signed in to change notification settings - Fork 861
feat(oxc_language_server): Support nested configs #9724
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,8 @@ use futures::future::join_all; | |||||||||||||||||||||||||||||||||
| use globset::Glob; | ||||||||||||||||||||||||||||||||||
| use ignore::gitignore::Gitignore; | ||||||||||||||||||||||||||||||||||
| use log::{debug, error, info}; | ||||||||||||||||||||||||||||||||||
| use rustc_hash::FxBuildHasher; | ||||||||||||||||||||||||||||||||||
| use oxc_linter::{ConfigStore, ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc}; | ||||||||||||||||||||||||||||||||||
| use rustc_hash::{FxBuildHasher, FxHashMap}; | ||||||||||||||||||||||||||||||||||
| use serde::{Deserialize, Serialize}; | ||||||||||||||||||||||||||||||||||
| use tokio::sync::{Mutex, OnceCell, RwLock, SetError}; | ||||||||||||||||||||||||||||||||||
| use tower_lsp::{ | ||||||||||||||||||||||||||||||||||
|
|
@@ -15,14 +16,12 @@ use tower_lsp::{ | |||||||||||||||||||||||||||||||||
| CodeAction, CodeActionKind, CodeActionOrCommand, CodeActionParams, CodeActionResponse, | ||||||||||||||||||||||||||||||||||
| ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, | ||||||||||||||||||||||||||||||||||
| DidChangeWatchedFilesParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, | ||||||||||||||||||||||||||||||||||
| DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult, | ||||||||||||||||||||||||||||||||||
| InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, Url, | ||||||||||||||||||||||||||||||||||
| WorkspaceEdit, | ||||||||||||||||||||||||||||||||||
| DidSaveTextDocumentParams, ExecuteCommandParams, FileChangeType, InitializeParams, | ||||||||||||||||||||||||||||||||||
| InitializeResult, InitializedParams, NumberOrString, Position, Range, ServerInfo, TextEdit, | ||||||||||||||||||||||||||||||||||
| Url, WorkspaceEdit, | ||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| use oxc_linter::{ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc}; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| use crate::capabilities::{CODE_ACTION_KIND_SOURCE_FIX_ALL_OXC, Capabilities}; | ||||||||||||||||||||||||||||||||||
| use crate::linter::error_with_position::DiagnosticReport; | ||||||||||||||||||||||||||||||||||
| use crate::linter::server_linter::ServerLinter; | ||||||||||||||||||||||||||||||||||
|
|
@@ -33,13 +32,16 @@ mod linter; | |||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| type ConcurrentHashMap<K, V> = papaya::HashMap<K, V, FxBuildHasher>; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const OXC_CONFIG_FILE: &str = ".oxlintrc.json"; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| 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>>, | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't know if this is the correct type.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| #[derive(Debug, Serialize, Deserialize, Default, PartialEq, PartialOrd, Clone, Copy)] | ||||||||||||||||||||||||||||||||||
| #[serde(rename_all = "camelCase")] | ||||||||||||||||||||||||||||||||||
|
|
@@ -54,11 +56,17 @@ struct Options { | |||||||||||||||||||||||||||||||||
| run: Run, | ||||||||||||||||||||||||||||||||||
| enable: bool, | ||||||||||||||||||||||||||||||||||
| config_path: String, | ||||||||||||||||||||||||||||||||||
| use_nested_configs: bool, | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| impl Default for Options { | ||||||||||||||||||||||||||||||||||
| fn default() -> Self { | ||||||||||||||||||||||||||||||||||
| Self { enable: true, run: Run::default(), config_path: ".oxlintrc.json".into() } | ||||||||||||||||||||||||||||||||||
| Self { | ||||||||||||||||||||||||||||||||||
| enable: true, | ||||||||||||||||||||||||||||||||||
| run: Run::default(), | ||||||||||||||||||||||||||||||||||
| config_path: OXC_CONFIG_FILE.into(), | ||||||||||||||||||||||||||||||||||
| use_nested_configs: false, | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
@@ -180,8 +188,38 @@ impl LanguageServer for Backend { | |||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async fn did_change_watched_files(&self, _params: DidChangeWatchedFilesParams) { | ||||||||||||||||||||||||||||||||||
| async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) { | ||||||||||||||||||||||||||||||||||
| debug!("watched file did change"); | ||||||||||||||||||||||||||||||||||
| if self.options.lock().await.use_nested_configs { | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 This just keeps the nested config map up to date with any workspace changes. |
||||||||||||||||||||||||||||||||||
| 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()); | ||||||||||||||||||||||||||||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| params.changes.iter().for_each(|x| { | ||||||||||||||||||||||||||||||||||
| 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(); | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+201
to
+207
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could avoid a duplicate unwrap here and some work by saving
Suggested change
|
||||||||||||||||||||||||||||||||||
| let dir_path = file_path.parent().unwrap(); | ||||||||||||||||||||||||||||||||||
| let oxlintrc = Oxlintrc::from_file(&dir_path.join(OXC_CONFIG_FILE)).unwrap(); | ||||||||||||||||||||||||||||||||||
| let config_store_builder = | ||||||||||||||||||||||||||||||||||
| ConfigStoreBuilder::from_oxlintrc(false, oxlintrc).unwrap(); | ||||||||||||||||||||||||||||||||||
| let config_store = config_store_builder.build().unwrap(); | ||||||||||||||||||||||||||||||||||
| config_files.insert(dir_path.to_path_buf(), config_store); | ||||||||||||||||||||||||||||||||||
| // spellchecker:off -- "typ" is accurate | ||||||||||||||||||||||||||||||||||
| } else if x.typ == FileChangeType::DELETED { | ||||||||||||||||||||||||||||||||||
| // spellchecker:on | ||||||||||||||||||||||||||||||||||
| config_files.remove(&x.uri.to_file_path().unwrap()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||
| *self.nested_configs.write().await = config_files; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| self.init_linter_config().await; | ||||||||||||||||||||||||||||||||||
| self.revalidate_open_files().await; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
@@ -492,21 +530,46 @@ impl Backend { | |||||||||||||||||||||||||||||||||
| if config.exists() { | ||||||||||||||||||||||||||||||||||
| config_path = Some(config); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| let mut oxlintrc = None; | ||||||||||||||||||||||||||||||||||
| let mut config_store = None; | ||||||||||||||||||||||||||||||||||
| let mut linter = self.server_linter.write().await; | ||||||||||||||||||||||||||||||||||
| if let Some(config_path) = config_path { | ||||||||||||||||||||||||||||||||||
| let mut linter = self.server_linter.write().await; | ||||||||||||||||||||||||||||||||||
| let config = Oxlintrc::from_file(&config_path) | ||||||||||||||||||||||||||||||||||
| .expect("should have initialized linter with new options"); | ||||||||||||||||||||||||||||||||||
| let config_store = ConfigStoreBuilder::from_oxlintrc(true, config.clone()) | ||||||||||||||||||||||||||||||||||
| .expect("failed to build config") | ||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||
| .expect("failed to build config"); | ||||||||||||||||||||||||||||||||||
| oxlintrc = Some( | ||||||||||||||||||||||||||||||||||
| Oxlintrc::from_file(&config_path) | ||||||||||||||||||||||||||||||||||
| .expect("should have initialized linter with new options"), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| config_store = Some( | ||||||||||||||||||||||||||||||||||
| ConfigStoreBuilder::from_oxlintrc(true, oxlintrc.clone().unwrap()) | ||||||||||||||||||||||||||||||||||
| .expect("failed to build config") | ||||||||||||||||||||||||||||||||||
| .build() | ||||||||||||||||||||||||||||||||||
| .expect("failed to build config"), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if self.options.lock().await.use_nested_configs { | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If using nested configs, use |
||||||||||||||||||||||||||||||||||
| let mut config_files: FxHashMap<PathBuf, ConfigStore> = 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()); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copying the map so that I can pass it into
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| *linter = ServerLinter::new_with_linter( | ||||||||||||||||||||||||||||||||||
| Linter::new_with_nested_configs( | ||||||||||||||||||||||||||||||||||
| LintOptions::default(), | ||||||||||||||||||||||||||||||||||
| config_store.unwrap(), | ||||||||||||||||||||||||||||||||||
| config_files, | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
| .with_fix(FixKind::SafeFix), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||
| *linter = ServerLinter::new_with_linter( | ||||||||||||||||||||||||||||||||||
| Linter::new(LintOptions::default(), config_store).with_fix(FixKind::SafeFix), | ||||||||||||||||||||||||||||||||||
| Linter::new(LintOptions::default(), config_store.unwrap()) | ||||||||||||||||||||||||||||||||||
| .with_fix(FixKind::SafeFix), | ||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||
| return Some(config); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| None | ||||||||||||||||||||||||||||||||||
| Some(oxlintrc.clone().unwrap()) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| async fn handle_file_update(&self, uri: Url, content: Option<String>, version: Option<i32>) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -568,6 +631,7 @@ async fn main() { | |||||||||||||||||||||||||||||||||
| diagnostics_report_map, | ||||||||||||||||||||||||||||||||||
| options: Mutex::new(Options::default()), | ||||||||||||||||||||||||||||||||||
| gitignore_glob: Mutex::new(vec![]), | ||||||||||||||||||||||||||||||||||
| nested_configs: RwLock::new(FxHashMap::default()), | ||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||
| .finish(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ impl Clone for ResolvedLinterState { | |
| } | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| #[derive(Debug, Clone)] | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needed so that I can clone my map that contains this struct. |
||
| struct Config { | ||
| /// The basic linter state for this configuration. | ||
| base: ResolvedLinterState, | ||
|
|
@@ -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)] | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needed so that I can clone my map that contains this struct. |
||
| pub struct ConfigStore { | ||
| base: Config, | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,23 @@ | ||
| import { promises as fsPromises } from 'node:fs'; | ||
|
|
||
| import { commands, ExtensionContext, StatusBarAlignment, StatusBarItem, ThemeColor, window, workspace } from 'vscode'; | ||
| import { | ||
| commands, | ||
| ExtensionContext, | ||
| RelativePattern, | ||
| StatusBarAlignment, | ||
| StatusBarItem, | ||
| ThemeColor, | ||
| window, | ||
| workspace, | ||
| } from 'vscode'; | ||
|
|
||
| import { ExecuteCommandRequest, MessageType, ShowMessageNotification } from 'vscode-languageclient'; | ||
| import { | ||
| DidChangeWatchedFilesNotification, | ||
| ExecuteCommandRequest, | ||
| FileChangeType, | ||
| MessageType, | ||
| ShowMessageNotification, | ||
| } from 'vscode-languageclient'; | ||
|
|
||
| import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node'; | ||
|
|
||
|
|
@@ -12,6 +27,7 @@ import { ConfigService } from './ConfigService'; | |
| const languageClientName = 'oxc'; | ||
| const outputChannelName = 'Oxc'; | ||
| const commandPrefix = 'oxc'; | ||
| const oxlintConfigFileGlob = '**/.oxlintrc.json'; | ||
|
|
||
| const enum OxcCommands { | ||
| RestartServer = `${commandPrefix}.restartServer`, | ||
|
|
@@ -98,6 +114,10 @@ export async function activate(context: ExtensionContext) { | |
| configService, | ||
| ); | ||
|
|
||
| const workspaceConfigPatterns = (workspace.workspaceFolders || []).map((workspaceFolder) => | ||
| new RelativePattern(workspaceFolder, configService.config.configPath) | ||
| ); | ||
|
|
||
| const outputChannel = window.createOutputChannel(outputChannelName, { log: true }); | ||
|
|
||
| async function findBinary(): Promise<string> { | ||
|
|
@@ -171,8 +191,10 @@ export async function activate(context: ExtensionContext) { | |
| synchronize: { | ||
| // Notify the server about file config changes in the workspace | ||
| fileEvents: [ | ||
| workspace.createFileSystemWatcher('**/.oxlint{.json,rc.json}'), | ||
| workspace.createFileSystemWatcher('**/oxlint{.json,rc.json}'), | ||
| workspace.createFileSystemWatcher(oxlintConfigFileGlob), | ||
| ...workspaceConfigPatterns.map((workspaceConfigPattern) => { | ||
| return workspace.createFileSystemWatcher(workspaceConfigPattern); | ||
| }), | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Watch any .oxlintrc.json file in the workspace. This stops watching all variations of oxlint config files that were previously watched. Which, I think is safe. |
||
| ], | ||
| }, | ||
| initializationOptions: { | ||
|
|
@@ -242,7 +264,17 @@ export async function activate(context: ExtensionContext) { | |
| myStatusBarItem.backgroundColor = bgColor; | ||
| } | ||
| updateStatsBar(configService.config.enable); | ||
| client.start(); | ||
| await client.start(); | ||
|
|
||
| const initialConfigFiles = | ||
| (await Promise.all([oxlintConfigFileGlob, ...workspaceConfigPatterns].map(async globPattern => { | ||
| return workspace.findFiles(globPattern); | ||
| }))).flat(); | ||
| await client.sendNotification(DidChangeWatchedFilesNotification.type, { | ||
| changes: initialConfigFiles.map(file => { | ||
| return { uri: file.toString(), type: FileChangeType.Created }; | ||
| }), | ||
| }); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. |
||
| } | ||
|
|
||
| export function deactivate(): Thenable<void> | undefined { | ||
|
|
||
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.