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(abs_path) = self.worktree_store.read(cx).absolutize(&path, cx) else {
321 return Task::ready(None);
322 };
323 let environment = self.project_environment.clone();
324 cx.spawn(async move |cx| {
325 let project_env = environment
326 .update(cx, |environment, cx| {
327 environment.get_directory_environment(abs_path.as_path().into(), cx)
328 })
329 .ok()?
330 .await;
331
332 cx.background_spawn(async move {
333 let language = registry
334 .language_for_name(language_name.as_ref())
335 .await
336 .ok()?;
337 let toolchains = language.toolchain_lister()?;
338 Some(toolchains.list(abs_path.to_path_buf(), project_env).await)
339 })
340 .await
341 })
342 }
343 pub(crate) fn active_toolchain(
344 &self,
345 path: ProjectPath,
346 language_name: LanguageName,
347 _: &App,
348 ) -> Task<Option<Toolchain>> {
349 let ancestors = path.path.ancestors();
350 Task::ready(
351 self.active_toolchains
352 .get(&(path.worktree_id, language_name))
353 .and_then(|paths| {
354 ancestors
355 .into_iter()
356 .find_map(|root_path| paths.get(root_path))
357 })
358 .cloned(),
359 )
360 }
361}
362struct RemoteToolchainStore {
363 client: AnyProtoClient,
364 project_id: u64,
365}
366
367impl RemoteToolchainStore {
368 pub(crate) fn activate_toolchain(
369 &self,
370 project_path: ProjectPath,
371 toolchain: Toolchain,
372 cx: &App,
373 ) -> Task<Option<()>> {
374 let project_id = self.project_id;
375 let client = self.client.clone();
376 cx.background_spawn(async move {
377 let path = PathBuf::from(toolchain.path.to_string());
378 let _ = client
379 .request(proto::ActivateToolchain {
380 project_id,
381 worktree_id: project_path.worktree_id.to_proto(),
382 language_name: toolchain.language_name.into(),
383 toolchain: Some(proto::Toolchain {
384 name: toolchain.name.into(),
385 path: path.to_proto(),
386 raw_json: toolchain.as_json.to_string(),
387 }),
388 path: Some(project_path.path.to_string_lossy().into_owned()),
389 })
390 .await
391 .log_err()?;
392 Some(())
393 })
394 }
395
396 pub(crate) fn list_toolchains(
397 &self,
398 path: ProjectPath,
399 language_name: LanguageName,
400 cx: &App,
401 ) -> Task<Option<ToolchainList>> {
402 let project_id = self.project_id;
403 let client = self.client.clone();
404 cx.background_spawn(async move {
405 let response = client
406 .request(proto::ListToolchains {
407 project_id,
408 worktree_id: path.worktree_id.to_proto(),
409 language_name: language_name.clone().into(),
410 path: Some(path.path.to_string_lossy().into_owned()),
411 })
412 .await
413 .log_err()?;
414 if !response.has_values {
415 return None;
416 }
417 let toolchains = response
418 .toolchains
419 .into_iter()
420 .filter_map(|toolchain| {
421 Some(Toolchain {
422 language_name: language_name.clone(),
423 name: toolchain.name.into(),
424 // todo(windows)
425 // Do we need to convert path to native string?
426 path: PathBuf::from_proto(toolchain.path)
427 .to_string_lossy()
428 .to_string()
429 .into(),
430 as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
431 })
432 })
433 .collect();
434 let groups = response
435 .groups
436 .into_iter()
437 .filter_map(|group| {
438 Some((usize::try_from(group.start_index).ok()?, group.name.into()))
439 })
440 .collect();
441 Some(ToolchainList {
442 toolchains,
443 default: None,
444 groups,
445 })
446 })
447 }
448 pub(crate) fn active_toolchain(
449 &self,
450 path: ProjectPath,
451 language_name: LanguageName,
452 cx: &App,
453 ) -> Task<Option<Toolchain>> {
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::ActiveToolchain {
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
467 response.toolchain.and_then(|toolchain| {
468 Some(Toolchain {
469 language_name: language_name.clone(),
470 name: toolchain.name.into(),
471 // todo(windows)
472 // Do we need to convert path to native string?
473 path: PathBuf::from_proto(toolchain.path)
474 .to_string_lossy()
475 .to_string()
476 .into(),
477 as_json: serde_json::Value::from_str(&toolchain.raw_json).ok()?,
478 })
479 })
480 })
481 }
482}