toolchain_selector.rs

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