1mod active_toolchain;
2
3pub use active_toolchain::ActiveToolchain;
4use anyhow::Context as _;
5use convert_case::Casing as _;
6use editor::Editor;
7use file_finder::OpenPathDelegate;
8use futures::channel::oneshot;
9use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
10use gpui::{
11 Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
12 Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window,
13 actions, pulsating_between,
14};
15use language::{Language, LanguageName, Toolchain, ToolchainScope};
16use picker::{Picker, PickerDelegate};
17use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId};
18use std::{
19 borrow::Cow,
20 path::{Path, PathBuf},
21 sync::Arc,
22 time::Duration,
23};
24use ui::{
25 Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable,
26 NavigableEntry, prelude::*,
27};
28use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath};
29use workspace::{ModalView, Workspace};
30
31actions!(
32 toolchain,
33 [
34 /// Selects a toolchain for the current project.
35 Select,
36 /// Adds a new toolchain for the current project.
37 AddToolchain
38 ]
39);
40
41pub fn init(cx: &mut App) {
42 cx.observe_new(ToolchainSelector::register).detach();
43}
44
45pub struct ToolchainSelector {
46 state: State,
47 create_search_state: Arc<dyn Fn(&mut Window, &mut Context<Self>) -> SearchState + 'static>,
48 language: Option<Arc<Language>>,
49 project: Entity<Project>,
50 language_name: LanguageName,
51 worktree_id: WorktreeId,
52 relative_path: Arc<RelPath>,
53}
54
55#[derive(Clone)]
56struct SearchState {
57 picker: Entity<Picker<ToolchainSelectorDelegate>>,
58}
59
60struct AddToolchainState {
61 state: AddState,
62 project: Entity<Project>,
63 language_name: LanguageName,
64 root_path: ProjectPath,
65 weak: WeakEntity<ToolchainSelector>,
66 worktree_root_path: Arc<Path>,
67}
68
69struct ScopePickerState {
70 entries: [NavigableEntry; 3],
71 selected_scope: ToolchainScope,
72}
73
74#[expect(
75 dead_code,
76 reason = "These tasks have to be kept alive to run to completion"
77)]
78enum PathInputState {
79 WaitingForPath(Task<()>),
80 Resolving(Task<()>),
81}
82
83enum AddState {
84 Path {
85 picker: Entity<Picker<file_finder::OpenPathDelegate>>,
86 error: Option<Arc<str>>,
87 input_state: PathInputState,
88 _subscription: Subscription,
89 },
90 Name {
91 toolchain: Toolchain,
92 editor: Entity<Editor>,
93 scope_picker: ScopePickerState,
94 },
95}
96
97impl AddToolchainState {
98 fn new(
99 project: Entity<Project>,
100 language_name: LanguageName,
101 root_path: ProjectPath,
102 window: &mut Window,
103 cx: &mut Context<ToolchainSelector>,
104 ) -> anyhow::Result<Entity<Self>> {
105 let weak = cx.weak_entity();
106 let worktree_root_path = project
107 .read(cx)
108 .worktree_for_id(root_path.worktree_id, cx)
109 .map(|worktree| worktree.read(cx).abs_path())
110 .context("Could not find worktree")?;
111 Ok(cx.new(|cx| {
112 let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx);
113 let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx));
114
115 Self {
116 state: AddState::Path {
117 _subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
118 cx.stop_propagation();
119 }),
120 picker,
121 error: None,
122 input_state: Self::wait_for_path(rx, window, cx),
123 },
124 project,
125 language_name,
126 root_path,
127 weak,
128 worktree_root_path,
129 }
130 }))
131 }
132
133 fn create_path_browser_delegate(
134 project: Entity<Project>,
135 cx: &mut Context<Self>,
136 ) -> (OpenPathDelegate, oneshot::Receiver<Option<Vec<PathBuf>>>) {
137 let (tx, rx) = oneshot::channel();
138 let weak = cx.weak_entity();
139 let lister = OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, cx)
140 .show_hidden()
141 .with_footer(Arc::new(move |_, cx| {
142 let error = weak
143 .read_with(cx, |this, _| {
144 if let AddState::Path { error, .. } = &this.state {
145 error.clone()
146 } else {
147 None
148 }
149 })
150 .ok()
151 .flatten();
152 let is_loading = weak
153 .read_with(cx, |this, _| {
154 matches!(
155 this.state,
156 AddState::Path {
157 input_state: PathInputState::Resolving(_),
158 ..
159 }
160 )
161 })
162 .unwrap_or_default();
163 Some(
164 v_flex()
165 .child(Divider::horizontal())
166 .child(
167 h_flex()
168 .p_1()
169 .justify_between()
170 .gap_2()
171 .child(Label::new("Select Toolchain Path").color(Color::Muted).map(
172 |this| {
173 if is_loading {
174 this.with_animation(
175 "select-toolchain-label",
176 Animation::new(Duration::from_secs(2))
177 .repeat()
178 .with_easing(pulsating_between(0.4, 0.8)),
179 |label, delta| label.alpha(delta),
180 )
181 .into_any()
182 } else {
183 this.into_any_element()
184 }
185 },
186 ))
187 .when_some(error, |this, error| {
188 this.child(Label::new(error).color(Color::Error))
189 }),
190 )
191 .into_any(),
192 )
193 }));
194
195 (lister, rx)
196 }
197 fn resolve_path(
198 path: PathBuf,
199 root_path: ProjectPath,
200 language_name: LanguageName,
201 project: Entity<Project>,
202 window: &mut Window,
203 cx: &mut Context<Self>,
204 ) -> PathInputState {
205 PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| {
206 _ = maybe!(async move {
207 let toolchain = project
208 .update(cx, |this, cx| {
209 this.resolve_toolchain(path.clone(), language_name, cx)
210 })
211 .await;
212 let Ok(toolchain) = toolchain else {
213 // Go back to the path input state
214 _ = this.update_in(cx, |this, window, cx| {
215 if let AddState::Path {
216 input_state,
217 picker,
218 error,
219 ..
220 } = &mut this.state
221 && matches!(input_state, PathInputState::Resolving(_))
222 {
223 let Err(e) = toolchain else { unreachable!() };
224 *error = Some(Arc::from(e.to_string()));
225 let (delegate, rx) =
226 Self::create_path_browser_delegate(this.project.clone(), cx);
227 picker.update(cx, |picker, cx| {
228 *picker = Picker::uniform_list(delegate, window, cx);
229 picker.set_query(
230 Arc::from(path.to_string_lossy().as_ref()),
231 window,
232 cx,
233 );
234 });
235 *input_state = Self::wait_for_path(rx, window, cx);
236 this.focus_handle(cx).focus(window, cx);
237 }
238 });
239 return Err(anyhow::anyhow!("Failed to resolve toolchain"));
240 };
241 let resolved_toolchain_path = project.read_with(cx, |this, cx| {
242 this.find_project_path(&toolchain.path.as_ref(), cx)
243 });
244
245 // Suggest a default scope based on the applicability.
246 let scope = if let Some(project_path) = resolved_toolchain_path {
247 if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
248 let worktree_root_path = project
249 .read_with(cx, |this, cx| {
250 this.worktree_for_id(root_path.worktree_id, cx)
251 .map(|worktree| worktree.read(cx).abs_path())
252 })
253 .context("Could not find a worktree with a given worktree ID")?;
254 ToolchainScope::Subproject(worktree_root_path, root_path.path)
255 } else {
256 ToolchainScope::Project
257 }
258 } else {
259 // This path lies outside of the project.
260 ToolchainScope::Global
261 };
262
263 _ = this.update_in(cx, |this, window, cx| {
264 let scope_picker = ScopePickerState {
265 entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
266 selected_scope: scope,
267 };
268 this.state = AddState::Name {
269 editor: cx.new(|cx| {
270 let mut editor = Editor::single_line(window, cx);
271 editor.set_text(toolchain.name.as_ref(), window, cx);
272 editor
273 }),
274 toolchain,
275 scope_picker,
276 };
277 this.focus_handle(cx).focus(window, cx);
278 });
279
280 Result::<_, anyhow::Error>::Ok(())
281 })
282 .await;
283 }))
284 }
285
286 fn wait_for_path(
287 rx: oneshot::Receiver<Option<Vec<PathBuf>>>,
288 window: &mut Window,
289 cx: &mut Context<Self>,
290 ) -> PathInputState {
291 let task = cx.spawn_in(window, async move |this, cx| {
292 maybe!(async move {
293 let result = rx.await.log_err()?;
294
295 let path = result
296 .into_iter()
297 .flat_map(|paths| paths.into_iter())
298 .next()?;
299 this.update_in(cx, |this, window, cx| {
300 if let AddState::Path {
301 input_state, error, ..
302 } = &mut this.state
303 && matches!(input_state, PathInputState::WaitingForPath(_))
304 {
305 error.take();
306 *input_state = Self::resolve_path(
307 path,
308 this.root_path.clone(),
309 this.language_name.clone(),
310 this.project.clone(),
311 window,
312 cx,
313 );
314 }
315 })
316 .ok()?;
317 Some(())
318 })
319 .await;
320 });
321 PathInputState::WaitingForPath(task)
322 }
323
324 fn confirm_toolchain(
325 &mut self,
326 _: &menu::Confirm,
327 window: &mut Window,
328 cx: &mut Context<Self>,
329 ) {
330 let AddState::Name {
331 toolchain,
332 editor,
333 scope_picker,
334 } = &mut self.state
335 else {
336 return;
337 };
338
339 let text = editor.read(cx).text(cx);
340 if text.is_empty() {
341 return;
342 }
343
344 toolchain.name = SharedString::from(text);
345 self.project.update(cx, |this, cx| {
346 this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx);
347 });
348 _ = self.weak.update(cx, |this, cx| {
349 this.state = State::Search((this.create_search_state)(window, cx));
350 this.focus_handle(cx).focus(window, cx);
351 cx.notify();
352 });
353 }
354}
355impl Focusable for AddToolchainState {
356 fn focus_handle(&self, cx: &App) -> FocusHandle {
357 match &self.state {
358 AddState::Path { picker, .. } => picker.focus_handle(cx),
359 AddState::Name { editor, .. } => editor.focus_handle(cx),
360 }
361 }
362}
363
364impl AddToolchainState {
365 fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context<Self>) {
366 if let AddState::Name { scope_picker, .. } = &mut self.state {
367 scope_picker.selected_scope = scope;
368 cx.notify();
369 }
370 }
371}
372
373impl Focusable for State {
374 fn focus_handle(&self, cx: &App) -> FocusHandle {
375 match self {
376 State::Search(state) => state.picker.focus_handle(cx),
377 State::AddToolchain(state) => state.focus_handle(cx),
378 }
379 }
380}
381impl Render for AddToolchainState {
382 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
383 let theme = cx.theme().clone();
384 let weak = self.weak.upgrade();
385 let label = SharedString::new_static("Add");
386
387 v_flex()
388 .size_full()
389 // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3`
390 // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state
391 .bg(cx.theme().colors().elevated_surface_background)
392 .border_1()
393 .border_color(cx.theme().colors().border_variant)
394 .rounded_lg()
395 .when_some(weak, |this, weak| {
396 this.on_action(window.listener_for(
397 &weak,
398 |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| {
399 this.state = State::Search((this.create_search_state)(window, cx));
400 this.state.focus_handle(cx).focus(window, cx);
401 cx.notify();
402 },
403 ))
404 })
405 .on_action(cx.listener(Self::confirm_toolchain))
406 .map(|this| match &self.state {
407 AddState::Path { picker, .. } => this.child(picker.clone()),
408 AddState::Name {
409 editor,
410 scope_picker,
411 ..
412 } => {
413 let scope_options = [
414 ToolchainScope::Global,
415 ToolchainScope::Project,
416 ToolchainScope::Subproject(
417 self.worktree_root_path.clone(),
418 self.root_path.path.clone(),
419 ),
420 ];
421
422 let mut navigable_scope_picker = Navigable::new(
423 v_flex()
424 .child(
425 h_flex()
426 .w_full()
427 .p_2()
428 .border_b_1()
429 .border_color(theme.colors().border)
430 .child(editor.clone()),
431 )
432 .child(
433 v_flex()
434 .child(
435 Label::new("Scope")
436 .size(LabelSize::Small)
437 .color(Color::Muted)
438 .mt_1()
439 .ml_2(),
440 )
441 .child(List::new().children(
442 scope_options.iter().enumerate().map(|(i, scope)| {
443 let is_selected = *scope == scope_picker.selected_scope;
444 let label = scope.label();
445 let description = scope.description();
446 let scope_clone_for_action = scope.clone();
447 let scope_clone_for_click = scope.clone();
448
449 div()
450 .id(SharedString::from(format!("scope-option-{i}")))
451 .track_focus(&scope_picker.entries[i].focus_handle)
452 .on_action(cx.listener(
453 move |this, _: &menu::Confirm, _, cx| {
454 this.select_scope(
455 scope_clone_for_action.clone(),
456 cx,
457 );
458 },
459 ))
460 .child(
461 ListItem::new(SharedString::from(format!(
462 "scope-{i}"
463 )))
464 .toggle_state(
465 is_selected
466 || scope_picker.entries[i]
467 .focus_handle
468 .contains_focused(window, cx),
469 )
470 .inset(true)
471 .spacing(ListItemSpacing::Sparse)
472 .child(
473 h_flex()
474 .gap_2()
475 .child(Label::new(label))
476 .child(
477 Label::new(description)
478 .size(LabelSize::Small)
479 .color(Color::Muted),
480 ),
481 )
482 .on_click(cx.listener(move |this, _, _, cx| {
483 this.select_scope(
484 scope_clone_for_click.clone(),
485 cx,
486 );
487 })),
488 )
489 }),
490 ))
491 .child(Divider::horizontal())
492 .child(h_flex().p_1p5().justify_end().map(|this| {
493 let is_disabled = editor.read(cx).is_empty(cx);
494 let handle = self.focus_handle(cx);
495 this.child(
496 Button::new("add-toolchain", label)
497 .disabled(is_disabled)
498 .key_binding(KeyBinding::for_action_in(
499 &menu::Confirm,
500 &handle,
501 cx,
502 ))
503 .on_click(cx.listener(|this, _, window, cx| {
504 this.confirm_toolchain(
505 &menu::Confirm,
506 window,
507 cx,
508 );
509 }))
510 .map(|this| {
511 if false {
512 this.with_animation(
513 "inspecting-user-toolchain",
514 Animation::new(Duration::from_millis(
515 500,
516 ))
517 .repeat()
518 .with_easing(pulsating_between(
519 0.4, 0.8,
520 )),
521 |label, delta| label.alpha(delta),
522 )
523 .into_any()
524 } else {
525 this.into_any_element()
526 }
527 }),
528 )
529 })),
530 )
531 .into_any_element(),
532 );
533
534 for entry in &scope_picker.entries {
535 navigable_scope_picker = navigable_scope_picker.entry(entry.clone());
536 }
537
538 this.child(navigable_scope_picker.render(window, cx))
539 }
540 })
541 }
542}
543
544#[derive(Clone)]
545enum State {
546 Search(SearchState),
547 AddToolchain(Entity<AddToolchainState>),
548}
549
550impl RenderOnce for State {
551 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
552 match self {
553 State::Search(state) => state.picker.into_any_element(),
554 State::AddToolchain(state) => state.into_any_element(),
555 }
556 }
557}
558impl ToolchainSelector {
559 fn register(
560 workspace: &mut Workspace,
561 _window: Option<&mut Window>,
562 _: &mut Context<Workspace>,
563 ) {
564 workspace.register_action(move |workspace, _: &Select, window, cx| {
565 Self::toggle(workspace, window, cx);
566 });
567 workspace.register_action(move |workspace, _: &AddToolchain, window, cx| {
568 let Some(toolchain_selector) = workspace.active_modal::<Self>(cx) else {
569 Self::toggle(workspace, window, cx);
570 return;
571 };
572
573 toolchain_selector.update(cx, |toolchain_selector, cx| {
574 toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx);
575 });
576 });
577 }
578
579 fn toggle(
580 workspace: &mut Workspace,
581 window: &mut Window,
582 cx: &mut Context<Workspace>,
583 ) -> Option<()> {
584 let (_, buffer, _) = workspace
585 .active_item(cx)?
586 .act_as::<Editor>(cx)?
587 .read(cx)
588 .active_excerpt(cx)?;
589 let project = workspace.project().clone();
590
591 let language_name = buffer.read(cx).language()?.name();
592 let worktree_id = buffer.read(cx).file()?.worktree_id(cx);
593 let relative_path: Arc<RelPath> = buffer.read(cx).file()?.path().parent()?.into();
594 let worktree_root_path = project
595 .read(cx)
596 .worktree_for_id(worktree_id, cx)?
597 .read(cx)
598 .abs_path();
599 let weak = workspace.weak_handle();
600 cx.spawn_in(window, async move |workspace, cx| {
601 let active_toolchain = project
602 .read_with(cx, |this, cx| {
603 this.active_toolchain(
604 ProjectPath {
605 worktree_id,
606 path: relative_path.clone(),
607 },
608 language_name.clone(),
609 cx,
610 )
611 })
612 .await;
613 workspace
614 .update_in(cx, |this, window, cx| {
615 this.toggle_modal(window, cx, move |window, cx| {
616 ToolchainSelector::new(
617 weak,
618 project,
619 active_toolchain,
620 worktree_id,
621 worktree_root_path,
622 relative_path,
623 language_name,
624 window,
625 cx,
626 )
627 });
628 })
629 .ok();
630 anyhow::Ok(())
631 })
632 .detach();
633
634 Some(())
635 }
636
637 fn new(
638 workspace: WeakEntity<Workspace>,
639 project: Entity<Project>,
640 active_toolchain: Option<Toolchain>,
641 worktree_id: WorktreeId,
642 worktree_root: Arc<Path>,
643 relative_path: Arc<RelPath>,
644 language_name: LanguageName,
645 window: &mut Window,
646 cx: &mut Context<Self>,
647 ) -> Self {
648 let language_registry = project.read(cx).languages().clone();
649 cx.spawn({
650 let language_name = language_name.clone();
651 async move |this, cx| {
652 let language = language_registry
653 .language_for_name(&language_name.0)
654 .await
655 .ok();
656 this.update(cx, |this, cx| {
657 this.language = language;
658 cx.notify();
659 })
660 .ok();
661 }
662 })
663 .detach();
664 let project_clone = project.clone();
665 let language_name_clone = language_name.clone();
666 let relative_path_clone = relative_path.clone();
667
668 let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context<Self>| {
669 let toolchain_selector = cx.entity().downgrade();
670 let picker = cx.new(|cx| {
671 let delegate = ToolchainSelectorDelegate::new(
672 active_toolchain.clone(),
673 toolchain_selector,
674 workspace.clone(),
675 worktree_id,
676 worktree_root.clone(),
677 project_clone.clone(),
678 relative_path_clone.clone(),
679 language_name_clone.clone(),
680 window,
681 cx,
682 );
683 Picker::uniform_list(delegate, window, cx)
684 });
685 let picker_focus_handle = picker.focus_handle(cx);
686 picker.update(cx, |picker, _| {
687 picker.delegate.focus_handle = picker_focus_handle.clone();
688 });
689 SearchState { picker }
690 });
691
692 Self {
693 state: State::Search(create_search_state(window, cx)),
694 create_search_state,
695 language: None,
696 project,
697 language_name,
698 worktree_id,
699 relative_path,
700 }
701 }
702
703 fn handle_add_toolchain(
704 &mut self,
705 _: &AddToolchain,
706 window: &mut Window,
707 cx: &mut Context<Self>,
708 ) {
709 if matches!(self.state, State::Search(_)) {
710 let Ok(state) = AddToolchainState::new(
711 self.project.clone(),
712 self.language_name.clone(),
713 ProjectPath {
714 worktree_id: self.worktree_id,
715 path: self.relative_path.clone(),
716 },
717 window,
718 cx,
719 ) else {
720 return;
721 };
722 self.state = State::AddToolchain(state);
723 self.state.focus_handle(cx).focus(window, cx);
724 cx.notify();
725 }
726 }
727}
728
729impl Render for ToolchainSelector {
730 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
731 let mut key_context = KeyContext::new_with_defaults();
732 key_context.add("ToolchainSelector");
733
734 v_flex()
735 .key_context(key_context)
736 .w(rems(34.))
737 .on_action(cx.listener(Self::handle_add_toolchain))
738 .child(self.state.clone().render(window, cx))
739 }
740}
741
742impl Focusable for ToolchainSelector {
743 fn focus_handle(&self, cx: &App) -> FocusHandle {
744 self.state.focus_handle(cx)
745 }
746}
747
748impl EventEmitter<DismissEvent> for ToolchainSelector {}
749impl ModalView for ToolchainSelector {}
750
751pub struct ToolchainSelectorDelegate {
752 toolchain_selector: WeakEntity<ToolchainSelector>,
753 candidates: Arc<[(Toolchain, Option<ToolchainScope>)]>,
754 matches: Vec<StringMatch>,
755 selected_index: usize,
756 workspace: WeakEntity<Workspace>,
757 worktree_id: WorktreeId,
758 worktree_abs_path_root: Arc<Path>,
759 relative_path: Arc<RelPath>,
760 placeholder_text: Arc<str>,
761 add_toolchain_text: Arc<str>,
762 project: Entity<Project>,
763 focus_handle: FocusHandle,
764 _fetch_candidates_task: Task<Option<()>>,
765}
766
767impl ToolchainSelectorDelegate {
768 fn new(
769 active_toolchain: Option<Toolchain>,
770 toolchain_selector: WeakEntity<ToolchainSelector>,
771 workspace: WeakEntity<Workspace>,
772 worktree_id: WorktreeId,
773 worktree_abs_path_root: Arc<Path>,
774 project: Entity<Project>,
775 relative_path: Arc<RelPath>,
776 language_name: LanguageName,
777 window: &mut Window,
778 cx: &mut Context<Picker<Self>>,
779 ) -> Self {
780 let _project = project.clone();
781 let path_style = project.read(cx).path_style(cx);
782
783 let _fetch_candidates_task = cx.spawn_in(window, {
784 async move |this, cx| {
785 let meta = _project
786 .read_with(cx, |this, _| {
787 Project::toolchain_metadata(this.languages().clone(), language_name.clone())
788 })
789 .await?;
790 let relative_path = this
791 .update(cx, |this, cx| {
792 this.delegate.add_toolchain_text = format!(
793 "Add {}",
794 meta.term.as_ref().to_case(convert_case::Case::Title)
795 )
796 .into();
797 cx.notify();
798 this.delegate.relative_path.clone()
799 })
800 .ok()?;
801
802 let Toolchains {
803 toolchains: available_toolchains,
804 root_path: relative_path,
805 user_toolchains,
806 } = _project
807 .update(cx, |this, cx| {
808 this.available_toolchains(
809 ProjectPath {
810 worktree_id,
811 path: relative_path.clone(),
812 },
813 language_name,
814 cx,
815 )
816 })
817 .await?;
818 let pretty_path = {
819 if relative_path.is_empty() {
820 Cow::Borrowed("worktree root")
821 } else {
822 Cow::Owned(format!("`{}`", relative_path.display(path_style)))
823 }
824 };
825 let placeholder_text =
826 format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into();
827 let _ = this.update_in(cx, move |this, window, cx| {
828 this.delegate.relative_path = relative_path;
829 this.delegate.placeholder_text = placeholder_text;
830 this.refresh_placeholder(window, cx);
831 });
832
833 let _ = this.update_in(cx, move |this, window, cx| {
834 this.delegate.candidates = user_toolchains
835 .into_iter()
836 .flat_map(|(scope, toolchains)| {
837 toolchains
838 .into_iter()
839 .map(move |toolchain| (toolchain, Some(scope.clone())))
840 })
841 .chain(
842 available_toolchains
843 .toolchains
844 .into_iter()
845 .map(|toolchain| (toolchain, None)),
846 )
847 .collect();
848
849 if let Some(active_toolchain) = active_toolchain
850 && let Some(position) = this
851 .delegate
852 .candidates
853 .iter()
854 .position(|(toolchain, _)| *toolchain == active_toolchain)
855 {
856 this.delegate.set_selected_index(position, window, cx);
857 }
858 this.update_matches(this.query(cx), window, cx);
859 });
860
861 Some(())
862 }
863 });
864 let placeholder_text = "Select a toolchain…".to_string().into();
865 Self {
866 toolchain_selector,
867 candidates: Default::default(),
868 matches: vec![],
869 selected_index: 0,
870 workspace,
871 worktree_id,
872 worktree_abs_path_root,
873 placeholder_text,
874 relative_path,
875 _fetch_candidates_task,
876 project,
877 focus_handle: cx.focus_handle(),
878 add_toolchain_text: Arc::from("Add Toolchain"),
879 }
880 }
881 fn relativize_path(
882 path: SharedString,
883 worktree_root: &Path,
884 path_style: PathStyle,
885 ) -> SharedString {
886 Path::new(&path.as_ref())
887 .strip_prefix(&worktree_root)
888 .ok()
889 .and_then(|suffix| suffix.to_str())
890 .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into())
891 .unwrap_or(path)
892 }
893}
894
895impl PickerDelegate for ToolchainSelectorDelegate {
896 type ListItem = ListItem;
897
898 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
899 self.placeholder_text.clone()
900 }
901
902 fn match_count(&self) -> usize {
903 self.matches.len()
904 }
905
906 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
907 if let Some(string_match) = self.matches.get(self.selected_index) {
908 let (toolchain, _) = self.candidates[string_match.candidate_id].clone();
909 if let Some(workspace_id) = self
910 .workspace
911 .read_with(cx, |this, _| this.database_id())
912 .ok()
913 .flatten()
914 {
915 let workspace = self.workspace.clone();
916 let worktree_id = self.worktree_id;
917 let worktree_abs_path_root = self.worktree_abs_path_root.clone();
918 let path = self.relative_path.clone();
919 let relative_path = self.relative_path.clone();
920 cx.spawn_in(window, async move |_, cx| {
921 workspace::WORKSPACE_DB
922 .set_toolchain(
923 workspace_id,
924 worktree_abs_path_root,
925 relative_path,
926 toolchain.clone(),
927 )
928 .await
929 .log_err();
930 workspace
931 .update(cx, |this, cx| {
932 this.project().update(cx, |this, cx| {
933 this.activate_toolchain(
934 ProjectPath { worktree_id, path },
935 toolchain,
936 cx,
937 )
938 })
939 })
940 .ok()?
941 .await;
942 Some(())
943 })
944 .detach();
945 }
946 }
947 self.dismissed(window, cx);
948 }
949
950 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
951 self.toolchain_selector
952 .update(cx, |_, cx| cx.emit(DismissEvent))
953 .log_err();
954 }
955
956 fn selected_index(&self) -> usize {
957 self.selected_index
958 }
959
960 fn set_selected_index(
961 &mut self,
962 ix: usize,
963 _window: &mut Window,
964 _: &mut Context<Picker<Self>>,
965 ) {
966 self.selected_index = ix;
967 }
968
969 fn update_matches(
970 &mut self,
971 query: String,
972 window: &mut Window,
973 cx: &mut Context<Picker<Self>>,
974 ) -> gpui::Task<()> {
975 let background = cx.background_executor().clone();
976 let candidates = self.candidates.clone();
977 let worktree_root_path = self.worktree_abs_path_root.clone();
978 let path_style = self.project.read(cx).path_style(cx);
979 cx.spawn_in(window, async move |this, cx| {
980 let matches = if query.is_empty() {
981 candidates
982 .into_iter()
983 .enumerate()
984 .map(|(index, (candidate, _))| {
985 let path = Self::relativize_path(
986 candidate.path.clone(),
987 &worktree_root_path,
988 path_style,
989 );
990 let string = format!("{}{}", candidate.name, path);
991 StringMatch {
992 candidate_id: index,
993 string,
994 positions: Vec::new(),
995 score: 0.0,
996 }
997 })
998 .collect()
999 } else {
1000 let candidates = candidates
1001 .into_iter()
1002 .enumerate()
1003 .map(|(candidate_id, (toolchain, _))| {
1004 let path = Self::relativize_path(
1005 toolchain.path.clone(),
1006 &worktree_root_path,
1007 path_style,
1008 );
1009 let string = format!("{}{}", toolchain.name, path);
1010 StringMatchCandidate::new(candidate_id, &string)
1011 })
1012 .collect::<Vec<_>>();
1013 match_strings(
1014 &candidates,
1015 &query,
1016 false,
1017 true,
1018 100,
1019 &Default::default(),
1020 background,
1021 )
1022 .await
1023 };
1024
1025 this.update(cx, |this, cx| {
1026 let delegate = &mut this.delegate;
1027 delegate.matches = matches;
1028 delegate.selected_index = delegate
1029 .selected_index
1030 .min(delegate.matches.len().saturating_sub(1));
1031 cx.notify();
1032 })
1033 .log_err();
1034 })
1035 }
1036
1037 fn render_match(
1038 &self,
1039 ix: usize,
1040 selected: bool,
1041 _: &mut Window,
1042 cx: &mut Context<Picker<Self>>,
1043 ) -> Option<Self::ListItem> {
1044 let mat = &self.matches.get(ix)?;
1045 let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?;
1046
1047 let label = toolchain.name.clone();
1048 let path_style = self.project.read(cx).path_style(cx);
1049 let path = Self::relativize_path(
1050 toolchain.path.clone(),
1051 &self.worktree_abs_path_root,
1052 path_style,
1053 );
1054 let (name_highlights, mut path_highlights) = mat
1055 .positions
1056 .iter()
1057 .cloned()
1058 .partition::<Vec<_>, _>(|index| *index < label.len());
1059 path_highlights.iter_mut().for_each(|index| {
1060 *index -= label.len();
1061 });
1062 let id: SharedString = format!("toolchain-{ix}",).into();
1063 Some(
1064 ListItem::new(id)
1065 .inset(true)
1066 .spacing(ListItemSpacing::Sparse)
1067 .toggle_state(selected)
1068 .child(HighlightedLabel::new(label, name_highlights))
1069 .child(
1070 HighlightedLabel::new(path, path_highlights)
1071 .size(LabelSize::Small)
1072 .color(Color::Muted),
1073 )
1074 .when_some(scope.as_ref(), |this, scope| {
1075 let id: SharedString = format!(
1076 "delete-custom-toolchain-{}-{}",
1077 toolchain.name, toolchain.path
1078 )
1079 .into();
1080 let toolchain = toolchain.clone();
1081 let scope = scope.clone();
1082
1083 this.end_slot(IconButton::new(id, IconName::Trash).on_click(cx.listener(
1084 move |this, _, _, cx| {
1085 this.delegate.project.update(cx, |this, cx| {
1086 this.remove_toolchain(toolchain.clone(), scope.clone(), cx)
1087 });
1088
1089 this.delegate.matches.retain_mut(|m| {
1090 if m.candidate_id == ix {
1091 return false;
1092 } else if m.candidate_id > ix {
1093 m.candidate_id -= 1;
1094 }
1095 true
1096 });
1097
1098 this.delegate.candidates = this
1099 .delegate
1100 .candidates
1101 .iter()
1102 .enumerate()
1103 .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone()))
1104 .collect();
1105
1106 if this.delegate.selected_index >= ix {
1107 this.delegate.selected_index =
1108 this.delegate.selected_index.saturating_sub(1);
1109 }
1110 cx.stop_propagation();
1111 cx.notify();
1112 },
1113 )))
1114 }),
1115 )
1116 }
1117 fn render_footer(
1118 &self,
1119 _window: &mut Window,
1120 cx: &mut Context<Picker<Self>>,
1121 ) -> Option<AnyElement> {
1122 Some(
1123 v_flex()
1124 .rounded_b_md()
1125 .child(Divider::horizontal())
1126 .child(
1127 h_flex()
1128 .p_1p5()
1129 .gap_0p5()
1130 .justify_end()
1131 .child(
1132 Button::new("xd", self.add_toolchain_text.clone())
1133 .key_binding(KeyBinding::for_action_in(
1134 &AddToolchain,
1135 &self.focus_handle,
1136 cx,
1137 ))
1138 .on_click(|_, window, cx| {
1139 window.dispatch_action(Box::new(AddToolchain), cx)
1140 }),
1141 )
1142 .child(
1143 Button::new("select", "Select")
1144 .key_binding(KeyBinding::for_action_in(
1145 &menu::Confirm,
1146 &self.focus_handle,
1147 cx,
1148 ))
1149 .on_click(|_, window, cx| {
1150 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1151 }),
1152 ),
1153 )
1154 .into_any_element(),
1155 )
1156 }
1157}