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