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