Add language toolchains (#19576)

This PR adds support for selecting toolchains for a given language (e.g.
Rust toolchains or Python virtual environments) with support for SSH
projects provided out of the box. For Python we piggy-back off of
[PET](https://github.com/microsoft/python-environment-tools), a library
maintained by Microsoft.
Closes #16421
Closes #7646

Release Notes:

- Added toolchain selector to the status bar (with initial support for
Python virtual environments)
This commit is contained in:
Piotr Osiewicz 2024-10-28 15:34:03 +01:00 committed by GitHub
parent 03bd95405b
commit cdddb4d360
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 2221 additions and 133 deletions

View file

@ -3,9 +3,16 @@ use async_trait::async_trait;
use collections::HashMap;
use gpui::AppContext;
use gpui::AsyncAppContext;
use language::LanguageName;
use language::LanguageToolchainStore;
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::LanguageServerBinary;
use node_runtime::NodeRuntime;
use pet_core::python_environment::PythonEnvironmentKind;
use pet_core::Configuration;
use project::lsp_store::language_server_settings;
use serde_json::Value;
@ -200,12 +207,35 @@ impl LspAdapter for PythonLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
adapter: &Arc<dyn LspAdapterDelegate>,
toolchains: Arc<dyn LanguageToolchainStore>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
cx.update(|cx| {
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
.unwrap_or_default()
let toolchain = toolchains
.active_toolchain(adapter.worktree_id(), LanguageName::new("Python"), cx)
.await;
cx.update(move |cx| {
let mut user_settings =
language_server_settings(adapter.as_ref(), &Self::SERVER_NAME, cx)
.and_then(|s| s.settings.clone())
.unwrap_or_default();
// If python.pythonPath is not set in user config, do so using our toolchain picker.
if let Some(toolchain) = toolchain {
if user_settings.is_null() {
user_settings = Value::Object(serde_json::Map::default());
}
let object = user_settings.as_object_mut().unwrap();
if let Some(python) = object
.entry("python")
.or_insert(Value::Object(serde_json::Map::default()))
.as_object_mut()
{
python
.entry("pythonPath")
.or_insert(Value::String(toolchain.path.into()));
}
}
user_settings
})
}
}
@ -320,6 +350,83 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String {
.to_string()
}
#[derive(Default)]
pub(crate) struct PythonToolchainProvider {}
static ENV_PRIORITY_LIST: &'static [PythonEnvironmentKind] = &[
// Prioritize non-Conda environments.
PythonEnvironmentKind::Poetry,
PythonEnvironmentKind::Pipenv,
PythonEnvironmentKind::VirtualEnvWrapper,
PythonEnvironmentKind::Venv,
PythonEnvironmentKind::VirtualEnv,
PythonEnvironmentKind::Conda,
PythonEnvironmentKind::Pyenv,
PythonEnvironmentKind::GlobalPaths,
PythonEnvironmentKind::Homebrew,
];
fn env_priority(kind: Option<PythonEnvironmentKind>) -> usize {
if let Some(kind) = kind {
ENV_PRIORITY_LIST
.iter()
.position(|blessed_env| blessed_env == &kind)
.unwrap_or(ENV_PRIORITY_LIST.len())
} else {
// Unknown toolchains are less useful than non-blessed ones.
ENV_PRIORITY_LIST.len() + 1
}
}
#[async_trait(?Send)]
impl ToolchainLister for PythonToolchainProvider {
async fn list(&self, worktree_root: PathBuf) -> ToolchainList {
let environment = pet_core::os_environment::EnvironmentApi::new();
let locators = pet::locators::create_locators(
Arc::new(pet_conda::Conda::from(&environment)),
Arc::new(pet_poetry::Poetry::from(&environment)),
&environment,
);
let mut config = Configuration::default();
config.workspace_directories = Some(vec![worktree_root]);
let reporter = pet_reporter::collect::create_reporter();
pet::find::find_and_report_envs(&reporter, config, &locators, &environment, None);
let mut toolchains = reporter
.environments
.lock()
.ok()
.map_or(Vec::new(), |mut guard| std::mem::take(&mut guard));
toolchains.sort_by(|lhs, rhs| {
env_priority(lhs.kind)
.cmp(&env_priority(rhs.kind))
.then_with(|| lhs.executable.cmp(&rhs.executable))
});
let mut toolchains: Vec<_> = toolchains
.into_iter()
.filter_map(|toolchain| {
let name = if let Some(version) = &toolchain.version {
format!("Python {version} ({:?})", toolchain.kind?)
} else {
format!("{:?}", toolchain.kind?)
}
.into();
Some(Toolchain {
name,
path: toolchain.executable?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
})
})
.collect();
toolchains.dedup();
ToolchainList {
toolchains,
default: None,
groups: Default::default(),
}
}
}
#[cfg(test)]
mod tests {
use gpui::{BorrowAppContext, Context, ModelContext, TestAppContext};