diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index e6c2f24dcda06..4572f8efb8f9b 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -543,15 +543,15 @@ impl Backend { let Ok(root_path) = uri.to_file_path() else { return None; }; - let mut config_path = None; - let config = root_path.join(self.options.lock().await.get_config_path().unwrap()); - if config.exists() { - config_path = Some(config); - } + let relative_config_path = self.options.lock().await.get_config_path(); + let oxlintrc = if relative_config_path.is_some() { + let config = root_path.join(relative_config_path.unwrap()); + config.try_exists().expect("Invalid config file path"); + Oxlintrc::from_file(&config).expect("Failed to initialize oxlintrc config") + } else { + Oxlintrc::default() + }; - let config_path = config_path?; - let oxlintrc = Oxlintrc::from_file(&config_path) - .expect("should have initialized linter with new options"); let config_store = ConfigStoreBuilder::from_oxlintrc(true, oxlintrc.clone()) .expect("failed to build config") .build() diff --git a/editors/vscode/client/Config.ts b/editors/vscode/client/Config.ts index 917d8036bbbe3..3a6326f846664 100644 --- a/editors/vscode/client/Config.ts +++ b/editors/vscode/client/Config.ts @@ -10,6 +10,7 @@ export class Config implements ConfigInterface { private _trace!: TraceLevel; private _configPath!: string; private _binPath: string | undefined; + private _flags!: Record; constructor() { this.refresh(); @@ -17,12 +18,15 @@ export class Config implements ConfigInterface { public refresh(): void { const conf = workspace.getConfiguration(Config._namespace); + const flags = conf.get>('flags') ?? {}; + const useNestedConfigs = !('disable_nested_config' in flags); this._runTrigger = conf.get('lint.run') || 'onType'; this._enable = conf.get('enable') ?? true; this._trace = conf.get('trace.server') || 'off'; - this._configPath = conf.get('configPath') || oxlintConfigFileName; + this._configPath = conf.get('configPath') || (useNestedConfigs ? '' : oxlintConfigFileName); this._binPath = conf.get('path.server'); + this._flags = flags; } get runTrigger(): Trigger { @@ -80,11 +84,23 @@ export class Config implements ConfigInterface { .update('path.server', value); } + get flags(): Record { + return this._flags; + } + + updateFlags(value: Record): PromiseLike { + this._flags = value; + return workspace + .getConfiguration(Config._namespace) + .update('flags', value); + } + public toLanguageServerConfig(): LanguageServerConfig { return { run: this.runTrigger, enable: this.enable, configPath: this.configPath, + flags: this.flags, }; } } @@ -93,6 +109,7 @@ interface LanguageServerConfig { configPath: string; enable: boolean; run: Trigger; + flags: Record; } export type Trigger = 'onSave' | 'onType'; diff --git a/editors/vscode/client/config.spec.ts b/editors/vscode/client/config.spec.ts index 4522361c86de0..f8be07976b6ac 100644 --- a/editors/vscode/client/config.spec.ts +++ b/editors/vscode/client/config.spec.ts @@ -1,11 +1,11 @@ -import { strictEqual } from 'assert'; +import { deepStrictEqual, strictEqual } from 'assert'; import { workspace } from 'vscode'; import { Config } from './Config.js'; suite('Config', () => { setup(async () => { const wsConfig = workspace.getConfiguration('oxc'); - const keys = ['lint.run', 'enable', 'trace.server', 'configPath', 'path.server']; + const keys = ['lint.run', 'enable', 'trace.server', 'configPath', 'path.server', 'flags']; await Promise.all(keys.map(key => wsConfig.update(key, undefined))); }); @@ -18,6 +18,29 @@ suite('Config', () => { strictEqual(config.trace, 'off'); strictEqual(config.configPath, '.oxlintrc.json'); strictEqual(config.binPath, ''); + deepStrictEqual(config.flags, {}); + }); + + test('configPath defaults to empty string when using nested configs and configPath is empty', async () => { + const wsConfig = workspace.getConfiguration('oxc'); + await wsConfig.update('configPath', ''); + await wsConfig.update('flags', {}); + + const config = new Config(); + + deepStrictEqual(config.flags, {}); + strictEqual(config.configPath, ''); + }); + + test('configPath defaults to .oxlintrc.json when not using nested configs and configPath is empty', async () => { + const wsConfig = workspace.getConfiguration('oxc'); + await wsConfig.update('configPath', ''); + await wsConfig.update('flags', { disable_nested_config: '' }); + + const config = new Config(); + + deepStrictEqual(config.flags, { disable_nested_config: '' }); + strictEqual(config.configPath, '.oxlintrc.json'); }); test('updating values updates the workspace configuration', async () => { @@ -29,6 +52,7 @@ suite('Config', () => { config.updateTrace('messages'), config.updateConfigPath('./somewhere'), config.updateBinPath('./binary'), + config.updateFlags({ test: 'value' }), ]); const wsConfig = workspace.getConfiguration('oxc'); @@ -38,5 +62,6 @@ suite('Config', () => { strictEqual(wsConfig.get('trace.server'), 'messages'); strictEqual(wsConfig.get('configPath'), './somewhere'); strictEqual(wsConfig.get('path.server'), './binary'); + deepStrictEqual(wsConfig.get('flags'), { test: 'value' }); }); }); diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 026b57c5326e6..085cdd6c53217 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -7,11 +7,18 @@ import { StatusBarAlignment, StatusBarItem, ThemeColor, + Uri, 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'; @@ -231,7 +238,10 @@ export async function activate(context: ExtensionContext) { if (event.affectsConfiguration('oxc.configPath')) { client.clientOptions.synchronize = client.clientOptions.synchronize ?? {}; client.clientOptions.synchronize.fileEvents = createFileEventWatchers(configService.config.configPath); - client.restart(); + client.restart().then(async () => { + const configFiles = await findOxlintrcConfigFiles(); + await sendDidChangeWatchedFilesNotificationWith(client, configFiles); + }); } }; @@ -255,7 +265,10 @@ export async function activate(context: ExtensionContext) { myStatusBarItem.backgroundColor = bgColor; } updateStatsBar(configService.config.enable); - client.start(); + await client.start(); + + const configFiles = await findOxlintrcConfigFiles(); + await sendDidChangeWatchedFilesNotificationWith(client, configFiles); } export function deactivate(): Thenable | undefined { @@ -277,3 +290,15 @@ function createFileEventWatchers(configRelativePath: string) { }), ]; } + +async function findOxlintrcConfigFiles() { + return workspace.findFiles(`**/${oxlintConfigFileName}`); +} + +async function sendDidChangeWatchedFilesNotificationWith(languageClient: LanguageClient, files: Uri[]) { + await languageClient.sendNotification(DidChangeWatchedFilesNotification.type, { + changes: files.map(file => { + return { uri: file.toString(), type: FileChangeType.Created }; + }), + }); +} diff --git a/editors/vscode/package.json b/editors/vscode/package.json index 81809d0fa0b40..0c3763dda2e73 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -104,6 +104,12 @@ "type": "string", "scope": "window", "description": "Path to Oxc language server binary." + }, + "oxc.flags": { + "type": "object", + "scope": "window", + "default": {}, + "description": "Oxc options that would normally be passed when executing on the command line." } } },