Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/oxc_language_server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
96 changes: 79 additions & 17 deletions crates/oxc_language_server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
// #
Expand Down Expand Up @@ -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<InitializeResult> {
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::<Vec<WorkspaceOption>>(value.clone())
Expand Down Expand Up @@ -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) = &params.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 {
Expand Down Expand Up @@ -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 = &params.text_document.uri;
Expand Down
20 changes: 15 additions & 5 deletions editors/vscode/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`\| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
| `oxc.path.server` | - | `<string>` | Path to Oxc language server binary. Mostly for testing the language server. |
| `oxc.flags` | - | `Record<string, string>` | 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`\| `<string>` | Path to ESlint configuration. Keep it empty to enable nested configuration. |
| `oxc.flags` | - | `Record<string, string>` | 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"`
Expand Down
82 changes: 69 additions & 13 deletions editors/vscode/client/ConfigService.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,28 @@
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';
private readonly _disposables: IDisposable[] = [];

public vsCodeConfig: VSCodeConfig;

private _workspaceConfig: WorkspaceConfig;
private workspaceConfigs: Map<string, WorkspaceConfig> = new Map();

public onConfigChange:
| ((this: ConfigService, config: ConfigurationChangeEvent) => Promise<void>)
| 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(
Expand All @@ -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<void> {
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);
}
}
Expand Down
6 changes: 3 additions & 3 deletions editors/vscode/client/VSCodeConfig.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'];

Expand All @@ -20,15 +20,15 @@ suite('Config', () => {
});

test('default values on initialization', () => {
const config = new VSCodeConfig(conf);
const config = new VSCodeConfig();

strictEqual(config.enable, true);
strictEqual(config.trace, 'off');
strictEqual(config.binPath, '');
});

test('updating values updates the workspace configuration', async () => {
const config = new VSCodeConfig(conf);
const config = new VSCodeConfig();

await Promise.all([
config.updateEnable(false),
Expand Down
32 changes: 14 additions & 18 deletions editors/vscode/client/VSCodeConfig.ts
Original file line number Diff line number Diff line change
@@ -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<boolean>('enable') ?? true;
this._trace = configuration.get<TraceLevel>('trace.server') || 'off';
this._binPath = configuration.get<string>('path.server');
public refresh(): void {
this._enable = this.configuration.get<boolean>('enable') ?? true;
this._trace = this.configuration.get<TraceLevel>('trace.server') || 'off';
this._binPath = this.configuration.get<string>('path.server');
}

get enable(): boolean {
Expand All @@ -24,9 +26,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {

updateEnable(value: boolean): PromiseLike<void> {
this._enable = value;
return workspace
.getConfiguration(ConfigService.namespace)
.update('enable', value);
return this.configuration.update('enable', value);
}

get trace(): TraceLevel {
Expand All @@ -35,9 +35,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {

updateTrace(value: TraceLevel): PromiseLike<void> {
this._trace = value;
return workspace
.getConfiguration(ConfigService.namespace)
.update('trace.server', value);
return this.configuration.update('trace.server', value);
}

get binPath(): string | undefined {
Expand All @@ -46,9 +44,7 @@ export class VSCodeConfig implements VSCodeConfigInterface {

updateBinPath(value: string | undefined): PromiseLike<void> {
this._binPath = value;
return workspace
.getConfiguration(ConfigService.namespace)
.update('path.server', value);
return this.configuration.update('path.server', value);
}
}

Expand Down
Loading
Loading