Skip to content

Commit dab1bd8

Browse files
committedApr 4, 2025·
feat(language_server): search for nested configurations by initialization (#10120)
Now the client does not need to send a second request after initialization. The initialization part is done on the server side. The client only needs to send future changes because file-watchers on servers are not recommended: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_didChangeWatchedFiles
1 parent fa5f991 commit dab1bd8

File tree

4 files changed

+151
-56
lines changed

4 files changed

+151
-56
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
use std::{
2+
ffi::OsStr,
3+
path::PathBuf,
4+
sync::{Arc, mpsc},
5+
};
6+
7+
use ignore::DirEntry;
8+
9+
use crate::OXC_CONFIG_FILE;
10+
11+
pub struct ConfigWalker {
12+
inner: ignore::WalkParallel,
13+
}
14+
15+
struct WalkBuilder {
16+
sender: mpsc::Sender<Vec<Arc<OsStr>>>,
17+
}
18+
19+
impl<'s> ignore::ParallelVisitorBuilder<'s> for WalkBuilder {
20+
fn build(&mut self) -> Box<dyn ignore::ParallelVisitor + 's> {
21+
Box::new(WalkCollector { paths: vec![], sender: self.sender.clone() })
22+
}
23+
}
24+
25+
struct WalkCollector {
26+
paths: Vec<Arc<OsStr>>,
27+
sender: mpsc::Sender<Vec<Arc<OsStr>>>,
28+
}
29+
30+
impl Drop for WalkCollector {
31+
fn drop(&mut self) {
32+
let paths = std::mem::take(&mut self.paths);
33+
self.sender.send(paths).unwrap();
34+
}
35+
}
36+
37+
impl ignore::ParallelVisitor for WalkCollector {
38+
fn visit(&mut self, entry: Result<ignore::DirEntry, ignore::Error>) -> ignore::WalkState {
39+
match entry {
40+
Ok(entry) => {
41+
if Self::is_wanted_entry(&entry) {
42+
self.paths.push(entry.path().as_os_str().into());
43+
}
44+
ignore::WalkState::Continue
45+
}
46+
Err(_err) => ignore::WalkState::Skip,
47+
}
48+
}
49+
}
50+
51+
impl WalkCollector {
52+
fn is_wanted_entry(entry: &DirEntry) -> bool {
53+
let Some(file_type) = entry.file_type() else { return false };
54+
if file_type.is_dir() {
55+
return false;
56+
}
57+
let Some(file_name) = entry.path().file_name() else { return false };
58+
59+
file_name == OXC_CONFIG_FILE
60+
}
61+
}
62+
63+
impl ConfigWalker {
64+
/// Will not canonicalize paths.
65+
/// # Panics
66+
pub fn new(path: &PathBuf) -> Self {
67+
// Turning off `follow_links` because:
68+
// * following symlinks is a really slow syscall
69+
// * it is super rare to have symlinked source code
70+
let inner: ignore::WalkParallel = ignore::WalkBuilder::new(path)
71+
// disable skip hidden, which will not not search for files starting with a dot
72+
.hidden(false)
73+
// disable all gitignore features
74+
.parents(false)
75+
.ignore(false)
76+
.git_global(false)
77+
.follow_links(false)
78+
.build_parallel();
79+
80+
Self { inner }
81+
}
82+
83+
pub fn paths(self) -> Vec<Arc<OsStr>> {
84+
let (sender, receiver) = mpsc::channel::<Vec<Arc<OsStr>>>();
85+
let mut builder = WalkBuilder { sender };
86+
self.inner.visit(&mut builder);
87+
drop(builder);
88+
receiver.into_iter().flatten().collect()
89+
}
90+
}

‎crates/oxc_language_server/src/linter/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use oxc_data_structures::rope::{Rope, get_line_column};
22
use tower_lsp::lsp_types::Position;
33

4+
pub mod config_walker;
45
pub mod error_with_position;
56
mod isolated_lint_handler;
67
pub mod server_linter;

