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