[ty] Use python version and path from Python extension (#19012)

This commit is contained in:
Micha Reiser 2025-07-14 11:47:27 +02:00 committed by GitHub
parent 26f736bc46
commit 90026047f9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 344 additions and 99 deletions

1
Cargo.lock generated
View file

@ -4307,6 +4307,7 @@ dependencies = [
"lsp-types", "lsp-types",
"ruff_db", "ruff_db",
"ruff_notebook", "ruff_notebook",
"ruff_python_ast",
"ruff_source_file", "ruff_source_file",
"ruff_text_size", "ruff_text_size",
"rustc-hash", "rustc-hash",

View file

@ -121,17 +121,17 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
None => ProjectMetadata::discover(&project_path, &system)?, None => ProjectMetadata::discover(&project_path, &system)?,
}; };
let options = args.into_options();
project_metadata.apply_options(options.clone());
project_metadata.apply_configuration_files(&system)?; project_metadata.apply_configuration_files(&system)?;
let project_options_overrides = ProjectOptionsOverrides::new(config_file, args.into_options());
project_metadata.apply_overrides(&project_options_overrides);
let mut db = ProjectDatabase::new(project_metadata, system)?; let mut db = ProjectDatabase::new(project_metadata, system)?;
if !check_paths.is_empty() { if !check_paths.is_empty() {
db.project().set_included_paths(&mut db, check_paths); db.project().set_included_paths(&mut db, check_paths);
} }
let project_options_overrides = ProjectOptionsOverrides::new(config_file, options);
let (main_loop, main_loop_cancellation_token) = let (main_loop, main_loop_cancellation_token) =
MainLoop::new(project_options_overrides, printer); MainLoop::new(project_options_overrides, printer);

View file

@ -3,6 +3,7 @@ use std::panic::{AssertUnwindSafe, RefUnwindSafe};
use std::sync::Arc; use std::sync::Arc;
use std::{cmp, fmt}; use std::{cmp, fmt};
pub use self::changes::ChangeResult;
use crate::metadata::settings::file_settings; use crate::metadata::settings::file_settings;
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter}; use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
use crate::{ProgressReporter, Project, ProjectMetadata}; use crate::{ProgressReporter, Project, ProjectMetadata};

View file

