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