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