From 390918e7901f81556d659c687a83188e7ad91cfa Mon Sep 17 00:00:00 2001 From: Dhruv Manilawala Date: Tue, 17 Jun 2025 13:50:45 +0530 Subject: [PATCH] [ty] Add `python.ty.disableLanguageServices` config (#18230) ## Summary PR adding support for it in the VS Code extension: https://github.com/astral-sh/ty-vscode/pull/36 This PR adds support for `python.ty.disableLanguageServices` to the ty language server by accepting this as server setting. This has the same issue as https://github.com/astral-sh/ty/issues/282 in that it only works when configured globally. Fixing that requires support for multiple workspaces in the server itself. I also went ahead and did a similar refactor as the Ruff server to use "Options" and "Settings" to keep the code consistent although the combine functionality doesn't exists yet because workspace settings isn't supported in the ty server. ## Test Plan Refer to https://github.com/astral-sh/ty-vscode/pull/36 for the test demo. --- crates/ty_server/src/lib.rs | 2 +- crates/ty_server/src/server.rs | 50 ++++-- .../src/server/api/requests/completion.rs | 4 + .../api/requests/goto_type_definition.rs | 4 + .../src/server/api/requests/hover.rs | 4 + .../src/server/api/requests/inlay_hints.rs | 4 + crates/ty_server/src/session.rs | 21 ++- crates/ty_server/src/session/index.rs | 14 +- crates/ty_server/src/session/options.rs | 158 ++++++++++++++++++ crates/ty_server/src/session/settings.rs | 114 +------------ 10 files changed, 239 insertions(+), 136 deletions(-) create mode 100644 crates/ty_server/src/session/options.rs diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index cd7d71a73c..9c7ad00e4e 100644 --- a/crates/ty_server/src/lib.rs +++ b/crates/ty_server/src/lib.rs @@ -1,7 +1,7 @@ use crate::server::{ConnectionInitializer, Server}; use anyhow::Context; pub use document::{NotebookDocument, PositionEncoding, TextDocument}; -pub use session::{ClientSettings, DocumentQuery, DocumentSnapshot, Session}; +pub use session::{DocumentQuery, DocumentSnapshot, Session}; use std::num::NonZeroUsize; mod document; diff --git a/crates/ty_server/src/server.rs b/crates/ty_server/src/server.rs index 1d1f78293d..7271c12919 100644 --- a/crates/ty_server/src/server.rs +++ b/crates/ty_server/src/server.rs @@ -2,7 +2,7 @@ use self::schedule::spawn_main_loop; use crate::PositionEncoding; -use crate::session::{AllSettings, ClientSettings, Session}; +use crate::session::{AllOptions, ClientOptions, Session}; use lsp_server::Connection; use lsp_types::{ ClientCapabilities, DiagnosticOptions, DiagnosticServerCapabilities, HoverProviderCapability, @@ -42,10 +42,10 @@ impl Server { ) -> crate::Result { let (id, init_params) = connection.initialize_start()?; - let AllSettings { - global_settings, - mut workspace_settings, - } = AllSettings::from_value( + let AllOptions { + global: global_options, + workspace: mut workspace_options, + } = AllOptions::from_value( init_params .initialization_options .unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())), @@ -68,17 +68,20 @@ impl Server { let client = Client::new(main_loop_sender.clone(), connection.sender.clone()); crate::logging::init_logging( - global_settings.tracing.log_level.unwrap_or_default(), - global_settings.tracing.log_file.as_deref(), + global_options.tracing.log_level.unwrap_or_default(), + global_options.tracing.log_file.as_deref(), ); let mut workspace_for_url = |url: Url| { - let Some(workspace_settings) = workspace_settings.as_mut() else { - return (url, ClientSettings::default()); + let Some(workspace_settings) = workspace_options.as_mut() else { + return (url, ClientOptions::default()); }; let settings = workspace_settings.remove(&url).unwrap_or_else(|| { - tracing::warn!("No workspace settings found for {}", url); - ClientSettings::default() + tracing::warn!( + "No workspace options found for {}, using default options", + url + ); + ClientOptions::default() }); (url, settings) }; @@ -86,16 +89,27 @@ impl Server { let workspaces = init_params .workspace_folders .filter(|folders| !folders.is_empty()) - .map(|folders| folders.into_iter().map(|folder| { - workspace_for_url(folder.uri) - }).collect()) + .map(|folders| { + folders + .into_iter() + .map(|folder| workspace_for_url(folder.uri)) + .collect() + }) .or_else(|| { - tracing::warn!("No workspace(s) were provided during initialization. Using the current working directory as a default workspace..."); - let uri = Url::from_file_path(std::env::current_dir().ok()?).ok()?; + let current_dir = std::env::current_dir().ok()?; + tracing::warn!( + "No workspace(s) were provided during initialization. \ + Using the current working directory as a default workspace: {}", + current_dir.display() + ); + let uri = Url::from_file_path(current_dir).ok()?; Some(vec![workspace_for_url(uri)]) }) .ok_or_else(|| { - anyhow::anyhow!("Failed to get the current working directory while creating a default workspace.") + anyhow::anyhow!( + "Failed to get the current working directory while creating a \ + default workspace." + ) })?; let workspaces = if workspaces.len() > 1 { @@ -121,7 +135,7 @@ impl Server { session: Session::new( &client_capabilities, position_encoding, - global_settings, + global_options, &workspaces, )?, client_capabilities, diff --git a/crates/ty_server/src/server/api/requests/completion.rs b/crates/ty_server/src/server/api/requests/completion.rs index 11d61b1450..7567a00987 100644 --- a/crates/ty_server/src/server/api/requests/completion.rs +++ b/crates/ty_server/src/server/api/requests/completion.rs @@ -30,6 +30,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler { _client: &Client, params: CompletionParams, ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + let Some(file) = snapshot.file(db) else { tracing::debug!("Failed to resolve file for {:?}", params); return Ok(None); diff --git a/crates/ty_server/src/server/api/requests/goto_type_definition.rs b/crates/ty_server/src/server/api/requests/goto_type_definition.rs index 197e61dd06..ead442c1a5 100644 --- a/crates/ty_server/src/server/api/requests/goto_type_definition.rs +++ b/crates/ty_server/src/server/api/requests/goto_type_definition.rs @@ -28,6 +28,10 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler { _client: &Client, params: GotoTypeDefinitionParams, ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + let Some(file) = snapshot.file(db) else { tracing::debug!("Failed to resolve file for {:?}", params); return Ok(None); diff --git a/crates/ty_server/src/server/api/requests/hover.rs b/crates/ty_server/src/server/api/requests/hover.rs index f244cc81a3..6919e17237 100644 --- a/crates/ty_server/src/server/api/requests/hover.rs +++ b/crates/ty_server/src/server/api/requests/hover.rs @@ -28,6 +28,10 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler { _client: &Client, params: HoverParams, ) -> crate::server::Result> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + let Some(file) = snapshot.file(db) else { tracing::debug!("Failed to resolve file for {:?}", params); return Ok(None); diff --git a/crates/ty_server/src/server/api/requests/inlay_hints.rs b/crates/ty_server/src/server/api/requests/inlay_hints.rs index bba7cd6ba2..62ffe111a2 100644 --- a/crates/ty_server/src/server/api/requests/inlay_hints.rs +++ b/crates/ty_server/src/server/api/requests/inlay_hints.rs @@ -27,6 +27,10 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler { _client: &Client, params: InlayHintParams, ) -> crate::server::Result>> { + if snapshot.client_settings().is_language_services_disabled() { + return Ok(None); + } + let Some(file) = snapshot.file(db) else { tracing::debug!("Failed to resolve file for {:?}", params); return Ok(None); diff --git a/crates/ty_server/src/session.rs b/crates/ty_server/src/session.rs index f76138c139..2f4ded2fa6 100644 --- a/crates/ty_server/src/session.rs +++ b/crates/ty_server/src/session.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use anyhow::anyhow; use lsp_types::{ClientCapabilities, TextDocumentContentChangeEvent, Url}; +use options::GlobalOptions; use ruff_db::Db; use ruff_db::files::{File, system_path_to_file}; use ruff_db::system::SystemPath; @@ -14,8 +15,8 @@ use ty_project::{ProjectDatabase, ProjectMetadata}; pub(crate) use self::capabilities::ResolvedClientCapabilities; pub use self::index::DocumentQuery; -pub(crate) use self::settings::AllSettings; -pub use self::settings::ClientSettings; +pub(crate) use self::options::{AllOptions, ClientOptions}; +use self::settings::ClientSettings; use crate::document::{DocumentKey, DocumentVersion, NotebookDocument}; use crate::session::request_queue::RequestQueue; use crate::system::{AnySystemPath, LSPSystem}; @@ -24,6 +25,7 @@ use crate::{PositionEncoding, TextDocument}; mod capabilities; pub(crate) mod client; pub(crate) mod index; +mod options; mod request_queue; mod settings; @@ -58,12 +60,13 @@ impl Session { pub(crate) fn new( client_capabilities: &ClientCapabilities, position_encoding: PositionEncoding, - global_settings: ClientSettings, - workspace_folders: &[(Url, ClientSettings)], + global_options: GlobalOptions, + workspace_folders: &[(Url, ClientOptions)], ) -> crate::Result { let mut workspaces = BTreeMap::new(); - let index = Arc::new(index::Index::new(global_settings)); + let index = Arc::new(index::Index::new(global_options.into_settings())); + // TODO: Consider workspace settings for (url, _) in workspace_folders { let path = url .to_file_path() @@ -168,6 +171,7 @@ impl Session { let key = self.key_from_url(url).ok()?; Some(DocumentSnapshot { resolved_client_capabilities: self.resolved_client_capabilities.clone(), + client_settings: self.index().global_settings(), document_ref: self.index().make_document_ref(&key)?, position_encoding: self.position_encoding, }) @@ -303,6 +307,7 @@ impl Drop for MutIndexGuard<'_> { #[derive(Debug)] pub struct DocumentSnapshot { resolved_client_capabilities: Arc, + client_settings: Arc, document_ref: index::DocumentQuery, position_encoding: PositionEncoding, } @@ -312,7 +317,7 @@ impl DocumentSnapshot { &self.resolved_client_capabilities } - pub fn query(&self) -> &index::DocumentQuery { + pub(crate) fn query(&self) -> &index::DocumentQuery { &self.document_ref } @@ -320,6 +325,10 @@ impl DocumentSnapshot { self.position_encoding } + pub(crate) fn client_settings(&self) -> &ClientSettings { + &self.client_settings + } + pub(crate) fn file(&self, db: &dyn Db) -> Option { match AnySystemPath::try_from_url(self.document_ref.file_url()).ok()? { AnySystemPath::System(path) => system_path_to_file(db, path).ok(), diff --git a/crates/ty_server/src/session/index.rs b/crates/ty_server/src/session/index.rs index 3a15b1c9a3..16c430ddfe 100644 --- a/crates/ty_server/src/session/index.rs +++ b/crates/ty_server/src/session/index.rs @@ -3,16 +3,15 @@ use std::sync::Arc; use lsp_types::Url; use rustc_hash::FxHashMap; +use crate::session::settings::ClientSettings; use crate::{ PositionEncoding, TextDocument, document::{DocumentKey, DocumentVersion, NotebookDocument}, system::AnySystemPath, }; -use super::ClientSettings; - /// Stores and tracks all open documents in a session, along with their associated settings. -#[derive(Default, Debug)] +#[derive(Debug)] pub(crate) struct Index { /// Maps all document file paths to the associated document controller documents: FxHashMap, @@ -21,8 +20,7 @@ pub(crate) struct Index { notebook_cells: FxHashMap, /// Global settings provided by the client. - #[expect(dead_code)] - global_settings: ClientSettings, + global_settings: Arc, } impl Index { @@ -30,7 +28,7 @@ impl Index { Self { documents: FxHashMap::default(), notebook_cells: FxHashMap::default(), - global_settings, + global_settings: Arc::new(global_settings), } } @@ -177,6 +175,10 @@ impl Index { Ok(()) } + pub(crate) fn global_settings(&self) -> Arc { + self.global_settings.clone() + } + fn document_controller_for_key( &mut self, key: &DocumentKey, diff --git a/crates/ty_server/src/session/options.rs b/crates/ty_server/src/session/options.rs new file mode 100644 index 0000000000..c9422f33f7 --- /dev/null +++ b/crates/ty_server/src/session/options.rs @@ -0,0 +1,158 @@ +use std::path::PathBuf; + +use lsp_types::Url; +use rustc_hash::FxHashMap; +use serde::Deserialize; + +use crate::logging::LogLevel; +use crate::session::settings::ClientSettings; + +pub(crate) type WorkspaceOptionsMap = FxHashMap; + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct GlobalOptions { + #[serde(flatten)] + client: ClientOptions, + + // These settings are only needed for tracing, and are only read from the global configuration. + // These will not be in the resolved settings. + #[serde(flatten)] + pub(crate) tracing: TracingOptions, +} + +impl GlobalOptions { + pub(crate) fn into_settings(self) -> ClientSettings { + ClientSettings { + disable_language_services: self + .client + .python + .and_then(|python| python.ty) + .and_then(|ty| ty.disable_language_services) + .unwrap_or_default(), + } + } +} + +/// This is a direct representation of the workspace settings schema, which inherits the schema of +/// [`ClientOptions`] and adds extra fields to describe the workspace it applies to. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct WorkspaceOptions { + #[serde(flatten)] + options: ClientOptions, + workspace: Url, +} + +/// This is a direct representation of the settings schema sent by the client. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct ClientOptions { + /// Settings under the `python.*` namespace in VS Code that are useful for the ty language + /// server. + python: Option, +} + +// TODO(dhruvmanila): We need to mirror the "python.*" namespace on the server side but ideally it +// would be useful to instead use `workspace/configuration` instead. This would be then used to get +// all settings and not just the ones in "python.*". + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct Python { + ty: Option, +} + +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +struct Ty { + disable_language_services: Option, +} + +/// This is a direct representation of the settings schema sent by the client. +/// Settings needed to initialize tracing. These will only be read from the global configuration. +#[derive(Debug, Deserialize, Default)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(rename_all = "camelCase")] +pub(crate) struct TracingOptions { + pub(crate) log_level: Option, + + /// Path to the log file - tildes and environment variables are supported. + pub(crate) log_file: Option, +} + +/// This is the exact schema for initialization options sent in by the client during +/// initialization. +#[derive(Debug, Deserialize)] +#[cfg_attr(test, derive(PartialEq, Eq))] +#[serde(untagged)] +enum InitializationOptions { + #[serde(rename_all = "camelCase")] + HasWorkspaces { + #[serde(rename = "globalSettings")] + global: GlobalOptions, + #[serde(rename = "settings")] + workspace: Vec, + }, + GlobalOnly { + #[serde(default)] + settings: GlobalOptions, + }, +} + +impl Default for InitializationOptions { + fn default() -> Self { + Self::GlobalOnly { + settings: GlobalOptions::default(), + } + } +} + +/// Built from the initialization options provided by the client. +#[derive(Debug)] +pub(crate) struct AllOptions { + pub(crate) global: GlobalOptions, + /// If this is `None`, the client only passed in global settings. + pub(crate) workspace: Option, +} + +impl AllOptions { + /// Initializes the controller from the serialized initialization options. This fails if + /// `options` are not valid initialization options. + pub(crate) fn from_value(options: serde_json::Value) -> Self { + Self::from_init_options( + serde_json::from_value(options) + .map_err(|err| { + tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); + }) + .unwrap_or_default(), + ) + } + + fn from_init_options(options: InitializationOptions) -> Self { + let (global_options, workspace_options) = match options { + InitializationOptions::GlobalOnly { settings: options } => (options, None), + InitializationOptions::HasWorkspaces { + global: global_options, + workspace: workspace_options, + } => (global_options, Some(workspace_options)), + }; + + Self { + global: global_options, + workspace: workspace_options.map(|workspace_options| { + workspace_options + .into_iter() + .map(|workspace_options| { + (workspace_options.workspace, workspace_options.options) + }) + .collect() + }), + } + } +} diff --git a/crates/ty_server/src/session/settings.rs b/crates/ty_server/src/session/settings.rs index ad33e15cca..aba23a5c14 100644 --- a/crates/ty_server/src/session/settings.rs +++ b/crates/ty_server/src/session/settings.rs @@ -1,110 +1,14 @@ -use std::path::PathBuf; - -use lsp_types::Url; -use rustc_hash::FxHashMap; -use serde::Deserialize; - -/// Maps a workspace URI to its associated client settings. Used during server initialization. -pub(crate) type WorkspaceSettingsMap = FxHashMap; - -/// This is a direct representation of the settings schema sent by the client. -#[derive(Debug, Deserialize, Default)] +/// Resolved client settings for a specific document. These settings are meant to be +/// used directly by the server, and are *not* a 1:1 representation with how the client +/// sends them. +#[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub struct ClientSettings { - // These settings are only needed for tracing, and are only read from the global configuration. - // These will not be in the resolved settings. - #[serde(flatten)] - pub(crate) tracing: TracingSettings, +pub(crate) struct ClientSettings { + pub(super) disable_language_services: bool, } -/// Settings needed to initialize tracing. These will only be -/// read from the global configuration. -#[derive(Debug, Deserialize, Default)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -pub(crate) struct TracingSettings { - pub(crate) log_level: Option, - /// Path to the log file - tildes and environment variables are supported. - pub(crate) log_file: Option, -} - -/// This is a direct representation of the workspace settings schema, -/// which inherits the schema of [`ClientSettings`] and adds extra fields -/// to describe the workspace it applies to. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(rename_all = "camelCase")] -struct WorkspaceSettings { - #[serde(flatten)] - settings: ClientSettings, - workspace: Url, -} - -/// This is the exact schema for initialization options sent in by the client -/// during initialization. -#[derive(Debug, Deserialize)] -#[cfg_attr(test, derive(PartialEq, Eq))] -#[serde(untagged)] -enum InitializationOptions { - #[serde(rename_all = "camelCase")] - HasWorkspaces { - global_settings: ClientSettings, - #[serde(rename = "settings")] - workspace_settings: Vec, - }, - GlobalOnly { - #[serde(default)] - settings: ClientSettings, - }, -} - -/// Built from the initialization options provided by the client. -#[derive(Debug)] -pub(crate) struct AllSettings { - pub(crate) global_settings: ClientSettings, - /// If this is `None`, the client only passed in global settings. - pub(crate) workspace_settings: Option, -} - -impl AllSettings { - /// Initializes the controller from the serialized initialization options. - /// This fails if `options` are not valid initialization options. - pub(crate) fn from_value(options: serde_json::Value) -> Self { - Self::from_init_options( - serde_json::from_value(options) - .map_err(|err| { - tracing::error!("Failed to deserialize initialization options: {err}. Falling back to default client settings..."); - }) - .unwrap_or_default(), - ) - } - - fn from_init_options(options: InitializationOptions) -> Self { - let (global_settings, workspace_settings) = match options { - InitializationOptions::GlobalOnly { settings } => (settings, None), - InitializationOptions::HasWorkspaces { - global_settings, - workspace_settings, - } => (global_settings, Some(workspace_settings)), - }; - - Self { - global_settings, - workspace_settings: workspace_settings.map(|workspace_settings| { - workspace_settings - .into_iter() - .map(|settings| (settings.workspace, settings.settings)) - .collect() - }), - } - } -} - -impl Default for InitializationOptions { - fn default() -> Self { - Self::GlobalOnly { - settings: ClientSettings::default(), - } +impl ClientSettings { + pub(crate) fn is_language_services_disabled(&self) -> bool { + self.disable_language_services } }