1mod active_toolchain;
2
3pub use active_toolchain::ActiveToolchain;
4use editor::Editor;
5use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
6use gpui::{
7 actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
8 ParentElement, Render, Styled, Task, View, ViewContext, VisualContext, WeakView,
9};
10use language::{LanguageName, Toolchain, ToolchainList};
11use picker::{Picker, PickerDelegate};
12use project::{Project, WorktreeId};
13use std::{path::Path, sync::Arc};
14use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
15use util::ResultExt;
16use workspace::{ModalView, Workspace};
17
18actions!(toolchain, [Select]);
19
20pub fn init(cx: &mut AppContext) {
21 cx.observe_new_views(ToolchainSelector::register).detach();
22}
23
24pub struct ToolchainSelector {
25 picker: View<Picker<ToolchainSelectorDelegate>>,
26}
27
28impl ToolchainSelector {
29 fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
30 workspace.register_action(move |workspace, _: &Select, cx| {
31 Self::toggle(workspace, cx);
32 });
33 }
34
35 fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
36 let (_, buffer, _) = workspace
37 .active_item(cx)?
38 .act_as::<Editor>(cx)?
39 .read(cx)
40 .active_excerpt(cx)?;
41 let project = workspace.project().clone();
42
43 let language_name = buffer.read(cx).language()?.name();
44 let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
45 let worktree_root_path = project
46 .read(cx)
47 .worktree_for_id(worktree_id, cx)?
48 .read(cx)
49 .abs_path();
50 let workspace_id = workspace.database_id()?;
51 let weak = workspace.weak_handle();
52 cx.spawn(move |workspace, mut cx| async move {
53 let active_toolchain = workspace::WORKSPACE_DB
54 .toolchain(workspace_id, worktree_id, language_name.clone())
55 .await
56 .ok()
57 .flatten();
58 workspace
59 .update(&mut cx, |this, cx| {
60 this.toggle_modal(cx, move |cx| {
61 ToolchainSelector::new(
62 weak,
63 project,
64 active_toolchain,
65 worktree_id,
66 worktree_root_path,
67 language_name,
68 cx,
69 )
70 });
71 })
72 .ok();
73 })
74 .detach();
75
76 Some(())
77 }
78
79 fn new(
80 workspace: WeakView<Workspace>,
81 project: Model<Project>,
82 active_toolchain: Option<Toolchain>,
83 worktree_id: WorktreeId,
84 worktree_root: Arc<Path>,
85 language_name: LanguageName,
86 cx: &mut ViewContext<Self>,
87 ) -> Self {
88 let view = cx.view().downgrade();
89 let picker = cx.new_view(|cx| {
90 let delegate = ToolchainSelectorDelegate::new(
91 active_toolchain,
92 view,
93 workspace,
94 worktree_id,
95 worktree_root,
96 project,
97 language_name,
98 cx,
99 );
100 Picker::uniform_list(delegate, cx)
101 });
102 Self { picker }
103 }
104}
105
106impl Render for ToolchainSelector {
107 fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
108 v_flex().w(rems(34.)).child(self.picker.clone())
109 }
110}
111
112impl FocusableView for ToolchainSelector {
113 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
114 self.picker.focus_handle(cx)
115 }
116}
117
118impl EventEmitter<DismissEvent> for ToolchainSelector {}
119impl ModalView for ToolchainSelector {}
120
121pub struct ToolchainSelectorDelegate {
122 toolchain_selector: WeakView<ToolchainSelector>,
123 candidates: ToolchainList,
124 matches: Vec<StringMatch>,
125 selected_index: usize,
126 workspace: WeakView<Workspace>,
127 worktree_id: WorktreeId,
128 worktree_abs_path_root: Arc<Path>,
129 placeholder_text: Arc<str>,
130 _fetch_candidates_task: Task<Option<()>>,
131}
132
133impl ToolchainSelectorDelegate {
134 #[allow(clippy::too_many_arguments)]
135 fn new(
136 active_toolchain: Option<Toolchain>,
137 language_selector: WeakView<ToolchainSelector>,
138 workspace: WeakView<Workspace>,
139 worktree_id: WorktreeId,
140 worktree_abs_path_root: Arc<Path>,
141 project: Model<Project>,
142 language_name: LanguageName,
143 cx: &mut ViewContext<Picker<Self>>,
144 ) -> Self {
145 let _fetch_candidates_task = cx.spawn({
146 let project = project.clone();
147 move |this, mut cx| async move {
148 let term = project
149 .update(&mut cx, |this, _| {
150 Project::toolchain_term(this.languages().clone(), language_name.clone())
151 })
152 .ok()?
153 .await?;
154 let placeholder_text = format!("Select a {}…", term.to_lowercase()).into();
155 let _ = this.update(&mut cx, move |this, cx| {
156 this.delegate.placeholder_text = placeholder_text;
157 this.refresh_placeholder(cx);
158 });
159 let available_toolchains = project
160 .update(&mut cx, |this, cx| {
161 this.available_toolchains(worktree_id, language_name, cx)
162 })
163 .ok()?
164 .await?;
165
166 let _ = this.update(&mut cx, move |this, cx| {
167 this.delegate.candidates = available_toolchains;
168
169 if let Some(active_toolchain) = active_toolchain {
170 if let Some(position) = this
171 .delegate
172 .candidates
173 .toolchains
174 .iter()
175 .position(|toolchain| *toolchain == active_toolchain)
176 {
177 this.delegate.set_selected_index(position, cx);
178 }
179 }
180 this.update_matches(this.query(cx), cx);
181 });
182
183 Some(())
184 }
185 });
186 let placeholder_text = "Select a toolchain…".to_string().into();
187 Self {
188 toolchain_selector: language_selector,
189 candidates: Default::default(),
190 matches: vec![],
191 selected_index: 0,
192 workspace,
193 worktree_id,
194 worktree_abs_path_root,
195 placeholder_text,
196 _fetch_candidates_task,
197 }
198 }
199 fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString {
200 Path::new(&path.as_ref())
201 .strip_prefix(&worktree_root)
202 .ok()
203 .map(|suffix| Path::new(".").join(suffix))
204 .and_then(|path| path.to_str().map(String::from).map(SharedString::from))
205 .unwrap_or(path)
206 }
207}
208
209impl PickerDelegate for ToolchainSelectorDelegate {
210 type ListItem = ListItem;
211
212 fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
213 self.placeholder_text.clone()
214 }
215
216 fn match_count(&self) -> usize {
217 self.matches.len()
218 }
219
220 fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
221 if let Some(string_match) = self.matches.get(self.selected_index) {
222 let toolchain = self.candidates.toolchains[string_match.candidate_id].clone();
223 if let Some(workspace_id) = self
224 .workspace
225 .update(cx, |this, _| this.database_id())
226 .ok()
227 .flatten()
228 {
229 let workspace = self.workspace.clone();
230 let worktree_id = self.worktree_id;
231 cx.spawn(|_, mut cx| async move {
232 workspace::WORKSPACE_DB
233 .set_toolchain(workspace_id, worktree_id, toolchain.clone())
234 .await
235 .log_err();
236 workspace
237 .update(&mut cx, |this, cx| {
238 this.project().update(cx, |this, cx| {
239 this.activate_toolchain(worktree_id, toolchain, cx)
240 })
241 })
242 .ok()?
243 .await;
244 Some(())
245 })
246 .detach();
247 }
248 }
249 self.dismissed(cx);
250 }
251
252 fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
253 self.toolchain_selector
254 .update(cx, |_, cx| cx.emit(DismissEvent))
255 .log_err();
256 }
257
258 fn selected_index(&self) -> usize {
259 self.selected_index
260 }
261
262 fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
263 self.selected_index = ix;
264 }
265
266 fn update_matches(
267 &mut self,
268 query: String,
269 cx: &mut ViewContext<Picker<Self>>,
270 ) -> gpui::Task<()> {
271 let background = cx.background_executor().clone();
272 let candidates = self.candidates.clone();
273 let worktree_root_path = self.worktree_abs_path_root.clone();
274 cx.spawn(|this, mut cx| async move {
275 let matches = if query.is_empty() {
276 candidates
277 .toolchains
278 .into_iter()
279 .enumerate()
280 .map(|(index, candidate)| {
281 let path = Self::relativize_path(candidate.path, &worktree_root_path);
282 let string = format!("{}{}", candidate.name, path);
283 StringMatch {
284 candidate_id: index,
285 string,
286 positions: Vec::new(),
287 score: 0.0,
288 }
289 })
290 .collect()
291 } else {
292 let candidates = candidates
293 .toolchains
294 .into_iter()
295 .enumerate()
296 .map(|(candidate_id, toolchain)| {
297 let path = Self::relativize_path(toolchain.path, &worktree_root_path);
298 let string = format!("{}{}", toolchain.name, path);
299 StringMatchCandidate::new(candidate_id, &string)
300 })
301 .collect::<Vec<_>>();
302 match_strings(
303 &candidates,
304 &query,
305 false,
306 100,
307 &Default::default(),
308 background,
309 )
310 .await
311 };
312
313 this.update(&mut cx, |this, cx| {
314 let delegate = &mut this.delegate;
315 delegate.matches = matches;
316 delegate.selected_index = delegate
317 .selected_index
318 .min(delegate.matches.len().saturating_sub(1));
319 cx.notify();
320 })
321 .log_err();
322 })
323 }
324
325 fn render_match(
326 &self,
327 ix: usize,
328 selected: bool,
329 _: &mut ViewContext<Picker<Self>>,
330 ) -> Option<Self::ListItem> {
331 let mat = &self.matches[ix];
332 let toolchain = &self.candidates.toolchains[mat.candidate_id];
333
334 let label = toolchain.name.clone();
335 let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);
336 let (name_highlights, mut path_highlights) = mat
337 .positions
338 .iter()
339 .cloned()
340 .partition::<Vec<_>, _>(|index| *index < label.len());
341 path_highlights.iter_mut().for_each(|index| {
342 *index -= label.len();
343 });
344 Some(
345 ListItem::new(ix)
346 .inset(true)
347 .spacing(ListItemSpacing::Sparse)
348 .toggle_state(selected)
349 .child(HighlightedLabel::new(label, name_highlights))
350 .child(
351 HighlightedLabel::new(path, path_highlights)
352 .size(LabelSize::Small)
353 .color(Color::Muted),
354 ),
355 )
356 }
357}