diff --git a/crates/ty_server/src/lib.rs b/crates/ty_server/src/lib.rs index cd7d71a73c186..9c7ad00e4eeb5 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 1d1f78293de1c..7271c1291977d 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 11d61b1450e58..7567a00987943 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 197e61dd0696a..ead442c1a5907 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 f244cc81a3f6f..6919e172372c0 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 bba7cd6ba26c8..62ffe111a2436 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 f76138c13990c..2f4ded2fa6a05 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 3a15b1c9a3f85..16c430ddfe42b 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 0000000000000..c9422f33f76ec --- /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 ad33e15cca3bb..aba23a5c1480a 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)] -#[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, -} - -/// 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)] +/// 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(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() - }), - } - } +pub(crate) struct ClientSettings { + pub(super) disable_language_services: bool, } -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 } }