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