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