Skip to content

Commit 35761ae

Browse files
committed
refactor(language_server/editor): refresh file watchers without restarting the server (didChangeConfiguration) (oxc-project#11112)
1 parent 9a2a6c6 commit 35761ae

File tree

6 files changed

+112
-50
lines changed

6 files changed

+112
-50
lines changed

crates/oxc_language_server/README.md

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ The client can pass the workspace options like following:
5252

5353
### [initialized](https://microsoft.github.io/language-server-protocol/specification#initialized)
5454

55-
When the client did not pass the workspace configuration in [initialize](#initialize), the server will request the configuration for every workspace with [workspace/configuration](#workspace/configuration).
55+
When the client did not pass the workspace configuration in [initialize](#initialize), the server will request the configuration for every workspace with [workspace/configuration](#workspaceconfiguration).
56+
The server will tell the client with [client/registerCapability](#clientregistercapability) to watch for `.oxlintrc.json` files or a custom `oxc.configPath`.
5657

5758
### [shutdown](https://microsoft.github.io/language-server-protocol/specification#shutdown)
5859

@@ -80,6 +81,10 @@ The client can pass the workspace options like following:
8081
When the client does not pass workspace options, the server will request them with [workspace/configuration](#workspace/configuration).
8182
The server will revalidate or reset the diagnostics for all open files and send one or more [textDocument/publishDiagnostics](#textdocumentpublishdiagnostics) requests to the client.
8283

84+
When changing the `oxc.configPath` settings:
85+
The server will tell clients with [client/registerCapability](#clientregistercapability) to watch for `.oxlintrc.json` files or a custom `oxc.configPath`.
86+
The server will tell clients with [client/unregisterCapability](#clientunregistercapability) to stop watching for `.oxlintrc.json` files or a custom `oxc.configPath`.
87+
8388
#### [workspace/didChangeWatchedFiles](https://microsoft.github.io/language-server-protocol/specification#workspace_didChangeWatchedFiles)
8489

8590
The server expects this request when one oxlint configuration is changed, added or deleted.
@@ -91,6 +96,8 @@ Note: When nested configuration is active, the client should send all `.oxlintrc
9196

9297
The server expects this request when adding or removing workspace folders.
9398
The server will request the specific workspace configuration, if the client supports it.
99+
The server will tell clients with [client/registerCapability](#clientregistercapability) to watch for `.oxlintrc.json` files or a custom `oxc.configPath`.
100+
The server will tell clients with [client/unregisterCapability](#clientunregistercapability) to stop watching for `.oxlintrc.json` files or a custom `oxc.configPath`.
94101

95102
#### [workspace/executeCommand](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_executeCommand)
96103

@@ -126,7 +133,17 @@ Returns a list of [CodeAction](https://microsoft.github.io/language-server-proto
126133

127134
Returns a [PublishDiagnostic object](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#publishDiagnosticsParams)
128135

129-
# Optional LSP Specifications from Client
136+
## Optional LSP Specifications from Client
137+
138+
### Client
139+
140+
#### [client/registerCapability](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#client_registerCapability)
141+
142+
The server will send this request to watch for specific files. The method `workspace/didChangeWatchedFiles` will be used with custom `registerOptions`.
143+
144+
#### [client/unregisterCapability](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#client_unregisterCapability)
145+
146+
The server will send this request to stop watching for specific files. The `id` will match from [client/registerCapability](#clientregistercapability).
130147

131148
### Workspace
132149

crates/oxc_language_server/src/main.rs

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ impl LanguageServer for Backend {
203203
let workers = self.workspace_workers.lock().await;
204204
let new_diagnostics: papaya::HashMap<String, Vec<Diagnostic>, FxBuildHasher> =
205205
ConcurrentHashMap::default();
206+
let mut removing_registrations = vec![];
207+
let mut adding_registrations = vec![];
206208

207209
// new valid configuration is passed
208210
let options = serde_json::from_value::<Vec<WorkspaceOption>>(params.settings.clone())
@@ -233,16 +235,31 @@ impl LanguageServer for Backend {
233235
continue;
234236
};
235237

236-
let Some(diagnostics) = worker.did_change_configuration(&option.options).await
237-
else {
238-
continue;
239-
};
238+
let (diagnostics, watcher) = worker.did_change_configuration(&option.options).await;
240239

241-
for (uri, reports) in &diagnostics.pin() {
242-
new_diagnostics.pin().insert(
243-
uri.clone(),
244-
reports.iter().map(|d| d.diagnostic.clone()).collect(),
245-
);
240+
if let Some(diagnostics) = diagnostics {
241+
for (uri, reports) in &diagnostics.pin() {
242+
new_diagnostics.pin().insert(
243+
uri.clone(),
244+
reports.iter().map(|d| d.diagnostic.clone()).collect(),
245+
);
246+
}
247+
}
248+
249+
if let Some(watcher) = watcher {
250+
// remove the old watcher
251+
removing_registrations.push(Unregistration {
252+
id: format!("watcher-{}", worker.get_root_uri().as_str()),
253+
method: "workspace/didChangeWatchedFiles".to_string(),
254+
});
255+
// add the new watcher
256+
adding_registrations.push(Registration {
257+
id: format!("watcher-{}", worker.get_root_uri().as_str()),
258+
method: "workspace/didChangeWatchedFiles".to_string(),
259+
register_options: Some(json!(DidChangeWatchedFilesRegistrationOptions {
260+
watchers: vec![watcher]
261+
})),
262+
});
246263
}
247264
}
248265
// else check if the client support workspace configuration requests
@@ -263,15 +280,32 @@ impl LanguageServer for Backend {
263280
let Some(config) = &configs[index] else {
264281
continue;
265282
};
266-
let Some(diagnostics) = worker.did_change_configuration(config).await else {
267-
continue;
268-
};
269283

270-
for (uri, reports) in &diagnostics.pin() {
271-
new_diagnostics.pin().insert(
272-
uri.clone(),
273-
reports.iter().map(|d| d.diagnostic.clone()).collect(),
274-
);
284+
let (diagnostics, watcher) = worker.did_change_configuration(config).await;
285+
286+
if let Some(diagnostics) = diagnostics {
287+
for (uri, reports) in &diagnostics.pin() {
288+
new_diagnostics.pin().insert(
289+
uri.clone(),
290+
reports.iter().map(|d| d.diagnostic.clone()).collect(),
291+
);
292+
}
293+
}
294+
295+
if let Some(watcher) = watcher {
296+
// remove the old watcher
297+
removing_registrations.push(Unregistration {
298+
id: format!("watcher-{}", worker.get_root_uri().as_str()),
299+
method: "workspace/didChangeWatchedFiles".to_string(),
300+
});
301+
// add the new watcher
302+
adding_registrations.push(Registration {
303+
id: format!("watcher-{}", worker.get_root_uri().as_str()),
304+
method: "workspace/didChangeWatchedFiles".to_string(),
305+
register_options: Some(json!(DidChangeWatchedFilesRegistrationOptions {
306+
watchers: vec![watcher]
307+
})),
308+
});
275309
}
276310
}
277311
} else {
@@ -281,17 +315,28 @@ impl LanguageServer for Backend {
281315
return;
282316
}
283317

284-
if new_diagnostics.is_empty() {
285-
return;
286-
}
318+
if !new_diagnostics.is_empty() {
319+
let x = &new_diagnostics
320+
.pin()
321+
.into_iter()
322+
.map(|(key, value)| (key.clone(), value.clone()))
323+
.collect::<Vec<_>>();
287324

288-
let x = &new_diagnostics
289-
.pin()
290-
.into_iter()
291-
.map(|(key, value)| (key.clone(), value.clone()))
292-
.collect::<Vec<_>>();
325+
self.publish_all_diagnostics(x).await;
326+
}
293327

294-
self.publish_all_diagnostics(x).await;
328+
if self.capabilities.get().is_some_and(|capabilities| capabilities.dynamic_watchers) {
329+
if !removing_registrations.is_empty() {
330+
if let Err(err) = self.client.unregister_capability(removing_registrations).await {
331+
warn!("sending unregisterCapability.didChangeWatchedFiles failed: {err}");
332+
}
333+
}
334+
if !adding_registrations.is_empty() {
335+
if let Err(err) = self.client.register_capability(adding_registrations).await {
336+
warn!("sending registerCapability.didChangeWatchedFiles failed: {err}");
337+
}
338+
}
339+
}
295340
}
296341

297342
async fn did_change_watched_files(&self, params: DidChangeWatchedFilesParams) {

crates/oxc_language_server/src/worker.rs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ impl WorkspaceWorker {
246246
pub async fn did_change_configuration(
247247
&self,
248248
changed_options: &Options,
249-
) -> Option<ConcurrentHashMap<String, Vec<DiagnosticReport>>> {
249+
) -> (Option<ConcurrentHashMap<String, Vec<DiagnosticReport>>>, Option<FileSystemWatcher>) {
250250
let current_option = &self.options.lock().await.clone();
251251

252252
debug!(
@@ -261,10 +261,28 @@ impl WorkspaceWorker {
261261

262262
if Self::needs_linter_restart(current_option, changed_options) {
263263
self.refresh_server_linter().await;
264-
return Some(self.revalidate_diagnostics().await);
264+
265+
if current_option.config_path != changed_options.config_path {
266+
return (
267+
Some(self.revalidate_diagnostics().await),
268+
Some(FileSystemWatcher {
269+
glob_pattern: GlobPattern::Relative(RelativePattern {
270+
base_uri: OneOf::Right(self.root_uri.clone()),
271+
pattern: changed_options
272+
.config_path
273+
.as_ref()
274+
.unwrap_or(&"**/.oxlintrc.json".to_string())
275+
.to_owned(),
276+
}),
277+
kind: Some(WatchKind::all()), // created, deleted, changed
278+
}),
279+
);
280+
}
281+
282+
return (Some(self.revalidate_diagnostics().await), None);
265283
}
266284

267-
None
285+
(None, None)
268286
}
269287
}
270288

editors/vscode/client/ConfigService.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -59,15 +59,6 @@ export class ConfigService implements IDisposable {
5959
return false;
6060
}
6161

62-
public effectsWorkspaceConfigPathChange(event: ConfigurationChangeEvent): boolean {
63-
for (const workspaceConfig of this.workspaceConfigs.values()) {
64-
if (workspaceConfig.effectsConfigPathChange(event)) {
65-
return true;
66-
}
67-
}
68-
return false;
69-
}
70-
7162
private async onVscodeConfigChange(event: ConfigurationChangeEvent): Promise<void> {
7263
let isConfigChanged = false;
7364

editors/vscode/client/WorkspaceConfig.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,6 @@ export class WorkspaceConfig {
6969
return false;
7070
}
7171

72-
public effectsConfigPathChange(event: ConfigurationChangeEvent): boolean {
73-
return event.affectsConfiguration(`${ConfigService.namespace}.configPath`, this.workspace);
74-
}
75-
7672
public get isCustomConfigPath(): boolean {
7773
return this.configPath !== null && this.configPath !== oxlintConfigFileName;
7874
}

editors/vscode/client/extension.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,7 @@ export async function activate(context: ExtensionContext) {
258258
// update the initializationOptions for a possible restart
259259
client.clientOptions.initializationOptions = this.languageServerConfig;
260260

261-
if (configService.effectsWorkspaceConfigPathChange(event)) {
262-
// Server does not support currently adding/removing watchers on the fly
263-
if (client.isRunning()) {
264-
await client.restart();
265-
}
266-
} else if (configService.effectsWorkspaceConfigChange(event) && client.isRunning()) {
261+
if (configService.effectsWorkspaceConfigChange(event) && client.isRunning()) {
267262
await client.sendNotification(
268263
'workspace/didChangeConfiguration',
269264
{

0 commit comments

Comments
 (0)