diff --git a/crates/oxc_language_server/README.md b/crates/oxc_language_server/README.md index 989e6f1d7796d..201823ee43a26 100644 --- a/crates/oxc_language_server/README.md +++ b/crates/oxc_language_server/README.md @@ -87,6 +87,11 @@ The server will revalidate the diagnostics for all open files and send one or mo Note: When nested configuration is active, the client should send all `.oxlintrc.json` configurations to the server after the [initialized](#initialized) response. +#### [workspace/didChangeWorkspaceFolders](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWorkspaceFolders) + +The server expects this request when adding or removing workspace folders. +The server will request the specific workspace configuration, if the client supports it. + #### [workspace/executeCommand](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand) Executes a [Command](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand) if it exists. See [Server Capabilities](#server-capabilities) diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index fbacdbd85757e..8d575531db9b0 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -11,9 +11,9 @@ use tower_lsp_server::{ lsp_types::{ CodeActionParams, CodeActionResponse, ConfigurationItem, Diagnostic, DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, - DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, ServerInfo, - Uri, WorkspaceEdit, + DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, + DidSaveTextDocumentParams, ExecuteCommandParams, InitializeParams, InitializeResult, + InitializedParams, ServerInfo, Uri, WorkspaceEdit, }, }; // # @@ -88,9 +88,10 @@ impl Options { } impl LanguageServer for Backend { - #[expect(deprecated)] // TODO: FIXME + #[expect(deprecated)] // `params.root_uri` is deprecated, we are only falling back to it if no workspace folder is provided async fn initialize(&self, params: InitializeParams) -> Result { let server_version = env!("CARGO_PKG_VERSION"); + // initialization_options can be anything, so we are requesting `workspace/configuration` when no initialize options are provided let options = params.initialization_options.and_then(|mut value| { // the client supports the new settings object if let Ok(new_settings) = serde_json::from_value::>(value.clone()) @@ -120,27 +121,43 @@ impl LanguageServer for Backend { let capabilities = Capabilities::from(params.capabilities); - // ToDo: add support for multiple workspace folders - // maybe fallback when the client does not support it - let root_worker = WorkspaceWorker::new(params.root_uri.unwrap()); + // client sent workspace folders + let workers = if let Some(workspace_folders) = ¶ms.workspace_folders { + workspace_folders + .iter() + .map(|workspace_folder| WorkspaceWorker::new(workspace_folder.uri.clone())) + .collect() + // client sent deprecated root uri + } else if let Some(root_uri) = params.root_uri { + vec![WorkspaceWorker::new(root_uri)] + // client is in single file mode, create no workers + } else { + vec![] + }; // When the client did not send our custom `initialization_options`, // or the client does not support `workspace/configuration` request, // start the linter. We do not start the linter when the client support the request, // we will init the linter after requesting for the workspace configuration. if !capabilities.workspace_configuration || options.is_some() { - root_worker - .init_linter( - &options - .unwrap_or_default() - .first() - .map(|workspace_options| workspace_options.options.clone()) - .unwrap_or_default(), - ) - .await; + for worker in &workers { + worker + .init_linter( + &options + .clone() + .unwrap_or_default() + .iter() + .find(|workspace_option| { + worker.is_responsible_for_uri(&workspace_option.workspace_uri) + }) + .map(|workspace_options| workspace_options.options.clone()) + .unwrap_or_default(), + ) + .await; + } } - *self.workspace_workers.lock().await = vec![root_worker]; + *self.workspace_workers.lock().await = workers; self.capabilities.set(capabilities.clone()).map_err(|err| { let message = match err { @@ -339,6 +356,51 @@ impl LanguageServer for Backend { self.publish_all_diagnostics(x).await; } + async fn did_change_workspace_folders(&self, params: DidChangeWorkspaceFoldersParams) { + let mut workers = self.workspace_workers.lock().await; + let mut cleared_diagnostics = vec![]; + + for folder in params.event.removed { + let Some((index, worker)) = workers + .iter() + .enumerate() + .find(|(_, worker)| worker.is_responsible_for_uri(&folder.uri)) + else { + continue; + }; + cleared_diagnostics.extend(worker.get_clear_diagnostics()); + workers.remove(index); + } + + self.publish_all_diagnostics(&cleared_diagnostics).await; + + // client support `workspace/configuration` request + if self.capabilities.get().is_some_and(|capabilities| capabilities.workspace_configuration) + { + let configurations = self + .request_workspace_configuration( + params.event.added.iter().map(|w| &w.uri).collect(), + ) + .await; + + for (index, folder) in params.event.added.iter().enumerate() { + let worker = WorkspaceWorker::new(folder.uri.clone()); + // get the configuration from the response and init the linter + let options = configurations.get(index).unwrap_or(&None); + worker.init_linter(options.as_ref().unwrap_or(&Options::default())).await; + workers.push(worker); + } + // client does not support the request + } else { + for folder in params.event.added { + let worker = WorkspaceWorker::new(folder.uri); + // use default options + worker.init_linter(&Options::default()).await; + workers.push(worker); + } + } + } + async fn did_save(&self, params: DidSaveTextDocumentParams) { debug!("oxc server did save"); let uri = ¶ms.text_document.uri; diff --git a/editors/vscode/README.md b/editors/vscode/README.md index af05b6f836473..b0d112d0086aa 100644 --- a/editors/vscode/README.md +++ b/editors/vscode/README.md @@ -19,21 +19,31 @@ This is the linter for Oxc. The currently supported features are listed below. - Command to fix all auto-fixable content within the current text editor. - Support for `source.fixAll.oxc` as a code action provider. Configure this in your settings `editor.codeActionsOnSave` to automatically apply fixes when saving the file. +- Support for multi root workspaces ## Configuration -Following configuration are supported via `settings.json`: +### Window Configuration + +Following configuration are supported via `settings.json` and effect the window editor: | Key | Default Value | Possible Values | Description | | ------------------ | ------------- | -------------------------------- | --------------------------------------------------------------------------- | -| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) | | `oxc.enable` | `true` | `true` \| `false` | Enables the language server to receive lint diagnostics | | `oxc.trace.server` | `off` | `off` \| `messages` \| `verbose` | races the communication between VS Code and the language server. | -| `oxc.configPath` | `null` | `null`\| `` | Path to ESlint configuration. Keep it empty to enable nested configuration. | | `oxc.path.server` | - | `` | Path to Oxc language server binary. Mostly for testing the language server. | -| `oxc.flags` | - | `Record` | Custom flags passed to the language server. | -### Flags +### Workspace Configuration + +Following configuration are supported via `settings.json` and can be changed for each workspace: + +| Key | Default Value | Possible Values | Description | +| ---------------- | ------------- | ------------------------ | --------------------------------------------------------------------------- | +| `oxc.lint.run` | `onType` | `onSave` \| `onType` | Run the linter on save (onSave) or on type (onType) | +| `oxc.configPath` | `null` | `null`\| `` | Path to ESlint configuration. Keep it empty to enable nested configuration. | +| `oxc.flags` | - | `Record` | Custom flags passed to the language server. | + +#### Flags - `key: disable_nested_config`: Disabled nested configuration and searches only for `configPath` - `key: fix_kind`: default: `"safe_fix"`, possible values `"safe_fix" | "safe_fix_or_suggestion" | "dangerous_fix" | "dangerous_fix_or_suggestion" | "none" | "all"` diff --git a/editors/vscode/client/ConfigService.ts b/editors/vscode/client/ConfigService.ts index ffca822431bda..9096ce4b4da98 100644 --- a/editors/vscode/client/ConfigService.ts +++ b/editors/vscode/client/ConfigService.ts @@ -1,7 +1,7 @@ -import { ConfigurationChangeEvent, workspace } from 'vscode'; +import { ConfigurationChangeEvent, Uri, workspace, WorkspaceFolder } from 'vscode'; import { IDisposable } from './types'; import { VSCodeConfig } from './VSCodeConfig'; -import { WorkspaceConfig } from './WorkspaceConfig'; +import { oxlintConfigFileName, WorkspaceConfig, WorkspaceConfigInterface } from './WorkspaceConfig'; export class ConfigService implements IDisposable { public static readonly namespace = 'oxc'; @@ -9,16 +9,20 @@ export class ConfigService implements IDisposable { public vsCodeConfig: VSCodeConfig; - private _workspaceConfig: WorkspaceConfig; + private workspaceConfigs: Map = new Map(); public onConfigChange: | ((this: ConfigService, config: ConfigurationChangeEvent) => Promise) | undefined; constructor() { - const conf = workspace.getConfiguration(ConfigService.namespace); - this.vsCodeConfig = new VSCodeConfig(conf); - this._workspaceConfig = new WorkspaceConfig(conf); + this.vsCodeConfig = new VSCodeConfig(); + const workspaceFolders = workspace.workspaceFolders; + if (workspaceFolders) { + for (const folder of workspaceFolders) { + this.addWorkspaceConfig(folder); + } + } this.onConfigChange = undefined; const disposeChangeListener = workspace.onDidChangeConfiguration( @@ -27,19 +31,71 @@ export class ConfigService implements IDisposable { this._disposables.push(disposeChangeListener); } - public get rootServerConfig(): WorkspaceConfig { - return this._workspaceConfig; + public get languageServerConfig(): { workspaceUri: string; options: WorkspaceConfigInterface }[] { + return [...this.workspaceConfigs.entries()].map(([path, config]) => ({ + workspaceUri: Uri.file(path).toString(), + options: config.toLanguageServerConfig(), + })); + } + + public addWorkspaceConfig(workspace: WorkspaceFolder): WorkspaceConfig { + let workspaceConfig = new WorkspaceConfig(workspace); + this.workspaceConfigs.set(workspace.uri.path, workspaceConfig); + return workspaceConfig; + } + + public removeWorkspaceConfig(workspace: WorkspaceFolder): void { + this.workspaceConfigs.delete(workspace.uri.path); } - public refresh(): void { - const conf = workspace.getConfiguration(ConfigService.namespace); - this.vsCodeConfig.refresh(conf); - this.rootServerConfig.refresh(conf); + public getWorkspaceConfig(workspace: Uri): WorkspaceConfig | undefined { + return this.workspaceConfigs.get(workspace.path); + } + + public effectsWorkspaceConfigChange(event: ConfigurationChangeEvent): boolean { + for (const workspaceConfig of this.workspaceConfigs.values()) { + if (workspaceConfig.effectsConfigChange(event)) { + return true; + } + } + return false; + } + + public effectsWorkspaceConfigPathChange(event: ConfigurationChangeEvent): boolean { + for (const workspaceConfig of this.workspaceConfigs.values()) { + if (workspaceConfig.effectsConfigPathChange(event)) { + return true; + } + } + return false; + } + + public getOxlintCustomConfigs(): string[] { + const customConfigs: string[] = []; + for (const [path, config] of this.workspaceConfigs.entries()) { + if (config.configPath && config.configPath !== oxlintConfigFileName) { + customConfigs.push(`${path}/${config.configPath}`); + } + } + return customConfigs; } private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise { + let isConfigChanged = false; + if (event.affectsConfiguration(ConfigService.namespace)) { - this.refresh(); + this.vsCodeConfig.refresh(); + isConfigChanged = true; + } + + for (const workspaceConfig of this.workspaceConfigs.values()) { + if (workspaceConfig.effectsConfigChange(event)) { + workspaceConfig.refresh(); + isConfigChanged = true; + } + } + + if (isConfigChanged) { await this.onConfigChange?.(event); } } diff --git a/editors/vscode/client/VSCodeConfig.spec.ts b/editors/vscode/client/VSCodeConfig.spec.ts index 334e02b0c78d3..96c814f1da6b7 100644 --- a/editors/vscode/client/VSCodeConfig.spec.ts +++ b/editors/vscode/client/VSCodeConfig.spec.ts @@ -4,7 +4,7 @@ import { VSCodeConfig } from './VSCodeConfig.js'; const conf = workspace.getConfiguration('oxc'); -suite('Config', () => { +suite('VSCodeConfig', () => { setup(async () => { const keys = ['enable', 'trace.server', 'path.server']; @@ -20,7 +20,7 @@ suite('Config', () => { }); test('default values on initialization', () => { - const config = new VSCodeConfig(conf); + const config = new VSCodeConfig(); strictEqual(config.enable, true); strictEqual(config.trace, 'off'); @@ -28,7 +28,7 @@ suite('Config', () => { }); test('updating values updates the workspace configuration', async () => { - const config = new VSCodeConfig(conf); + const config = new VSCodeConfig(); await Promise.all([ config.updateEnable(false), diff --git a/editors/vscode/client/VSCodeConfig.ts b/editors/vscode/client/VSCodeConfig.ts index c574d8e73afae..e9f8a5122e7eb 100644 --- a/editors/vscode/client/VSCodeConfig.ts +++ b/editors/vscode/client/VSCodeConfig.ts @@ -1,21 +1,23 @@ -import { workspace, WorkspaceConfiguration } from 'vscode'; +import { workspace } from 'vscode'; import { ConfigService } from './ConfigService'; -export const oxlintConfigFileName = '.oxlintrc.json'; - export class VSCodeConfig implements VSCodeConfigInterface { private _enable!: boolean; private _trace!: TraceLevel; private _binPath: string | undefined; - constructor(configuration: WorkspaceConfiguration) { - this.refresh(configuration); + constructor() { + this.refresh(); + } + + private get configuration() { + return workspace.getConfiguration(ConfigService.namespace); } - public refresh(configuration: WorkspaceConfiguration): void { - this._enable = configuration.get('enable') ?? true; - this._trace = configuration.get('trace.server') || 'off'; - this._binPath = configuration.get('path.server'); + public refresh(): void { + this._enable = this.configuration.get('enable') ?? true; + this._trace = this.configuration.get('trace.server') || 'off'; + this._binPath = this.configuration.get('path.server'); } get enable(): boolean { @@ -24,9 +26,7 @@ export class VSCodeConfig implements VSCodeConfigInterface { updateEnable(value: boolean): PromiseLike { this._enable = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('enable', value); + return this.configuration.update('enable', value); } get trace(): TraceLevel { @@ -35,9 +35,7 @@ export class VSCodeConfig implements VSCodeConfigInterface { updateTrace(value: TraceLevel): PromiseLike { this._trace = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('trace.server', value); + return this.configuration.update('trace.server', value); } get binPath(): string | undefined { @@ -46,9 +44,7 @@ export class VSCodeConfig implements VSCodeConfigInterface { updateBinPath(value: string | undefined): PromiseLike { this._binPath = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('path.server', value); + return this.configuration.update('path.server', value); } } diff --git a/editors/vscode/client/WorkspaceConfig.spec.ts b/editors/vscode/client/WorkspaceConfig.spec.ts index 5e01270419994..95079bc1d37d1 100644 --- a/editors/vscode/client/WorkspaceConfig.spec.ts +++ b/editors/vscode/client/WorkspaceConfig.spec.ts @@ -1,10 +1,12 @@ import { deepStrictEqual, strictEqual } from 'assert'; import { Uri, workspace, WorkspaceEdit } from 'vscode'; +import { WORKSPACE_FOLDER } from './test-helpers.js'; import { WorkspaceConfig } from './WorkspaceConfig.js'; -suite('Config', () => { +const uri = WORKSPACE_FOLDER; +suite('WorkspaceConfig', () => { setup(async () => { - const wsConfig = workspace.getConfiguration('oxc'); + const wsConfig = workspace.getConfiguration('oxc', uri); const keys = ['lint.run', 'configPath', 'flags']; await Promise.all(keys.map(key => wsConfig.update(key, undefined))); @@ -19,36 +21,36 @@ suite('Config', () => { }); test('default values on initialization', () => { - const config = new WorkspaceConfig(workspace.getConfiguration('oxc')); + const config = new WorkspaceConfig(uri); strictEqual(config.runTrigger, 'onType'); strictEqual(config.configPath, null); deepStrictEqual(config.flags, {}); }); test('configPath defaults to null when using nested configs and configPath is empty', async () => { - const wsConfig = workspace.getConfiguration('oxc'); + const wsConfig = workspace.getConfiguration('oxc', uri); await wsConfig.update('configPath', ''); await wsConfig.update('flags', {}); - const config = new WorkspaceConfig(workspace.getConfiguration('oxc')); + const config = new WorkspaceConfig(uri); deepStrictEqual(config.flags, {}); strictEqual(config.configPath, null); }); test('configPath defaults to .oxlintrc.json when not using nested configs and configPath is empty', async () => { - const wsConfig = workspace.getConfiguration('oxc'); + const wsConfig = workspace.getConfiguration('oxc', uri); await wsConfig.update('configPath', undefined); await wsConfig.update('flags', { disable_nested_config: '' }); - const config = new WorkspaceConfig(workspace.getConfiguration('oxc')); + const config = new WorkspaceConfig(uri); deepStrictEqual(config.flags, { disable_nested_config: '' }); strictEqual(config.configPath, '.oxlintrc.json'); }); test('updating values updates the workspace configuration', async () => { - const config = new WorkspaceConfig(workspace.getConfiguration('oxc')); + const config = new WorkspaceConfig(uri); await Promise.all([ config.updateRunTrigger('onSave'), @@ -56,7 +58,7 @@ suite('Config', () => { config.updateFlags({ test: 'value' }), ]); - const wsConfig = workspace.getConfiguration('oxc'); + const wsConfig = workspace.getConfiguration('oxc', uri); strictEqual(wsConfig.get('lint.run'), 'onSave'); strictEqual(wsConfig.get('configPath'), './somewhere'); diff --git a/editors/vscode/client/WorkspaceConfig.ts b/editors/vscode/client/WorkspaceConfig.ts index e036a6c5ec480..7ae9296abd8eb 100644 --- a/editors/vscode/client/WorkspaceConfig.ts +++ b/editors/vscode/client/WorkspaceConfig.ts @@ -1,6 +1,7 @@ -import { workspace, WorkspaceConfiguration } from 'vscode'; +import { ConfigurationChangeEvent, ConfigurationTarget, workspace, WorkspaceFolder } from 'vscode'; import { ConfigService } from './ConfigService'; -import { oxlintConfigFileName } from './VSCodeConfig'; + +export const oxlintConfigFileName = '.oxlintrc.json'; export type Trigger = 'onSave' | 'onType'; @@ -37,29 +38,52 @@ export class WorkspaceConfig { private _runTrigger: Trigger = 'onType'; private _flags: Record = {}; - constructor(configuration: WorkspaceConfiguration) { - this.refresh(configuration); + constructor(private readonly workspace: WorkspaceFolder) { + this.refresh(); + } + + private get configuration() { + return workspace.getConfiguration(ConfigService.namespace, this.workspace); } - public refresh(configuration: WorkspaceConfiguration): void { - const flags = configuration.get>('flags') ?? {}; + public refresh(): void { + const flags = this.configuration.get>('flags') ?? {}; const useNestedConfigs = !('disable_nested_config' in flags); - this._runTrigger = configuration.get('lint.run') || 'onType'; - this._configPath = configuration.get('configPath') || + this._runTrigger = this.configuration.get('lint.run') || 'onType'; + this._configPath = this.configuration.get('configPath') || (useNestedConfigs ? null : oxlintConfigFileName); this._flags = flags; } + public effectsConfigChange(event: ConfigurationChangeEvent): boolean { + if (event.affectsConfiguration(`${ConfigService.namespace}.configPath`, this.workspace)) { + return true; + } + if (event.affectsConfiguration(`${ConfigService.namespace}.lint.run`, this.workspace)) { + return true; + } + if (event.affectsConfiguration(`${ConfigService.namespace}.flags`, this.workspace)) { + return true; + } + return false; + } + + public effectsConfigPathChange(event: ConfigurationChangeEvent): boolean { + return event.affectsConfiguration(`${ConfigService.namespace}.configPath`, this.workspace); + } + + public get isCustomConfigPath(): boolean { + return this.configPath !== null && this.configPath !== oxlintConfigFileName; + } + get runTrigger(): Trigger { return this._runTrigger; } updateRunTrigger(value: Trigger): PromiseLike { this._runTrigger = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('lint.run', value); + return this.configuration.update('lint.run', value, ConfigurationTarget.WorkspaceFolder); } get configPath(): string | null { @@ -68,9 +92,7 @@ export class WorkspaceConfig { updateConfigPath(value: string): PromiseLike { this._configPath = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('configPath', value); + return this.configuration.update('configPath', value, ConfigurationTarget.WorkspaceFolder); } get flags(): Record { @@ -79,9 +101,7 @@ export class WorkspaceConfig { updateFlags(value: Record): PromiseLike { this._flags = value; - return workspace - .getConfiguration(ConfigService.namespace) - .update('flags', value); + return this.configuration.update('flags', value, ConfigurationTarget.WorkspaceFolder); } public toLanguageServerConfig(): WorkspaceConfigInterface { diff --git a/editors/vscode/client/extension.ts b/editors/vscode/client/extension.ts index 449f0f49318f6..351e624228922 100644 --- a/editors/vscode/client/extension.ts +++ b/editors/vscode/client/extension.ts @@ -4,10 +4,10 @@ import { commands, ExtensionContext, FileSystemWatcher, - RelativePattern, StatusBarAlignment, StatusBarItem, ThemeColor, + Uri, window, workspace, } from 'vscode'; @@ -23,7 +23,7 @@ import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from import { join } from 'node:path'; import { ConfigService } from './ConfigService'; -import { oxlintConfigFileName } from './VSCodeConfig'; +import { oxlintConfigFileName } from './WorkspaceConfig'; const languageClientName = 'oxc'; const outputChannelName = 'Oxc'; @@ -122,7 +122,7 @@ export async function activate(context: ExtensionContext) { ); const outputChannel = window.createOutputChannel(outputChannelName, { log: true }); - const fileWatchers = createFileEventWatchers(configService.rootServerConfig.configPath); + const fileWatchers = createFileEventWatchers(configService.getOxlintCustomConfigs()); context.subscriptions.push( applyAllFixesFile, @@ -193,8 +193,6 @@ export async function activate(context: ExtensionContext) { debug: run, }; - let firstWorkspaceUri = workspace.workspaceFolders?.at(0)?.uri; - // If the extension is launched in debug mode then the debug server options are used // Otherwise the run options are used // Options to control the language client @@ -215,12 +213,7 @@ export async function activate(context: ExtensionContext) { // Notify the server about file config changes in the workspace fileEvents: fileWatchers, }, - initializationOptions: firstWorkspaceUri - ? [{ - workspaceUri: firstWorkspaceUri.toString(), - options: configService.rootServerConfig.toLanguageServerConfig(), - }] - : null, + initializationOptions: configService.languageServerConfig, outputChannel, traceOutputChannel: outputChannel, middleware: { @@ -230,8 +223,11 @@ export async function activate(context: ExtensionContext) { if (item.section !== 'oxc_language_server') { return null; } + if (item.scopeUri === undefined) { + return null; + } - return configService.rootServerConfig.toLanguageServerConfig() ?? null; + return configService.getWorkspaceConfig(Uri.parse(item.scopeUri))?.toLanguageServerConfig() ?? null; }); }, }, @@ -277,23 +273,53 @@ export async function activate(context: ExtensionContext) { context.subscriptions.push(onDeleteFilesDispose); + const onDidChangeWorkspaceFoldersDispose = workspace.onDidChangeWorkspaceFolders(async (event) => { + let needRestart = false; + for (const folder of event.added) { + const workspaceConfig = configService.addWorkspaceConfig(folder); + + if (workspaceConfig.isCustomConfigPath) { + needRestart = true; + } + } + for (const folder of event.removed) { + const workspaceConfig = configService.getWorkspaceConfig(folder.uri); + if (workspaceConfig?.isCustomConfigPath) { + needRestart = true; + } + configService.removeWorkspaceConfig(folder); + } + + if (client === undefined) { + return; + } + + if (needRestart) { + client.clientOptions.synchronize = client.clientOptions.synchronize ?? {}; + client.clientOptions.synchronize.fileEvents = createFileEventWatchers(configService.getOxlintCustomConfigs()); + + if (client.isRunning()) { + await client.restart(); + } + } + }); + + context.subscriptions.push(onDidChangeWorkspaceFoldersDispose); + configService.onConfigChange = async function onConfigChange(event) { - let settings = this.rootServerConfig.toLanguageServerConfig(); updateStatsBar(this.vsCodeConfig.enable); if (client === undefined) { return; } - let firstWorkspaceUri = workspace.workspaceFolders?.at(0)?.uri; // update the initializationOptions for a possible restart - client.clientOptions.initializationOptions = firstWorkspaceUri - ? [{ workspaceUri: firstWorkspaceUri.toString(), options: settings }] - : null; + client.clientOptions.initializationOptions = this.languageServerConfig; if (event.affectsConfiguration('oxc.configPath')) { client.clientOptions.synchronize = client.clientOptions.synchronize ?? {}; - client.clientOptions.synchronize.fileEvents = createFileEventWatchers(settings.configPath); + client.clientOptions.synchronize.fileEvents = createFileEventWatchers(this.getOxlintCustomConfigs()); + if (client.isRunning()) { await client.restart(); } @@ -301,7 +327,7 @@ export async function activate(context: ExtensionContext) { await client.sendNotification( 'workspace/didChangeConfiguration', { - settings: firstWorkspaceUri ? [{ workspaceUri: firstWorkspaceUri.toString(), options: settings }] : null, + settings: this.languageServerConfig, }, ); } @@ -342,23 +368,19 @@ export async function deactivate(): Promise { } // FileSystemWatcher are not ready on the start and can take some seconds on bigger repositories -function createFileEventWatchers(configRelativePath: string | null): FileSystemWatcher[] { +function createFileEventWatchers(configAbsolutePaths: string[]): FileSystemWatcher[] { // cleanup old watchers globalWatchers.forEach((watcher) => watcher.dispose()); globalWatchers.length = 0; // create new watchers - let localWatchers; - if (configRelativePath !== null) { - localWatchers = (workspace.workspaceFolders || []).map((workspaceFolder) => - workspace.createFileSystemWatcher(new RelativePattern(workspaceFolder, configRelativePath)) - ); - } else { - localWatchers = [ - workspace.createFileSystemWatcher(`**/${oxlintConfigFileName}`), - ]; + let localWatchers: FileSystemWatcher[] = []; + if (configAbsolutePaths.length) { + localWatchers = configAbsolutePaths.map((path) => workspace.createFileSystemWatcher(path)); } + localWatchers.push(workspace.createFileSystemWatcher(`**/${oxlintConfigFileName}`)); + // assign watchers to global variable, so we can cleanup them on next call globalWatchers.push(...localWatchers); diff --git a/editors/vscode/client/test-helpers.ts b/editors/vscode/client/test-helpers.ts index 34f0df5331e96..3ed3a5f929f3c 100644 --- a/editors/vscode/client/test-helpers.ts +++ b/editors/vscode/client/test-helpers.ts @@ -29,7 +29,8 @@ export type OxlintConfig = { ignorePatterns?: OxlintConfigIgnorePatterns; }; -export const WORKSPACE_DIR = workspace.workspaceFolders![0].uri; +export const WORKSPACE_FOLDER = workspace.workspaceFolders![0]; +export const WORKSPACE_DIR = WORKSPACE_FOLDER.uri; const rootOxlintConfigUri = Uri.joinPath(WORKSPACE_DIR, '.oxlintrc.json'); diff --git a/editors/vscode/package.json b/editors/vscode/package.json index f9231760f8f79..4561ba7cbdf26 100644 --- a/editors/vscode/package.json +++ b/editors/vscode/package.json @@ -77,6 +77,7 @@ "oxc.enable": { "type": "boolean", "default": true, + "scope": "window", "description": "enable oxc language server" }, "oxc.trace.server": { @@ -95,23 +96,23 @@ "default": "off", "description": "Traces the communication between VS Code and the language server." }, + "oxc.path.server": { + "type": "string", + "scope": "window", + "description": "Path to Oxc language server binary. Mostly for testing the language server." + }, "oxc.configPath": { "type": [ "string", "null" ], - "scope": "window", + "scope": "resource", "default": null, "description": "Path to ESlint configuration. Keep it empty to enable nested configuration." }, - "oxc.path.server": { - "type": "string", - "scope": "window", - "description": "Path to Oxc language server binary. Mostly for testing the language server." - }, "oxc.flags": { "type": "object", - "scope": "window", + "scope": "resource", "default": {}, "description": "Specific Oxlint flags to pass to the language server." }