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