toolchains: Allow users to provide custom paths to toolchains (#37009)

- **toolchains: Add new state to toolchain selector**
- **Use toolchain term for Add Toolchain button**
- **Hoist out a meta function for toolchain listers**

Closes #27332

Release Notes:

- python: Users can now specify a custom path to their virtual
environment from within the picker.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Piotr Osiewicz
2025-09-06 00:47:39 +02:00
committed by GitHub
co-authored by Danilo Leal
parent 59bdbf5a5d
commit 6a7b84eb87
22 changed files with 1506 additions and 233 deletions
+2 -2
View File
@@ -3933,8 +3933,8 @@ impl LspStore {
event: &ToolchainStoreEvent,
_: &mut Context<Self>,
) {
match event {
ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(),
if let ToolchainStoreEvent::ToolchainActivated = event {
self.request_workspace_config_refresh()
}
}
+65 -9
View File
@@ -48,7 +48,7 @@ use clock::ReplicaId;
use dap::client::DebugAdapterClient;
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashMap, HashSet, IndexSet};
use debounced_delay::DebouncedDelay;
pub use debugger::breakpoint_store::BreakpointWithPosition;
use debugger::{
@@ -74,8 +74,9 @@ use gpui::{
};
use language::{
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
Unclipped, language_settings::InlayHintKind, proto::split_operations,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainMetadata,
ToolchainScope, Transaction, Unclipped, language_settings::InlayHintKind,
proto::split_operations,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@@ -104,6 +105,7 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
borrow::Cow,
collections::BTreeMap,
ops::Range,
path::{Component, Path, PathBuf},
pin::pin,
@@ -117,7 +119,7 @@ use terminals::Terminals;
use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _,
ResultExt as _, maybe,
paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
};
use worktree::{CreatedEntry, Snapshot, Traversal};
@@ -142,7 +144,7 @@ pub use lsp_store::{
LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
pub use toolchain_store::ToolchainStore;
pub use toolchain_store::{ToolchainStore, Toolchains};
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
const MAX_SEARCH_RESULT_FILES: usize = 5_000;
const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
@@ -3370,7 +3372,7 @@ impl Project {
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
) -> Task<Option<Toolchains>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
cx.spawn(async move |cx| {
toolchain_store
@@ -3383,16 +3385,70 @@ impl Project {
}
}
pub async fn toolchain_term(
pub async fn toolchain_metadata(
languages: Arc<LanguageRegistry>,
language_name: LanguageName,
) -> Option<SharedString> {
) -> Option<ToolchainMetadata> {
languages
.language_for_name(language_name.as_ref())
.await
.ok()?
.toolchain_lister()
.map(|lister| lister.term())
.map(|lister| lister.meta())
}
pub fn add_toolchain(
&self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
maybe!({
self.toolchain_store.as_ref()?.update(cx, |this, cx| {
this.add_toolchain(toolchain, scope, cx);
});
Some(())
});
}
pub fn remove_toolchain(
&self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
maybe!({
self.toolchain_store.as_ref()?.update(cx, |this, cx| {
this.remove_toolchain(toolchain, scope, cx);
});
Some(())
});
}
pub fn user_toolchains(
&self,
cx: &App,
) -> Option<BTreeMap<ToolchainScope, IndexSet<Toolchain>>> {
Some(self.toolchain_store.as_ref()?.read(cx).user_toolchains())
}
pub fn resolve_toolchain(
&self,
path: PathBuf,
language_name: LanguageName,
cx: &App,
) -> Task<Result<Toolchain>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
cx.spawn(async move |cx| {
toolchain_store
.update(cx, |this, cx| {
this.resolve_toolchain(path, language_name, cx)
})?
.await
})
} else {
Task::ready(Err(anyhow!("This project does not support toolchains")))
}
}
pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
+21 -8
View File
@@ -22,7 +22,7 @@ use itertools::Itertools;
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister,
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister,
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
tree_sitter_rust, tree_sitter_typescript,
};
@@ -727,7 +727,12 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
// We're not using venvs at all here, so both folders should fall under the same root.
assert_eq!(server.server_id(), LanguageServerId(0));
// Now, let's select a different toolchain for one of subprojects.
let (available_toolchains_for_b, root_path) = project
let Toolchains {
toolchains: available_toolchains_for_b,
root_path,
..
} = project
.update(cx, |this, cx| {
let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
this.available_toolchains(
@@ -9213,13 +9218,21 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
..Default::default()
}
}
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString {
SharedString::new_static("virtual environment")
async fn resolve(
&self,
_: PathBuf,
_: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain> {
Err(anyhow::anyhow!("Not implemented"))
}
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName {
SharedString::new_static("pyproject.toml").into()
fn meta(&self) -> ToolchainMetadata {
ToolchainMetadata {
term: SharedString::new_static("Virtual Environment"),
new_toolchain_placeholder: SharedString::new_static(
"A path to the python3 executable within a virtual environment, or path to virtual environment itself",
),
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
}
}
async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec<String> {
vec![]
+248 -38
View File
@@ -4,20 +4,23 @@ use std::{
sync::Arc,
};
use anyhow::{Result, bail};
use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use collections::BTreeMap;
use collections::{BTreeMap, IndexSet};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
};
use language::{
LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain,
ToolchainList,
ToolchainList, ToolchainScope,
};
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, FromProto, ToProto},
proto::{
self, FromProto, ResolveToolchainResponse, ToProto,
resolve_toolchain_response::Response as ResolveResponsePayload,
},
};
use settings::WorktreeId;
use util::ResultExt as _;
@@ -28,24 +31,31 @@ use crate::{
worktree_store::WorktreeStore,
};
pub struct ToolchainStore(ToolchainStoreInner);
enum ToolchainStoreInner {
Local(
Entity<LocalToolchainStore>,
#[allow(dead_code)] Subscription,
),
Remote(
Entity<RemoteToolchainStore>,
#[allow(dead_code)] Subscription,
),
pub struct ToolchainStore {
mode: ToolchainStoreInner,
user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
_sub: Subscription,
}
enum ToolchainStoreInner {
Local(Entity<LocalToolchainStore>),
Remote(Entity<RemoteToolchainStore>),
}
pub struct Toolchains {
/// Auto-detected toolchains.
pub toolchains: ToolchainList,
/// Path of the project root at which we ran the automatic toolchain detection.
pub root_path: Arc<Path>,
pub user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
}
impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
impl ToolchainStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_activate_toolchain);
client.add_entity_request_handler(Self::handle_list_toolchains);
client.add_entity_request_handler(Self::handle_active_toolchain);
client.add_entity_request_handler(Self::handle_resolve_toolchain);
}
pub fn local(
@@ -62,18 +72,26 @@ impl ToolchainStore {
active_toolchains: Default::default(),
manifest_tree,
});
let subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
});
Self(ToolchainStoreInner::Local(entity, subscription))
Self {
mode: ToolchainStoreInner::Local(entity),
user_toolchains: Default::default(),
_sub,
}
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) -> Self {
let entity = cx.new(|_| RemoteToolchainStore { client, project_id });
let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
});
Self(ToolchainStoreInner::Remote(entity, _subscription))
Self {
mode: ToolchainStoreInner::Remote(entity),
user_toolchains: Default::default(),
_sub,
}
}
pub(crate) fn activate_toolchain(
&self,
@@ -81,43 +99,130 @@ impl ToolchainStore {
toolchain: Toolchain,
cx: &mut App,
) -> Task<Option<()>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
match &self.mode {
ToolchainStoreInner::Local(local) => {
local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
}
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
}
}
}
pub(crate) fn user_toolchains(&self) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
self.user_toolchains.clone()
}
pub(crate) fn add_toolchain(
&mut self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
let did_insert = self
.user_toolchains
.entry(scope)
.or_default()
.insert(toolchain);
if did_insert {
cx.emit(ToolchainStoreEvent::CustomToolchainsModified);
}
}
pub(crate) fn remove_toolchain(
&mut self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
let mut did_remove = false;
self.user_toolchains
.entry(scope)
.and_modify(|toolchains| did_remove = toolchains.shift_remove(&toolchain));
if did_remove {
cx.emit(ToolchainStoreEvent::CustomToolchainsModified);
}
}
pub(crate) fn resolve_toolchain(
&self,
abs_path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
debug_assert!(abs_path.is_absolute());
match &self.mode {
ToolchainStoreInner::Local(local) => local.update(cx, |this, cx| {
this.resolve_toolchain(abs_path, language_name, cx)
}),
ToolchainStoreInner::Remote(remote) => remote.update(cx, |this, cx| {
this.resolve_toolchain(abs_path, language_name, cx)
}),
}
}
pub(crate) fn list_toolchains(
&self,
path: ProjectPath,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
) -> Task<Option<Toolchains>> {
let user_toolchains = self
.user_toolchains
.iter()
.filter(|(scope, _)| {
if let ToolchainScope::Subproject(worktree_id, relative_path) = scope {
path.worktree_id == *worktree_id && relative_path.starts_with(&path.path)
} else {
true
}
})
.map(|(scope, toolchains)| {
(
scope.clone(),
toolchains
.iter()
.filter(|toolchain| toolchain.language_name == language_name)
.cloned()
.collect::<IndexSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
let task = match &self.mode {
ToolchainStoreInner::Local(local) => {
local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
}
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.read(cx).list_toolchains(path, language_name, cx)
}
}
};
cx.spawn(async move |_, _| {
let (mut toolchains, root_path) = task.await?;
toolchains.toolchains.retain(|toolchain| {
!user_toolchains
.values()
.any(|toolchains| toolchains.contains(toolchain))
});
Some(Toolchains {
toolchains,
root_path,
user_toolchains,
})
})
}
pub(crate) fn active_toolchain(
&self,
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<Toolchain>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain(
match &self.mode {
ToolchainStoreInner::Local(local) => Task::ready(local.read(cx).active_toolchain(
path.worktree_id,
&path.path,
language_name,
)),
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.read(cx).active_toolchain(path, language_name, cx)
}
}
@@ -197,7 +302,7 @@ impl ToolchainStore {
})?
.await;
let has_values = toolchains.is_some();
let groups = if let Some((toolchains, _)) = &toolchains {
let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains {
toolchains
.groups
.iter()
@@ -211,7 +316,12 @@ impl ToolchainStore {
} else {
vec![]
};
let (toolchains, relative_path) = if let Some((toolchains, relative_path)) = toolchains {
let (toolchains, relative_path) = if let Some(Toolchains {
toolchains,
root_path: relative_path,
..
}) = toolchains
{
let toolchains = toolchains
.toolchains
.into_iter()
@@ -236,16 +346,45 @@ impl ToolchainStore {
relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()),
})
}
async fn handle_resolve_toolchain(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ResolveToolchain>,
mut cx: AsyncApp,
) -> Result<proto::ResolveToolchainResponse> {
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let path = PathBuf::from(envelope.payload.abs_path);
this.resolve_toolchain(path, language_name, cx)
})?
.await;
let response = match toolchain {
Ok(toolchain) => {
let toolchain = proto::Toolchain {
name: toolchain.name.to_string(),
path: toolchain.path.to_string(),
raw_json: toolchain.as_json.to_string(),
};
ResolveResponsePayload::Toolchain(toolchain)
}
Err(e) => ResolveResponsePayload::Error(e.to_string()),
};
Ok(ResolveToolchainResponse {
response: Some(response),
})
}
pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())),
match &self.mode {
ToolchainStoreInner::Local(local) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
}
}
pub fn as_local_store(&self) -> Option<&Entity<LocalToolchainStore>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Some(local),
ToolchainStoreInner::Remote(_, _) => None,
match &self.mode {
ToolchainStoreInner::Local(local) => Some(local),
ToolchainStoreInner::Remote(_) => None,
}
}
}
@@ -311,6 +450,7 @@ struct RemoteStore(WeakEntity<RemoteToolchainStore>);
#[derive(Clone)]
pub enum ToolchainStoreEvent {
ToolchainActivated,
CustomToolchainsModified,
}
impl EventEmitter<ToolchainStoreEvent> for LocalToolchainStore {}
@@ -351,7 +491,7 @@ impl LocalToolchainStore {
.await
.ok()?;
let toolchains = language.toolchain_lister()?;
let manifest_name = toolchains.manifest_name();
let manifest_name = toolchains.meta().manifest_name;
let (snapshot, worktree) = this
.update(cx, |this, cx| {
this.worktree_store
@@ -414,6 +554,33 @@ impl LocalToolchainStore {
})
.cloned()
}
fn resolve_toolchain(
&self,
path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
let registry = self.languages.clone();
let environment = self.project_environment.clone();
cx.spawn(async move |_, cx| {
let language = cx
.background_spawn(registry.language_for_name(&language_name.0))
.await
.with_context(|| format!("Language {} not found", language_name.0))?;
let toolchain_lister = language.toolchain_lister().with_context(|| {
format!("Language {} does not support toolchains", language_name.0)
})?;
let project_env = environment
.update(cx, |environment, cx| {
environment.get_directory_environment(path.as_path().into(), cx)
})?
.await;
cx.background_spawn(async move { toolchain_lister.resolve(path, project_env).await })
.await
})
}
}
impl EventEmitter<ToolchainStoreEvent> for RemoteToolchainStore {}
@@ -556,4 +723,47 @@ impl RemoteToolchainStore {
})
})
}
fn resolve_toolchain(
&self,
abs_path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.background_spawn(async move {
let response: proto::ResolveToolchainResponse = client
.request(proto::ResolveToolchain {
project_id,
language_name: language_name.clone().into(),
abs_path: abs_path.to_string_lossy().into_owned(),
})
.await?;
let response = response
.response
.context("Failed to resolve toolchain via RPC")?;
use proto::resolve_toolchain_response::Response;
match response {
Response::Toolchain(toolchain) => {
Ok(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from_proto(toolchain.path)
.to_string_lossy()
.to_string()
.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)
.context("Deserializing ResolveToolchain LSP response")?,
})
}
Response::Error(error) => {
anyhow::bail!("{error}");
}
}
})
}
}