toolchain_store.rs

  1use std::{path::PathBuf, str::FromStr, sync::Arc};
  2
  3use anyhow::{bail, Result};
  4
  5use async_trait::async_trait;
  6use collections::BTreeMap;
  7use gpui::{
  8    App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
  9};
 10use language::{LanguageName, LanguageRegistry, LanguageToolchainStore, Toolchain, ToolchainList};
 11use rpc::{
 12    proto::{self, FromProto, ToProto},
 13    AnyProtoClient, TypedEnvelope,
 14};
 15use settings::WorktreeId;
 16use util::ResultExt as _;
 17
 18use crate::{worktree_store::WorktreeStore, ProjectEnvironment};
 19
 20pub struct ToolchainStore(ToolchainStoreInner);
 21enum ToolchainStoreInner {
 22    Local(
 23        Entity<LocalToolchainStore>,
 24        #[allow(dead_code)] Subscription,
 25    ),
 26    Remote(Entity<RemoteToolchainStore>),
 27}
 28
 29impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
 30impl ToolchainStore {
 31    pub fn init(client: &AnyProtoClient) {
 32        client.add_entity_request_handler(Self::handle_activate_toolchain);
 33        client.add_entity_request_handler(Self::handle_list_toolchains);
 34        client.add_entity_request_handler(Self::handle_active_toolchain);
 35    }
 36
 37    pub fn local(
 38        languages: Arc<LanguageRegistry>,
 39        worktree_store: Entity<WorktreeStore>,
 40        project_environment: Entity<ProjectEnvironment>,
 41        cx: &mut Context<Self>,
 42    ) -> Self {
 43        let entity = cx.new(|_| LocalToolchainStore {
 44            languages,
 45            worktree_store,
 46            project_environment,
 47            active_toolchains: Default::default(),
 48        });
 49        let subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
 50            cx.emit(e.clone())
 51        });
 52        Self(ToolchainStoreInner::Local(entity, subscription))
 53    }
 54    pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut App) -> Self {
 55        Self(ToolchainStoreInner::Remote(
 56            cx.new(|_| RemoteToolchainStore { client, project_id }),
 57        ))
 58    }
 59    pub(crate) fn activate_toolchain(
 60        &self,
 61        worktree_id: WorktreeId,
 62        toolchain: Toolchain,
 63        cx: &mut App,
 64    ) -> Task<Option<()>> {
 65        match &self.0 {
 66            ToolchainStoreInner::Local(local, _) => local.update(cx, |this, cx| {
 67                this.activate_toolchain(worktree_id, toolchain, cx)
 68            }),
 69            ToolchainStoreInner::Remote(remote) => {
 70                remote
 71                    .read(cx)
 72                    .activate_toolchain(worktree_id, toolchain, cx)
 73            }
 74        }
 75    }
 76    pub(crate) fn list_toolchains(
 77        &self,
 78        worktree_id: WorktreeId,
 79        language_name: LanguageName,
 80        cx: &App,
 81    ) -> Task<Option<ToolchainList>> {
 82        match &self.0 {
 83            ToolchainStoreInner::Local(local, _) => {
 84                local
 85                    .read(cx)
 86                    .list_toolchains(worktree_id, language_name, cx)
 87            }
 88            ToolchainStoreInner::Remote(remote) => {
 89                remote
 90                    .read(cx)
 91                    .list_toolchains(worktree_id, language_name, cx)
 92            }
 93        }
 94    }
 95    pub(crate) fn active_toolchain(
 96        &self,
 97        worktree_id: WorktreeId,
 98        language_name: LanguageName,
 99        cx: &App,
100    ) -> Task<Option<Toolchain>> {
101        match &self.0 {
102            ToolchainStoreInner::Local(local, _) => {
103                local
104                    .read(cx)
105                    .active_toolchain(worktree_id, language_name, cx)
106            }
107            ToolchainStoreInner::Remote(remote) => {
108                remote
109                    .read(cx)
110                    .active_toolchain(worktree_id, language_name, cx)
111            }
112        }
113    }
114    async fn handle_activate_toolchain(
115        this: Entity<Self>,
116        envelope: TypedEnvelope<proto::ActivateToolchain>,
117        mut cx: AsyncApp,
118    ) -> Result<proto::Ack> {
119        this.update(&mut cx, |this, cx| {
120            let language_name = LanguageName::from_proto(envelope.payload.language_name);
121            let Some(toolchain) = envelope.payload.toolchain else {
122                bail!("Missing `toolchain` in payload");
123            };
124            let toolchain = Toolchain {
125                name: toolchain.name.into(),
126                // todo(windows)
127                // Do we need to convert path to native string?
128                path: PathBuf::from(toolchain.path).to_proto().into(),
129                as_json: serde_json::Value::from_str(&toolchain.raw_json)?,
130                language_name,
131            };
132            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
133            Ok(this.activate_toolchain(worktree_id, toolchain, cx))
134        })??
135        .await;
136        Ok(proto::Ack {})
137    }
138    async fn handle_active_toolchain(
139        this: Entity<Self>,
140        envelope: TypedEnvelope<proto::ActiveToolchain>,
141        mut cx: AsyncApp,
142    ) -> Result<proto::ActiveToolchainResponse> {
143        let toolchain = this
144            .update(&mut cx, |this, cx| {
145                let language_name = LanguageName::from_proto(envelope.payload.language_name);
146                let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
147                this.active_toolchain(worktree_id, language_name, cx)
148            })?
149            .await;
150
151        Ok(proto::ActiveToolchainResponse {
152            toolchain: toolchain.map(|toolchain| {
153                let path = PathBuf::from(toolchain.path.to_string());
154                proto::Toolchain {
155                    name: toolchain.name.into(),
156                    path: path.to_proto(),
157                    raw_json: toolchain.as_json.to_string(),
158                }
159            }),
160        })
161    }
162
163    async fn handle_list_toolchains(
164        this: Entity<Self>,
165        envelope: TypedEnvelope<proto::ListToolchains>,
166        mut cx: AsyncApp,
167    ) -> Result<proto::ListToolchainsResponse> {
168        let toolchains = this
169            .update(&mut cx, |this, cx| {
170                let language_name = LanguageName::from_proto(envelope.payload.language_name);
171                let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
172                this.list_toolchains(worktree_id, language_name, cx)
173            })?
174            .await;
175        let has_values = toolchains.is_some();
176        let groups = if let Some(toolchains) = &toolchains {
177            toolchains
178                .groups
179                .iter()
180                .filter_map(|group| {
181                    Some(proto::ToolchainGroup {
182                        start_index: u64::try_from(group.0).ok()?,
183                        name: String::from(group.1.as_ref()),
184                    })
185                })
186                .collect()
187        } else {
188            vec![]
189        };
190        let toolchains = if let Some(toolchains) = toolchains {
191            toolchains
192                .toolchains
193                .into_iter()
194                .map(|toolchain| {
195                    let path = PathBuf::from(toolchain.path.to_string());
196                    proto::Toolchain {
197                        name: toolchain.name.to_string(),
198                        path: path.to_proto(),
199                        raw_json: toolchain.as_json.to_string(),
200                    }
201                })
202                .collect::<Vec<_>>()
203        } else {
204            vec![]
205        };
206
207        Ok(proto::ListToolchainsResponse {
208            has_values,
209            toolchains,
210            groups,
211        })
212    }
213    pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
214        match &self.0 {
215            ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
216            ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
217        }
218    }
219}
220
221struct LocalToolchainStore {
222    languages: Arc<LanguageRegistry>,
223    worktree_store: Entity<WorktreeStore>,
224    project_environment: Entity<ProjectEnvironment>,
225    active_toolchains: BTreeMap<(WorktreeId, LanguageName), Toolchain>,
226}
227
228#[async_trait(?Send)]
229impl language::LanguageToolchainStore for LocalStore {
230    async fn active_toolchain(
231        self: Arc<Self>,
232        worktree_id: WorktreeId,
233        language_name: LanguageName,
234        cx: &mut AsyncApp,
235    ) -> Option<Toolchain> {
236        self.0
237            .update(cx, |this, cx| {
238                this.active_toolchain(worktree_id, language_name, cx)
239            })
240            .ok()?
241            .await
242    }
243}
244
245#[async_trait(?Send)]
246impl language::LanguageToolchainStore for RemoteStore {
247    async fn active_toolchain(
248        self: Arc<Self>,
249        worktree_id: WorktreeId,
250        language_name: LanguageName,
251        cx: &mut AsyncApp,
252    ) -> Option<Toolchain> {
253        self.0
254            .update(cx, |this, cx| {
255                this.active_toolchain(worktree_id, language_name, cx)
256            })
257            .ok()?
258            .await
259    }
260}
261
262pub(crate) struct EmptyToolchainStore;
263#[async_trait(?Send)]
264impl language::LanguageToolchainStore for EmptyToolchainStore {
265    async fn active_toolchain(
266        self: Arc<Self>,
267        _: WorktreeId,
268        _: LanguageName,
269        _: &mut AsyncApp,
270    ) -> Option<Toolchain> {
271        None
272    }
273}
274struct LocalStore(WeakEntity<LocalToolchainStore>);
275struct RemoteStore(WeakEntity<RemoteToolchainStore>);
276
277#[derive(Clone)]
278pub(crate) enum ToolchainStoreEvent {
279    ToolchainActivated,
280}
281
282impl EventEmitter<ToolchainStoreEvent> for LocalToolchainStore {}
283
284impl LocalToolchainStore {
285    pub(crate) fn activate_toolchain(
286        &self,
287        worktree_id: WorktreeId,
288        toolchain: Toolchain,
289        cx: &mut Context<Self>,
290    ) -> Task<Option<()>> {
291        cx.spawn(move |this, mut cx| async move {
292            this.update(&mut cx, |this, cx| {
293                this.active_toolchains.insert(
294                    (worktree_id, toolchain.language_name.clone()),
295                    toolchain.clone(),
296                );
297                cx.emit(ToolchainStoreEvent::ToolchainActivated);
298            })
299            .ok();
300            Some(())
301        })
302    }
303    pub(crate) fn list_toolchains(
304        &self,
305        worktree_id: WorktreeId,
306        language_name: LanguageName,
307        cx: &App,
308    ) -> Task<Option<ToolchainList>> {
309        let registry = self.languages.clone();
310        let Some(root) = self
311            .worktree_store
312            .read(cx)
313            .worktree_for_id(worktree_id, cx)
314            .map(|worktree| worktree.read(cx).abs_path())
315        else {
316            return Task::ready(None);
317        };
318
319        let environment = self.project_environment.clone();
320        cx.spawn(|mut cx| async move {
321            let project_env = environment
322                .update(&mut cx, |environment, cx| {
323                    environment.get_environment(Some(worktree_id), Some(root.clone()), cx)
324                })
325                .ok()?
326                .await;
327
328            cx.background_executor()
329                .spawn(async move {
330                    let language = registry
331                        .language_for_name(language_name.as_ref())
332                        .await
333                        .ok()?;
334                    let toolchains = language.toolchain_lister()?;
335                    Some(toolchains.list(root.to_path_buf(), project_env).await)
336                })
337                .await
338        })
339    }
340    pub(crate) fn active_toolchain(
341        &self,
342        worktree_id: WorktreeId,
343        language_name: LanguageName,
344        _: &App,
345    ) -> Task<Option<Toolchain>> {
346        Task::ready(
347            self.active_toolchains
348                .get(&(worktree_id, language_name))
349                .cloned(),
350        )
351    }
352}
353struct RemoteToolchainStore {
354    client: AnyProtoClient,
355    project_id: u64,
356}
357
358impl RemoteToolchainStore {
359    pub(crate) fn activate_toolchain(
360        &self,
361        worktree_id: WorktreeId,
362        toolchain: Toolchain,
363        cx: &App,
364    ) -> Task<Option<()>> {
365        let project_id = self.project_id;
366        let client = self.client.clone();
367        cx.spawn(move |_| async move {
368            let path = PathBuf::from(toolchain.path.to_string());
369            let _ = client
370                .request(proto::ActivateToolchain {
371                    project_id,
372                    worktree_id: worktree_id.to_proto(),
373                    language_name: toolchain.language_name.into(),
374                    toolchain: Some(proto::Toolchain {
375                        name: toolchain.name.into(),
376                        path: path.to_proto(),
377                        raw_json: toolchain.as_json.to_string(),
378                    }),
379                })
380                .await
381                .log_err()?;
382            Some(())
383        })
384    }
385
386    pub(crate) fn list_toolchains(
387        &self,
388        worktree_id: WorktreeId,
389        language_name: LanguageName,
390        cx: &App,
391    ) -> Task<Option<ToolchainList>> {
392        let project_id = self.project_id;
393        let client = self.client.clone();
394        cx.spawn(move |_| async move {
395            let response = client
396                .request(proto::ListToolchains {
397                    project_id,
398                    worktree_id: worktree_id.to_proto(),
399                    language_name: language_name.clone().into(),
400                })
401                .await
402                .log_err()?;
403            if !response.has_values {
404                return None;
405            }
406            let toolchains = response
407                .toolchains
408                .into_iter()
409                .filter_map(|toolchain| {
410                    Some(Toolchain {
411                        language_name: language_name.clone(),
412                        name: toolchain.name.into(),
413                        // todo(windows)
414                        // Do we need to convert path to native string?
415                        path: PathBuf::from_proto(toolchain.path)
416                            .to_string_lossy()
417                            .to_string()
418                            .into(),
419                        as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
420                    })
421                })
422                .collect();
423            let groups = response
424                .groups
425                .into_iter()
426                .filter_map(|group| {
427                    Some((usize::try_from(group.start_index).ok()?, group.name.into()))
428                })
429                .collect();
430            Some(ToolchainList {
431                toolchains,
432                default: None,
433                groups,
434            })
435        })
436    }
437    pub(crate) fn active_toolchain(
438        &self,
439        worktree_id: WorktreeId,
440        language_name: LanguageName,
441        cx: &App,
442    ) -> Task<Option<Toolchain>> {
443        let project_id = self.project_id;
444        let client = self.client.clone();
445        cx.spawn(move |_| async move {
446            let response = client
447                .request(proto::ActiveToolchain {
448                    project_id,
449                    worktree_id: worktree_id.to_proto(),
450                    language_name: language_name.clone().into(),
451                })
452                .await
453                .log_err()?;
454
455            response.toolchain.and_then(|toolchain| {
456                Some(Toolchain {
457                    language_name: language_name.clone(),
458                    name: toolchain.name.into(),
459                    // todo(windows)
460                    // Do we need to convert path to native string?
461                    path: PathBuf::from_proto(toolchain.path)
462                        .to_string_lossy()
463                        .to_string()
464                        .into(),
465                    as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
466                })
467            })
468        })
469    }
470}