use std::{path::PathBuf, str::FromStr, sync::Arc}; use anyhow::{Context as _, Result, bail}; use async_trait::async_trait; use collections::{BTreeMap, IndexSet}; use gpui::{ App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, }; use language::{ LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain, ToolchainList, ToolchainScope, }; use rpc::{ AnyProtoClient, TypedEnvelope, proto::{ self, ResolveToolchainResponse, resolve_toolchain_response::Response as ResolveResponsePayload, }, }; use settings::WorktreeId; use util::{ResultExt as _, rel_path::RelPath}; use crate::{ ProjectEnvironment, ProjectPath, manifest_tree::{ManifestQueryDelegate, ManifestTree}, worktree_store::WorktreeStore, }; pub struct ToolchainStore { mode: ToolchainStoreInner, user_toolchains: BTreeMap>, _sub: Subscription, } enum ToolchainStoreInner { Local(Entity), Remote(Entity), } 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, pub user_toolchains: BTreeMap>, } impl EventEmitter 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( languages: Arc, worktree_store: Entity, project_environment: Entity, manifest_tree: Entity, cx: &mut Context, ) -> Self { let entity = cx.new(|_| LocalToolchainStore { languages, worktree_store, project_environment, active_toolchains: Default::default(), manifest_tree, }); let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) }); Self { mode: ToolchainStoreInner::Local(entity), user_toolchains: Default::default(), _sub, } } pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context) -> Self { let entity = cx.new(|_| RemoteToolchainStore { client, project_id }); let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) }); Self { mode: ToolchainStoreInner::Remote(entity), user_toolchains: Default::default(), _sub, } } pub(crate) fn activate_toolchain( &self, path: ProjectPath, toolchain: Toolchain, cx: &mut App, ) -> Task> { match &self.mode { ToolchainStoreInner::Local(local) => { local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } ToolchainStoreInner::Remote(remote) => { remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx)) } } } pub(crate) fn user_toolchains(&self) -> BTreeMap> { self.user_toolchains.clone() } pub(crate) fn add_toolchain( &mut self, toolchain: Toolchain, scope: ToolchainScope, cx: &mut Context, ) { 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, ) { 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, ) -> Task> { 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, ) -> Task> { 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::>(), ) }) .collect::>(); let task = match &self.mode { ToolchainStoreInner::Local(local) => { local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx)) } 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> { match &self.mode { ToolchainStoreInner::Local(local) => Task::ready(local.read(cx).active_toolchain( path.worktree_id, &path.path, language_name, )), ToolchainStoreInner::Remote(remote) => { remote.read(cx).active_toolchain(path, language_name, cx) } } } async fn handle_activate_toolchain( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { this.update(&mut cx, |this, cx| { let language_name = LanguageName::from_proto(envelope.payload.language_name); let Some(toolchain) = envelope.payload.toolchain else { bail!("Missing `toolchain` in payload"); }; let toolchain = Toolchain { name: toolchain.name.into(), // todo(windows) // Do we need to convert path to native string? path: toolchain.path.into(), as_json: serde_json::Value::from_str(&toolchain.raw_json)?, language_name, }; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let path = if let Some(path) = envelope.payload.path { RelPath::from_proto(&path)? } else { RelPath::empty().into() }; Ok(this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)) })?? .await; Ok(proto::Ack {}) } async fn handle_active_toolchain( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { let path = RelPath::unix(envelope.payload.path.as_deref().unwrap_or(""))?; let toolchain = this .update(&mut cx, |this, cx| { let language_name = LanguageName::from_proto(envelope.payload.language_name); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); this.active_toolchain( ProjectPath { worktree_id, path: Arc::from(path), }, language_name, cx, ) })? .await; Ok(proto::ActiveToolchainResponse { toolchain: toolchain.map(|toolchain| { let path = PathBuf::from(toolchain.path.to_string()); proto::Toolchain { name: toolchain.name.into(), path: path.to_string_lossy().into_owned(), raw_json: toolchain.as_json.to_string(), } }), }) } async fn handle_list_toolchains( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { let toolchains = this .update(&mut cx, |this, cx| { let language_name = LanguageName::from_proto(envelope.payload.language_name); let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let path = RelPath::from_proto(envelope.payload.path.as_deref().unwrap_or(""))?; anyhow::Ok(this.list_toolchains( ProjectPath { worktree_id, path }, language_name, cx, )) })?? .await; let has_values = toolchains.is_some(); let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains { toolchains .groups .iter() .filter_map(|group| { Some(proto::ToolchainGroup { start_index: u64::try_from(group.0).ok()?, name: String::from(group.1.as_ref()), }) }) .collect() } else { vec![] }; let (toolchains, relative_path) = if let Some(Toolchains { toolchains, root_path: relative_path, .. }) = toolchains { let toolchains = toolchains .toolchains .into_iter() .map(|toolchain| { let path = PathBuf::from(toolchain.path.to_string()); proto::Toolchain { name: toolchain.name.to_string(), path: path.to_string_lossy().into_owned(), raw_json: toolchain.as_json.to_string(), } }) .collect::>(); (toolchains, relative_path) } else { (vec![], Arc::from(RelPath::empty())) }; Ok(proto::ListToolchainsResponse { has_values, toolchains, groups, relative_worktree_path: Some(relative_path.to_proto()), }) } async fn handle_resolve_toolchain( this: Entity, envelope: TypedEnvelope, mut cx: AsyncApp, ) -> Result { 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 { 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> { match &self.mode { ToolchainStoreInner::Local(local) => Some(local), ToolchainStoreInner::Remote(_) => None, } } } pub struct LocalToolchainStore { languages: Arc, worktree_store: Entity, project_environment: Entity, active_toolchains: BTreeMap<(WorktreeId, LanguageName), BTreeMap, Toolchain>>, manifest_tree: Entity, } #[async_trait(?Send)] impl language::LocalLanguageToolchainStore for LocalStore { fn active_toolchain( self: Arc, worktree_id: WorktreeId, path: &Arc, language_name: LanguageName, cx: &mut AsyncApp, ) -> Option { self.0 .update(cx, |this, _| { this.active_toolchain(worktree_id, path, language_name) }) .ok()? } } #[async_trait(?Send)] impl language::LanguageToolchainStore for RemoteStore { async fn active_toolchain( self: Arc, worktree_id: WorktreeId, path: Arc, language_name: LanguageName, cx: &mut AsyncApp, ) -> Option { self.0 .update(cx, |this, cx| { this.active_toolchain(ProjectPath { worktree_id, path }, language_name, cx) }) .ok()? .await } } pub struct EmptyToolchainStore; impl language::LocalLanguageToolchainStore for EmptyToolchainStore { fn active_toolchain( self: Arc, _: WorktreeId, _: &Arc, _: LanguageName, _: &mut AsyncApp, ) -> Option { None } } pub(crate) struct LocalStore(WeakEntity); struct RemoteStore(WeakEntity); #[derive(Clone)] pub enum ToolchainStoreEvent { ToolchainActivated, CustomToolchainsModified, } impl EventEmitter for LocalToolchainStore {} impl LocalToolchainStore { pub(crate) fn activate_toolchain( &self, path: ProjectPath, toolchain: Toolchain, cx: &mut Context, ) -> Task> { cx.spawn(async move |this, cx| { this.update(cx, |this, cx| { this.active_toolchains .entry((path.worktree_id, toolchain.language_name.clone())) .or_default() .insert(path.path, toolchain.clone()); cx.emit(ToolchainStoreEvent::ToolchainActivated); }) .ok(); Some(()) }) } pub(crate) fn list_toolchains( &mut self, path: ProjectPath, language_name: LanguageName, cx: &mut Context, ) -> Task)>> { let registry = self.languages.clone(); let manifest_tree = self.manifest_tree.downgrade(); let environment = self.project_environment.clone(); cx.spawn(async move |this, cx| { let language = cx .background_spawn(registry.language_for_name(language_name.as_ref())) .await .ok()?; let toolchains = language.toolchain_lister()?; let manifest_name = toolchains.meta().manifest_name; let (snapshot, worktree) = this .update(cx, |this, cx| { this.worktree_store .read(cx) .worktree_for_id(path.worktree_id, cx) .map(|worktree| (worktree.read(cx).snapshot(), worktree)) }) .ok() .flatten()?; let worktree_id = snapshot.id(); let worktree_root = snapshot.abs_path().to_path_buf(); let delegate = Arc::from(ManifestQueryDelegate::new(snapshot)) as Arc; let relative_path = manifest_tree .update(cx, |this, cx| { this.root_for_path(&path, &manifest_name, &delegate, cx) }) .ok()? .unwrap_or_else(|| ProjectPath { path: Arc::from(RelPath::empty()), worktree_id, }); let abs_path = worktree .update(cx, |this, _| this.absolutize(&relative_path.path)) .ok()?; let project_env = environment .update(cx, |environment, cx| { environment.get_directory_environment(abs_path.as_path().into(), cx) }) .ok()? .await; cx.background_spawn(async move { Some(( toolchains .list(worktree_root, relative_path.path.clone(), project_env) .await, relative_path.path, )) }) .await }) } pub(crate) fn active_toolchain( &self, worktree_id: WorktreeId, relative_path: &Arc, language_name: LanguageName, ) -> Option { let ancestors = relative_path.ancestors(); self.active_toolchains .get(&(worktree_id, language_name)) .and_then(|paths| { ancestors .into_iter() .find_map(|root_path| paths.get(root_path)) }) .cloned() } fn resolve_toolchain( &self, path: PathBuf, language_name: LanguageName, cx: &mut Context, ) -> Task> { 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 for RemoteToolchainStore {} struct RemoteToolchainStore { client: AnyProtoClient, project_id: u64, } impl RemoteToolchainStore { pub(crate) fn activate_toolchain( &self, project_path: ProjectPath, toolchain: Toolchain, cx: &mut Context, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); cx.spawn(async move |this, cx| { let did_activate = cx .background_spawn(async move { let path = PathBuf::from(toolchain.path.to_string()); let _ = client .request(proto::ActivateToolchain { project_id, worktree_id: project_path.worktree_id.to_proto(), language_name: toolchain.language_name.into(), toolchain: Some(proto::Toolchain { name: toolchain.name.into(), path: path.to_string_lossy().into_owned(), raw_json: toolchain.as_json.to_string(), }), path: Some(project_path.path.to_proto()), }) .await .log_err()?; Some(()) }) .await; did_activate.and_then(|_| { this.update(cx, |_, cx| { cx.emit(ToolchainStoreEvent::ToolchainActivated); }) .ok() }) }) } pub(crate) fn list_toolchains( &self, path: ProjectPath, language_name: LanguageName, cx: &App, ) -> Task)>> { let project_id = self.project_id; let client = self.client.clone(); cx.background_spawn(async move { let response = client .request(proto::ListToolchains { project_id, worktree_id: path.worktree_id.to_proto(), language_name: language_name.clone().into(), path: Some(path.path.to_proto()), }) .await .log_err()?; if !response.has_values { return None; } let toolchains = response .toolchains .into_iter() .filter_map(|toolchain| { Some(Toolchain { language_name: language_name.clone(), name: toolchain.name.into(), path: toolchain.path.into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, }) }) .collect(); let groups = response .groups .into_iter() .filter_map(|group| { Some((usize::try_from(group.start_index).ok()?, group.name.into())) }) .collect(); let relative_path = RelPath::from_proto( response .relative_worktree_path .as_deref() .unwrap_or_default(), ) .log_err()?; Some(( ToolchainList { toolchains, default: None, groups, }, relative_path, )) }) } pub(crate) fn active_toolchain( &self, path: ProjectPath, language_name: LanguageName, cx: &App, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); cx.background_spawn(async move { let response = client .request(proto::ActiveToolchain { project_id, worktree_id: path.worktree_id.to_proto(), language_name: language_name.clone().into(), path: Some(path.path.to_proto()), }) .await .log_err()?; response.toolchain.and_then(|toolchain| { Some(Toolchain { language_name: language_name.clone(), name: toolchain.name.into(), path: toolchain.path.into(), as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?, }) }) }) } fn resolve_toolchain( &self, abs_path: PathBuf, language_name: LanguageName, cx: &mut Context, ) -> Task> { 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(), path: toolchain.path.into(), as_json: serde_json::Value::from_str(&toolchain.raw_json) .context("Deserializing ResolveToolchain LSP response")?, }), Response::Error(error) => { anyhow::bail!("{error}"); } } }) } }