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