toolchain_selector.rs

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