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