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