mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 05:44:56 +00:00
[ty] Use python version and path from Python extension (#19012)
This commit is contained in:
parent
26f736bc46
commit
90026047f9
22 changed files with 344 additions and 99 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -4307,6 +4307,7 @@ dependencies = [
|
|||
"lsp-types",
|
||||
"ruff_db",
|
||||
"ruff_notebook",
|
||||
"ruff_python_ast",
|
||||
"ruff_source_file",
|
||||
"ruff_text_size",
|
||||
"rustc-hash",
|
||||
|
|
|
@ -121,17 +121,17 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
|||
None => ProjectMetadata::discover(&project_path, &system)?,
|
||||
};
|
||||
|
||||
let options = args.into_options();
|
||||
project_metadata.apply_options(options.clone());
|
||||
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)?;
|
||||
|
||||
if !check_paths.is_empty() {
|
||||
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) =
|
||||
MainLoop::new(project_options_overrides, printer);
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ use std::panic::{AssertUnwindSafe, RefUnwindSafe};
|
|||
use std::sync::Arc;
|
||||
use std::{cmp, fmt};
|
||||
|
||||
pub use self::changes::ChangeResult;
|
||||
use crate::metadata::settings::file_settings;
|
||||
use crate::{DEFAULT_LINT_REGISTRY, DummyReporter};
|
||||
use crate::{ProgressReporter, Project, ProjectMetadata};
|
||||
|
|
|
@ -41,8 +41,6 @@ impl ProjectDatabase {
|
|||
let project_root = project.root(self).to_path_buf();
|
||||
let config_file_override =
|
||||
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 custom_stdlib_versions_path = program
|
||||
.custom_stdlib_search_path(self)
|
||||
|
@ -218,16 +216,16 @@ impl ProjectDatabase {
|
|||
};
|
||||
match new_project_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()) {
|
||||
tracing::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()) {
|
||||
Ok(program_settings) => {
|
||||
let program = Program::get(self);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::glob::{GlobFilterCheckMode, IncludeResult};
|
||||
use crate::metadata::options::{OptionDiagnostic, ToSettingsError};
|
||||
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 metadata::settings::Settings;
|
||||
pub use metadata::{ProjectMetadata, ProjectMetadataError};
|
||||
|
|
|
@ -7,6 +7,7 @@ use thiserror::Error;
|
|||
use ty_python_semantic::ProgramSettings;
|
||||
|
||||
use crate::combine::Combine;
|
||||
use crate::metadata::options::ProjectOptionsOverrides;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
use crate::metadata::value::ValueSource;
|
||||
pub use options::Options;
|
||||
|
@ -276,6 +277,10 @@ impl ProjectMetadata {
|
|||
.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.
|
||||
pub fn apply_options(&mut self, options: Options) {
|
||||
self.options = options.combine(std::mem::take(&mut self.options));
|
||||
|
|
|
@ -121,6 +121,9 @@ impl Options {
|
|||
ValueSource::File(path) => PythonVersionSource::ConfigFile(
|
||||
PythonVersionFileSource::new(path.clone(), ranged_version.range()),
|
||||
),
|
||||
ValueSource::PythonVSCodeExtension => {
|
||||
PythonVersionSource::PythonVSCodeExtension
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -140,6 +143,7 @@ impl Options {
|
|||
ValueSource::File(path) => {
|
||||
SysPrefixPathOrigin::ConfigFileSetting(path.clone(), python_path.range())
|
||||
}
|
||||
ValueSource::PythonVSCodeExtension => SysPrefixPathOrigin::PythonVSCodeExtension,
|
||||
};
|
||||
|
||||
Some(PythonEnvironment::new(
|
||||
|
@ -702,6 +706,10 @@ impl Rules {
|
|||
let lint_source = match source {
|
||||
ValueSource::File(_) => LintSource::File,
|
||||
ValueSource::Cli => LintSource::Cli,
|
||||
|
||||
ValueSource::PythonVSCodeExtension => {
|
||||
unreachable!("Can't configure rules from the Python VSCode extension")
|
||||
}
|
||||
};
|
||||
if let Ok(severity) = Severity::try_from(**level) {
|
||||
selection.enable(lint, severity, lint_source);
|
||||
|
@ -854,6 +862,7 @@ fn build_include_filter(
|
|||
Severity::Info,
|
||||
"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,
|
||||
"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
|
||||
/// 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
|
||||
#[derive(Debug, Default, PartialEq, Eq, Clone)]
|
||||
pub struct ProjectOptionsOverrides {
|
||||
pub config_file_override: Option<SystemPathBuf>,
|
||||
pub fallback_python_version: Option<RangedValue<PythonVersion>>,
|
||||
pub fallback_python: Option<RelativePathBuf>,
|
||||
pub options: Options,
|
||||
}
|
||||
|
||||
|
@ -1507,8 +1522,22 @@ impl ProjectOptionsOverrides {
|
|||
Self {
|
||||
config_file_override,
|
||||
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 {
|
||||
|
|
|
@ -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,
|
||||
/// long argument (`--extra-paths`) or `--config key=value`.
|
||||
Cli,
|
||||
|
||||
/// The value comes from an LSP client configuration.
|
||||
PythonVSCodeExtension,
|
||||
}
|
||||
|
||||
impl ValueSource {
|
||||
|
@ -34,6 +37,7 @@ impl ValueSource {
|
|||
match self {
|
||||
ValueSource::File(path) => Some(&**path),
|
||||
ValueSource::Cli => None,
|
||||
ValueSource::PythonVSCodeExtension => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +109,14 @@ impl<T> RangedValue<T> {
|
|||
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 {
|
||||
Self {
|
||||
value,
|
||||
|
@ -327,6 +339,10 @@ impl RelativePathBuf {
|
|||
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.
|
||||
pub fn path(&self) -> &SystemPath {
|
||||
&self.0
|
||||
|
@ -354,7 +370,7 @@ impl RelativePathBuf {
|
|||
pub fn absolute(&self, project_root: &SystemPath, system: &dyn System) -> SystemPathBuf {
|
||||
let relative_to = match &self.0.source {
|
||||
ValueSource::File(_) => project_root,
|
||||
ValueSource::Cli => system.current_directory(),
|
||||
ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(),
|
||||
};
|
||||
|
||||
SystemPath::absolute(&self.0, relative_to)
|
||||
|
@ -409,7 +425,7 @@ impl RelativeGlobPattern {
|
|||
) -> Result<AbsolutePortableGlobPattern, PortableGlobError> {
|
||||
let relative_to = match &self.0.source {
|
||||
ValueSource::File(_) => project_root,
|
||||
ValueSource::Cli => system.current_directory(),
|
||||
ValueSource::Cli | ValueSource::PythonVSCodeExtension => system.current_directory(),
|
||||
};
|
||||
|
||||
let pattern = PortableGlobPattern::parse(&self.0, kind)?;
|
||||
|
|
|
@ -113,6 +113,9 @@ pub enum PythonVersionSource {
|
|||
/// long argument (`--extra-paths`) or `--config key=value`.
|
||||
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.
|
||||
#[default]
|
||||
Default,
|
||||
|
|
|
@ -1065,6 +1065,8 @@ pub enum SysPrefixPathOrigin {
|
|||
ConfigFileSetting(Arc<SystemPathBuf>, Option<TextRange>),
|
||||
/// The `sys.prefix` path came from a `--python` CLI flag
|
||||
PythonCliFlag,
|
||||
/// The selected interpreter in the VS Code's Python extension.
|
||||
PythonVSCodeExtension,
|
||||
/// The `sys.prefix` path came from the `VIRTUAL_ENV` environment variable
|
||||
VirtualEnvVar,
|
||||
/// The `sys.prefix` path came from the `CONDA_PREFIX` environment variable
|
||||
|
@ -1086,6 +1088,7 @@ impl SysPrefixPathOrigin {
|
|||
Self::LocalVenv | Self::VirtualEnvVar => true,
|
||||
Self::ConfigFileSetting(..)
|
||||
| Self::PythonCliFlag
|
||||
| Self::PythonVSCodeExtension
|
||||
| Self::DerivedFromPyvenvCfg
|
||||
| Self::CondaPrefixVar => false,
|
||||
}
|
||||
|
@ -1097,7 +1100,9 @@ impl SysPrefixPathOrigin {
|
|||
/// the `sys.prefix` directory, e.g. the `--python` CLI flag.
|
||||
pub(crate) const fn must_point_directly_to_sys_prefix(&self) -> bool {
|
||||
match self {
|
||||
Self::PythonCliFlag | Self::ConfigFileSetting(..) => false,
|
||||
Self::PythonCliFlag | Self::ConfigFileSetting(..) | Self::PythonVSCodeExtension => {
|
||||
false
|
||||
}
|
||||
Self::VirtualEnvVar
|
||||
| Self::CondaPrefixVar
|
||||
| Self::DerivedFromPyvenvCfg
|
||||
|
@ -1115,6 +1120,9 @@ impl std::fmt::Display for SysPrefixPathOrigin {
|
|||
Self::CondaPrefixVar => f.write_str("`CONDA_PREFIX` environment variable"),
|
||||
Self::DerivedFromPyvenvCfg => f.write_str("derived `sys.prefix` path"),
|
||||
Self::LocalVenv => f.write_str("local virtual environment"),
|
||||
Self::PythonVSCodeExtension => {
|
||||
f.write_str("selected interpreter in the VS Code Python extension")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -62,6 +62,12 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
|
|||
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 {
|
||||
site_packages_parent_dir,
|
||||
} => {
|
||||
|
|
|
@ -13,6 +13,7 @@ license = { workspace = true }
|
|||
[dependencies]
|
||||
ruff_db = { workspace = true, features = ["os"] }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
|
|
|
@ -40,22 +40,16 @@ impl SyncNotificationHandler for DidChangeTextDocumentHandler {
|
|||
.update_text_document(&key, content_changes, version)
|
||||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
match key.path() {
|
||||
let changes = match key.path() {
|
||||
AnySystemPath::System(path) => {
|
||||
let db = match session.project_db_for_path_mut(path) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::file_content_changed(path.clone())], None);
|
||||
vec![ChangeEvent::file_content_changed(path.clone())]
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
db.apply_changes(
|
||||
vec![ChangeEvent::ChangedVirtual(virtual_path.clone())],
|
||||
None,
|
||||
);
|
||||
}
|
||||
vec![ChangeEvent::ChangedVirtual(virtual_path.clone())]
|
||||
}
|
||||
};
|
||||
|
||||
session.apply_changes(key.path(), changes);
|
||||
|
||||
publish_diagnostics(session, &key, client);
|
||||
|
||||
|
|
|
@ -88,12 +88,7 @@ impl SyncNotificationHandler for DidChangeWatchedFiles {
|
|||
for (root, changes) in events_by_db {
|
||||
tracing::debug!("Applying changes to `{root}`");
|
||||
|
||||
// SAFETY: Only paths that are part of the workspace are registered for file watching.
|
||||
// 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);
|
||||
let result = session.apply_changes(&AnySystemPath::System(root), changes);
|
||||
|
||||
project_changed |= result.project_changed();
|
||||
}
|
||||
|
|
|
@ -39,10 +39,9 @@ impl SyncNotificationHandler for DidCloseTextDocumentHandler {
|
|||
.with_failure_code(ErrorCode::InternalError)?;
|
||||
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
|
||||
let db = session.default_project_db_mut();
|
||||
db.apply_changes(
|
||||
session.apply_changes(
|
||||
key.path(),
|
||||
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -39,10 +39,9 @@ impl SyncNotificationHandler for DidCloseNotebookHandler {
|
|||
.with_failure_code(lsp_server::ErrorCode::InternalError)?;
|
||||
|
||||
if let AnySystemPath::SystemVirtual(virtual_path) = key.path() {
|
||||
let db = session.default_project_db_mut();
|
||||
db.apply_changes(
|
||||
session.apply_changes(
|
||||
key.path(),
|
||||
vec![ChangeEvent::DeletedVirtual(virtual_path.clone())],
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -46,11 +46,7 @@ impl SyncNotificationHandler for DidOpenTextDocumentHandler {
|
|||
|
||||
match key.path() {
|
||||
AnySystemPath::System(system_path) => {
|
||||
let db = match session.project_db_for_path_mut(system_path) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
|
||||
session.apply_changes(key.path(), vec![ChangeEvent::Opened(system_path.clone())]);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
|
|
|
@ -40,11 +40,7 @@ impl SyncNotificationHandler for DidOpenNotebookHandler {
|
|||
|
||||
match &path {
|
||||
AnySystemPath::System(system_path) => {
|
||||
let db = match session.project_db_for_path_mut(system_path) {
|
||||
Some(db) => db,
|
||||
None => session.default_project_db_mut(),
|
||||
};
|
||||
db.apply_changes(vec![ChangeEvent::Opened(system_path.clone())], None);
|
||||
session.apply_changes(&path, vec![ChangeEvent::Opened(system_path.clone())]);
|
||||
}
|
||||
AnySystemPath::SystemVirtual(virtual_path) => {
|
||||
let db = session.default_project_db_mut();
|
||||
|
|
|
@ -33,7 +33,7 @@ impl BackgroundRequestHandler for WorkspaceDiagnosticRequestHandler {
|
|||
let index = snapshot.index();
|
||||
|
||||
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(
|
||||
WorkspaceDiagnosticReport { items: vec![] },
|
||||
));
|
||||
|
|
|
@ -13,7 +13,8 @@ use ruff_db::Db;
|
|||
use ruff_db::files::File;
|
||||
use ruff_db::system::{System, SystemPath, SystemPathBuf};
|
||||
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::index::DocumentQuery;
|
||||
|
@ -49,7 +50,12 @@ pub(crate) struct Session {
|
|||
/// The projects across all workspaces.
|
||||
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.
|
||||
position_encoding: PositionEncoding,
|
||||
|
@ -77,26 +83,15 @@ impl Session {
|
|||
|
||||
let mut workspaces = Workspaces::default();
|
||||
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 {
|
||||
position_encoding,
|
||||
workspaces,
|
||||
deferred_messages: VecDeque::new(),
|
||||
index: Some(index),
|
||||
default_project,
|
||||
default_project: DefaultProject::new(),
|
||||
projects: BTreeMap::new(),
|
||||
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||
client_capabilities,
|
||||
|
@ -109,7 +104,6 @@ impl Session {
|
|||
pub(crate) fn request_queue(&self) -> &RequestQueue {
|
||||
&self.request_queue
|
||||
}
|
||||
|
||||
pub(crate) fn request_queue_mut(&mut self) -> &mut RequestQueue {
|
||||
&mut self.request_queue
|
||||
}
|
||||
|
@ -204,21 +198,46 @@ impl Session {
|
|||
.map(|(_, db)| db)
|
||||
}
|
||||
|
||||
/// Returns a reference to the default project [`ProjectDatabase`]. The default project is the
|
||||
/// minimum root path in the project map.
|
||||
pub(crate) fn apply_changes(
|
||||
&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 {
|
||||
&self.default_project
|
||||
self.default_project.get(self.index.as_ref())
|
||||
}
|
||||
|
||||
/// Returns a mutable reference to the default project [`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> + '_ {
|
||||
self.projects
|
||||
.values_mut()
|
||||
.chain(std::iter::once(&mut self.default_project))
|
||||
let default_project = self.default_project.try_get_mut();
|
||||
self.projects.values_mut().chain(default_project)
|
||||
}
|
||||
|
||||
/// Returns the [`DocumentKey`] for the given URL.
|
||||
|
@ -232,16 +251,18 @@ impl Session {
|
|||
assert!(!self.workspaces.all_initialized());
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
// For now, create one project database per workspace.
|
||||
// In the future, index the workspace directories to find all projects
|
||||
// and create a project database for each.
|
||||
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)
|
||||
.context("Failed to find project configuration")
|
||||
.and_then(|mut metadata| {
|
||||
|
@ -249,6 +270,11 @@ impl Session {
|
|||
metadata
|
||||
.apply_configuration_files(&system)
|
||||
.context("Failed to apply configuration files")?;
|
||||
|
||||
if let Some(overrides) = workspace.settings.project_options_overrides() {
|
||||
metadata.apply_overrides(overrides);
|
||||
}
|
||||
|
||||
ProjectDatabase::new(metadata, system)
|
||||
.context("Failed to create project database")
|
||||
});
|
||||
|
@ -516,12 +542,19 @@ impl SessionSnapshot {
|
|||
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Workspaces {
|
||||
workspaces: BTreeMap<Url, Workspace>,
|
||||
workspaces: BTreeMap<SystemPathBuf, Workspace>,
|
||||
uninitialized: usize,
|
||||
}
|
||||
|
||||
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
|
||||
.to_file_path()
|
||||
.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)
|
||||
.map_err(|_| anyhow!("Workspace URL is not valid UTF8"))?;
|
||||
|
||||
self.workspaces.insert(
|
||||
url,
|
||||
Workspace {
|
||||
options,
|
||||
root: system_path,
|
||||
},
|
||||
);
|
||||
self.workspaces
|
||||
.insert(system_path, Workspace { url, settings });
|
||||
|
||||
self.uninitialized += 1;
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
url: &Url,
|
||||
options: ClientOptions,
|
||||
) -> Option<&mut Workspace> {
|
||||
if let Some(workspace) = self.workspaces.get_mut(url) {
|
||||
workspace.options = options;
|
||||
settings: ClientSettings,
|
||||
) -> Option<(SystemPathBuf, &mut Workspace)> {
|
||||
let path = url.to_file_path().ok()?;
|
||||
|
||||
// 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;
|
||||
Some(workspace)
|
||||
Some((system_path, workspace))
|
||||
} else {
|
||||
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> + '_ {
|
||||
self.workspaces.keys()
|
||||
self.workspaces.values().map(Workspace::url)
|
||||
}
|
||||
|
||||
pub(crate) fn all_initialized(&self) -> bool {
|
||||
|
@ -567,8 +612,8 @@ impl Workspaces {
|
|||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Workspaces {
|
||||
type Item = (&'a Url, &'a Workspace);
|
||||
type IntoIter = std::collections::btree_map::Iter<'a, Url, Workspace>;
|
||||
type Item = (&'a SystemPathBuf, &'a Workspace);
|
||||
type IntoIter = std::collections::btree_map::Iter<'a, SystemPathBuf, Workspace>;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
self.workspaces.iter()
|
||||
|
@ -577,12 +622,61 @@ impl<'a> IntoIterator for &'a Workspaces {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Workspace {
|
||||
root: SystemPathBuf,
|
||||
options: ClientOptions,
|
||||
/// The workspace root URL as sent by the client during initialization.
|
||||
url: Url,
|
||||
settings: ClientSettings,
|
||||
}
|
||||
|
||||
impl Workspace {
|
||||
pub(crate) fn root(&self) -> &SystemPath {
|
||||
&self.root
|
||||
pub(crate) fn url(&self) -> &Url {
|
||||
&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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,14 @@
|
|||
use lsp_types::Url;
|
||||
use ruff_db::system::SystemPathBuf;
|
||||
use ruff_python_ast::PythonVersion;
|
||||
use rustc_hash::FxHashMap;
|
||||
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::session::settings::ClientSettings;
|
||||
use crate::session::ClientSettings;
|
||||
|
||||
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.
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct ClientOptions {
|
||||
|
@ -52,6 +56,8 @@ pub(crate) struct ClientOptions {
|
|||
python: Option<Python>,
|
||||
/// Diagnostic mode for the language server.
|
||||
diagnostic_mode: Option<DiagnosticMode>,
|
||||
|
||||
python_extension: Option<PythonExtension>,
|
||||
}
|
||||
|
||||
/// Diagnostic mode for the language server.
|
||||
|
@ -75,6 +81,47 @@ impl DiagnosticMode {
|
|||
impl ClientOptions {
|
||||
/// Returns the client settings that are relevant to the language server.
|
||||
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 {
|
||||
disable_language_services: self
|
||||
.python
|
||||
|
@ -82,6 +129,7 @@ impl ClientOptions {
|
|||
.and_then(|ty| ty.disable_language_services)
|
||||
.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
|
||||
// all settings and not just the ones in "python.*".
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Python {
|
||||
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))]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Ty {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use super::options::DiagnosticMode;
|
||||
|
||||
use ty_project::metadata::options::ProjectOptionsOverrides;
|
||||
|
||||
/// 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.
|
||||
|
@ -8,6 +10,7 @@ use super::options::DiagnosticMode;
|
|||
pub(crate) struct ClientSettings {
|
||||
pub(super) disable_language_services: bool,
|
||||
pub(super) diagnostic_mode: DiagnosticMode,
|
||||
pub(super) overrides: Option<ProjectOptionsOverrides>,
|
||||
}
|
||||
|
||||
impl ClientSettings {
|
||||
|
@ -18,4 +21,8 @@ impl ClientSettings {
|
|||
pub(crate) fn diagnostic_mode(&self) -> DiagnosticMode {
|
||||
self.diagnostic_mode
|
||||
}
|
||||
|
||||
pub(crate) fn project_options_overrides(&self) -> Option<&ProjectOptionsOverrides> {
|
||||
self.overrides.as_ref()
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue