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