mirror of
https://github.com/astral-sh/ruff.git
synced 2025-11-01 20:31:57 +00:00
## Summary This PR is a follow-up from https://github.com/astral-sh/ruff/pull/19551 and adds a new `ty.experimental.rename` setting to conditionally register for the rename capability. The complementary PR in ty VS Code extension is https://github.com/astral-sh/ty-vscode/pull/111. This is done using dynamic registration after the settings have been resolved. The experimental group is part of the global settings because they're applied for all workspaces that are managed by the client. ## Test Plan Add E2E tests. In VS Code, with the following setting: ```json { "ty.experimental.rename": "true", "python.languageServer": "None" } ``` I get the relevant log entry: ``` 2025-08-07 16:05:40.598709000 DEBUG client_response{id=3 method="client/registerCapability"}: Registered rename capability ``` And, I'm able to rename a symbol. Once I set it to `false`, then I can see this log entry: ``` 2025-08-07 16:08:39.027876000 DEBUG Rename capability is disabled in the client settings ``` And, I don't see the "Rename Symbol" open in the VS Code dropdown. https://github.com/user-attachments/assets/501659df-ba96-4252-bf51-6f22acb4920b
510 lines
16 KiB
Rust
510 lines
16 KiB
Rust
use anyhow::Result;
|
|
use lsp_types::{Position, notification::ShowMessage, request::RegisterCapability};
|
|
use ruff_db::system::SystemPath;
|
|
use serde_json::Value;
|
|
use ty_server::{ClientOptions, DiagnosticMode};
|
|
|
|
use crate::TestServerBuilder;
|
|
|
|
#[test]
|
|
fn empty_workspace_folders() -> Result<()> {
|
|
let server = TestServerBuilder::new()?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let initialization_result = server.initialization_result().unwrap();
|
|
|
|
insta::assert_json_snapshot!("initialization", initialization_result);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn single_workspace_folder() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let server = TestServerBuilder::new()?
|
|
.with_workspace(workspace_root, None)?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let initialization_result = server.initialization_result().unwrap();
|
|
|
|
insta::assert_json_snapshot!("initialization_with_workspace", initialization_result);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if workspace diagnostics
|
|
/// are enabled via initialization options and dynamic registration is enabled, even if the
|
|
/// workspace configuration is not supported by the client.
|
|
#[test]
|
|
fn workspace_diagnostic_registration_without_configuration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_initialization_options(
|
|
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace),
|
|
)
|
|
.with_workspace(workspace_root, None)?
|
|
.enable_workspace_configuration(false)
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?;
|
|
|
|
// No need to wait for workspaces to initialize as the client does not support workspace
|
|
// configuration.
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": true,
|
|
"workspaceDiagnostics": true
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if open files diagnostics
|
|
/// are enabled via initialization options and dynamic registration is enabled, even if the
|
|
/// workspace configuration is not supported by the client.
|
|
#[test]
|
|
fn open_files_diagnostic_registration_without_configuration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_initialization_options(
|
|
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::OpenFilesOnly),
|
|
)
|
|
.with_workspace(workspace_root, None)?
|
|
.enable_workspace_configuration(false)
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?;
|
|
|
|
// No need to wait for workspaces to initialize as the client does not support workspace
|
|
// configuration.
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": false,
|
|
"workspaceDiagnostics": false
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if workspace diagnostics
|
|
/// are enabled via initialization options and dynamic registration is enabled.
|
|
#[test]
|
|
fn workspace_diagnostic_registration_via_initialization() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_initialization_options(
|
|
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace),
|
|
)
|
|
.with_workspace(workspace_root, None)?
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": true,
|
|
"workspaceDiagnostics": true
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if open files diagnostics
|
|
/// are enabled via initialization options and dynamic registration is enabled.
|
|
#[test]
|
|
fn open_files_diagnostic_registration_via_initialization() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_initialization_options(
|
|
ClientOptions::default().with_diagnostic_mode(DiagnosticMode::OpenFilesOnly),
|
|
)
|
|
.with_workspace(workspace_root, None)?
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": false,
|
|
"workspaceDiagnostics": false
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if workspace diagnostics
|
|
/// are enabled and dynamic registration is enabled.
|
|
#[test]
|
|
fn workspace_diagnostic_registration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions::default().with_diagnostic_mode(DiagnosticMode::Workspace)),
|
|
)?
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": true,
|
|
"workspaceDiagnostics": true
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for diagnostics if workspace diagnostics are
|
|
/// disabled and dynamic registration is enabled.
|
|
#[test]
|
|
fn open_files_diagnostic_registration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions::default().with_diagnostic_mode(DiagnosticMode::OpenFilesOnly)),
|
|
)?
|
|
.enable_diagnostic_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/diagnostic",
|
|
"method": "textDocument/diagnostic",
|
|
"registerOptions": {
|
|
"documentSelector": null,
|
|
"identifier": "ty",
|
|
"interFileDependencies": true,
|
|
"workDoneProgress": false,
|
|
"workspaceDiagnostics": false
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server can disable language services for a workspace via initialization options.
|
|
#[test]
|
|
fn disable_language_services_set_on_initialization() -> Result<()> {
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return 42
|
|
";
|
|
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_initialization_options(ClientOptions::default().with_disable_language_services(true))
|
|
.with_workspace(workspace_root, None)?
|
|
.enable_pull_diagnostics(true)
|
|
.with_file(foo, foo_content)?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
server.open_text_document(foo, &foo_content, 1);
|
|
let hover = server.hover_request(foo, Position::new(0, 5))?;
|
|
|
|
assert!(
|
|
hover.is_none(),
|
|
"Expected no hover information, got: {hover:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server can disable language services for a workspace via workspace configuration
|
|
/// request.
|
|
#[test]
|
|
fn disable_language_services_set_on_workspace() -> Result<()> {
|
|
let workspace_root = SystemPath::new("src");
|
|
let foo = SystemPath::new("src/foo.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return 42
|
|
";
|
|
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions::default().with_disable_language_services(true)),
|
|
)?
|
|
.enable_pull_diagnostics(true)
|
|
.with_file(foo, foo_content)?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
server.open_text_document(foo, &foo_content, 1);
|
|
let hover = server.hover_request(foo, Position::new(0, 5))?;
|
|
|
|
assert!(
|
|
hover.is_none(),
|
|
"Expected no hover information, got: {hover:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server can disable language services for one workspace while keeping them
|
|
/// enabled for another.
|
|
#[test]
|
|
#[ignore = "Requires multiple workspace support in the server and test server"]
|
|
fn disable_language_services_for_one_workspace() -> Result<()> {
|
|
let workspace_a = SystemPath::new("src/a");
|
|
let workspace_b = SystemPath::new("src/b");
|
|
let foo = SystemPath::new("src/a/foo.py");
|
|
let bar = SystemPath::new("src/b/bar.py");
|
|
let foo_content = "\
|
|
def foo() -> str:
|
|
return 42
|
|
";
|
|
let bar_content = "\
|
|
def bar() -> str:
|
|
return 42
|
|
";
|
|
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_a,
|
|
Some(ClientOptions::default().with_disable_language_services(true)),
|
|
)?
|
|
.with_workspace(workspace_b, None)?
|
|
.enable_pull_diagnostics(true)
|
|
.with_file(foo, foo_content)?
|
|
.with_file(bar, bar_content)?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
server.open_text_document(foo, &foo_content, 1);
|
|
let hover_foo = server.hover_request(foo, Position::new(0, 5))?;
|
|
assert!(
|
|
hover_foo.is_none(),
|
|
"Expected no hover information for workspace A, got: {hover_foo:?}"
|
|
);
|
|
|
|
server.open_text_document(bar, &bar_content, 1);
|
|
let hover_bar = server.hover_request(bar, Position::new(0, 5))?;
|
|
assert!(
|
|
hover_bar.is_some(),
|
|
"Expected hover information for workspace B, got: {hover_bar:?}"
|
|
);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a warning notification if user provided unknown options during
|
|
/// initialization.
|
|
#[test]
|
|
fn unknown_initialization_options() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(workspace_root, None)?
|
|
.with_initialization_options(
|
|
ClientOptions::default().with_unknown([("bar".to_string(), Value::Null)].into()),
|
|
)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let show_message_params = server.await_notification::<ShowMessage>()?;
|
|
|
|
insta::assert_json_snapshot!(show_message_params, @r#"
|
|
{
|
|
"type": 2,
|
|
"message": "Received unknown options during initialization: 'bar'. Refer to the logs for more details"
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a warning notification if user provided unknown options in the
|
|
/// workspace configuration.
|
|
#[test]
|
|
fn unknown_options_in_workspace_configuration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(
|
|
workspace_root,
|
|
Some(ClientOptions::default().with_unknown([("bar".to_string(), Value::Null)].into())),
|
|
)?
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let show_message_params = server.await_notification::<ShowMessage>()?;
|
|
|
|
insta::assert_json_snapshot!(show_message_params, @r#"
|
|
{
|
|
"type": 2,
|
|
"message": "Received unknown options for workspace `file://<temp_dir>/foo`: 'bar'. Refer to the logs for more details."
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server sends a registration request for the rename capability if the client
|
|
/// setting is set to true and dynamic registration is enabled.
|
|
#[test]
|
|
fn register_rename_capability_when_enabled() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
let mut server = TestServerBuilder::new()?
|
|
.with_workspace(workspace_root, None)?
|
|
.with_initialization_options(ClientOptions::default().with_experimental_rename(true))
|
|
.enable_rename_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let (_, params) = server.await_request::<RegisterCapability>()?;
|
|
let [registration] = params.registrations.as_slice() else {
|
|
panic!(
|
|
"Expected a single registration, got: {:#?}",
|
|
params.registrations
|
|
);
|
|
};
|
|
|
|
insta::assert_json_snapshot!(registration, @r#"
|
|
{
|
|
"id": "ty/textDocument/rename",
|
|
"method": "textDocument/rename",
|
|
"registerOptions": {
|
|
"prepareProvider": true
|
|
}
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that rename capability is statically registered during initialization if the client
|
|
/// doesn't support dynamic registration, but the server is configured to support it.
|
|
#[test]
|
|
fn rename_available_without_dynamic_registration() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
|
|
let server = TestServerBuilder::new()?
|
|
.with_workspace(workspace_root, None)?
|
|
.with_initialization_options(ClientOptions::default().with_experimental_rename(true))
|
|
.enable_rename_dynamic_registration(false)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
let initialization_result = server.initialization_result().unwrap();
|
|
insta::assert_json_snapshot!(initialization_result.capabilities.rename_provider, @r#"
|
|
{
|
|
"prepareProvider": true
|
|
}
|
|
"#);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Tests that the server does not send a registration request for the rename capability if the
|
|
/// client setting is set to false and dynamic registration is enabled.
|
|
#[test]
|
|
fn not_register_rename_capability_when_disabled() -> Result<()> {
|
|
let workspace_root = SystemPath::new("foo");
|
|
|
|
TestServerBuilder::new()?
|
|
.with_workspace(workspace_root, None)?
|
|
.with_initialization_options(ClientOptions::default().with_experimental_rename(false))
|
|
.enable_rename_dynamic_registration(true)
|
|
.build()?
|
|
.wait_until_workspaces_are_initialized()?;
|
|
|
|
// The `Drop` implementation will make sure that the client did not receive any registration
|
|
// request.
|
|
|
|
Ok(())
|
|
}
|