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