@ -41,8 +41,6 @@ impl ProjectDatabase {
let project_root = project.root(self).to_path_buf(); let project_root = project.root(self).to_path_buf();
let config_file_override = let config_file_override =
project_options_overrides.and_then(|options| options.config_file_override.clone()); project_options_overrides.and_then(|options| options.config_file_override.clone());
let options =
project_options_overrides.map(|project_options| project_options.options.clone());
let program = Program::get(self); let program = Program::get(self);
let custom_stdlib_versions_path = program let custom_stdlib_versions_path = program
.custom_stdlib_search_path(self) .custom_stdlib_search_path(self)
@ -218,16 +216,16 @@ impl ProjectDatabase {
}; };
match new_project_metadata { match new_project_metadata {
Ok(mut metadata) => { Ok(mut metadata) => {
if let Some(cli_options) = options {
metadata.apply_options(cli_options);
}
if let Err(error) = metadata.apply_configuration_files(self.system()) { if let Err(error) = metadata.apply_configuration_files(self.system()) {
tracing::error!( tracing::error!(
"Failed to apply configuration files, continuing without applying them: {error}" "Failed to apply configuration files, continuing without applying them: {error}"
); );
} }
if let Some(overrides) = project_options_overrides {
metadata.apply_overrides(overrides);
}
match metadata.to_program_settings(self.system(), self.vendored()) { match metadata.to_program_settings(self.system(), self.vendored()) {
Ok(program_settings) => { Ok(program_settings) => {
let program = Program::get(self); let program = Program::get(self);

View file

@ -1,7 +1,7 @@
use crate::glob::{GlobFilterCheckMode, IncludeResult}; use crate::glob::{GlobFilterCheckMode, IncludeResult};
use crate::metadata::options::{OptionDiagnostic, ToSettingsError}; use crate::metadata::options::{OptionDiagnostic, ToSettingsError};
use crate::walk::{ProjectFilesFilter, ProjectFilesWalker}; use crate::walk::{ProjectFilesFilter, ProjectFilesWalker};
pub use db::{CheckMode, Db, ProjectDatabase, SalsaMemoryDump}; pub use db::{ChangeResult, CheckMode, Db, ProjectDatabase, SalsaMemoryDump};
use files::{Index, Indexed, IndexedFiles}; use files::{Index, Indexed, IndexedFiles};
use metadata::settings::Settings; use metadata::settings::Settings;
pub use metadata::{ProjectMetadata, ProjectMetadataError}; pub use metadata::{ProjectMetadata, ProjectMetadataError};

View file

@ -7,6 +7,7 @@ use thiserror::Error;
use ty_python_semantic::ProgramSettings; use ty_python_semantic::ProgramSettings;
use crate::combine::Combine; use crate::combine::Combine;
use crate::metadata::options::ProjectOptionsOverrides;
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError}; use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
use crate::metadata::value::ValueSource; use crate::metadata::value::ValueSource;
pub use options::Options; pub use options::Options;
@ -276,6 +277,10 @@ impl ProjectMetadata {
.to_program_settings(self.root(), self.name(), system, vendored) .to_program_settings(self.root(), self.name(), system, vendored)
} }
pub fn apply_overrides(&mut self, overrides: &ProjectOptionsOverrides) {
self.options = overrides.apply_to(std::mem::take(&mut self.options));
}
/// Combine the project options with the CLI options where the CLI options take precedence. /// Combine the project options with the CLI options where the CLI options take precedence.
pub fn apply_options(&mut self, options: Options) { pub fn apply_options(&mut self, options: Options) {
self.options = options.combine(std::mem::take(&mut self.options)); self.options = options.combine(std::mem::take(&mut self.options));

View file

@ -121,6 +121,9 @@ impl Options {
ValueSource::File(path) => PythonVersionSource::ConfigFile( ValueSource::File(path) => PythonVersionSource::ConfigFile(
PythonVersionFileSource::new(path.clone(), ranged_version.range()), PythonVersionFileSource::new(path.clone(), ranged_version.range()),
), ),
ValueSource::PythonVSCodeExtension => {
PythonVersionSource::PythonVSCodeExtension
}
}, },
}); });
@ -140,6 +143,7 @@ impl Options {
ValueSource::File(path) => { ValueSource::File(path) => {
SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range()) SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range())
} }
ValueSource::PythonVSCodeExtension => SysPrefixPathOrigin::PythonVSCodeExtension,
}; };
Some(PythonEnvironment::new( Some(PythonEnvironment::new(
@ -702,6 +706,10 @@ impl Rules {
let lint_source = match source { let lint_source = match source {
ValueSource::File(_) => LintSource::File, ValueSource::File(_) => LintSource::File,
ValueSource::Cli => LintSource::Cli, ValueSource::Cli => LintSource::Cli,
ValueSource::PythonVSCodeExtension => {
unreachable!("Can't configure rules from the Python VSCode extension")
}
}; };
if let Ok(severity) = Severity::try_from(**level) { if let Ok(severity) = Severity::try_from(**level) {
selection.enable(lint, severity, lint_source); selection.enable(lint, severity, lint_source);
@ -854,6 +862,7 @@ fn build_include_filter(
Severity::Info, Severity::Info,
"The pattern was specified on the CLI", "The pattern was specified on the CLI",
)), )),
ValueSource::PythonVSCodeExtension => unreachable!("Can't configure includes from the Python VSCode extension"),
} }
})?; })?;
} }
@ -936,6 +945,9 @@ fn build_exclude_filter(
Severity::Info, Severity::Info,
"The pattern was specified on the CLI", "The pattern was specified on the CLI",
)), )),
ValueSource::PythonVSCodeExtension => unreachable!(
"Can't configure excludes from the Python VSCode extension"
)
} }
})?; })?;
} }
@ -1497,8 +1509,11 @@ impl OptionDiagnostic {
/// This is a wrapper for options that actually get loaded from configuration files /// This is a wrapper for options that actually get loaded from configuration files
/// and the CLI, which also includes a `config_file_override` option that overrides /// and the CLI, which also includes a `config_file_override` option that overrides
/// default configuration discovery with an explicitly-provided path to a configuration file /// default configuration discovery with an explicitly-provided path to a configuration file
#[derive(Debug, Default, PartialEq, Eq, Clone)]
pub struct ProjectOptionsOverrides { pub struct ProjectOptionsOverrides {
pub config_file_override: Option<SystemPathBuf>, pub config_file_override: Option<SystemPathBuf>,
pub fallback_python_version: Option<RangedValue<PythonVersion>>,
pub fallback_python: Option<RelativePathBuf>,
pub options: Options, pub options: Options,
} }
@ -1507,8 +1522,22 @@ impl ProjectOptionsOverrides {
Self { Self {
config_file_override, config_file_override,
options, options,
..Self::default()
} }
} }
pub fn apply_to(&self, options: Options) -> Options {
let mut combined = self.options.clone().combine(options);
// Set the fallback python version and path if set
combined.environment.combine_with(Some(EnvironmentOptions {
python_version: self.fallback_python_version.clone(),
python: self.fallback_python.clone(),
..EnvironmentOptions::default()
}));
combined
}
} }
trait OrDefault { trait OrDefault {

View file

@ -27,6 +27,9 @@ pub enum ValueSource {
/// The value comes from a CLI argument, while it's left open if specified using a short argument, /// The value comes from a CLI argument, while it's left open if specified using a short argument,
/// long argument (`--extra-paths`) or `--config key=value`. /// long argument (`--extra-paths`) or `--config key=value`.
Cli, Cli,
/// The value comes from an LSP client configuration.
PythonVSCodeExtension,
} }
impl ValueSource { impl ValueSource {
@ -34,6 +37,7 @@ impl ValueSource {
match self { match self {
ValueSource::File(path) => Some(&**path), ValueSource::File(path) => Some(&**path),
ValueSource::Cli => None, ValueSource::Cli => None,
ValueSource::PythonVSCodeExtension => None,
} }
} }
@ -105,6 +109,14 @@ impl<T> RangedValue<T> {
Self::with_range(value, ValueSource::Cli, TextRange::default()) Self::with_range(value, ValueSource::Cli, TextRange::default())
} }
pub fn python_extension(value: T) -> Self {
Self::with_range(
value,
ValueSource::PythonVSCodeExtension,
TextRange::default(),
)
}
pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self { pub fn with_range(value: T, source: ValueSource, range: TextRange) -> Self {
Self { Self {
value, value,
@ -327,6 +339,10 @@ impl RelativePathBuf {
Self::new(path, ValueSource::Cli) Self::new(path, ValueSource::Cli)
} }
pub fn python_extension(path: impl AsRef<SystemPath>) -> Self {
Self::new(path, ValueSource::PythonVSCodeExtension)
}
/// Returns the relative path as specified by the user. /// Returns the relative path as specified by the user.
pub fn path(&self) -> &SystemPath { pub fn path(&self) -> &SystemPath {
&self.0 &self.0
@ -354,7 +370,7 @@ impl RelativePathBuf {
pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf { pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf {
let relative_to = match &self.0.source { let relative_to = match &self.0.source {
ValueSource::File(_) => project_root, ValueSource::File(_) => project_root,
ValueSource::Cli => system.current_directory(), ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(),
}; };
SystemPath::absolute(&self.0, relative_to) SystemPath::absolute(&self.0, relative_to)
@ -409,7 +425,7 @@ impl RelativeGlobPattern {
) -> Result<AbsolutePortableGlobPattern, PortableGlobError> { ) -> Result<AbsolutePortableGlobPattern, PortableGlobError> {
let relative_to = match &self.0.source { let relative_to = match &self.0.source {
ValueSource::File(_) => project_root, ValueSource::File(_) => project_root,
ValueSource::Cli => system.current_directory(), ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(),
}; };
let pattern = PortableGlobPattern::parse(&self.0, kind)?; let pattern = PortableGlobPattern::parse(&self.0, kind)?;

View file

@ -113,6 +113,9 @@ pub enum PythonVersionSource {
/// long argument (`--extra-paths`) or `--config key=value`. /// long argument (`--extra-paths`) or `--config key=value`.
Cli, Cli,
/// The value comes from the Python VS Code extension (the selected interpreter).
PythonVSCodeExtension,
/// We fell back to a default value because the value was not specified via the CLI or a config file. /// We fell back to a default value because the value was not specified via the CLI or a config file.
#[default] #[default]
Default, Default,

View file

@ -1065,6 +1065,8 @@ pub enum SysPrefixPathOrigin {
ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>), ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>),
/// The `sys.prefix` path came from a `--python` CLI flag /// The `sys.prefix` path came from a `--python` CLI flag
PythonCliFlag, PythonCliFlag,
/// The selected interpreter in the VS Code's Python extension.
PythonVSCodeExtension,
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable /// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
VirtualEnvVar, VirtualEnvVar,
/// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable /// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable
@ -1086,6 +1088,7 @@ impl SysPrefixPathOrigin {
Self::LocalVenv | Self::VirtualEnvVar => true, Self::LocalVenv | Self::VirtualEnvVar => true,
Self::ConfigFileSetting(..) Self::ConfigFileSetting(..)
| Self::PythonCliFlag | Self::PythonCliFlag
| Self::PythonVSCodeExtension
| Self::DerivedFromPyvenvCfg | Self::DerivedFromPyvenvCfg
| Self::CondaPrefixVar => false, | Self::CondaPrefixVar => false,
} }
@ -1097,7 +1100,9 @@ impl SysPrefixPathOrigin {
/// the `sys.prefix` directory, e.g. the `--python` CLI flag. /// the `sys.prefix` directory, e.g. the `--python` CLI flag.
pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool { pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool {
match self { match self {
Self::PythonCliFlag | Self::ConfigFileSetting(..) => false, Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::PythonVSCodeExtension => {
false
}
Self::VirtualEnvVar Self::VirtualEnvVar
| Self::CondaPrefixVar | Self::CondaPrefixVar
| Self::DerivedFromPyvenvCfg | Self::DerivedFromPyvenvCfg
@ -1115,6 +1120,9 @@ impl std::fmt::Display for SysPrefixPathOrigin {
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"), Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"), Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
Self::LocalVenv => f.write_str("local virtual environment"), Self::LocalVenv => f.write_str("local virtual environment"),
Self::PythonVSCodeExtension => {
f.write_str("selected interpreter in the VS Code Python extension")
}
} }
} }
} }

View file

@ -62,6 +62,12 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
or in a configuration file", or in a configuration file",
); );
} }
crate::PythonVersionSource::PythonVSCodeExtension => {
diagnostic.info(format_args!(
"Python {version} was assumed when {action} \
because it's the version of the selected Python interpreter in the VS Code Python extension",
));
}
crate::PythonVersionSource::InstallationDirectoryLayout { crate::PythonVersionSource::InstallationDirectoryLayout {
site_packages_parent_dir, site_packages_parent_dir,
} => { } => {

View file

@ -13,6 +13,7 @@ license = { workspace = true }
[dependencies] [dependencies]
ruff_db = { workspace = true, features = ["os"] } ruff_db = { workspace = true, features = ["os"] }
ruff_notebook = { workspace = true } ruff_notebook = { workspace = true }
ruff_python_ast = { workspace = true }
ruff_source_file = { workspace = true } ruff_source_file = { workspace = true }
ruff_text_size = { workspace = true } ruff_text_size = { workspace = true }

View file

@ -40,22 +40,16 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
.update_text_document(&key, content_changes, version) .update_text_document(&key, content_changes, version)
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
match key.path() { let changes = match key.path() {
AnySystemPath::System(path) => { AnySystemPath::System(path) => {
let db = match session.project_db_for_path_mut(path) { vec![ChangeEvent::file_content_changed(path.clone())]
Some(db) => db,
None => session.default_project_db_mut(),
};
db.apply_changes(vec![ChangeEvent::file_content_changed(path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); vec![ChangeEvent::ChangedVirtual(virtual_path.clone())]
db.apply_changes(
vec![ChangeEvent::ChangedVirtual(virtual_path.clone())],
None,
);
}
} }
};
session.apply_changes(key.path(), changes);
publish_diagnostics(session, &key, client); publish_diagnostics(session, &key, client);

View file

@ -88,12 +88,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
for (root, changes) in events_by_db { for (root, changes) in events_by_db {
tracing::debug!("Applying changes to `{root}`"); tracing::debug!("Applying changes to `{root}`");
// SAFETY: Only paths that are part of the workspace are registered for file watching. let result = session.apply_changes(&AnySystemPath::System(root), changes);
// So, virtual paths and paths that are outside of a workspace does not trigger this
// notification.
let db = session.project_db_for_path_mut(&*root).unwrap();
let result = db.apply_changes(changes, None);
project_changed |= result.project_changed(); project_changed |= result.project_changed();
} }

View file

@ -39,10 +39,9 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
.with_failure_code(ErrorCode::InternalError)?; .with_failure_code(ErrorCode::InternalError)?;
if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
let db = session.default_project_db_mut(); session.apply_changes(
db.apply_changes( key.path(),
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
None,
); );
} }

View file

@ -39,10 +39,9 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
.with_failure_code(lsp_server::ErrorCode::InternalError)?; .with_failure_code(lsp_server::ErrorCode::InternalError)?;
if let AnySystemPath::SystemVirtual(virtual_path) = key.path() { if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
let db = session.default_project_db_mut(); session.apply_changes(
db.apply_changes( key.path(),
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())], vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
None,
); );
} }

View file

@ -46,11 +46,7 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
match key.path() { match key.path() {
AnySystemPath::System(system_path) => { AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(system_path) { session.apply_changes(key.path(), vec![ChangeEvent::Opened(system_path.clone())]);
Some(db) => db,
None => session.default_project_db_mut(),
};
db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();

View file

@ -40,11 +40,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
match &path { match &path {
AnySystemPath::System(system_path) => { AnySystemPath::System(system_path) => {
let db = match session.project_db_for_path_mut(system_path) { session.apply_changes(&path, vec![ChangeEvent::Opened(system_path.clone())]);
Some(db) => db,
None => session.default_project_db_mut(),
};
db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
} }
AnySystemPath::SystemVirtual(virtual_path) => { AnySystemPath::SystemVirtual(virtual_path) => {
let db = session.default_project_db_mut(); let db = session.default_project_db_mut();

View file

@ -33,7 +33,7 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
let index = snapshot.index(); let index = snapshot.index();
if !index.global_settings().diagnostic_mode().is_workspace() { if !index.global_settings().diagnostic_mode().is_workspace() {
tracing::debug!("Workspace diagnostics is disabled; returning empty report"); tracing::trace!("Workspace diagnostics is disabled; returning empty report");
return Ok(WorkspaceDiagnosticReportResult::Report( return Ok(WorkspaceDiagnosticReportResult::Report(
WorkspaceDiagnosticReport { items: vec![] }, WorkspaceDiagnosticReport { items: vec![] },
)); ));

View file

@ -13,7 +13,8 @@ use ruff_db::Db;
use ruff_db::files::File; use ruff_db::files::File;
use ruff_db::system::{System, SystemPath, SystemPathBuf}; use ruff_db::system::{System, SystemPath, SystemPathBuf};
use ty_project::metadata::Options; use ty_project::metadata::Options;
use ty_project::{ProjectDatabase, ProjectMetadata}; use ty_project::watch::ChangeEvent;
use ty_project::{ChangeResult, ProjectDatabase, ProjectMetadata};
pub(crate) use self::capabilities::ResolvedClientCapabilities; pub(crate) use self::capabilities::ResolvedClientCapabilities;
pub(crate) use self::index::DocumentQuery; pub(crate) use self::index::DocumentQuery;
@ -49,7 +50,12 @@ pub(crate) struct Session {
/// The projects across all workspaces. /// The projects across all workspaces.
projects: BTreeMap<SystemPathBuf, ProjectDatabase>, projects: BTreeMap<SystemPathBuf, ProjectDatabase>,
default_project: ProjectDatabase, /// The project to use for files outside any workspace. For example, if the user
/// opens the project `<home>/my_project` in VS code but they then opens a Python file from their Desktop.
/// This file isn't part of the active workspace, nor is it part of any project. But we still want
/// to provide some basic functionality like navigation, completions, syntax highlighting, etc.
/// That's what we use the default project for.
default_project: DefaultProject,
/// The global position encoding, negotiated during LSP initialization. /// The global position encoding, negotiated during LSP initialization.
position_encoding: PositionEncoding, position_encoding: PositionEncoding,
@ -77,26 +83,15 @@ impl Session {
let mut workspaces = Workspaces::default(); let mut workspaces = Workspaces::default();
for (url, options) in workspace_folders { for (url, options) in workspace_folders {
workspaces.register(url, options)?; workspaces.register(url, options.into_settings())?;
} }
let default_project = {
let system = LSPSystem::new(index.clone());
let metadata = ProjectMetadata::from_options(
Options::default(),
system.current_directory().to_path_buf(),
None,
)
.unwrap();
ProjectDatabase::new(metadata, system).unwrap()
};
Ok(Self { Ok(Self {
position_encoding, position_encoding,
workspaces, workspaces,
deferred_messages: VecDeque::new(), deferred_messages: VecDeque::new(),
index: Some(index), index: Some(index),
default_project, default_project: DefaultProject::new(),
projects: BTreeMap::new(), projects: BTreeMap::new(),
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new( resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
client_capabilities, client_capabilities,
@ -109,7 +104,6 @@ impl Session {
pub(crate) fn request_queue(&self) -> &RequestQueue { pub(crate) fn request_queue(&self) -> &RequestQueue {
&self.request_queue &self.request_queue
} }
pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue { pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue {
&mut self.request_queue &mut self.request_queue
} }
@ -204,21 +198,46 @@ impl Session {
.map(|(_, db)| db) .map(|(_, db)| db)
} }
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the pub(crate) fn apply_changes(
/// minimum root path in the project map. &mut self,
path: &AnySystemPath,
changes: Vec<ChangeEvent>,
) -> ChangeResult {
let overrides = path.as_system().and_then(|root| {
self.workspaces()
.for_path(root)?
.settings()
.project_options_overrides()
.cloned()
});
let db = match path {
AnySystemPath::System(path) => match self.project_db_for_path_mut(path) {
Some(db) => db,
None => self.default_project_db_mut(),
},
AnySystemPath::SystemVirtual(_) => self.default_project_db_mut(),
};
db.apply_changes(changes, overrides.as_ref())
}
/// Returns a reference to the default project [`ProjectDatabase`].
pub(crate) fn default_project_db(&self) -> &ProjectDatabase { pub(crate) fn default_project_db(&self) -> &ProjectDatabase {
&self.default_project self.default_project.get(self.index.as_ref())
} }
/// Returns a mutable reference to the default project [`ProjectDatabase`]. /// Returns a mutable reference to the default project [`ProjectDatabase`].
pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase { pub(crate) fn default_project_db_mut(&mut self) -> &mut ProjectDatabase {
&mut self.default_project self.default_project.get_mut(self.index.as_ref())
} }
/// Returns a mutable iterator over all project databases that have been initialized to this point.
///
/// This iterator will only yield the default project database if it has been used.
fn projects_mut(&mut self) -> impl Iterator<Item = &'_ mut ProjectDatabase> + '_ { fn projects_mut(&mut self) -> impl Iterator<Item = &'_ mut ProjectDatabase> + '_ {
self.projects let default_project = self.default_project.try_get_mut();
.values_mut() self.projects.values_mut().chain(default_project)
.chain(std::iter::once(&mut self.default_project))
} }
/// Returns the [`DocumentKey`] for the given URL. /// Returns the [`DocumentKey`] for the given URL.
@ -232,16 +251,18 @@ impl Session {
assert!(!self.workspaces.all_initialized()); assert!(!self.workspaces.all_initialized());
for (url, options) in workspace_settings { for (url, options) in workspace_settings {
let Some(workspace) = self.workspaces.initialize(&url, options) else { tracing::debug!("Initializing workspace `{url}`");
let settings = options.into_settings();
let Some((root, workspace)) = self.workspaces.initialize(&url, settings) else {
continue; continue;
}; };
// For now, create one project database per workspace. // For now, create one project database per workspace.
// In the future, index the workspace directories to find all projects // In the future, index the workspace directories to find all projects
// and create a project database for each. // and create a project database for each.
let system = LSPSystem::new(self.index.as_ref().unwrap().clone()); let system = LSPSystem::new(self.index.as_ref().unwrap().clone());
let system_path = workspace.root();
let root = system_path.to_path_buf();
let project = ProjectMetadata::discover(&root, &system) let project = ProjectMetadata::discover(&root, &system)
.context("Failed to find project configuration") .context("Failed to find project configuration")
.and_then(|mut metadata| { .and_then(|mut metadata| {
@ -249,6 +270,11 @@ impl Session {
metadata metadata
.apply_configuration_files(&system) .apply_configuration_files(&system)
.context("Failed to apply configuration files")?; .context("Failed to apply configuration files")?;
if let Some(overrides) = workspace.settings.project_options_overrides() {
metadata.apply_overrides(overrides);
}
ProjectDatabase::new(metadata, system) ProjectDatabase::new(metadata, system)
.context("Failed to create project database") .context("Failed to create project database")
}); });
@ -516,12 +542,19 @@ impl SessionSnapshot {
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct Workspaces { pub(crate) struct Workspaces {
workspaces: BTreeMap<Url, Workspace>, workspaces: BTreeMap<SystemPathBuf, Workspace>,
uninitialized: usize, uninitialized: usize,
} }
impl Workspaces { impl Workspaces {
pub(crate) fn register(&mut self, url: Url, options: ClientOptions) -> anyhow::Result<()> { /// Registers a new workspace with the given URL and default settings for the workspace.
///
/// It's the caller's responsibility to later call [`initialize`] with the resolved settings
/// for this workspace. Registering and initializing a workspace is a two-step process because
/// the workspace are announced to the server during the `initialize` request, but the
/// resolved settings are only available after the client has responded to the `workspace/configuration`
/// request.
pub(crate) fn register(&mut self, url: Url, settings: ClientSettings) -> anyhow::Result<()> {
let path = url let path = url
.to_file_path() .to_file_path()
.map_err(|()| anyhow!("Workspace URL is not a file or directory: {url:?}"))?; .map_err(|()| anyhow!("Workspace URL is not a file or directory: {url:?}"))?;
@ -530,35 +563,47 @@ impl Workspaces {
let system_path = SystemPathBuf::from_path_buf(path) let system_path = SystemPathBuf::from_path_buf(path)
.map_err(|_| anyhow!("Workspace URL is not valid UTF8"))?; .map_err(|_| anyhow!("Workspace URL is not valid UTF8"))?;
self.workspaces.insert( self.workspaces
url, .insert(system_path, Workspace { url, settings });
Workspace {
options,
root: system_path,
},
);
self.uninitialized += 1; self.uninitialized += 1;
Ok(()) Ok(())
} }
/// Initializes the workspace with the resolved client settings for the workspace.
///
/// ## Returns
///
/// `None` if URL doesn't map to a valid path or if the workspace is not registered.
pub(crate) fn initialize( pub(crate) fn initialize(
&mut self, &mut self,
url: &Url, url: &Url,
options: ClientOptions, settings: ClientSettings,
) -> Option<&mut Workspace> { ) -> Option<(SystemPathBuf, &mut Workspace)> {
if let Some(workspace) = self.workspaces.get_mut(url) { let path = url.to_file_path().ok()?;
workspace.options = options;
// Realistically I don't think this can fail because we got the path from a Url
let system_path = SystemPathBuf::from_path_buf(path).ok()?;
if let Some(workspace) = self.workspaces.get_mut(&system_path) {
workspace.settings = settings;
self.uninitialized -= 1; self.uninitialized -= 1;
Some(workspace) Some((system_path, workspace))
} else { } else {
None None
} }
} }
pub(crate) fn for_path(&self, path: impl AsRef<SystemPath>) -> Option<&Workspace> {
self.workspaces
.range(..=path.as_ref().to_path_buf())
.next_back()
.map(|(_, db)| db)
}
pub(crate) fn urls(&self) -> impl Iterator<Item = &Url> + '_ { pub(crate) fn urls(&self) -> impl Iterator<Item = &Url> + '_ {
self.workspaces.keys() self.workspaces.values().map(Workspace::url)
} }
pub(crate) fn all_initialized(&self) -> bool { pub(crate) fn all_initialized(&self) -> bool {
@ -567,8 +612,8 @@ impl Workspaces {
} }
impl<'a> IntoIterator for &'a Workspaces { impl<'a> IntoIterator for &'a Workspaces {
type Item = (&'a Url, &'a Workspace); type Item = (&'a SystemPathBuf, &'a Workspace);
type IntoIter = std::collections::btree_map::Iter<'a, Url, Workspace>; type IntoIter = std::collections::btree_map::Iter<'a, SystemPathBuf, Workspace>;
fn into_iter(self) -> Self::IntoIter { fn into_iter(self) -> Self::IntoIter {
self.workspaces.iter() self.workspaces.iter()
@ -577,12 +622,61 @@ impl<'a> IntoIterator for &'a Workspaces {
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct Workspace { pub(crate) struct Workspace {
root: SystemPathBuf, /// The workspace root URL as sent by the client during initialization.
options: ClientOptions, url: Url,
settings: ClientSettings,
} }
impl Workspace { impl Workspace {
pub(crate) fn root(&self) -> &SystemPath { pub(crate) fn url(&self) -> &Url {
&self.root &self.url
}
pub(crate) fn settings(&self) -> &ClientSettings {
&self.settings
}
}
/// Thin wrapper around the default project database that ensures it only gets initialized
/// when it's first accessed.
///
/// There are a few advantages to this:
///
/// 1. Salsa has a fast-path for query lookups for the first created database.
/// We really want that to be the actual project database and not our fallback database.
/// 2. The logs when the server starts can be confusing if it once shows it uses Python X (for the default db)
/// but then has another log that it uses Python Y (for the actual project db).
struct DefaultProject(std::sync::OnceLock<ProjectDatabase>);
impl DefaultProject {
pub(crate) fn new() -> Self {
DefaultProject(std::sync::OnceLock::new())
}
pub(crate) fn get(&self, index: Option<&Arc<Index>>) -> &ProjectDatabase {
self.0.get_or_init(|| {
tracing::info!("Initialize default project");
let system = LSPSystem::new(index.unwrap().clone());
let metadata = ProjectMetadata::from_options(
Options::default(),
system.current_directory().to_path_buf(),
None,
)
.unwrap();
ProjectDatabase::new(metadata, system).unwrap()
})
}
pub(crate) fn get_mut(&mut self, index: Option<&Arc<Index>>) -> &mut ProjectDatabase {
let _ = self.get(index);
// SAFETY: The `OnceLock` is guaranteed to be initialized at this point because
// we called `get` above, which initializes it if it wasn't already.
self.0.get_mut().unwrap()
}
pub(crate) fn try_get_mut(&mut self) -> Option<&mut ProjectDatabase> {
self.0.get_mut()
} }
} }

View file

@ -1,10 +1,14 @@
use lsp_types::Url; use lsp_types::Url;
use ruff_db::system::SystemPathBuf; use ruff_db::system::SystemPathBuf;
use ruff_python_ast::PythonVersion;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use serde::Deserialize; use serde::Deserialize;
use ty_project::metadata::Options;
use ty_project::metadata::options::ProjectOptionsOverrides;
use ty_project::metadata::value::{RangedValue, RelativePathBuf};
use crate::logging::LogLevel; use crate::logging::LogLevel;
use crate::session::settings::ClientSettings; use crate::session::ClientSettings;
pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>; pub(crate) type WorkspaceOptionsMap = FxHashMap<Url, ClientOptions>;
@ -43,7 +47,7 @@ struct WorkspaceOptions {
} }
/// This is a direct representation of the settings schema sent by the client. /// This is a direct representation of the settings schema sent by the client.
#[derive(Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub(crate) struct ClientOptions { pub(crate) struct ClientOptions {
@ -52,6 +56,8 @@ pub(crate) struct ClientOptions {
python: Option<Python>, python: Option<Python>,
/// Diagnostic mode for the language server. /// Diagnostic mode for the language server.
diagnostic_mode: Option<DiagnosticMode>, diagnostic_mode: Option<DiagnosticMode>,
python_extension: Option<PythonExtension>,
} }
/// Diagnostic mode for the language server. /// Diagnostic mode for the language server.
@ -75,6 +81,47 @@ impl DiagnosticMode {
impl ClientOptions { impl ClientOptions {
/// Returns the client settings that are relevant to the language server. /// Returns the client settings that are relevant to the language server.
pub(crate) fn into_settings(self) -> ClientSettings { pub(crate) fn into_settings(self) -> ClientSettings {
let overrides = self.python_extension.and_then(|extension| {
let active_environment = extension.active_environment?;
let mut overrides = ProjectOptionsOverrides::new(None, Options::default());
overrides.fallback_python = if let Some(environment) = &active_environment.environment {
environment.folder_uri.to_file_path().ok().and_then(|path| {
Some(RelativePathBuf::python_extension(
SystemPathBuf::from_path_buf(path).ok()?,
))
})
} else {
Some(RelativePathBuf::python_extension(
active_environment.executable.sys_prefix.clone(),
))
};
overrides.fallback_python_version =
active_environment.version.as_ref().and_then(|version| {
Some(RangedValue::python_extension(PythonVersion::from((
u8::try_from(version.major).ok()?,
u8::try_from(version.minor).ok()?,
))))
});
if let Some(python) = &overrides.fallback_python {
tracing::debug!(
"Using the Python environment selected in the VS Code Python extension in case the configuration doesn't specify a Python environment: {python}",
python = python.path()
);
}
if let Some(version) = &overrides.fallback_python_version {
tracing::debug!(
"Using the Python version selected in the VS Code Python extension: {version} in case the configuration doesn't specify a Python version",
);
}
Some(overrides)
});
ClientSettings { ClientSettings {
disable_language_services: self disable_language_services: self
.python .python
@ -82,6 +129,7 @@ impl ClientOptions {
.and_then(|ty| ty.disable_language_services) .and_then(|ty| ty.disable_language_services)
.unwrap_or_default(), .unwrap_or_default(),
diagnostic_mode: self.diagnostic_mode.unwrap_or_default(), diagnostic_mode: self.diagnostic_mode.unwrap_or_default(),
overrides,
} }
} }
} }
@ -90,14 +138,63 @@ impl ClientOptions {
// would be useful to instead use `workspace/configuration` instead. This would be then used to get // 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.*". // all settings and not just the ones in "python.*".
#[derive(Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Python { struct Python {
ty: Option<Ty>, ty: Option<Ty>,
} }
#[derive(Debug, Deserialize, Default)] #[derive(Clone, Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
struct PythonExtension {
active_environment: Option<ActiveEnvironment>,
}
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct ActiveEnvironment {
pub(crate) executable: PythonExecutable,
pub(crate) environment: Option<PythonEnvironment>,
pub(crate) version: Option<EnvironmentVersion>,
}
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct EnvironmentVersion {
pub(crate) major: i64,
pub(crate) minor: i64,
#[allow(dead_code)]
pub(crate) patch: i64,
#[allow(dead_code)]
pub(crate) sys_version: String,
}
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct PythonEnvironment {
pub(crate) folder_uri: Url,
#[allow(dead_code)]
#[serde(rename = "type")]
pub(crate) kind: String,
#[allow(dead_code)]
pub(crate) name: Option<String>,
}
#[derive(Clone, Debug, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")]
pub(crate) struct PythonExecutable {
#[allow(dead_code)]
pub(crate) uri: Url,
pub(crate) sys_prefix: SystemPathBuf,
}
#[derive(Clone, Debug, Deserialize, Default)]
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
struct Ty { struct Ty {

View file

@ -1,5 +1,7 @@
use super::options::DiagnosticMode; use super::options::DiagnosticMode;
use ty_project::metadata::options::ProjectOptionsOverrides;
/// Resolved client settings for a specific document. These settings are meant to be /// 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 /// used directly by the server, and are *not* a 1:1 representation with how the client
/// sends them. /// sends them.
@ -8,6 +10,7 @@ use super::options::DiagnosticMode;
pub(crate) struct ClientSettings { pub(crate) struct ClientSettings {
pub(super) disable_language_services: bool, pub(super) disable_language_services: bool,
pub(super) diagnostic_mode: DiagnosticMode, pub(super) diagnostic_mode: DiagnosticMode,
pub(super) overrides: Option<ProjectOptionsOverrides>,
} }
impl ClientSettings { impl ClientSettings {
@ -18,4 +21,8 @@ impl ClientSettings {
pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode { pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode {
self.diagnostic_mode self.diagnostic_mode
} }
pub(crate) fn project_options_overrides(&self) -> Option<&ProjectOptionsOverrides> {
self.overrides.as_ref()
}
} }