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