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