[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.
This commit is contained in:
Dhruv Manilawala 2025-06-17 13:50:45 +05:30 committed by GitHub
parent a1c69ca460
commit 390918e790
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 239 additions and 136 deletions

View file

@ -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;

View file

@ -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<Self> {
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,

View file

@ -30,6 +30,10 @@ impl BackgroundDocumentRequestHandler for CompletionRequestHandler {
_client: &Client,
params: CompletionParams,
) -> crate::server::Result<Option<CompletionResponse>> {
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);

View file

@ -28,6 +28,10 @@ impl BackgroundDocumentRequestHandler for GotoTypeDefinitionRequestHandler {
_client: &Client,
params: GotoTypeDefinitionParams,
) -> crate::server::Result<Option<GotoDefinitionResponse>> {
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);

View file

@ -28,6 +28,10 @@ impl BackgroundDocumentRequestHandler for HoverRequestHandler {
_client: &Client,
params: HoverParams,
) -> crate::server::Result<Option<lsp_types::Hover>> {
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);

View file

@ -27,6 +27,10 @@ impl BackgroundDocumentRequestHandler for InlayHintRequestHandler {
_client: &Client,
params: InlayHintParams,
) -> crate::server::Result<Option<Vec<lsp_types::InlayHint>>> {
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);

View file

@ -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<Self> {
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<ResolvedClientCapabilities>,
client_settings: Arc<ClientSettings>,
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<File> {
match AnySystemPath::try_from_url(self.document_ref.file_url()).ok()? {
AnySystemPath::System(path) => system_path_to_file(db, path).ok(),

View file

@ -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<AnySystemPath, DocumentController>,
@ -21,8 +20,7 @@ pub(crate) struct Index {
notebook_cells: FxHashMap<Url, AnySystemPath>,
/// Global settings provided by the client.
#[expect(dead_code)]
global_settings: ClientSettings,
global_settings: Arc<ClientSettings>,
}
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<ClientSettings> {
self.global_settings.clone()
}
fn document_controller_for_key(
&mut self,
key: &DocumentKey,

View file

@ -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<Url, ClientOptions>;
#[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<Python>,
}
// 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<Ty>,
}
#[derive(Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct Ty {
disable_language_services: Option<bool>,
}
/// 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<LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<PathBuf>,
}
/// 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<WorkspaceOptions>,
},
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<WorkspaceOptionsMap>,
}
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()
}),
}
}
}

View file

@ -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<Url, ClientSettings>;
/// 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<crate::logging::LogLevel>,
/// Path to the log file - tildes and environment variables are supported.
pub(crate) log_file: Option<PathBuf>,
}
/// 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<WorkspaceSettings>,
},
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<WorkspaceSettingsMap>,
}
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
}
}