Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
41 changes: 37 additions & 4 deletions crates/ty_server/src/capabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use lsp_types::{
};

use crate::PositionEncoding;
use crate::session::GlobalSettings;

bitflags::bitflags! {
/// Represents the resolved client capabilities for the language server.
Expand All @@ -31,6 +32,7 @@ bitflags::bitflags! {
const FILE_WATCHER_SUPPORT = 1 << 12;
const DIAGNOSTIC_DYNAMIC_REGISTRATION = 1 << 13;
const WORKSPACE_CONFIGURATION = 1 << 14;
const RENAME_DYNAMIC_REGISTRATION = 1 << 15;
}
}

Expand Down Expand Up @@ -110,6 +112,11 @@ impl ResolvedClientCapabilities {
self.contains(Self::DIAGNOSTIC_DYNAMIC_REGISTRATION)
}

/// Returns `true` if the client supports dynamic registration for rename capabilities.
pub(crate) const fn supports_rename_dynamic_registration(self) -> bool {
self.contains(Self::RENAME_DYNAMIC_REGISTRATION)
}

pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
let mut flags = Self::empty();

Expand Down Expand Up @@ -246,6 +253,13 @@ impl ResolvedClientCapabilities {
flags |= Self::HIERARCHICAL_DOCUMENT_SYMBOL_SUPPORT;
}

if text_document
.and_then(|text_document| text_document.rename.as_ref()?.dynamic_registration)
.unwrap_or_default()
{
flags |= Self::RENAME_DYNAMIC_REGISTRATION;
}

if client_capabilities
.window
.as_ref()
Expand All @@ -259,9 +273,12 @@ impl ResolvedClientCapabilities {
}
}

/// Creates the server capabilities based on the resolved client capabilities and resolved global
/// settings from the initialization options.
pub(crate) fn server_capabilities(
position_encoding: PositionEncoding,
resolved_client_capabilities: ResolvedClientCapabilities,
global_settings: &GlobalSettings,
) -> ServerCapabilities {
let diagnostic_provider =
if resolved_client_capabilities.supports_diagnostic_dynamic_registration() {
Expand All @@ -275,6 +292,18 @@ pub(crate) fn server_capabilities(
))
};

let rename_provider = if resolved_client_capabilities.supports_rename_dynamic_registration() {
// If the client supports dynamic registration, we will register the rename capabilities
// dynamically based on the `ty.experimental.rename` setting.
None
} else {
// Otherwise, we check whether user has enabled rename support via the resolved settings
// from initialization options.
global_settings
.is_rename_enabled()
.then(|| OneOf::Right(server_rename_options()))
};

Comment on lines +299 to +306
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the behavior that was discussed in the review feedback.

ServerCapabilities {
position_encoding: Some(position_encoding.into()),
diagnostic_provider,
Expand All @@ -289,10 +318,7 @@ pub(crate) fn server_capabilities(
definition_provider: Some(OneOf::Left(true)),
declaration_provider: Some(DeclarationCapability::Simple(true)),
references_provider: Some(OneOf::Left(true)),
rename_provider: Some(OneOf::Right(RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
})),
rename_provider,
document_highlight_provider: Some(OneOf::Left(true)),
hover_provider: Some(HoverProviderCapability::Simple(true)),
signature_help_provider: Some(SignatureHelpOptions {
Expand Down Expand Up @@ -344,3 +370,10 @@ pub(crate) fn server_diagnostic_options(workspace_diagnostics: bool) -> Diagnost
},
}
}

