Skip to content

Commit 0973356

Browse files
authoredMar 18, 2025··
feat(editor): Support nested configs (#9743)
Based off of #9739.
1 parent 961b95d commit 0973356

File tree

5 files changed

+87
-14
lines changed

5 files changed

+87
-14
lines changed
 

‎crates/oxc_language_server/src/main.rs

+8-8
Original file line numberDiff line numberDiff line change
@@ -543,15 +543,15 @@ impl Backend {
543543
let Ok(root_path) = uri.to_file_path() else {
544544
return None;
545545
};
546-
let mut config_path = None;
547-
let config = root_path.join(self.options.lock().await.get_config_path().unwrap());
548-
if config.exists() {
549-
config_path = Some(config);
550-
}
546+
let relative_config_path = self.options.lock().await.get_config_path();
547+
let oxlintrc = if relative_config_path.is_some() {
548+
let config = root_path.join(relative_config_path.unwrap());
549+
config.try_exists().expect("Invalid config file path");
550+
Oxlintrc::from_file(&config).expect("Failed to initialize oxlintrc config")
551+
} else {
552+
Oxlintrc::default()
553+
};
551554

552-
let config_path = config_path?;
553-
let oxlintrc = Oxlintrc::from_file(&config_path)
554-
.expect("should have initialized linter with new options");
555555
let config_store = ConfigStoreBuilder::from_oxlintrc(true, oxlintrc.clone())
556556
.expect("failed to build config")
557557
.build()

‎editors/vscode/client/Config.ts

+18-1
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,23 @@ export class Config implements ConfigInterface {
1010
private _trace!: TraceLevel;
1111
private _configPath!: string;
1212
private _binPath: string | undefined;
13+
private _flags!: Record<string, string>;
1314

1415
constructor() {
1516
this.refresh();
1617
}
1718

1819
public refresh(): void {
1920
const conf = workspace.getConfiguration(Config._namespace);
21+
const flags = conf.get<Record<string, string>>('flags') ?? {};
22+
const useNestedConfigs = !('disable_nested_config' in flags);
2023

2124
this._runTrigger = conf.get<Trigger>('lint.run') || 'onType';
2225
this._enable = conf.get<boolean>('enable') ?? true;
2326
this._trace = conf.get<TraceLevel>('trace.server') || 'off';
24-
this._configPath = conf.get<string>('configPath') || oxlintConfigFileName;
27+
this._configPath = conf.get<string>('configPath') || (useNestedConfigs ? '' : oxlintConfigFileName);
2528
this._binPath = conf.get<string>('path.server');
29+
this._flags = flags;
2630
}
2731

2832
get runTrigger(): Trigger {
@@ -80,11 +84,23 @@ export class Config implements ConfigInterface {
8084
.update('path.server', value);
8185
}
8286

87+
get flags(): Record<string, string> {
88+
return this._flags;
89+
}
90+
91+
updateFlags(value: Record<string, string>): PromiseLike<void> {
92+
this._flags = value;
93+
return workspace
94+
.getConfiguration(Config._namespace)
95+
.update('flags', value);
96+
}
97+
8398
public toLanguageServerConfig(): LanguageServerConfig {
8499
return {
85100
run: this.runTrigger,
86101
enable: this.enable,
87102
configPath: this.configPath,
103+
flags: this.flags,
88104
};
89105
}
90106
}
@@ -93,6 +109,7 @@ interface LanguageServerConfig {
93109
configPath: string;
94110
enable: boolean;
95111
run: Trigger;
112+
flags: Record<string, string>;
96113
}
97114

98115
export type Trigger = 'onSave' | 'onType';

‎editors/vscode/client/config.spec.ts

+27-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { strictEqual } from 'assert';
1+
import { deepStrictEqual, strictEqual } from 'assert';
22
import { workspace } from 'vscode';
33
import { Config } from './Config.js';
44

55
suite('Config', () => {
66
setup(async () => {
77
const wsConfig = workspace.getConfiguration('oxc');
8-
const keys = ['lint.run', 'enable', 'trace.server', 'configPath', 'path.server'];
8+
const keys = ['lint.run', 'enable', 'trace.server', 'configPath', 'path.server', 'flags'];
99

1010
await Promise.all(keys.map(key => wsConfig.update(key, undefined)));
1111
});
@@ -18,6 +18,29 @@ suite('Config', () => {
1818
strictEqual(config.trace, 'off');
1919
strictEqual(config.configPath, '.oxlintrc.json');
2020
strictEqual(config.binPath, '');
21+
deepStrictEqual(config.flags, {});
22+
});
23+
24+
test('configPath defaults to empty string when using nested configs and configPath is empty', async () => {
25+
const wsConfig = workspace.getConfiguration('oxc');
26+
await wsConfig.update('configPath', '');
27+
await wsConfig.update('flags', {});
28+
29+
const config = new Config();
30+
31+
deepStrictEqual(config.flags, {});
32+
strictEqual(config.configPath, '');
33+
});
34+
35+
test('configPath defaults to .oxlintrc.json when not using nested configs and configPath is empty', async () => {
36+
const wsConfig = workspace.getConfiguration('oxc');
37+
await wsConfig.update('configPath', '');
38+
await wsConfig.update('flags', { disable_nested_config: '' });
39+
40+
const config = new Config();
41+
42+
deepStrictEqual(config.flags, { disable_nested_config: '' });
43+
strictEqual(config.configPath, '.oxlintrc.json');
2144
});
2245

2346
test('updating values updates the workspace configuration', async () => {
@@ -29,6 +52,7 @@ suite('Config', () => {
2952
config.updateTrace('messages'),
3053
config.updateConfigPath('./somewhere'),
3154
config.updateBinPath('./binary'),
55+
config.updateFlags({ test: 'value' }),
3256
]);
3357

3458
const wsConfig = workspace.getConfiguration('oxc');
@@ -38,5 +62,6 @@ suite('Config', () => {
3862
strictEqual(wsConfig.get('trace.server'), 'messages');
3963
strictEqual(wsConfig.get('configPath'), './somewhere');
4064
strictEqual(wsConfig.get('path.server'), './binary');
65+
deepStrictEqual(wsConfig.get('flags'), { test: 'value' });
4166
});
4267
});

‎editors/vscode/client/extension.ts

+28-3
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import {
77
StatusBarAlignment,
88
StatusBarItem,
99
ThemeColor,
10+
Uri,
1011
window,
1112
workspace,
1213
} from 'vscode';
1314

14-
import { ExecuteCommandRequest, MessageType, ShowMessageNotification } from 'vscode-languageclient';
15+
import {
16+
DidChangeWatchedFilesNotification,
17+
ExecuteCommandRequest,
18+
FileChangeType,
19+
MessageType,
20+
ShowMessageNotification,
21+
} from 'vscode-languageclient';
1522

1623
import { Executable, LanguageClient, LanguageClientOptions, ServerOptions } from 'vscode-languageclient/node';
1724

@@ -231,7 +238,10 @@ export async function activate(context: ExtensionContext) {
231238
if (event.affectsConfiguration('oxc.configPath')) {
232239
client.clientOptions.synchronize = client.clientOptions.synchronize ?? {};
233240
client.clientOptions.synchronize.fileEvents = createFileEventWatchers(configService.config.configPath);
234-
client.restart();
241+
client.restart().then(async () => {
242+
const configFiles = await findOxlintrcConfigFiles();
243+
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
244+
});
235245
}
236246
};
237247

@@ -255,7 +265,10 @@ export async function activate(context: ExtensionContext) {
255265
myStatusBarItem.backgroundColor = bgColor;
256266
}
257267
updateStatsBar(configService.config.enable);
258-
client.start();
268+
await client.start();
269+
270+
const configFiles = await findOxlintrcConfigFiles();
271+
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
259272
}
260273

261274
export function deactivate(): Thenable<void> | undefined {
@@ -277,3 +290,15 @@ function createFileEventWatchers(configRelativePath: string) {
277290
}),
278291
];
279292
}
293+
294+
async function findOxlintrcConfigFiles() {
295+
return workspace.findFiles(`**/${oxlintConfigFileName}`);
296+
}
297+
298+
async function sendDidChangeWatchedFilesNotificationWith(languageClient: LanguageClient, files: Uri[]) {
299+
await languageClient.sendNotification(DidChangeWatchedFilesNotification.type, {
300+
changes: files.map(file => {
301+
return { uri: file.toString(), type: FileChangeType.Created };
302+
}),
303+
});
304+
}

‎editors/vscode/package.json

+6
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,12 @@
104104
"type": "string",
105105
"scope": "window",
106106
"description": "Path to Oxc language server binary."
107+
},
108+
"oxc.flags": {
109+
"type": "object",
110+
"scope": "window",
111+
"default": {},
112+
"description": "Oxc options that would normally be passed when executing on the command line."
107113
}
108114
}
109115
},

0 commit comments

Comments
 (0)
Please sign in to comment.