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 worktree = this
342 .update(cx, |this, cx| {
343 let store = this.worktree_store.read(cx);
344 store.worktree_for_id(path.worktree_id, cx)
345 })
346 .ok()
347 .flatten()?;
348 let snapshot = worktree
349 .read_with(cx, |worktree, _| worktree.snapshot())
350 .ok()?;
351 let worktree_id = snapshot.id();
352 let worktree_root = snapshot.abs_path().to_path_buf();
353 let relative_path = manifest_tree
354 .update(cx, |this, cx| {
355 this.root_for_path(
356 path,
357 &mut std::iter::once(manifest_name.clone()),
358 Arc::new(ManifestQueryDelegate::new(snapshot)),
359 cx,
360 )
361 })
362 .ok()?
363 .remove(&manifest_name)
364 .unwrap_or_else(|| ProjectPath {
365 path: Arc::from(Path::new("")),
366 worktree_id,
367 });
368 let abs_path = worktree
369 .update(cx, |this, _| this.absolutize(&relative_path.path).ok())
370 .ok()
371 .flatten()?;
372
373 let project_env = environment
374 .update(cx, |environment, cx| {
375 environment.get_directory_environment(abs_path.as_path().into(), cx)
376 })
377 .ok()?
378 .await;
379
380 cx.background_spawn(async move {
381 Some((
382 toolchains
383 .list(
384 worktree_root,
385 Some(relative_path.path.clone())
386 .filter(|_| *relative_path.path != *Path::new("")),
387 project_env,
388 )
389 .await,
390 relative_path.path,
391 ))
392 })
393 .await
394 })
395 }
396 pub(crate) fn active_toolchain(
397 &self,
398 path: ProjectPath,
399 language_name: LanguageName,
400 _: &App,
401 ) -> Task<Option<Toolchain>> {
402 let ancestors = path.path.ancestors();
403 Task::ready(
404 self.active_toolchains
405 .get(&(path.worktree_id, language_name))
406 .and_then(|paths| {
407 ancestors
408 .into_iter()
409 .find_map(|root_path| paths.get(root_path))
410 })
411 .cloned(),
412 )
413 }
414}
415struct RemoteToolchainStore {
416 client: AnyProtoClient,
417 project_id: u64,
418}
419
420impl RemoteToolchainStore {
421 pub(crate) fn activate_toolchain(
422 &self,
423 project_path: ProjectPath,
424 toolchain: Toolchain,
425 cx: &App,
426 ) -> Task<Option<()>> {
427 let project_id = self.project_id;
428 let client = self.client.clone();
429 cx.background_spawn(async move {
430 let path = PathBuf::from(toolchain.path.to_string());
431 let _ = client
432 .request(proto::ActivateToolchain {
433 project_id,
434 worktree_id: project_path.worktree_id.to_proto(),
435 language_name: toolchain.language_name.into(),
436 toolchain: Some(proto::Toolchain {
437 name: toolchain.name.into(),
438 path: path.to_proto(),
439 raw_json: toolchain.as_json.to_string(),
440 }),
441 path: Some(project_path.path.to_string_lossy().into_owned()),
442 })
443 .await
444 .log_err()?;
445 Some(())
446 })
447 }
448
449 pub(crate) fn list_toolchains(
450 &self,
451 path: ProjectPath,
452 language_name: LanguageName,
453 cx: &App,
454 ) -> Task<Option<(ToolchainList, Arc<Path>)>> {
455 let project_id = self.project_id;
456 let client = self.client.clone();
457 cx.background_spawn(async move {
458 let response = client
459 .request(proto::ListToolchains {
460 project_id,
461 worktree_id: path.worktree_id.to_proto(),
462 language_name: language_name.clone().into(),
463 path: Some(path.path.to_string_lossy().into_owned()),
464 })
465 .await
466 .log_err()?;
467 if !response.has_values {
468 return None;
469 }
470 let toolchains = response
471 .toolchains
472 .into_iter()
473 .filter_map(|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 .collect();
487 let groups = response
488 .groups
489 .into_iter()
490 .filter_map(|group| {
491 Some((usize::try_from(group.start_index).ok()?, group.name.into()))
492 })
493 .collect();
494 let relative_path = Arc::from(Path::new(
495 response
496 .relative_worktree_path
497 .as_deref()
498 .unwrap_or_default(),
499 ));
500 Some((
501 ToolchainList {
502 toolchains,
503 default: None,
504 groups,
505 },
506 relative_path,
507 ))
508 })
509 }
510 pub(crate) fn active_toolchain(
511 &self,
512 path: ProjectPath,
513 language_name: LanguageName,
514 cx: &App,
515 ) -> Task<Option<Toolchain>> {
516 let project_id = self.project_id;
517 let client = self.client.clone();
518 cx.background_spawn(async move {
519 let response = client
520 .request(proto::ActiveToolchain {
521 project_id,
522 worktree_id: path.worktree_id.to_proto(),
523 language_name: language_name.clone().into(),
524 path: Some(path.path.to_string_lossy().into_owned()),
525 })
526 .await
527 .log_err()?;
528
529 response.toolchain.and_then(|toolchain| {
530 Some(Toolchain {
531 language_name: language_name.clone(),
532 name: toolchain.name.into(),
533 // todo(windows)
534 // Do we need to convert path to native string?
535 path: PathBuf::from_proto(toolchain.path)
536 .to_string_lossy()
537 .to_string()
538 .into(),
539 as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
540 })
541 })
542 })
543 }
544}