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