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