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