use std::{str::FromStr, sync::Arc}; use anyhow::{bail, Result}; use async_trait::async_trait; use collections::BTreeMap; use gpui::{ AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, WeakModel, }; use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList}; use rpc::{proto, AnyProtoClient, TypedEnvelope}; use settings::WorktreeId; use util::ResultExt as _; use crate::{worktree_store::WorktreeStore, ProjectEnvironment}; pub struct ToolchainStore(ToolchainStoreInner); enum ToolchainStoreInner { Local(Model, #[allow(dead_code)] Subscription), Remote(Model), } impl EventEmitter for ToolchainStore {} impl ToolchainStore { pub fn init(client: &AnyProtoClient) { client.add_model_request_handler(Self::handle_activate_toolchain); client.add_model_request_handler(Self::handle_list_toolchains); client.add_model_request_handler(Self::handle_active_toolchain); } pub fn local( languages: Arc, worktree_store: Model, project_environment: Model, cx: &mut ModelContext, ) -> Self { let model = cx.new_model(|_| LocalToolchainStore { languages, worktree_store, project_environment, active_toolchains: Default::default(), }); let subscription = cx.subscribe(&model, |_, _, e: &ToolchainStoreEvent, cx| { cx.emit(e.clone()) }); Self(ToolchainStoreInner::Local(model, subscription)) } pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut AppContext) -> Self { Self(ToolchainStoreInner::Remote( cx.new_model(|_| RemoteToolchainStore { client, project_id }), )) } pub(crate) fn activate_toolchain( &self, worktree_id: WorktreeId, toolchain: Toolchain, cx: &mut AppContext, ) -> Task> { match &self.0 { ToolchainStoreInner::Local(local, _) => local.update(cx, |this, cx| { this.activate_toolchain(worktree_id, toolchain, cx) }), ToolchainStoreInner::Remote(remote) => { remote .read(cx) .activate_toolchain(worktree_id, toolchain, cx) } } } pub(crate) fn list_toolchains( &self, worktree_id: WorktreeId, language_name: LanguageName, cx: &AppContext, ) -> Task> { match &self.0 { ToolchainStoreInner::Local(local, _) => { local .read(cx) .list_toolchains(worktree_id, language_name, cx) } ToolchainStoreInner::Remote(remote) => { remote .read(cx) .list_toolchains(worktree_id, language_name, cx) } } } pub(crate) fn active_toolchain( &self, worktree_id: WorktreeId, language_name: LanguageName, cx: &AppContext, ) -> Task> { match &self.0 { ToolchainStoreInner::Local(local, _) => { local .read(cx) .active_toolchain(worktree_id, language_name, cx) } ToolchainStoreInner::Remote(remote) => { remote .read(cx) .active_toolchain(worktree_id, language_name, cx) } } } async fn handle_activate_toolchain( this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> 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(), 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); Ok(this.activate_toolchain(worktree_id, toolchain, cx)) })?? .await; Ok(proto::Ack {}) } async fn handle_active_toolchain( this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> Result { 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(worktree_id, language_name, cx) })? .await; Ok(proto::ActiveToolchainResponse { toolchain: toolchain.map(|toolchain| proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), raw_json: toolchain.as_json.to_string(), }), }) } async fn handle_list_toolchains( this: Model, envelope: TypedEnvelope, mut cx: AsyncAppContext, ) -> 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); this.list_toolchains(worktree_id, language_name, cx) })? .await; let has_values = toolchains.is_some(); let groups = if let Some(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 = if let Some(toolchains) = toolchains { toolchains .toolchains .into_iter() .map(|toolchain| proto::Toolchain { name: toolchain.name.to_string(), path: toolchain.path.to_string(), raw_json: toolchain.as_json.to_string(), }) .collect::>() } else { vec![] }; Ok(proto::ListToolchainsResponse { has_values, toolchains, groups, }) } pub fn as_language_toolchain_store(&self) -> Arc { match &self.0 { ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())), ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())), } } } struct LocalToolchainStore { languages: Arc, worktree_store: Model, project_environment: Model, active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>, } #[async_trait(?Send)] impl language::LanguageToolchainStore for LocalStore { async fn active_toolchain( self: Arc, worktree_id: WorktreeId, language_name: LanguageName, cx: &mut AsyncAppContext, ) -> Option { self.0 .update(cx, |this, cx| { this.active_toolchain(worktree_id, language_name, cx) }) .ok()? .await } } #[async_trait(?Send)] impl language::LanguageToolchainStore for RemoteStore { async fn active_toolchain( self: Arc, worktree_id: WorktreeId, language_name: LanguageName, cx: &mut AsyncAppContext, ) -> Option { self.0 .update(cx, |this, cx| { this.active_toolchain(worktree_id, language_name, cx) }) .ok()? .await } } pub(crate) struct EmptyToolchainStore; #[async_trait(?Send)] impl language::LanguageToolchainStore for EmptyToolchainStore { async fn active_toolchain( self: Arc, _: WorktreeId, _: LanguageName, _: &mut AsyncAppContext, ) -> Option { None } } struct LocalStore(WeakModel); struct RemoteStore(WeakModel); #[derive(Clone)] pub(crate) enum ToolchainStoreEvent { ToolchainActivated, } impl EventEmitter for LocalToolchainStore {} impl LocalToolchainStore { pub(crate) fn activate_toolchain( &self, worktree_id: WorktreeId, toolchain: Toolchain, cx: &mut ModelContext, ) -> Task> { cx.spawn(move |this, mut cx| async move { this.update(&mut cx, |this, cx| { this.active_toolchains.insert( (worktree_id, toolchain.language_name.clone()), toolchain.clone(), ); cx.emit(ToolchainStoreEvent::ToolchainActivated); }) .ok(); Some(()) }) } pub(crate) fn list_toolchains( &self, worktree_id: WorktreeId, language_name: LanguageName, cx: &AppContext, ) -> Task> { let registry = self.languages.clone(); let Some(root) = self .worktree_store .read(cx) .worktree_for_id(worktree_id, cx) .map(|worktree| worktree.read(cx).abs_path()) else { return Task::ready(None); }; let environment = self.project_environment.clone(); cx.spawn(|mut cx| async move { let project_env = environment .update(&mut cx, |environment, cx| { environment.get_environment(Some(worktree_id), Some(root.clone()), cx) }) .ok()? .await; cx.background_executor() .spawn(async move { let language = registry.language_for_name(&language_name.0).await.ok()?; let toolchains = language.toolchain_lister()?; Some(toolchains.list(root.to_path_buf(), project_env).await) }) .await }) } pub(crate) fn active_toolchain( &self, worktree_id: WorktreeId, language_name: LanguageName, _: &AppContext, ) -> Task> { Task::ready( self.active_toolchains .get(&(worktree_id, language_name)) .cloned(), ) } } struct RemoteToolchainStore { client: AnyProtoClient, project_id: u64, } impl RemoteToolchainStore { pub(crate) fn activate_toolchain( &self, worktree_id: WorktreeId, toolchain: Toolchain, cx: &AppContext, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); cx.spawn(move |_| async move { let _ = client .request(proto::ActivateToolchain { project_id, worktree_id: worktree_id.to_proto(), language_name: toolchain.language_name.into(), toolchain: Some(proto::Toolchain { name: toolchain.name.into(), path: toolchain.path.into(), raw_json: toolchain.as_json.to_string(), }), }) .await .log_err()?; Some(()) }) } pub(crate) fn list_toolchains( &self, worktree_id: WorktreeId, language_name: LanguageName, cx: &AppContext, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); cx.spawn(move |_| async move { let response = client .request(proto::ListToolchains { project_id, worktree_id: worktree_id.to_proto(), language_name: language_name.clone().into(), }) .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(); Some(ToolchainList { toolchains, default: None, groups, }) }) } pub(crate) fn active_toolchain( &self, worktree_id: WorktreeId, language_name: LanguageName, cx: &AppContext, ) -> Task> { let project_id = self.project_id; let client = self.client.clone(); cx.spawn(move |_| async move { let response = client .request(proto::ActiveToolchain { project_id, worktree_id: worktree_id.to_proto(), language_name: language_name.clone().into(), }) .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()?, }) }) }) } }