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