diff --git a/crates/ruff_server/src/server.rs b/crates/ruff_server/src/server.rs index 58cd033ae5cb75..c02b008a717230 100644 --- a/crates/ruff_server/src/server.rs +++ b/crates/ruff_server/src/server.rs @@ -59,7 +59,8 @@ impl Server { let client_capabilities = init_params.capabilities; let position_encoding = Self::find_best_position_encoding(&client_capabilities); - let server_capabilities = Self::server_capabilities(position_encoding); + let server_capabilities = + Self::server_capabilities(position_encoding, &client_capabilities); let connection = connection.initialize_finish( id, @@ -150,7 +151,41 @@ impl Server { .unwrap_or_default() } - fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities { + fn supports_dynamic_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.formatting) + .and_then(|formatting| formatting.dynamic_registration) + .unwrap_or_default() + } + + fn supports_dynamic_range_formatting(client_capabilities: &ClientCapabilities) -> bool { + client_capabilities + .text_document + .as_ref() + .and_then(|text_document| text_document.range_formatting) + .and_then(|range_formatting| range_formatting.dynamic_registration) + .unwrap_or_default() + } + + fn server_capabilities( + position_encoding: PositionEncoding, + client_capabilities: &ClientCapabilities, + ) -> types::ServerCapabilities { + let document_formatting_provider = if Self::supports_dynamic_formatting(client_capabilities) + { + None + } else { + Some(true.into()) + }; + let document_range_formatting_provider = + if Self::supports_dynamic_range_formatting(client_capabilities) { + None + } else { + Some(true.into()) + }; + types::ServerCapabilities { position_encoding: Some(position_encoding.into()), code_action_provider: Some( @@ -176,8 +211,8 @@ impl Server { file_operations: None, text_document_content: None, }), - document_formatting_provider: Some(true.into()), - document_range_formatting_provider: Some(true.into()), + document_formatting_provider, + document_range_formatting_provider, diagnostic_provider: Some( DiagnosticOptions { identifier: Some(crate::DIAGNOSTIC_NAME.into()), diff --git a/crates/ruff_server/src/server/main_loop.rs b/crates/ruff_server/src/server/main_loop.rs index 370e84c7f14166..34357d22f57b4e 100644 --- a/crates/ruff_server/src/server/main_loop.rs +++ b/crates/ruff_server/src/server/main_loop.rs @@ -3,6 +3,7 @@ use crossbeam::select; use lsp_server::Message; use lsp_types::{ self as types, DidChangeWatchedFilesRegistrationOptions, FileSystemWatcher, Notification as _, + Request as _, }; use crate::{ @@ -135,20 +136,27 @@ impl Server { } fn initialize(&mut self, client: &Client) { - let dynamic_registration = self + let supports_watched_files = self .client_capabilities .workspace .as_ref() .and_then(|workspace| workspace.did_change_watched_files) .and_then(|watched_files| watched_files.dynamic_registration) .unwrap_or_default(); + let supports_formatting = Self::supports_dynamic_formatting(&self.client_capabilities); + let supports_range_formatting = + Self::supports_dynamic_range_formatting(&self.client_capabilities); + let dynamic_registration = + supports_watched_files || supports_formatting || supports_range_formatting; + if dynamic_registration { // Register all dynamic capabilities here + let mut registrations = vec![]; - // `workspace/didChangeWatchedFiles` - // (this registers the configuration file watcher) - let params = lsp_types::RegistrationParams { - registrations: vec![lsp_types::Registration { + if supports_watched_files { + // `workspace/didChangeWatchedFiles` + // (this registers the configuration file watcher) + registrations.push(lsp_types::Registration { id: "ruff-server-watch".into(), method: "workspace/didChangeWatchedFiles".into(), register_options: Some( @@ -176,11 +184,71 @@ impl Server { }) .unwrap(), ), - }], - }; + }); + } + + if supports_formatting || supports_range_formatting { + let document_selector = vec![ + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "python".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage { + language: "markdown".to_string(), + scheme: None, + pattern: None, + }) + .into(), + types::NotebookCellTextDocumentFilter { + notebook: "*".into(), + language: Some("python".into()), + } + .into(), + ]; + + let text_document_registration_options = types::TextDocumentRegistrationOptions { + document_selector: Some(document_selector), + }; + + if supports_formatting { + registrations.push(types::Registration { + id: "ruff-server-format".into(), + method: types::DocumentFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value(types::DocumentFormattingRegistrationOptions { + text_document_registration_options: + text_document_registration_options.clone(), + document_formatting_options: + types::DocumentFormattingOptions::default(), + }) + .unwrap(), + ), + }); + } + + if supports_range_formatting { + registrations.push(types::Registration { + id: "ruff-server-format-range".into(), + method: types::DocumentRangeFormattingRequest::METHOD.to_string(), + register_options: Some( + serde_json::to_value( + types::DocumentRangeFormattingRegistrationOptions { + text_document_registration_options, + document_range_formatting_options: + types::DocumentRangeFormattingOptions::default(), + }, + ) + .unwrap(), + ), + }); + } + } + let params = types::RegistrationParams { registrations }; let response_handler = |_: &Client, ()| { - tracing::info!("Configuration file watcher successfully registered"); + tracing::info!("Dynamic capabilities successfully registered"); }; if let Err(err) = client.send_request::( @@ -189,7 +257,7 @@ impl Server { response_handler, ) { tracing::error!( - "An error occurred when trying to register the configuration file watcher: {err}" + "An error occurred when trying to register dynamic capabilities: {err}" ); } } else { diff --git a/crates/ruff_server/tests/e2e/capabilities.rs b/crates/ruff_server/tests/e2e/capabilities.rs new file mode 100644 index 00000000000000..fe47aac6ec9479 --- /dev/null +++ b/crates/ruff_server/tests/e2e/capabilities.rs @@ -0,0 +1,77 @@ +use anyhow::Result; +use insta::assert_json_snapshot; +use lsp_types::Request as _; +use lsp_types::{DocumentFormattingRequest, DocumentRangeFormattingRequest, RegistrationRequest}; + +use crate::TestServerBuilder; + +#[test] +fn statically_registers_formatting_when_dynamic_registration_is_unsupported() -> Result<()> { + let server = TestServerBuilder::new()?.build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, Some(true.into())); + assert_eq!( + capabilities.document_range_formatting_provider, + Some(true.into()) + ); + + Ok(()) +} + +#[test] +fn dynamically_registers_formatting_and_range_formatting_for_python_and_markdown() -> Result<()> { + let mut server = TestServerBuilder::new()? + .enable_formatting_dynamic_registration(true) + .enable_range_formatting_dynamic_registration(true) + .build(); + let capabilities = &server + .initialization_result() + .expect("Server should return initialization capabilities") + .capabilities; + + assert_eq!(capabilities.document_formatting_provider, None); + assert_eq!(capabilities.document_range_formatting_provider, None); + + let (_, params) = server.await_request::(); + let [formatting, range_formatting] = params.registrations.as_slice() else { + panic!("Expected both dynamic formatting registrations"); + }; + + assert_eq!( + formatting.method, + DocumentFormattingRequest::METHOD.as_str() + ); + assert_eq!( + range_formatting.method, + DocumentRangeFormattingRequest::METHOD.as_str() + ); + assert_json_snapshot!( + formatting.register_options, + @r#" + { + "documentSelector": [ + { + "language": "python" + }, + { + "language": "markdown" + }, + { + "language": "python", + "notebook": "*" + } + ] + } + "# + ); + assert_eq!( + range_formatting.register_options, + formatting.register_options + ); + + Ok(()) +} diff --git a/crates/ruff_server/tests/e2e/main.rs b/crates/ruff_server/tests/e2e/main.rs index 6349ed3c0c676c..d8e53ee40d6ac0 100644 --- a/crates/ruff_server/tests/e2e/main.rs +++ b/crates/ruff_server/tests/e2e/main.rs @@ -25,6 +25,7 @@ //! [`await_request`]: TestServer::await_request //! [`await_notification`]: TestServer::await_notification +mod capabilities; mod code_action; mod custom_extension; mod diagnostics; @@ -530,7 +531,6 @@ impl TestServer { /// /// If receiving the request fails. #[track_caller] - #[expect(dead_code)] pub(crate) fn await_request(&mut self) -> (RequestId, R::Params) { match self.try_await_request::(None) { Ok(result) => result, @@ -653,7 +653,6 @@ impl TestServer { } /// Get the initialization result - #[expect(dead_code)] pub(crate) fn initialization_result(&self) -> Option<&InitializeResult> { self.initialize_response.as_ref() } @@ -1062,6 +1061,26 @@ impl TestServerBuilder { self } + pub(crate) fn enable_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + + pub(crate) fn enable_range_formatting_dynamic_registration(mut self, enabled: bool) -> Self { + self.client_capabilities + .text_document + .get_or_insert_default() + .range_formatting + .get_or_insert_default() + .dynamic_registration = Some(enabled); + self + } + /// Enable or disable workspace configuration capability #[expect(dead_code)] pub(crate) fn enable_workspace_configuration(mut self, enabled: bool) -> Self {