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(
335                        Some(path.worktree_id),
336                        Some(Arc::from(abs_path.as_path())),
337                        cx,
338                    )
339                })
340                .ok()?
341                .await;
342
343            cx.background_spawn(async move {
344                let language = registry
345                    .language_for_name(language_name.as_ref())
346                    .await
347                    .ok()?;
348                let toolchains = language.toolchain_lister()?;
349                Some(toolchains.list(abs_path.to_path_buf(), project_env).await)
350            })
351            .await
352        })
353    }
354    pub(crate) fn active_toolchain(
355        &self,
356        path: ProjectPath,
357        language_name: LanguageName,
358        _: &App,
359    ) -> Task<Option<Toolchain>> {
360        let ancestors = path.path.ancestors();
361        Task::ready(
362            self.active_toolchains
363                .get(&(path.worktree_id, language_name))
364                .and_then(|paths| {
365                    ancestors
366                        .into_iter()
367                        .find_map(|root_path| paths.get(root_path))
368                })
369                .cloned(),
370        )
371    }
372}
373struct RemoteToolchainStore {
374    client: AnyProtoClient,
375    project_id: u64,
376}
377
378impl RemoteToolchainStore {
379    pub(crate) fn activate_toolchain(
380        &self,
381        project_path: ProjectPath,
382        toolchain: Toolchain,
383        cx: &App,
384    ) -> Task<Option<()>> {
385        let project_id = self.project_id;
386        let client = self.client.clone();
387        cx.background_spawn(async move {
388            let path = PathBuf::from(toolchain.path.to_string());
389            let _ = client
390                .request(proto::ActivateToolchain {
391                    project_id,
392                    worktree_id: project_path.worktree_id.to_proto(),
393                    language_name: toolchain.language_name.into(),
394                    toolchain: Some(proto::Toolchain {
395                        name: toolchain.name.into(),
396                        path: path.to_proto(),
397                        raw_json: toolchain.as_json.to_string(),
398                    }),
399                    path: Some(project_path.path.to_string_lossy().into_owned()),
400                })
401                .await
402                .log_err()?;
403            Some(())
404        })
405    }
406
407    pub(crate) fn list_toolchains(
408        &self,
409        path: ProjectPath,
410        language_name: LanguageName,
411        cx: &App,
412    ) -> Task<Option<ToolchainList>> {
413        let project_id = self.project_id;
414        let client = self.client.clone();
415        cx.background_spawn(async move {
416            let response = client
417                .request(proto::ListToolchains {
418                    project_id,
419                    worktree_id: path.worktree_id.to_proto(),
420                    language_name: language_name.clone().into(),
421                    path: Some(path.path.to_string_lossy().into_owned()),
422                })
423                .await
424                .log_err()?;
425            if !response.has_values {
426                return None;
427            }
428            let toolchains = response
429                .toolchains
430                .into_iter()
431                .filter_map(|toolchain| {
432                    Some(Toolchain {
433                        language_name: language_name.clone(),
434                        name: toolchain.name.into(),
435                        // todo(windows)
436                        // Do we need to convert path to native string?
437                        path: PathBuf::from_proto(toolchain.path)
438                            .to_string_lossy()
439                            .to_string()
440                            .into(),
441                        as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
442                    })
443                })
444                .collect();
445            let groups = response
446                .groups
447                .into_iter()
448                .filter_map(|group| {
449                    Some((usize::try_from(group.start_index).ok()?, group.name.into()))
450                })
451                .collect();
452            Some(ToolchainList {
453                toolchains,
454                default: None,
455                groups,
456            })
457        })
458    }
459    pub(crate) fn active_toolchain(
460        &self,
461        path: ProjectPath,
462        language_name: LanguageName,
463        cx: &App,
464    ) -> Task<Option<Toolchain>> {
465        let project_id = self.project_id;
466        let client = self.client.clone();
467        cx.background_spawn(async move {
468            let response = client
469                .request(proto::ActiveToolchain {
470                    project_id,
471                    worktree_id: path.worktree_id.to_proto(),
472                    language_name: language_name.clone().into(),
473                    path: Some(path.path.to_string_lossy().into_owned()),
474                })
475                .await
476                .log_err()?;
477
478            response.toolchain.and_then(|toolchain| {
479                Some(Toolchain {
480                    language_name: language_name.clone(),
481                    name: toolchain.name.into(),
482                    // todo(windows)
483                    // Do we need to convert path to native string?
484                    path: PathBuf::from_proto(toolchain.path)
485                        .to_string_lossy()
486                        .to_string()
487                        .into(),
488                    as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
489                })
490            })
491        })
492    }
493}