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