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