From a21ff54bdc8551d363e6c6d9e08a73f35a22639f Mon Sep 17 00:00:00 2001 From: Sysix <3897725+Sysix@users.noreply.github.com> Date: Sat, 20 Sep 2025 18:04:55 +0000 Subject: [PATCH] feat(language_server): introduce `ServerFormatter` (#13700) First try on formatter integration. The server will only tell the client, that is supports `formatting` when `fmt.experimental` is `true`. It will respect the flag in `initialize`, `did_change_configuration` (changing configuration), or `did_change_workspace_folders`. It will not tell the client, it does not support formatting when the configuration is changed to `false`. This can be optimized but has no high priority, because the client needs to send the request and the `ServerFormatter` needs to be set. The last requirement is then false. Make `WorkspaceWorker.option` optional, so we the servers knows when the configuration in `initialize` or `initialized` (with `request_workspace_configuration`). Before the server knew it, by looking at the `WorkspaceWorker.server_linter` instance. --- Cargo.lock | 2 + crates/oxc_language_server/Cargo.toml | 2 + crates/oxc_language_server/README.md | 14 +- .../oxc_language_server/src/capabilities.rs | 15 +- crates/oxc_language_server/src/file_system.rs | 1 - .../oxc_language_server/src/formatter/mod.rs | 1 + .../src/formatter/options.rs | 70 ++++++++- .../src/formatter/server_formatter.rs | 57 ++++++++ .../src/linter/error_with_position.rs | 4 +- crates/oxc_language_server/src/main.rs | 135 ++++++++++++------ crates/oxc_language_server/src/options.rs | 2 + crates/oxc_language_server/src/tester.rs | 9 +- crates/oxc_language_server/src/worker.rs | 73 ++++++++-- 13 files changed, 309 insertions(+), 76 deletions(-) create mode 100644 crates/oxc_language_server/src/formatter/server_formatter.rs diff --git a/Cargo.lock b/Cargo.lock index 1062763c105d9..42f6d9a106831 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2008,7 +2008,9 @@ dependencies = [ "log", "oxc_allocator", "oxc_diagnostics", + "oxc_formatter", "oxc_linter", + "oxc_parser", "papaya", "rustc-hash", "serde", diff --git a/crates/oxc_language_server/Cargo.toml b/crates/oxc_language_server/Cargo.toml index e4330dee4dce7..40064206b45e0 100644 --- a/crates/oxc_language_server/Cargo.toml +++ b/crates/oxc_language_server/Cargo.toml @@ -24,7 +24,9 @@ doctest = false [dependencies] oxc_allocator = { workspace = true } oxc_diagnostics = { workspace = true } +oxc_formatter = { workspace = true } oxc_linter = { workspace = true, features = ["language_server"] } +oxc_parser = { workspace = true } # env_logger = { workspace = true, features = ["humantime"] } diff --git a/crates/oxc_language_server/README.md b/crates/oxc_language_server/README.md index 3cc7a7825b5eb..9020efc1598fc 100644 --- a/crates/oxc_language_server/README.md +++ b/crates/oxc_language_server/README.md @@ -27,6 +27,7 @@ These options can be passed with [initialize](#initialize), [workspace/didChange | `unusedDisableDirectives` | `"allow" \| "warn"` \| "deny"` | `"allow"` | Define how directive comments like `// oxlint-disable-line` should be reported, when no errors would have been reported on that line anyway | | `typeAware` | `true` \| `false` | `false` | Enables type-aware linting | | `flags` | `Map` | `` | Special oxc language server flags, currently only one flag key is supported: `disable_nested_config` | +| `fmt.experimental` | `true` \| `false` | `false` | Enables experimental formatting with `oxc_formatter` | ## Supported LSP Specifications from Server @@ -45,7 +46,8 @@ The client can pass the workspace options like following: "tsConfigPath": null, "unusedDisableDirectives": "allow", "typeAware": false, - "flags": {} + "flags": {}, + "fmt.experimental": false } }] } @@ -81,7 +83,8 @@ The client can pass the workspace options like following: "tsConfigPath": null, "unusedDisableDirectives": "allow", "typeAware": false, - "flags": {} + "flags": {}, + "fmt.experimental": false } }] } @@ -142,6 +145,10 @@ Returns a list of [CodeAction](https://microsoft.github.io/language-server-proto Returns a [PublishDiagnostic object](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#publishDiagnosticsParams) +#### [textDocument/formatting](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting) + +Returns a list of [TextEdit](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textEdit) + ## Optional LSP Specifications from Client ### Client @@ -170,6 +177,7 @@ The client can return a response like: "tsConfigPath": null, "unusedDisableDirectives": "allow", "typeAware": false, - "flags": {} + "flags": {}, + "fmt.experimental": false }] ``` diff --git a/crates/oxc_language_server/src/capabilities.rs b/crates/oxc_language_server/src/capabilities.rs index c8dcf99d4e570..5554e68a46914 100644 --- a/crates/oxc_language_server/src/capabilities.rs +++ b/crates/oxc_language_server/src/capabilities.rs @@ -40,12 +40,11 @@ impl From for Capabilities { watched_files.dynamic_registration.is_some_and(|dynamic| dynamic) }) }); - // TODO: enable it when we support formatting - // let formatting = value.text_document.as_ref().is_some_and(|text_document| { - // text_document.formatting.is_some_and(|formatting| { - // formatting.dynamic_registration.is_some_and(|dynamic| dynamic) - // }) - // }); + let dynamic_formatting = value.text_document.as_ref().is_some_and(|text_document| { + text_document.formatting.is_some_and(|formatting| { + formatting.dynamic_registration.is_some_and(|dynamic| dynamic) + }) + }); Self { code_action_provider, @@ -53,7 +52,7 @@ impl From for Capabilities { workspace_execute_command, workspace_configuration, dynamic_watchers, - dynamic_formatting: false, + dynamic_formatting, } } } @@ -100,6 +99,8 @@ impl From for ServerCapabilities { } else { None }, + // the server supports formatting, but it will tell the client if he enabled the setting + document_formatting_provider: None, ..ServerCapabilities::default() } } diff --git a/crates/oxc_language_server/src/file_system.rs b/crates/oxc_language_server/src/file_system.rs index 34e36bbcdc794..252e71645110c 100644 --- a/crates/oxc_language_server/src/file_system.rs +++ b/crates/oxc_language_server/src/file_system.rs @@ -16,7 +16,6 @@ impl LSPFileSystem { self.files.pin().insert(uri.clone(), content); } - #[expect(dead_code)] // used for the oxc_formatter in the future pub fn get(&self, uri: &Uri) -> Option { self.files.pin().get(uri).cloned() } diff --git a/crates/oxc_language_server/src/formatter/mod.rs b/crates/oxc_language_server/src/formatter/mod.rs index 66dd7795f500f..3aba1e5f93a8b 100644 --- a/crates/oxc_language_server/src/formatter/mod.rs +++ b/crates/oxc_language_server/src/formatter/mod.rs @@ -1 +1,2 @@ pub mod options; +pub mod server_formatter; diff --git a/crates/oxc_language_server/src/formatter/options.rs b/crates/oxc_language_server/src/formatter/options.rs index adba1d3d58e3d..210395bc7e544 100644 --- a/crates/oxc_language_server/src/formatter/options.rs +++ b/crates/oxc_language_server/src/formatter/options.rs @@ -1,5 +1,69 @@ -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Deserializer, Serialize, de::Error}; +use serde_json::Value; -#[derive(Debug, Default, Serialize, Deserialize, Clone)] +#[derive(Debug, Default, Serialize, Clone)] #[serde(rename_all = "camelCase")] -pub struct FormatOptions; +pub struct FormatOptions { + pub experimental: bool, +} + +impl<'de> Deserialize<'de> for FormatOptions { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let value = Value::deserialize(deserializer)?; + FormatOptions::try_from(value).map_err(Error::custom) + } +} + +impl TryFrom for FormatOptions { + type Error = String; + + fn try_from(value: Value) -> Result { + let Some(object) = value.as_object() else { + return Err("no object passed".to_string()); + }; + + Ok(Self { + experimental: object + .get("fmt.experimental") + .is_some_and(|run| serde_json::from_value::(run.clone()).unwrap_or_default()), + }) + } +} + +#[cfg(test)] +mod test { + use serde_json::json; + + use super::FormatOptions; + + #[test] + fn test_valid_options_json() { + let json = json!({ + "fmt.experimental": true, + }); + + let options = FormatOptions::try_from(json).unwrap(); + assert!(options.experimental); + } + + #[test] + fn test_empty_options_json() { + let json = json!({}); + + let options = FormatOptions::try_from(json).unwrap(); + assert!(!options.experimental); + } + + #[test] + fn test_invalid_options_json() { + let json = json!({ + "fmt.experimental": "what", // should be bool + }); + + let options = FormatOptions::try_from(json).unwrap(); + assert!(!options.experimental); + } +} diff --git a/crates/oxc_language_server/src/formatter/server_formatter.rs b/crates/oxc_language_server/src/formatter/server_formatter.rs new file mode 100644 index 0000000000000..874e3d557d3c6 --- /dev/null +++ b/crates/oxc_language_server/src/formatter/server_formatter.rs @@ -0,0 +1,57 @@ +use oxc_allocator::Allocator; +use oxc_formatter::{FormatOptions, Formatter, get_supported_source_type}; +use oxc_parser::{ParseOptions, Parser}; +use tower_lsp_server::{ + UriExt, + lsp_types::{Position, Range, TextEdit, Uri}, +}; + +use crate::LSP_MAX_INT; + +pub struct ServerFormatter; + +impl ServerFormatter { + pub fn new() -> Self { + Self {} + } + + #[expect(clippy::unused_self)] + pub fn run_single(&self, uri: &Uri, content: Option) -> Option> { + let path = uri.to_file_path()?; + let source_type = get_supported_source_type(&path)?; + let source_text = if let Some(content) = content { + content + } else { + std::fs::read_to_string(&path).ok()? + }; + + let allocator = Allocator::new(); + let ret = Parser::new(&allocator, &source_text, source_type) + .with_options(ParseOptions { + parse_regular_expression: false, + // Enable all syntax features + allow_v8_intrinsics: true, + allow_return_outside_function: true, + // `oxc_formatter` expects this to be false + preserve_parens: false, + }) + .parse(); + + if !ret.errors.is_empty() { + return None; + } + + let options = FormatOptions::default(); + let code = Formatter::new(&allocator, options).build(&ret.program); + + // nothing has changed + if code == source_text { + return Some(vec![]); + } + + Some(vec![TextEdit::new( + Range::new(Position::new(0, 0), Position::new(LSP_MAX_INT, 0)), + code, + )]) + } +} diff --git a/crates/oxc_language_server/src/linter/error_with_position.rs b/crates/oxc_language_server/src/linter/error_with_position.rs index 54f4fe98c14eb..5d8cd7657ecb0 100644 --- a/crates/oxc_language_server/src/linter/error_with_position.rs +++ b/crates/oxc_language_server/src/linter/error_with_position.rs @@ -7,9 +7,7 @@ use tower_lsp_server::lsp_types::{ use oxc_diagnostics::Severity; -// max range for LSP integer is 2^31 - 1 -// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseTypes -const LSP_MAX_INT: u32 = 2u32.pow(31) - 1; +use crate::LSP_MAX_INT; #[derive(Debug, Clone)] pub struct DiagnosticReport { diff --git a/crates/oxc_language_server/src/main.rs b/crates/oxc_language_server/src/main.rs index ad09b3636b7f6..a2ebda04c7eaf 100644 --- a/crates/oxc_language_server/src/main.rs +++ b/crates/oxc_language_server/src/main.rs @@ -14,8 +14,8 @@ use tower_lsp_server::{ DidChangeConfigurationParams, DidChangeTextDocumentParams, DidChangeWatchedFilesParams, DidChangeWatchedFilesRegistrationOptions, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, - ExecuteCommandParams, InitializeParams, InitializeResult, InitializedParams, Registration, - ServerInfo, Unregistration, Uri, WorkspaceEdit, + DocumentFormattingParams, ExecuteCommandParams, InitializeParams, InitializeResult, + InitializedParams, Registration, ServerInfo, TextEdit, Unregistration, Uri, WorkspaceEdit, }, }; @@ -43,6 +43,10 @@ type ConcurrentHashMap = papaya::HashMap; const OXC_CONFIG_FILE: &str = ".oxlintrc.json"; +// max range for LSP integer is 2^31 - 1 +// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#baseTypes +const LSP_MAX_INT: u32 = 2u32.pow(31) - 1; + struct Backend { client: Client, // Each Workspace has it own worker with Linter (and in the future the formatter). @@ -110,19 +114,17 @@ impl LanguageServer for Backend { // we will init the linter after requesting for the workspace configuration. if !capabilities.workspace_configuration || options.is_some() { 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; + let option = &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(); + + worker.start_worker(option).await; } } @@ -160,7 +162,7 @@ impl LanguageServer for Backend { ConcurrentHashMap::with_capacity_and_hasher(workers.len(), FxBuildHasher); let needed_configurations = needed_configurations.pin_owned(); for worker in workers { - if worker.needs_init_linter().await { + if worker.needs_init_options().await { needed_configurations.insert(worker.get_root_uri().clone(), worker); } } @@ -172,23 +174,20 @@ impl LanguageServer for Backend { // every worker should be initialized already in `initialize` request vec![Some(Options::default()); needed_configurations.len()] }; + let default_options = Options::default(); for (index, worker) in needed_configurations.values().enumerate() { - worker - .init_linter( - configurations - .get(index) - .unwrap_or(&None) - .as_ref() - .unwrap_or(&Options::default()), - ) - .await; + let configuration = + configurations.get(index).unwrap_or(&None).as_ref().unwrap_or(&default_options); + + worker.start_worker(configuration).await; } } + let mut registrations = vec![]; + // init all file watchers if capabilities.dynamic_watchers { - let mut registrations = vec![]; for worker in workers { registrations.push(Registration { id: format!("watcher-{}", worker.get_root_uri().as_str()), @@ -198,11 +197,33 @@ impl LanguageServer for Backend { })), }); } + } - if let Err(err) = self.client.register_capability(registrations).await { - warn!("sending registerCapability.didChangeWatchedFiles failed: {err}"); + if capabilities.dynamic_formatting { + // check if one workspace has formatting enabled + let mut started_worker = false; + for worker in workers { + if worker.has_active_formatter().await { + started_worker = true; + break; + } + } + + if started_worker { + registrations.push(Registration { + id: "dynamic-formatting".to_string(), + method: "textDocument/formatting".to_string(), + register_options: None, + }); } } + + if registrations.is_empty() { + return; + } + if let Err(err) = self.client.register_capability(registrations).await { + warn!("sending registerCapability.didChangeWatchedFiles failed: {err}"); + } } async fn shutdown(&self) -> Result<()> { @@ -273,6 +294,8 @@ impl LanguageServer for Backend { return; }; + let mut global_formatting_added = false; + for option in resolved_options { let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(&option.workspace_uri)) @@ -280,7 +303,13 @@ impl LanguageServer for Backend { continue; }; - let (diagnostics, watcher) = worker.did_change_configuration(&option.options).await; + let (diagnostics, watcher, formatter_activated) = + worker.did_change_configuration(&option.options).await; + + if formatter_activated && self.capabilities.get().is_some_and(|c| c.dynamic_formatting) + { + global_formatting_added = true; + } if let Some(diagnostics) = diagnostics { for (uri, reports) in &diagnostics.pin() { @@ -291,7 +320,9 @@ impl LanguageServer for Backend { } } - if let Some(watcher) = watcher { + if let Some(watcher) = watcher + && self.capabilities.get().is_some_and(|capabilities| capabilities.dynamic_watchers) + { // remove the old watcher removing_registrations.push(Unregistration { id: format!("watcher-{}", worker.get_root_uri().as_str()), @@ -318,16 +349,24 @@ impl LanguageServer for Backend { self.publish_all_diagnostics(x).await; } - if self.capabilities.get().is_some_and(|capabilities| capabilities.dynamic_watchers) { - if !removing_registrations.is_empty() { - if let Err(err) = self.client.unregister_capability(removing_registrations).await { - warn!("sending unregisterCapability.didChangeWatchedFiles failed: {err}"); - } + // override the existing formatting registration + // do not remove the registration, because other workspaces might still need it + if global_formatting_added { + adding_registrations.push(Registration { + id: "dynamic-formatting".to_string(), + method: "textDocument/formatting".to_string(), + register_options: None, + }); + } + + if !removing_registrations.is_empty() { + if let Err(err) = self.client.unregister_capability(removing_registrations).await { + warn!("sending unregisterCapability.didChangeWatchedFiles failed: {err}"); } - if !adding_registrations.is_empty() { - if let Err(err) = self.client.register_capability(adding_registrations).await { - warn!("sending registerCapability.didChangeWatchedFiles failed: {err}"); - } + } + if !adding_registrations.is_empty() { + if let Err(err) = self.client.register_capability(adding_registrations).await { + warn!("sending registerCapability.didChangeWatchedFiles failed: {err}"); } } } @@ -395,6 +434,8 @@ impl LanguageServer for Backend { self.publish_all_diagnostics(&cleared_diagnostics).await; + let default_options = Options::default(); + // client support `workspace/configuration` request if self.capabilities.get().is_some_and(|capabilities| capabilities.workspace_configuration) { @@ -408,7 +449,10 @@ impl LanguageServer for Backend { 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; + let options = options.as_ref().unwrap_or(&default_options); + + worker.start_worker(options).await; + added_registrations.push(Registration { id: format!("watcher-{}", worker.get_root_uri().as_str()), method: "workspace/didChangeWatchedFiles".to_string(), @@ -423,7 +467,7 @@ impl LanguageServer for Backend { for folder in params.event.added { let worker = WorkspaceWorker::new(folder.uri); // use default options - worker.init_linter(&Options::default()).await; + worker.start_worker(&default_options).await; workers.push(worker); } } @@ -590,6 +634,15 @@ impl LanguageServer for Backend { Err(Error::invalid_request()) } + + async fn formatting(&self, params: DocumentFormattingParams) -> Result>> { + let uri = ¶ms.text_document.uri; + let workers = self.workspace_workers.read().await; + let Some(worker) = workers.iter().find(|worker| worker.is_responsible_for_uri(uri)) else { + return Ok(None); + }; + Ok(worker.format_file(uri, self.file_system.read().await.get(uri)).await) + } } impl Backend { diff --git a/crates/oxc_language_server/src/options.rs b/crates/oxc_language_server/src/options.rs index 9f686d6ddd912..7e794a680c129 100644 --- a/crates/oxc_language_server/src/options.rs +++ b/crates/oxc_language_server/src/options.rs @@ -34,6 +34,7 @@ mod test { "options": { "run": true, "configPath": "./custom.json", + "fmt.experimental": true } }]); @@ -46,5 +47,6 @@ mod test { assert_eq!(options.lint.run, Run::OnType); // fallback assert_eq!(options.lint.config_path, Some("./custom.json".into())); assert!(options.lint.flags.is_empty()); + assert!(options.format.experimental); } } diff --git a/crates/oxc_language_server/src/tester.rs b/crates/oxc_language_server/src/tester.rs index 6de1667534ed2..db87263a76aae 100644 --- a/crates/oxc_language_server/src/tester.rs +++ b/crates/oxc_language_server/src/tester.rs @@ -113,12 +113,9 @@ impl Tester<'_> { .join(self.relative_root_dir); let uri = Uri::from_file_path(absolute_path).expect("could not convert current dir to uri"); let worker = WorkspaceWorker::new(uri); - worker - .init_linter(&Options { - lint: self.options.clone().unwrap_or_default(), - ..Default::default() - }) - .await; + let option = + &Options { lint: self.options.clone().unwrap_or_default(), ..Default::default() }; + worker.start_worker(option).await; worker } diff --git a/crates/oxc_language_server/src/worker.rs b/crates/oxc_language_server/src/worker.rs index fd09c98799efa..770bd929b27c5 100644 --- a/crates/oxc_language_server/src/worker.rs +++ b/crates/oxc_language_server/src/worker.rs @@ -16,6 +16,7 @@ use crate::{ apply_all_fix_code_action, apply_fix_code_actions, ignore_this_line_code_action, ignore_this_rule_code_action, }, + formatter::server_formatter::ServerFormatter, linter::{ error_with_position::{DiagnosticReport, PossibleFixContent}, server_linter::{ServerLinter, ServerLinterRun, normalize_path}, @@ -25,12 +26,18 @@ use crate::{ pub struct WorkspaceWorker { root_uri: Uri, server_linter: RwLock>, - options: Mutex, + server_formatter: RwLock>, + options: Mutex>, } impl WorkspaceWorker { pub fn new(root_uri: Uri) -> Self { - Self { root_uri, server_linter: RwLock::new(None), options: Mutex::new(Options::default()) } + Self { + root_uri, + server_linter: RwLock::new(None), + server_formatter: RwLock::new(None), + options: Mutex::new(None), + } } pub fn get_root_uri(&self) -> &Uri { @@ -44,9 +51,14 @@ impl WorkspaceWorker { false } - pub async fn init_linter(&self, options: &Options) { - *self.options.lock().await = options.clone(); + pub async fn start_worker(&self, options: &Options) { + *self.options.lock().await = Some(options.clone()); + *self.server_linter.write().await = Some(ServerLinter::new(&self.root_uri, &options.lint)); + if options.format.experimental { + debug!("experimental formatter enabled"); + *self.server_formatter.write().await = Some(ServerFormatter::new()); + } } // WARNING: start all programs (linter, formatter) before calling this function @@ -56,6 +68,8 @@ impl WorkspaceWorker { // clone the options to avoid locking the mutex let options = self.options.lock().await; + let default_options = Options::default(); + let options = options.as_ref().unwrap_or(&default_options); let use_nested_configs = options.lint.use_nested_configs(); // append the base watcher @@ -102,8 +116,12 @@ impl WorkspaceWorker { watchers } - pub async fn needs_init_linter(&self) -> bool { - self.server_linter.read().await.is_none() + pub async fn needs_init_options(&self) -> bool { + self.options.lock().await.is_none() + } + + pub async fn has_active_formatter(&self) -> bool { + self.server_formatter.read().await.is_some() } pub async fn remove_diagnostics(&self, uri: &Uri) { @@ -116,7 +134,9 @@ impl WorkspaceWorker { async fn refresh_server_linter(&self) { let options = self.options.lock().await; - let server_linter = ServerLinter::new(&self.root_uri, &options.lint); + let default_options = Options::default(); + let lint_options = &options.as_ref().unwrap_or(&default_options).lint; + let server_linter = ServerLinter::new(&self.root_uri, lint_options); *self.server_linter.write().await = Some(server_linter); } @@ -134,6 +154,14 @@ impl WorkspaceWorker { server_linter.run_single(uri, content, run_type).await } + pub async fn format_file(&self, uri: &Uri, content: Option) -> Option> { + let Some(server_formatter) = &*self.server_formatter.read().await else { + return None; + }; + + server_formatter.run_single(uri, content) + } + async fn revalidate_diagnostics( &self, uris: Vec, @@ -277,12 +305,20 @@ impl WorkspaceWorker { pub async fn did_change_configuration( &self, changed_options: &Options, - ) -> (Option>>, Option) { + ) -> ( + // Diagnostic reports that need to be revalidated + Option>>, + // File system watcher for lint config changes + Option, + // Is true, when the formatter was added to the workspace worker + bool, + ) { // Scope the first lock so it is dropped before the second lock let current_option = { let options_guard = self.options.lock().await; options_guard.clone() - }; + } + .unwrap_or_default(); debug!( " @@ -294,7 +330,19 @@ impl WorkspaceWorker { { let mut options_guard = self.options.lock().await; - *options_guard = changed_options.clone(); + *options_guard = Some(changed_options.clone()); + } + + let mut formatting = false; + if current_option.format.experimental != changed_options.format.experimental { + if changed_options.format.experimental { + debug!("experimental formatter enabled"); + *self.server_formatter.write().await = Some(ServerFormatter::new()); + formatting = true; + } else { + debug!("experimental formatter disabled"); + *self.server_formatter.write().await = None; + } } if ServerLinter::needs_restart(¤t_option.lint, &changed_options.lint) { @@ -324,13 +372,14 @@ impl WorkspaceWorker { }), kind: Some(WatchKind::all()), // created, deleted, changed }), + formatting, ); } - return (Some(self.revalidate_diagnostics(files).await), None); + return (Some(self.revalidate_diagnostics(files).await), None, formatting); } - (None, None) + (None, None, formatting) } }