pub(crate) fn server_rename_options() -> RenameOptions {
RenameOptions {
prepare_provider: Some(true),
work_done_progress_options: WorkDoneProgressOptions::default(),
}
}
13 changes: 10 additions & 3 deletions crates/ty_server/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,15 @@ impl Server {

let resolved_client_capabilities = ResolvedClientCapabilities::new(&client_capabilities);
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
let server_capabilities =
server_capabilities(position_encoding, resolved_client_capabilities);
let server_capabilities = server_capabilities(
position_encoding,
resolved_client_capabilities,
&initialization_options
.options
.global
.clone()
.into_settings(),
);

let version = ruff_db::program_version().unwrap_or("Unknown");
tracing::debug!("Version: {version}");
Expand Down Expand Up @@ -102,7 +109,7 @@ impl Server {
{
tracing::warn!(
"Received unknown options during initialization: {}",
serde_json::to_string_pretty(unknown_options)
serde_json::to_string_pretty(&unknown_options)
.unwrap_or_else(|_| format!("{unknown_options:?}"))
);

Expand Down
97 changes: 84 additions & 13 deletions crates/ty_server/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use index::DocumentQueryError;
use lsp_server::{Message, RequestId};
use lsp_types::notification::{Exit, Notification};
use lsp_types::request::{
DocumentDiagnosticRequest, RegisterCapability, Request, Shutdown, UnregisterCapability,
DocumentDiagnosticRequest, RegisterCapability, Rename, Request, Shutdown, UnregisterCapability,
WorkspaceDiagnosticRequest,
};
use lsp_types::{
Expand All @@ -16,8 +16,7 @@ use options::GlobalOptions;
use ruff_db::Db;
use ruff_db::files::File;
use ruff_db::system::{System, SystemPath, SystemPathBuf};
use settings::GlobalSettings;
use std::collections::{BTreeMap, VecDeque};
use std::collections::{BTreeMap, HashSet, VecDeque};
use std::ops::{Deref, DerefMut};
use std::panic::RefUnwindSafe;
use std::sync::Arc;
Expand All @@ -29,8 +28,10 @@ use ty_project::{ChangeResult, CheckMode, Db as _, ProjectDatabase, ProjectMetad
pub(crate) use self::index::DocumentQuery;
pub(crate) use self::options::InitializationOptions;
pub use self::options::{ClientOptions, DiagnosticMode};
pub(crate) use self::settings::WorkspaceSettings;
use crate::capabilities::{ResolvedClientCapabilities, server_diagnostic_options};
pub(crate) use self::settings::{GlobalSettings, WorkspaceSettings};
use crate::capabilities::{
ResolvedClientCapabilities, server_diagnostic_options, server_rename_options,
};
use crate::document::{DocumentKey, DocumentVersion, NotebookDocument};
use crate::server::{Action, publish_settings_diagnostics};
use crate::session::client::Client;
Expand Down Expand Up @@ -91,8 +92,6 @@ pub(crate) struct Session {
shutdown_requested: bool,

/// Whether the server has dynamically registered the diagnostic capability with the client.
diagnostic_capability_registered: bool,

/// Is the connected client a `TestServer` instance.
in_test: bool,

Expand All @@ -107,6 +106,10 @@ pub(crate) struct Session {
/// We'll re-run the request after every change to `Session` (see `revision`)
/// to see if there are now changes and, if so, respond to the client.
suspended_workspace_diagnostics_request: Option<SuspendedWorkspaceDiagnosticRequest>,

/// Registrations is a set of LSP methods that have been dynamically registered with the
/// client.
registrations: HashSet<String>,
}

/// LSP State for a Project
Expand Down Expand Up @@ -166,10 +169,10 @@ impl Session {
resolved_client_capabilities,
request_queue: RequestQueue::new(),
shutdown_requested: false,
diagnostic_capability_registered: false,
in_test,
suspended_workspace_diagnostics_request: None,
revision: 0,
registrations: HashSet::new(),
})
}

Expand Down Expand Up @@ -568,6 +571,7 @@ impl Session {
}

self.register_diagnostic_capability(client);
self.register_rename_capability(client);
Comment on lines 573 to +574
Copy link
Member

Choose a reason for hiding this comment

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

This isn't incorrect but the LSP supports sending multiple registrations in a single request. Should we change the regsiter_* to a register_dynamic_capability method that registers all dynamic capabilities (collects the one it must remove and then collects the one it needs to add)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I wanted to do something like that but the difference here is that for the rename capability we do want to unregister the capability but then we should avoid re-registering the capability. This scenario is only valid when we support didChangeConfiguration but I don't want to make any assumptions that might be incorrect later on.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh, I could have two separate methods unregister_dynamic_capability and register_dynamic_capability which would solve this.

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm going to do this as a follow-up.


assert!(
self.workspaces.all_initialized(),
Expand All @@ -583,11 +587,13 @@ impl Session {
}
}

// TODO: Merge the following two methods as `register_capability` and `unregister_capability`

/// Sends a registration notification to the client to enable / disable workspace diagnostics
/// as per the `diagnostic_mode`.
/// as per the `ty.diagnosticMode` global setting.
///
/// This method is a no-op if the client doesn't support dynamic registration of diagnostic
/// capabilities.
/// capability.
fn register_diagnostic_capability(&mut self, client: &Client) {
static DIAGNOSTIC_REGISTRATION_ID: &str = "ty/textDocument/diagnostic";

Expand All @@ -598,9 +604,11 @@ impl Session {
return;
}

let diagnostic_mode = self.global_settings.diagnostic_mode;
let registered = self
.registrations
.contains(DocumentDiagnosticRequest::METHOD);

if self.diagnostic_capability_registered {
if registered {
client.send_request::<UnregisterCapability>(
self,
UnregistrationParams {
Expand All @@ -615,6 +623,8 @@ impl Session {
);
}

let diagnostic_mode = self.global_settings.diagnostic_mode;

let registration = Registration {
id: DIAGNOSTIC_REGISTRATION_ID.into(),
method: DocumentDiagnosticRequest::METHOD.into(),
Expand Down Expand Up @@ -643,7 +653,68 @@ impl Session {
},
);

self.diagnostic_capability_registered = true;
if !registered {
self.registrations
.insert(DocumentDiagnosticRequest::METHOD.to_string());
}
}

/// Sends a registration notification to the client to enable / disable rename capability as
/// per the `ty.experimental.rename` global setting.
///
/// This method is a no-op if the client doesn't support dynamic registration of rename
/// capability.
fn register_rename_capability(&mut self, client: &Client) {
static RENAME_REGISTRATION_ID: &str = "ty/textDocument/rename";

if !self
.resolved_client_capabilities
.supports_rename_dynamic_registration()
{
return;
}

let registered = self.registrations.contains(Rename::METHOD);

if registered {
client.send_request::<UnregisterCapability>(
self,
UnregistrationParams {
unregisterations: vec![Unregistration {
id: RENAME_REGISTRATION_ID.into(),
method: Rename::METHOD.into(),
}],
},
move |_: &Client, ()| {
tracing::debug!("Unregistered rename capability");
},
);
}

if !self.global_settings.experimental.rename {
tracing::debug!("Rename capability is disabled in the client settings");
return;
}

let registration = Registration {
id: RENAME_REGISTRATION_ID.into(),
method: Rename::METHOD.into(),
register_options: Some(serde_json::to_value(server_rename_options()).unwrap()),
};

client.send_request::<RegisterCapability>(
self,
RegistrationParams {
registrations: vec![registration],
},
move |_: &Client, ()| {
tracing::debug!("Registered rename capability");
},
);

if !registered {
self.registrations.insert(Rename::METHOD.to_string());
}
}

/// Creates a document snapshot with the URL referencing the document to snapshot.
Expand Down
26 changes: 25 additions & 1 deletion crates/ty_server/src/session/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ use ty_project::metadata::value::{RangedValue, RelativePathBuf};

use crate::logging::LogLevel;

use super::settings::{GlobalSettings, WorkspaceSettings};
use super::settings::{ExperimentalSettings, GlobalSettings, WorkspaceSettings};

/// Initialization options that are set once at server startup that never change.
///
Expand Down Expand Up @@ -106,6 +106,12 @@ impl ClientOptions {
self
}

#[must_use]
pub fn with_experimental_rename(mut self, enabled: bool) -> Self {
self.global.experimental.get_or_insert_default().rename = Some(enabled);
self
}

#[must_use]
pub fn with_unknown(mut self, unknown: HashMap<String, Value>) -> Self {
self.unknown = unknown;
Expand All @@ -122,12 +128,23 @@ impl ClientOptions {
pub(crate) struct GlobalOptions {
/// Diagnostic mode for the language server.
diagnostic_mode: Option<DiagnosticMode>,

/// Experimental features that the server provides on an opt-in basis.
pub(crate) experimental: Option<Experimental>,
}

impl GlobalOptions {
pub(crate) fn into_settings(self) -> GlobalSettings {
let experimental = self
.experimental
.map(|experimental| ExperimentalSettings {
rename: experimental.rename.unwrap_or_default(),
})
.unwrap_or_default();

GlobalSettings {
diagnostic_mode: self.diagnostic_mode.unwrap_or_default(),
experimental,
}
}
}
Expand Down Expand Up @@ -238,6 +255,13 @@ impl Combine for DiagnosticMode {
}
}

#[derive(Clone, Combine, Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub(crate) struct Experimental {
/// Whether to enable the experimental symbol rename feature.
pub(crate) rename: Option<bool>,
}

#[derive(Clone, Debug, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
struct PythonExtension {
Expand Down
12 changes: 12 additions & 0 deletions crates/ty_server/src/session/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ use ty_project::metadata::options::ProjectOptionsOverrides;
#[derive(Clone, Default, Debug, PartialEq)]
pub(crate) struct GlobalSettings {
pub(super) diagnostic_mode: DiagnosticMode,
pub(super) experimental: ExperimentalSettings,
}

impl GlobalSettings {
pub(crate) fn is_rename_enabled(&self) -> bool {
self.experimental.rename
}
}

impl GlobalSettings {
Expand All @@ -14,6 +21,11 @@ impl GlobalSettings {
}
}

#[derive(Clone, Default, Debug, PartialEq)]
pub(crate) struct ExperimentalSettings {
pub(super) rename: bool,
}

/// Resolved client settings for a specific workspace.
///
/// These settings are meant to be used directly by the server, and are *not* a 1:1 representation
Expand Down
Loading
Loading