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