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 let weak_toolchain_selector = weak.clone();
123 let window_handle = window.window_handle();
124 Self {
125 state: AddState::Path {
126 _subscription: cx.subscribe(&picker, move |_, _, _: &DismissEvent, cx| {
127 if let Some(toolchain_selector) = weak_toolchain_selector.upgrade() {
128 window_handle
129 .update(cx, |_, window, cx| {
130 toolchain_selector.update(cx, |this, cx| {
131 this.cancel(&menu::Cancel, window, cx);
132 });
133 })
134 .ok();
135 }
136 }),
137 picker,
138 error: None,
139 input_state: Self::wait_for_path(rx, window, cx),
140 },
141 project,
142 language_name,
143 root_path,
144 weak,
145 worktree_root_path,
146 }
147 }))
148 }
149
150 fn create_path_browser_delegate(
151 project: Entity<Project>,
152 cx: &mut Context<Self>,
153 ) -> (OpenPathDelegate, oneshot::Receiver<Option<Vec<PathBuf>>>) {
154 let (tx, rx) = oneshot::channel();
155 let weak = cx.weak_entity();
156 let lister = OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, cx)
157 .show_hidden()
158 .with_footer(Arc::new(move |_, cx| {
159 let error = weak
160 .read_with(cx, |this, _| {
161 if let AddState::Path { error, .. } = &this.state {
162 error.clone()
163 } else {
164 None
165 }
166 })
167 .ok()
168 .flatten();
169 let is_loading = weak
170 .read_with(cx, |this, _| {
171 matches!(
172 this.state,
173 AddState::Path {
174 input_state: PathInputState::Resolving(_),
175 ..
176 }
177 )
178 })
179 .unwrap_or_default();
180 Some(
181 v_flex()
182 .child(Divider::horizontal())
183 .child(
184 h_flex()
185 .p_1()
186 .justify_between()
187 .gap_2()
188 .child(Label::new("Select Toolchain Path").color(Color::Muted).map(
189 |this| {
190 if is_loading {
191 this.with_animation(
192 "select-toolchain-label",
193 Animation::new(Duration::from_secs(2))
194 .repeat()
195 .with_easing(pulsating_between(0.4, 0.8)),
196 |label, delta| label.alpha(delta),
197 )
198 .into_any()
199 } else {
200 this.into_any_element()
201 }
202 },
203 ))
204 .when_some(error, |this, error| {
205 this.child(Label::new(error).color(Color::Error))
206 }),
207 )
208 .into_any(),
209 )
210 }));
211
212 (lister, rx)
213 }
214 fn resolve_path(
215 path: PathBuf,
216 root_path: ProjectPath,
217 language_name: LanguageName,
218 project: Entity<Project>,
219 window: &mut Window,
220 cx: &mut Context<Self>,
221 ) -> PathInputState {
222 PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| {
223 _ = maybe!(async move {
224 let toolchain = project
225 .update(cx, |this, cx| {
226 this.resolve_toolchain(path.clone(), language_name, cx)
227 })
228 .await;
229 let Ok(toolchain) = toolchain else {
230 // Go back to the path input state
231 _ = this.update_in(cx, |this, window, cx| {
232 if let AddState::Path {
233 input_state,
234 picker,
235 error,
236 ..
237 } = &mut this.state
238 && matches!(input_state, PathInputState::Resolving(_))
239 {
240 let Err(e) = toolchain else { unreachable!() };
241 *error = Some(Arc::from(e.to_string()));
242 let (delegate, rx) =
243 Self::create_path_browser_delegate(this.project.clone(), cx);
244 picker.update(cx, |picker, cx| {
245 *picker = Picker::uniform_list(delegate, window, cx);
246 picker.set_query(path.to_string_lossy().as_ref(), window, cx);
247 });
248 *input_state = Self::wait_for_path(rx, window, cx);
249 this.focus_handle(cx).focus(window, cx);
250 }
251 });
252 return Err(anyhow::anyhow!("Failed to resolve toolchain"));
253 };
254 let resolved_toolchain_path = project.read_with(cx, |this, cx| {
255 this.find_project_path(&toolchain.path.as_ref(), cx)
256 });
257
258 // Suggest a default scope based on the applicability.
259 let scope = if let Some(project_path) = resolved_toolchain_path {
260 if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) {
261 let worktree_root_path = project
262 .read_with(cx, |this, cx| {
263 this.worktree_for_id(root_path.worktree_id, cx)
264 .map(|worktree| worktree.read(cx).abs_path())
265 })
266 .context("Could not find a worktree with a given worktree ID")?;
267 ToolchainScope::Subproject(worktree_root_path, root_path.path)
268 } else {
269 ToolchainScope::Project
270 }
271 } else {
272 // This path lies outside of the project.
273 ToolchainScope::Global
274 };
275
276 _ = this.update_in(cx, |this, window, cx| {
277 let scope_picker = ScopePickerState {
278 entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
279 selected_scope: scope,
280 };
281 this.state = AddState::Name {
282 editor: cx.new(|cx| {
283 let mut editor = Editor::single_line(window, cx);
284 editor.set_text(toolchain.name.as_ref(), window, cx);
285 editor
286 }),
287 toolchain,
288 scope_picker,
289 };
290 this.focus_handle(cx).focus(window, cx);
291 });
292
293 Result::<_, anyhow::Error>::Ok(())
294 })
295 .await;
296 }))
297 }
298
299 fn wait_for_path(
300 rx: oneshot::Receiver<Option<Vec<PathBuf>>>,
301 window: &mut Window,
302 cx: &mut Context<Self>,
303 ) -> PathInputState {
304 let task = cx.spawn_in(window, async move |this, cx| {
305 maybe!(async move {
306 let result = rx.await.log_err()?;
307
308 let path = result
309 .into_iter()
310 .flat_map(|paths| paths.into_iter())
311 .next()?;
312 this.update_in(cx, |this, window, cx| {
313 if let AddState::Path {
314 input_state, error, ..
315 } = &mut this.state
316 && matches!(input_state, PathInputState::WaitingForPath(_))
317 {
318 error.take();
319 *input_state = Self::resolve_path(
320 path,
321 this.root_path.clone(),
322 this.language_name.clone(),
323 this.project.clone(),
324 window,
325 cx,
326 );
327 }
328 })
329 .ok()?;
330 Some(())
331 })
332 .await;
333 });
334 PathInputState::WaitingForPath(task)
335 }
336
337 fn confirm_toolchain(
338 &mut self,
339 _: &menu::Confirm,
340 window: &mut Window,
341 cx: &mut Context<Self>,
342 ) {
343 let AddState::Name {
344 toolchain,
345 editor,
346 scope_picker,
347 } = &mut self.state
348 else {
349 return;
350 };
351
352 let text = editor.read(cx).text(cx);
353 if text.is_empty() {
354 return;
355 }
356
357 toolchain.name = SharedString::from(text);
358 self.project.update(cx, |this, cx| {
359 this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx);
360 });
361 _ = self.weak.update(cx, |this, cx| {
362 this.state = State::Search((this.create_search_state)(window, cx));
363 this.focus_handle(cx).focus(window, cx);
364 cx.notify();
365 });
366 }
367}
368impl Focusable for AddToolchainState {
369 fn focus_handle(&self, cx: &App) -> FocusHandle {
370 match &self.state {
371 AddState::Path { picker, .. } => picker.focus_handle(cx),
372 AddState::Name { editor, .. } => editor.focus_handle(cx),
373 }
374 }
375}
376
377impl AddToolchainState {
378 fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context<Self>) {
379 if let AddState::Name { scope_picker, .. } = &mut self.state {
380 scope_picker.selected_scope = scope;
381 cx.notify();
382 }
383 }
384}
385
386impl Focusable for State {
387 fn focus_handle(&self, cx: &App) -> FocusHandle {
388 match self {
389 State::Search(state) => state.picker.focus_handle(cx),
390 State::AddToolchain(state) => state.focus_handle(cx),
391 }
392 }
393}
394impl Render for AddToolchainState {
395 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
396 let theme = cx.theme().clone();
397 let label = SharedString::new_static("Add");
398
399 v_flex()
400 .size_full()
401 // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3`
402 // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state
403 .bg(cx.theme().colors().elevated_surface_background)
404 .border_1()
405 .border_color(cx.theme().colors().border_variant)
406 .rounded_lg()
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 fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context<Self>) {
731 match &self.state {
732 State::Search(_) => {
733 cx.emit(DismissEvent);
734 }
735 State::AddToolchain(_) => {
736 self.state = State::Search((self.create_search_state)(window, cx));
737 self.state.focus_handle(cx).focus(window, cx);
738 cx.notify();
739 }
740 }
741 }
742}
743
744impl Render for ToolchainSelector {
745 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
746 let mut key_context = KeyContext::new_with_defaults();
747 key_context.add("ToolchainSelector");
748
749 v_flex()
750 .key_context(key_context)
751 .w(rems(34.))
752 .on_action(cx.listener(Self::cancel))
753 .on_action(cx.listener(Self::handle_add_toolchain))
754 .child(self.state.clone().render(window, cx))
755 }
756}
757
758impl Focusable for ToolchainSelector {
759 fn focus_handle(&self, cx: &App) -> FocusHandle {
760 self.state.focus_handle(cx)
761 }
762}
763
764impl EventEmitter<DismissEvent> for ToolchainSelector {}
765impl ModalView for ToolchainSelector {}
766
767pub struct ToolchainSelectorDelegate {
768 toolchain_selector: WeakEntity<ToolchainSelector>,
769 candidates: Arc<[(Toolchain, Option<ToolchainScope>)]>,
770 matches: Vec<StringMatch>,
771 selected_index: usize,
772 workspace: WeakEntity<Workspace>,
773 worktree_id: WorktreeId,
774 worktree_abs_path_root: Arc<Path>,
775 relative_path: Arc<RelPath>,
776 placeholder_text: Arc<str>,
777 add_toolchain_text: Arc<str>,
778 project: Entity<Project>,
779 focus_handle: FocusHandle,
780 _fetch_candidates_task: Task<Option<()>>,
781}
782
783impl ToolchainSelectorDelegate {
784 fn new(
785 active_toolchain: Option<Toolchain>,
786 toolchain_selector: WeakEntity<ToolchainSelector>,
787 workspace: WeakEntity<Workspace>,
788 worktree_id: WorktreeId,
789 worktree_abs_path_root: Arc<Path>,
790 project: Entity<Project>,
791 relative_path: Arc<RelPath>,
792 language_name: LanguageName,
793 window: &mut Window,
794 cx: &mut Context<Picker<Self>>,
795 ) -> Self {
796 let _project = project.clone();
797 let path_style = project.read(cx).path_style(cx);
798
799 let _fetch_candidates_task = cx.spawn_in(window, {
800 async move |this, cx| {
801 let meta = _project
802 .read_with(cx, |this, _| {
803 Project::toolchain_metadata(this.languages().clone(), language_name.clone())
804 })
805 .await?;
806 let relative_path = this
807 .update(cx, |this, cx| {
808 this.delegate.add_toolchain_text = format!(
809 "Add {}",
810 meta.term.as_ref().to_case(convert_case::Case::Title)
811 )
812 .into();
813 cx.notify();
814 this.delegate.relative_path.clone()
815 })
816 .ok()?;
817
818 let Toolchains {
819 toolchains: available_toolchains,
820 root_path: relative_path,
821 user_toolchains,
822 } = _project
823 .update(cx, |this, cx| {
824 this.available_toolchains(
825 ProjectPath {
826 worktree_id,
827 path: relative_path.clone(),
828 },
829 language_name,
830 cx,
831 )
832 })
833 .await?;
834 let pretty_path = {
835 if relative_path.is_empty() {
836 Cow::Borrowed("worktree root")
837 } else {
838 Cow::Owned(format!("`{}`", relative_path.display(path_style)))
839 }
840 };
841 let placeholder_text =
842 format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into();
843 let _ = this.update_in(cx, move |this, window, cx| {
844 this.delegate.relative_path = relative_path;
845 this.delegate.placeholder_text = placeholder_text;
846 this.refresh_placeholder(window, cx);
847 });
848
849 let _ = this.update_in(cx, move |this, window, cx| {
850 this.delegate.candidates = user_toolchains
851 .into_iter()
852 .flat_map(|(scope, toolchains)| {
853 toolchains
854 .into_iter()
855 .map(move |toolchain| (toolchain, Some(scope.clone())))
856 })
857 .chain(
858 available_toolchains
859 .toolchains
860 .into_iter()
861 .map(|toolchain| (toolchain, None)),
862 )
863 .collect();
864
865 if let Some(active_toolchain) = active_toolchain
866 && let Some(position) = this
867 .delegate
868 .candidates
869 .iter()
870 .position(|(toolchain, _)| *toolchain == active_toolchain)
871 {
872 this.delegate.set_selected_index(position, window, cx);
873 }
874 this.update_matches(this.query(cx), window, cx);
875 });
876
877 Some(())
878 }
879 });
880 let placeholder_text = "Select a toolchain…".to_string().into();
881 Self {
882 toolchain_selector,
883 candidates: Default::default(),
884 matches: vec![],
885 selected_index: 0,
886 workspace,
887 worktree_id,
888 worktree_abs_path_root,
889 placeholder_text,
890 relative_path,
891 _fetch_candidates_task,
892 project,
893 focus_handle: cx.focus_handle(),
894 add_toolchain_text: Arc::from("Add Toolchain"),
895 }
896 }
897 fn relativize_path(
898 path: SharedString,
899 worktree_root: &Path,
900 path_style: PathStyle,
901 ) -> SharedString {
902 Path::new(&path.as_ref())
903 .strip_prefix(&worktree_root)
904 .ok()
905 .and_then(|suffix| suffix.to_str())
906 .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into())
907 .unwrap_or(path)
908 }
909}
910
911impl PickerDelegate for ToolchainSelectorDelegate {
912 type ListItem = ListItem;
913
914 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
915 self.placeholder_text.clone()
916 }
917
918 fn match_count(&self) -> usize {
919 self.matches.len()
920 }
921
922 fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
923 if let Some(string_match) = self.matches.get(self.selected_index) {
924 let (toolchain, _) = self.candidates[string_match.candidate_id].clone();
925 if let Some(workspace_id) = self
926 .workspace
927 .read_with(cx, |this, _| this.database_id())
928 .ok()
929 .flatten()
930 {
931 let workspace = self.workspace.clone();
932 let worktree_id = self.worktree_id;
933 let worktree_abs_path_root = self.worktree_abs_path_root.clone();
934 let path = self.relative_path.clone();
935 let relative_path = self.relative_path.clone();
936 cx.spawn_in(window, async move |_, cx| {
937 workspace::WORKSPACE_DB
938 .set_toolchain(
939 workspace_id,
940 worktree_abs_path_root,
941 relative_path,
942 toolchain.clone(),
943 )
944 .await
945 .log_err();
946 workspace
947 .update(cx, |this, cx| {
948 this.project().update(cx, |this, cx| {
949 this.activate_toolchain(
950 ProjectPath { worktree_id, path },
951 toolchain,
952 cx,
953 )
954 })
955 })
956 .ok()?
957 .await;
958 Some(())
959 })
960 .detach();
961 }
962 }
963 self.dismissed(window, cx);
964 }
965
966 fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
967 self.toolchain_selector
968 .update(cx, |_, cx| cx.emit(DismissEvent))
969 .log_err();
970 }
971
972 fn selected_index(&self) -> usize {
973 self.selected_index
974 }
975
976 fn set_selected_index(
977 &mut self,
978 ix: usize,
979 _window: &mut Window,
980 _: &mut Context<Picker<Self>>,
981 ) {
982 self.selected_index = ix;
983 }
984
985 fn update_matches(
986 &mut self,
987 query: String,
988 window: &mut Window,
989 cx: &mut Context<Picker<Self>>,
990 ) -> gpui::Task<()> {
991 let background = cx.background_executor().clone();
992 let candidates = self.candidates.clone();
993 let worktree_root_path = self.worktree_abs_path_root.clone();
994 let path_style = self.project.read(cx).path_style(cx);
995 cx.spawn_in(window, async move |this, cx| {
996 let matches = if query.is_empty() {
997 candidates
998 .into_iter()
999 .enumerate()
1000 .map(|(index, (candidate, _))| {
1001 let path = Self::relativize_path(
1002 candidate.path.clone(),
1003 &worktree_root_path,
1004 path_style,
1005 );
1006 let string = format!("{}{}", candidate.name, path);
1007 StringMatch {
1008 candidate_id: index,
1009 string,
1010 positions: Vec::new(),
1011 score: 0.0,
1012 }
1013 })
1014 .collect()
1015 } else {
1016 let candidates = candidates
1017 .into_iter()
1018 .enumerate()
1019 .map(|(candidate_id, (toolchain, _))| {
1020 let path = Self::relativize_path(
1021 toolchain.path.clone(),
1022 &worktree_root_path,
1023 path_style,
1024 );
1025 let string = format!("{}{}", toolchain.name, path);
1026 StringMatchCandidate::new(candidate_id, &string)
1027 })
1028 .collect::<Vec<_>>();
1029 match_strings(
1030 &candidates,
1031 &query,
1032 false,
1033 true,
1034 100,
1035 &Default::default(),
1036 background,
1037 )
1038 .await
1039 };
1040
1041 this.update(cx, |this, cx| {
1042 let delegate = &mut this.delegate;
1043 delegate.matches = matches;
1044 delegate.selected_index = delegate
1045 .selected_index
1046 .min(delegate.matches.len().saturating_sub(1));
1047 cx.notify();
1048 })
1049 .log_err();
1050 })
1051 }
1052
1053 fn render_match(
1054 &self,
1055 ix: usize,
1056 selected: bool,
1057 _: &mut Window,
1058 cx: &mut Context<Picker<Self>>,
1059 ) -> Option<Self::ListItem> {
1060 let mat = &self.matches.get(ix)?;
1061 let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?;
1062
1063 let label = toolchain.name.clone();
1064 let path_style = self.project.read(cx).path_style(cx);
1065 let path = Self::relativize_path(
1066 toolchain.path.clone(),
1067 &self.worktree_abs_path_root,
1068 path_style,
1069 );
1070 let (name_highlights, mut path_highlights) = mat
1071 .positions
1072 .iter()
1073 .cloned()
1074 .partition::<Vec<_>, _>(|index| *index < label.len());
1075 path_highlights.iter_mut().for_each(|index| {
1076 *index -= label.len();
1077 });
1078 let id: SharedString = format!("toolchain-{ix}",).into();
1079 Some(
1080 ListItem::new(id)
1081 .inset(true)
1082 .spacing(ListItemSpacing::Sparse)
1083 .toggle_state(selected)
1084 .child(HighlightedLabel::new(label, name_highlights))
1085 .child(
1086 HighlightedLabel::new(path, path_highlights)
1087 .size(LabelSize::Small)
1088 .color(Color::Muted),
1089 )
1090 .when_some(scope.as_ref(), |this, scope| {
1091 let id: SharedString = format!(
1092 "delete-custom-toolchain-{}-{}",
1093 toolchain.name, toolchain.path
1094 )
1095 .into();
1096 let toolchain = toolchain.clone();
1097 let scope = scope.clone();
1098
1099 this.end_slot(IconButton::new(id, IconName::Trash).on_click(cx.listener(
1100 move |this, _, _, cx| {
1101 this.delegate.project.update(cx, |this, cx| {
1102 this.remove_toolchain(toolchain.clone(), scope.clone(), cx)
1103 });
1104
1105 this.delegate.matches.retain_mut(|m| {
1106 if m.candidate_id == ix {
1107 return false;
1108 } else if m.candidate_id > ix {
1109 m.candidate_id -= 1;
1110 }
1111 true
1112 });
1113
1114 this.delegate.candidates = this
1115 .delegate
1116 .candidates
1117 .iter()
1118 .enumerate()
1119 .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone()))
1120 .collect();
1121
1122 if this.delegate.selected_index >= ix {
1123 this.delegate.selected_index =
1124 this.delegate.selected_index.saturating_sub(1);
1125 }
1126 cx.stop_propagation();
1127 cx.notify();
1128 },
1129 )))
1130 }),
1131 )
1132 }
1133 fn render_footer(
1134 &self,
1135 _window: &mut Window,
1136 cx: &mut Context<Picker<Self>>,
1137 ) -> Option<AnyElement> {
1138 Some(
1139 v_flex()
1140 .rounded_b_md()
1141 .child(Divider::horizontal())
1142 .child(
1143 h_flex()
1144 .p_1p5()
1145 .gap_0p5()
1146 .justify_end()
1147 .child(
1148 Button::new("xd", self.add_toolchain_text.clone())
1149 .key_binding(KeyBinding::for_action_in(
1150 &AddToolchain,
1151 &self.focus_handle,
1152 cx,
1153 ))
1154 .on_click(|_, window, cx| {
1155 window.dispatch_action(Box::new(AddToolchain), cx)
1156 }),
1157 )
1158 .child(
1159 Button::new("select", "Select")
1160 .key_binding(KeyBinding::for_action_in(
1161 &menu::Confirm,
1162 &self.focus_handle,
1163 cx,
1164 ))
1165 .on_click(|_, window, cx| {
1166 window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1167 }),
1168 ),
1169 )
1170 .into_any_element(),
1171 )
1172 }
1173}