1use std::sync::Arc;
2
3use editor::Editor;
4use gpui::{
5 AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription,
6 Task, WeakEntity, Window, div,
7};
8use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
9use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
10use ui::{Button, ButtonCommon, Clickable, LabelSize, SharedString, Tooltip};
11use util::{maybe, rel_path::RelPath};
12use workspace::{StatusItemView, Workspace, item::ItemHandle};
13
14use crate::ToolchainSelector;
15
16pub struct ActiveToolchain {
17 active_toolchain: Option<Toolchain>,
18 term: SharedString,
19 workspace: WeakEntity<Workspace>,
20 active_buffer: Option<(WorktreeId, WeakEntity<Buffer>, Subscription)>,
21 _update_toolchain_task: Task<Option<()>>,
22}
23
24impl ActiveToolchain {
25 pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
26 if let Some(store) = workspace.project().read(cx).toolchain_store() {
27 cx.subscribe_in(
28 &store,
29 window,
30 |this, _, _: &ToolchainStoreEvent, window, cx| {
31 let editor = this
32 .workspace
33 .update(cx, |workspace, cx| {
34 workspace
35 .active_item(cx)
36 .and_then(|item| item.downcast::<Editor>())
37 })
38 .ok()
39 .flatten();
40 if let Some(editor) = editor {
41 this.update_lister(editor, window, cx);
42 }
43 },
44 )
45 .detach();
46 }
47 Self {
48 active_toolchain: None,
49 active_buffer: None,
50 term: SharedString::new_static("Toolchain"),
51 workspace: workspace.weak_handle(),
52
53 _update_toolchain_task: Self::spawn_tracker_task(window, cx),
54 }
55 }
56 fn spawn_tracker_task(window: &mut Window, cx: &mut Context<Self>) -> Task<Option<()>> {
57 cx.spawn_in(window, async move |this, cx| {
58 let did_set_toolchain = maybe!(async {
59 let active_file = this
60 .read_with(cx, |this, _| {
61 this.active_buffer
62 .as_ref()
63 .map(|(_, buffer, _)| buffer.clone())
64 })
65 .ok()
66 .flatten()?;
67 let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?;
68 let language_name = active_file
69 .read_with(cx, |this, _| Some(this.language()?.name()))
70 .ok()
71 .flatten()?;
72 let meta = workspace
73 .update(cx, |workspace, cx| {
74 let languages = workspace.project().read(cx).languages();
75 Project::toolchain_metadata(languages.clone(), language_name.clone())
76 })
77 .ok()?
78 .await?;
79 let _ = this.update(cx, |this, cx| {
80 this.term = meta.term;
81 cx.notify();
82 });
83 let (worktree_id, path) = active_file
84 .update(cx, |this, cx| {
85 this.file().and_then(|file| {
86 Some((file.worktree_id(cx), file.path().parent()?.into()))
87 })
88 })
89 .ok()
90 .flatten()?;
91 let toolchain =
92 Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?;
93 this.update(cx, |this, cx| {
94 this.active_toolchain = Some(toolchain);
95
96 cx.notify();
97 })
98 .ok()
99 })
100 .await
101 .is_some();
102 if !did_set_toolchain {
103 this.update(cx, |this, cx| {
104 this.active_toolchain = None;
105 cx.notify();
106 })
107 .ok();
108 }
109 did_set_toolchain.then_some(())
110 })
111 }
112
113 fn update_lister(
114 &mut self,
115 editor: Entity<Editor>,
116 window: &mut Window,
117 cx: &mut Context<Self>,
118 ) {
119 let editor = editor.read(cx);
120 if let Some((_, buffer, _)) = editor.active_excerpt(cx)
121 && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx))
122 {
123 let subscription = cx.subscribe_in(
124 &buffer,
125 window,
126 |this, _, event: &BufferEvent, window, cx| {
127 if matches!(event, BufferEvent::LanguageChanged(_)) {
128 this._update_toolchain_task = Self::spawn_tracker_task(window, cx);
129 }
130 },
131 );
132 self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription));
133 self._update_toolchain_task = Self::spawn_tracker_task(window, cx);
134 }
135
136 cx.notify();
137 }
138
139 fn active_toolchain(
140 workspace: WeakEntity<Workspace>,
141 worktree_id: WorktreeId,
142 relative_path: Arc<RelPath>,
143 language_name: LanguageName,
144 cx: &mut AsyncWindowContext,
145 ) -> Task<Option<Toolchain>> {
146 cx.spawn(async move |cx| {
147 let workspace_id = workspace
148 .read_with(cx, |this, _| this.database_id())
149 .ok()
150 .flatten()?;
151 let selected_toolchain = workspace
152 .update(cx, |this, cx| {
153 this.project().read(cx).active_toolchain(
154 ProjectPath {
155 worktree_id,
156 path: relative_path.clone(),
157 },
158 language_name.clone(),
159 cx,
160 )
161 })
162 .ok()?
163 .await;
164 if let Some(toolchain) = selected_toolchain {
165 Some(toolchain)
166 } else {
167 let project = workspace
168 .read_with(cx, |this, _| this.project().clone())
169 .ok()?;
170 let Toolchains {
171 toolchains,
172 root_path: relative_path,
173 user_toolchains,
174 } = cx
175 .update(|_, cx| {
176 project.read(cx).available_toolchains(
177 ProjectPath {
178 worktree_id,
179 path: relative_path.clone(),
180 },
181 language_name,
182 cx,
183 )
184 })
185 .ok()?
186 .await?;
187 // Since we don't have a selected toolchain, pick one for user here.
188 let default_choice = user_toolchains
189 .iter()
190 .find_map(|(scope, toolchains)| {
191 if scope == &ToolchainScope::Global {
192 // Ignore global toolchains when making a default choice. They're unlikely to be the right choice.
193 None
194 } else {
195 toolchains.first()
196 }
197 })
198 .or_else(|| toolchains.toolchains.first())
199 .cloned();
200 if let Some(toolchain) = &default_choice {
201 let worktree_root_path = project
202 .read_with(cx, |this, cx| {
203 this.worktree_for_id(worktree_id, cx)
204 .map(|worktree| worktree.read(cx).abs_path())
205 })
206 .ok()
207 .flatten()?;
208 workspace::WORKSPACE_DB
209 .set_toolchain(
210 workspace_id,
211 worktree_root_path,
212 relative_path.clone(),
213 toolchain.clone(),
214 )
215 .await
216 .ok()?;
217 project
218 .update(cx, |this, cx| {
219 this.activate_toolchain(
220 ProjectPath {
221 worktree_id,
222 path: relative_path,
223 },
224 toolchain.clone(),
225 cx,
226 )
227 })
228 .ok()?
229 .await;
230 }
231
232 default_choice
233 }
234 })
235 }
236}
237
238impl Render for ActiveToolchain {
239 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
240 let Some(active_toolchain) = self.active_toolchain.as_ref() else {
241 return div().hidden();
242 };
243
244 div().child(
245 Button::new("change-toolchain", active_toolchain.name.clone())
246 .label_size(LabelSize::Small)
247 .on_click(cx.listener(|this, _, window, cx| {
248 if let Some(workspace) = this.workspace.upgrade() {
249 workspace.update(cx, |workspace, cx| {
250 ToolchainSelector::toggle(workspace, window, cx)
251 });
252 }
253 }))
254 .tooltip(Tooltip::text(format!("Select {}", &self.term))),
255 )
256 }
257}
258
259impl StatusItemView for ActiveToolchain {
260 fn set_active_pane_item(
261 &mut self,
262 active_pane_item: Option<&dyn ItemHandle>,
263 window: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 if let Some(editor) = active_pane_item.and_then(|item| item.downcast::<Editor>()) {
267 self.update_lister(editor, window, cx);
268 }
269 cx.notify();
270 }
271}