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