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 worktree = this
342                .update(cx, |this, cx| {
343                    let store = this.worktree_store.read(cx);
344                    store.worktree_for_id(path.worktree_id, cx)
345                })
346                .ok()
347                .flatten()?;
348            let snapshot = worktree
349                .read_with(cx, |worktree, _| worktree.snapshot())
350                .ok()?;
351            let worktree_id = snapshot.id();
352            let worktree_root = snapshot.abs_path().to_path_buf();
353            let relative_path = manifest_tree
354                .update(cx, |this, cx| {
355                    this.root_for_path(
356                        path,
357                        &mut std::iter::once(manifest_name.clone()),
358                        Arc::new(ManifestQueryDelegate::new(snapshot)),
359                        cx,
360                    )
361                })
362                .ok()?
363                .remove(&manifest_name)
364                .unwrap_or_else(|| ProjectPath {
365                    path: Arc::from(Path::new("")),
366                    worktree_id,
367                });
368            let abs_path = worktree
369                .update(cx, |this, _| this.absolutize(&relative_path.path).ok())
370                .ok()
371                .flatten()?;
372
373            let project_env = environment
374                .update(cx, |environment, cx| {
375                    environment.get_directory_environment(abs_path.as_path().into(), cx)
376                })
377                .ok()?
378                .await;
379
380            cx.background_spawn(async move {
381                Some((
382                    toolchains
383                        .list(
384                            worktree_root,
385                            Some(relative_path.path.clone())
386                                .filter(|_| *relative_path.path != *Path::new("")),
387                            project_env,
388                        )
389                        .await,
390                    relative_path.path,
391                ))
392            })
393            .await
394        })
395    }
396    pub(crate) fn active_toolchain(
397        &self,
398        path: ProjectPath,
399        language_name: LanguageName,
400        _: &App,
401    ) -> Task<Option<Toolchain>> {
402        let ancestors = path.path.ancestors();
403        Task::ready(
404            self.active_toolchains
405                .get(&(path.worktree_id, language_name))
406                .and_then(|paths| {
407                    ancestors
408                        .into_iter()
409                        .find_map(|root_path| paths.get(root_path))
410                })
411                .cloned(),
412        )
413    }
414}
415struct RemoteToolchainStore {
416    client: AnyProtoClient,
417    project_id: u64,
418}
419
420impl RemoteToolchainStore {
421    pub(crate) fn activate_toolchain(
422        &self,
423        project_path: ProjectPath,
424        toolchain: Toolchain,
425        cx: &App,
426    ) -> Task<Option<()>> {
427        let project_id = self.project_id;
428        let client = self.client.clone();
429        cx.background_spawn(async move {
430            let path = PathBuf::from(toolchain.path.to_string());
431            let _ = client
432                .request(proto::ActivateToolchain {
433                    project_id,
434                    worktree_id: project_path.worktree_id.to_proto(),
435                    language_name: toolchain.language_name.into(),
436                    toolchain: Some(proto::Toolchain {
437                        name: toolchain.name.into(),
438                        path: path.to_proto(),
439                        raw_json: toolchain.as_json.to_string(),
440                    }),
441                    path: Some(project_path.path.to_string_lossy().into_owned()),
442                })
443                .await
444                .log_err()?;
445            Some(())
446        })
447    }
448
449    pub(crate) fn list_toolchains(
450        &self,
451        path: ProjectPath,
452        language_name: LanguageName,
453        cx: &App,
454    ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
455        let project_id = self.project_id;
456        let client = self.client.clone();
457        cx.background_spawn(async move {
458            let response = client
459                .request(proto::ListToolchains {
460                    project_id,
461                    worktree_id: path.worktree_id.to_proto(),
462                    language_name: language_name.clone().into(),
463                    path: Some(path.path.to_string_lossy().into_owned()),
464                })
465                .await
466                .log_err()?;
467            if !response.has_values {
468                return None;
469            }
470            let toolchains = response
471                .toolchains
472                .into_iter()
473                .filter_map(|toolchain| {
474                    Some(Toolchain {
475                        language_name: language_name.clone(),
476                        name: toolchain.name.into(),
477                        // todo(windows)
478                        // Do we need to convert path to native string?
479                        path: PathBuf::from_proto(toolchain.path)
480                            .to_string_lossy()
481                            .to_string()
482                            .into(),
483                        as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
484                    })
485                })
486                .collect();
487            let groups = response
488                .groups
489                .into_iter()
490                .filter_map(|group| {
491                    Some((usize::try_from(group.start_index).ok()?, group.name.into()))
492                })
493                .collect();
494            let relative_path = Arc::from(Path::new(
495                response
496                    .relative_worktree_path
497                    .as_deref()
498                    .unwrap_or_default(),
499            ));
500            Some((
501                ToolchainList {
502                    toolchains,
503                    default: None,
504                    groups,
505                },
506                relative_path,
507            ))
508        })
509    }
510    pub(crate) fn active_toolchain(
511        &self,
512        path: ProjectPath,
513        language_name: LanguageName,
514        cx: &App,
515    ) -> Task<Option<Toolchain>> {
516        let project_id = self.project_id;
517        let client = self.client.clone();
518        cx.background_spawn(async move {
519            let response = client
520                .request(proto::ActiveToolchain {
521                    project_id,
522                    worktree_id: path.worktree_id.to_proto(),
523                    language_name: language_name.clone().into(),
524                    path: Some(path.path.to_string_lossy().into_owned()),
525                })
526                .await
527                .log_err()?;
528
529            response.toolchain.and_then(|toolchain| {
530                Some(Toolchain {
531                    language_name: language_name.clone(),
532                    name: toolchain.name.into(),
533                    // todo(windows)
534                    // Do we need to convert path to native string?
535                    path: PathBuf::from_proto(toolchain.path)
536                        .to_string_lossy()
537                        .to_string()
538                        .into(),
539                    as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
540                })
541            })
542        })
543    }
544}