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