Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 39 additions & 4 deletions crates/ruff_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand All @@ -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()),
Expand Down
96 changes: 87 additions & 9 deletions crates/ruff_server/src/server/main_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -176,11 +184,81 @@ impl Server {
})
.unwrap(),
),
}],
};
});
}

if supports_formatting || supports_range_formatting {
let mut document_selector: types::DocumentSelector = ["python", "markdown"]
.into_iter()
.flat_map(|language| {
["file", "untitled"].into_iter().map(move |scheme| {
types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage {
language: language.to_string(),
scheme: Some(scheme.to_string()),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to set the schema? what about clients with custom schemas?

I think the smallest change here is to omit the schema, which matches our existing behavior

Suggested change
scheme: Some(scheme.to_string()),
scheme: None,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, yeah this also helped to get rid of all the into_iter stuff.

pattern: None,
})
.into()
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. The into_iterator here feels a bit too clever to me. I'd be inclined to simply repeat both selectors.

})
.collect();

document_selector.push(
types::TextDocumentFilter::Language(types::TextDocumentFilterLanguage {
language: "python".into(),
scheme: Some("vscode-notebook".into()),
pattern: None,
})
.into(),
);
document_selector.push(
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::<lsp_types::RegistrationRequest>(
Expand All @@ -189,7 +267,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 {
Expand Down
70 changes: 70 additions & 0 deletions crates/ruff_server/tests/e2e/capabilities.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use anyhow::Result;
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::<RegistrationRequest>();
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_eq!(
formatting.register_options,
Some(serde_json::json!({
"documentSelector": [
{ "language": "python", "scheme": "file" },
{ "language": "python", "scheme": "untitled" },
{ "language": "markdown", "scheme": "file" },
{ "language": "markdown", "scheme": "untitled" },
{ "language": "python", "scheme": "vscode-notebook" },
{ "notebook": "*", "language": "python" }
]
}))
);
assert_eq!(
range_formatting.register_options,
formatting.register_options
);

Ok(())
}
23 changes: 21 additions & 2 deletions crates/ruff_server/tests/e2e/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
//! [`await_request`]: TestServer::await_request
//! [`await_notification`]: TestServer::await_notification

mod capabilities;
mod code_action;
mod custom_extension;
mod diagnostics;
Expand Down Expand Up @@ -530,7 +531,6 @@ impl TestServer {
///
/// If receiving the request fails.
#[track_caller]
#[expect(dead_code)]
pub(crate) fn await_request<R: Request>(&mut self) -> (RequestId, R::Params) {
match self.try_await_request::<R>(None) {
Ok(result) => result,
Expand Down Expand Up @@ -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()
}
Expand Down Expand Up @@ -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 {
Expand Down
Loading