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