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