toolchain_store.rs

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