‎crates/oxc_language_server/src/main.rs

+41-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1-
use std::{fmt::Debug, path::PathBuf, str::FromStr};
2-
31
use commands::LSP_COMMANDS;
42
use futures::future::join_all;
53
use globset::Glob;
64
use ignore::gitignore::Gitignore;
5+
use linter::config_walker::ConfigWalker;
76
use log::{debug, error, info};
87
use oxc_linter::{ConfigStore, ConfigStoreBuilder, FixKind, LintOptions, Linter, Oxlintrc};
98
use rustc_hash::{FxBuildHasher, FxHashMap};
109
use serde::{Deserialize, Serialize};
10+
use std::{
11+
fmt::Debug,
12+
path::{Path, PathBuf},
13+
str::FromStr,
14+
};
1115
use tokio::sync::{Mutex, OnceCell, RwLock, SetError};
1216
use tower_lsp::{
1317
Client, LanguageServer, LspService, Server,
@@ -79,6 +83,7 @@ impl LanguageServer for Backend {
7983
*self.options.lock().await = value;
8084
}
8185

86+
self.init_nested_configs().await;
8287
let oxlintrc = self.init_linter_config().await;
8388
self.init_ignore_glob(oxlintrc).await;
8489
Ok(InitializeResult {
@@ -121,8 +126,9 @@ impl LanguageServer for Backend {
121126
"
122127
);
123128

124-
if changed_options.disable_nested_configs() {
129+
if changed_options.disable_nested_configs() != current_option.disable_nested_configs() {
125130
self.nested_configs.pin().clear();
131+
self.init_nested_configs().await;
126132
}
127133

128134
*self.options.lock().await = changed_options.clone();
@@ -501,6 +507,38 @@ impl Backend {
501507
.await;
502508
}
503509

510+
/// Searches inside root_uri recursively for the default oxlint config files
511+
/// and insert them inside the nested configuration
512+
async fn init_nested_configs(&self) {
513+
let Some(Some(uri)) = self.root_uri.get() else {
514+
return;
515+
};
516+
let Ok(root_path) = uri.to_file_path() else {
517+
return;
518+
};
519+
520+
// nested config is disabled, no need to search for configs
521+
if self.options.lock().await.disable_nested_configs() {
522+
return;
523+
}
524+
525+
let paths = ConfigWalker::new(&root_path).paths();
526+
let nested_configs = self.nested_configs.pin();
527+
528+
for path in paths {
529+
let file_path = Path::new(&path);
530+
let Some(dir_path) = file_path.parent() else {
531+
continue;
532+
};
533+
534+
let oxlintrc = Oxlintrc::from_file(file_path).expect("Failed to parse config file");
535+
let config_store_builder = ConfigStoreBuilder::from_oxlintrc(false, oxlintrc)
536+
.expect("Failed to create config store builder");
537+
let config_store = config_store_builder.build().expect("Failed to build config store");
538+
nested_configs.insert(dir_path.to_path_buf(), config_store);
539+
}
540+
}
541+
504542
async fn init_linter_config(&self) -> Option<Oxlintrc> {
505543
let Some(Some(uri)) = self.root_uri.get() else {
506544
return None;

‎editors/vscode/client/extension.ts

+19-53
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,11 @@ import {
77
StatusBarAlignment,
88
StatusBarItem,
99
ThemeColor,
10-
Uri,
1110
window,
1211
workspace,
1312
} from 'vscode';
1413

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

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

@@ -58,14 +51,9 @@ export async function activate(context: ExtensionContext) {
5851
try {
5952
if (client.isRunning()) {
6053
await client.restart();
61-
// ToDo: refactor it on the server side.
62-
// Do not touch watchers on client side, just simplify the restart of the server.
63-
const configFiles = await findOxlintrcConfigFiles();
64-
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
65-
6654
window.showInformationMessage('oxc server restarted.');
6755
} else {
68-
await startClient(client);
56+
await client.start();
6957
}
7058
} catch (err) {
7159
client.error('Restarting client failed', err, 'force');
@@ -95,7 +83,7 @@ export async function activate(context: ExtensionContext) {
9583
}
9684
} else {
9785
if (configService.config.enable) {
98-
await startClient(client);
86+
await client.start();
9987
}
10088
}
10189
},
@@ -187,6 +175,10 @@ export async function activate(context: ExtensionContext) {
187175
run,
188176
debug: run,
189177
};
178+
179+
const fileWatchers = createFileEventWatchers(configService.config.configPath);
180+
context.subscriptions.push(...fileWatchers);
181+
190182
// If the extension is launched in debug mode then the debug server options are used
191183
// Otherwise the run options are used
192184
// Options to control the language client
@@ -205,7 +197,7 @@ export async function activate(context: ExtensionContext) {
205197
})),
206198
synchronize: {
207199
// Notify the server about file config changes in the workspace
208-
fileEvents: createFileEventWatchers(configService.config.configPath),
200+
fileEvents: fileWatchers,
209201
},
210202
initializationOptions: {
211203
settings: configService.config.toLanguageServerConfig(),
@@ -269,8 +261,6 @@ export async function activate(context: ExtensionContext) {
269261

270262
if (client.isRunning()) {
271263
await client.restart();
272-
const configFiles = await findOxlintrcConfigFiles();
273-
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
274264
}
275265
}
276266
};
@@ -297,52 +287,28 @@ export async function activate(context: ExtensionContext) {
297287
updateStatsBar(configService.config.enable);
298288

299289
if (configService.config.enable) {
300-
await startClient(client);
301-
}
302-
}
303-
304-
// Starts the client, it does not check if it is already started
305-
async function startClient(client: LanguageClient | undefined) {
306-
if (client === undefined) {
307-
return;
290+
await client.start();
308291
}
309-
await client.start();
310-
// ToDo: refactor it on the server side.
311-
// Do not touch watchers on client side, just simplify the start of the server.
312-
const configFiles = await findOxlintrcConfigFiles();
313-
await sendDidChangeWatchedFilesNotificationWith(client, configFiles);
314292
}
315293

316-
export function deactivate(): Thenable<void> | undefined {
294+
export async function deactivate(): Promise<void> {
317295
if (!client) {
318296
return undefined;
319297
}
320-
return client.stop();
298+
await client.stop();
299+
client = undefined;
321300
}
322301

302+
// FileSystemWatcher are not ready on the start and can take some seconds on bigger repositories
303+
// ToDo: create test to make sure this will never break
323304
function createFileEventWatchers(configRelativePath: string | null) {
324-
const workspaceConfigPatterns = configRelativePath !== null
325-
? (workspace.workspaceFolders || []).map((workspaceFolder) =>
326-
new RelativePattern(workspaceFolder, configRelativePath)
327-
)
328-
: [];
305+
if (configRelativePath !== null) {
306+
return (workspace.workspaceFolders || []).map((workspaceFolder) =>
307+
workspace.createFileSystemWatcher(new RelativePattern(workspaceFolder, configRelativePath))
308+
);
309+
}
329310

330311
return [
331312
workspace.createFileSystemWatcher(`**/${oxlintConfigFileName}`),
332-
...workspaceConfigPatterns.map((pattern) => {
333-
return workspace.createFileSystemWatcher(pattern);
334-
}),
335313
];
336314
}
337-
338-
async function findOxlintrcConfigFiles() {
339-
return workspace.findFiles(`**/${oxlintConfigFileName}`);
340-
}
341-
342-
async function sendDidChangeWatchedFilesNotificationWith(languageClient: LanguageClient, files: Uri[]) {
343-
await languageClient.sendNotification(DidChangeWatchedFilesNotification.type, {
344-
changes: files.map(file => {
345-
return { uri: file.toString(), type: FileChangeType.Created };
346-
}),
347-
});
348-
}

0 commit comments

Comments
 (0)
Please sign in to comment.