worktree_picker.rs

   1use anyhow::Context as _;
   2use collections::HashSet;
   3use fuzzy::StringMatchCandidate;
   4
   5use git::repository::Worktree as GitWorktree;
   6use gpui::{
   7    Action, App, AsyncWindowContext, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
   8    Focusable, InteractiveElement, IntoElement, Modifiers, ModifiersChangedEvent, ParentElement,
   9    Render, SharedString, Styled, Subscription, Task, WeakEntity, Window, actions, rems,
  10};
  11use picker::{Picker, PickerDelegate, PickerEditorPosition};
  12use project::project_settings::ProjectSettings;
  13use project::{
  14    git_store::{Repository, RepositoryEvent},
  15    trusted_worktrees::{PathTrust, TrustedWorktrees},
  16};
  17use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
  18use remote_connection::{RemoteConnectionModal, connect};
  19use settings::Settings;
  20use std::{path::PathBuf, sync::Arc};
  21use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
  22use util::{ResultExt, debug_panic, paths::PathExt};
  23use workspace::{
  24    ModalView, MultiWorkspace, OpenMode, Workspace, notifications::DetachAndPromptErr,
  25};
  26
  27use crate::git_panel::show_error_toast;
  28
  29actions!(
  30    git,
  31    [
  32        WorktreeFromDefault,
  33        WorktreeFromDefaultOnWindow,
  34        DeleteWorktree
  35    ]
  36);
  37
  38pub fn open(
  39    workspace: &mut Workspace,
  40    _: &zed_actions::git::Worktree,
  41    window: &mut Window,
  42    cx: &mut Context<Workspace>,
  43) {
  44    let repository = workspace.project().read(cx).active_repository(cx);
  45    let workspace_handle = workspace.weak_handle();
  46    workspace.toggle_modal(window, cx, |window, cx| {
  47        WorktreeList::new(repository, workspace_handle, rems(34.), window, cx)
  48    })
  49}
  50
  51pub fn create_embedded(
  52    repository: Option<Entity<Repository>>,
  53    workspace: WeakEntity<Workspace>,
  54    width: Rems,
  55    window: &mut Window,
  56    cx: &mut Context<WorktreeList>,
  57) -> WorktreeList {
  58    WorktreeList::new_embedded(repository, workspace, width, window, cx)
  59}
  60
  61pub struct WorktreeList {
  62    width: Rems,
  63    pub picker: Entity<Picker<WorktreeListDelegate>>,
  64    picker_focus_handle: FocusHandle,
  65    _subscriptions: Vec<Subscription>,
  66    embedded: bool,
  67}
  68
  69impl WorktreeList {
  70    fn new(
  71        repository: Option<Entity<Repository>>,
  72        workspace: WeakEntity<Workspace>,
  73        width: Rems,
  74        window: &mut Window,
  75        cx: &mut Context<Self>,
  76    ) -> Self {
  77        let mut this = Self::new_inner(repository, workspace, width, false, window, cx);
  78        this._subscriptions
  79            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
  80                cx.emit(DismissEvent);
  81            }));
  82        this
  83    }
  84
  85    fn new_inner(
  86        repository: Option<Entity<Repository>>,
  87        workspace: WeakEntity<Workspace>,
  88        width: Rems,
  89        embedded: bool,
  90        window: &mut Window,
  91        cx: &mut Context<Self>,
  92    ) -> Self {
  93        let all_worktrees_request = repository
  94            .clone()
  95            .map(|repository| repository.update(cx, |repository, _| repository.worktrees()));
  96
  97        let default_branch_request = repository.clone().map(|repository| {
  98            repository.update(cx, |repository, _| repository.default_branch(false))
  99        });
 100
 101        cx.spawn_in(window, async move |this, cx| {
 102            let all_worktrees: Vec<_> = all_worktrees_request
 103                .context("No active repository")?
 104                .await??
 105                .into_iter()
 106                .filter(|worktree| !worktree.is_bare) // hide bare repositories
 107                .collect();
 108
 109            let default_branch = default_branch_request
 110                .context("No active repository")?
 111                .await
 112                .map(Result::ok)
 113                .ok()
 114                .flatten()
 115                .flatten();
 116
 117            this.update_in(cx, |this, window, cx| {
 118                this.picker.update(cx, |picker, cx| {
 119                    picker.delegate.all_worktrees = Some(all_worktrees);
 120                    picker.delegate.default_branch = default_branch;
 121                    picker.delegate.refresh_forbidden_deletion_path(cx);
 122                    picker.refresh(window, cx);
 123                })
 124            })?;
 125
 126            anyhow::Ok(())
 127        })
 128        .detach_and_log_err(cx);
 129
 130        let delegate = WorktreeListDelegate::new(workspace, repository.clone(), window, cx);
 131        let picker = cx.new(|cx| {
 132            Picker::uniform_list(delegate, window, cx)
 133                .show_scrollbar(true)
 134                .modal(!embedded)
 135        });
 136        let picker_focus_handle = picker.focus_handle(cx);
 137        picker.update(cx, |picker, _| {
 138            picker.delegate.focus_handle = picker_focus_handle.clone();
 139        });
 140
 141        let mut subscriptions = Vec::new();
 142        if let Some(repo) = &repository {
 143            let picker_entity = picker.clone();
 144            subscriptions.push(cx.subscribe(
 145                repo,
 146                move |_this, repo, event: &RepositoryEvent, cx| {
 147                    if matches!(event, RepositoryEvent::GitWorktreeListChanged) {
 148                        let worktrees_request = repo.update(cx, |repo, _| repo.worktrees());
 149                        let picker = picker_entity.clone();
 150                        cx.spawn(async move |_, cx| {
 151                            let all_worktrees: Vec<_> = worktrees_request
 152                                .await??
 153                                .into_iter()
 154                                .filter(|worktree| !worktree.is_bare)
 155                                .collect();
 156                            picker.update(cx, |picker, cx| {
 157                                picker.delegate.all_worktrees = Some(all_worktrees);
 158                                picker.delegate.refresh_forbidden_deletion_path(cx);
 159                            });
 160                            anyhow::Ok(())
 161                        })
 162                        .detach_and_log_err(cx);
 163                    }
 164                },
 165            ));
 166        }
 167
 168        Self {
 169            picker,
 170            picker_focus_handle,
 171            width,
 172            _subscriptions: subscriptions,
 173            embedded,
 174        }
 175    }
 176
 177    fn new_embedded(
 178        repository: Option<Entity<Repository>>,
 179        workspace: WeakEntity<Workspace>,
 180        width: Rems,
 181        window: &mut Window,
 182        cx: &mut Context<Self>,
 183    ) -> Self {
 184        let mut this = Self::new_inner(repository, workspace, width, true, window, cx);
 185        this._subscriptions
 186            .push(cx.subscribe(&this.picker, |_, _, _, cx| {
 187                cx.emit(DismissEvent);
 188            }));
 189        this
 190    }
 191
 192    pub fn handle_modifiers_changed(
 193        &mut self,
 194        ev: &ModifiersChangedEvent,
 195        _: &mut Window,
 196        cx: &mut Context<Self>,
 197    ) {
 198        self.picker
 199            .update(cx, |picker, _| picker.delegate.modifiers = ev.modifiers)
 200    }
 201
 202    pub fn handle_new_worktree(
 203        &mut self,
 204        replace_current_window: bool,
 205        window: &mut Window,
 206        cx: &mut Context<Self>,
 207    ) {
 208        self.picker.update(cx, |picker, cx| {
 209            let ix = picker.delegate.selected_index();
 210            let Some(entry) = picker.delegate.matches.get(ix) else {
 211                return;
 212            };
 213            let Some(default_branch) = picker.delegate.default_branch.clone() else {
 214                return;
 215            };
 216            if !entry.is_new {
 217                return;
 218            }
 219            picker.delegate.create_worktree(
 220                entry.worktree.display_name(),
 221                replace_current_window,
 222                Some(default_branch.into()),
 223                window,
 224                cx,
 225            );
 226        })
 227    }
 228
 229    pub fn handle_delete(
 230        &mut self,
 231        _: &DeleteWorktree,
 232        window: &mut Window,
 233        cx: &mut Context<Self>,
 234    ) {
 235        self.picker.update(cx, |picker, cx| {
 236            picker
 237                .delegate
 238                .delete_at(picker.delegate.selected_index, window, cx)
 239        })
 240    }
 241}
 242impl ModalView for WorktreeList {}
 243impl EventEmitter<DismissEvent> for WorktreeList {}
 244
 245impl Focusable for WorktreeList {
 246    fn focus_handle(&self, _: &App) -> FocusHandle {
 247        self.picker_focus_handle.clone()
 248    }
 249}
 250
 251impl Render for WorktreeList {
 252    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 253        v_flex()
 254            .key_context("GitWorktreeSelector")
 255            .w(self.width)
 256            .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed))
 257            .on_action(cx.listener(|this, _: &WorktreeFromDefault, w, cx| {
 258                this.handle_new_worktree(false, w, cx)
 259            }))
 260            .on_action(cx.listener(|this, _: &WorktreeFromDefaultOnWindow, w, cx| {
 261                this.handle_new_worktree(true, w, cx)
 262            }))
 263            .on_action(cx.listener(|this, _: &DeleteWorktree, window, cx| {
 264                this.handle_delete(&DeleteWorktree, window, cx)
 265            }))
 266            .child(self.picker.clone())
 267            .when(!self.embedded, |el| {
 268                el.on_mouse_down_out({
 269                    cx.listener(move |this, _, window, cx| {
 270                        this.picker.update(cx, |this, cx| {
 271                            this.cancel(&Default::default(), window, cx);
 272                        })
 273                    })
 274                })
 275            })
 276    }
 277}
 278
 279#[derive(Debug, Clone)]
 280struct WorktreeEntry {
 281    worktree: GitWorktree,
 282    positions: Vec<usize>,
 283    is_new: bool,
 284}
 285
 286impl WorktreeEntry {
 287    fn can_delete(&self, forbidden_deletion_path: Option<&PathBuf>) -> bool {
 288        !self.is_new
 289            && !self.worktree.is_main
 290            && forbidden_deletion_path != Some(&self.worktree.path)
 291    }
 292}
 293
 294pub struct WorktreeListDelegate {
 295    matches: Vec<WorktreeEntry>,
 296    all_worktrees: Option<Vec<GitWorktree>>,
 297    workspace: WeakEntity<Workspace>,
 298    repo: Option<Entity<Repository>>,
 299    selected_index: usize,
 300    last_query: String,
 301    modifiers: Modifiers,
 302    focus_handle: FocusHandle,
 303    default_branch: Option<SharedString>,
 304    forbidden_deletion_path: Option<PathBuf>,
 305    current_worktree_path: Option<PathBuf>,
 306}
 307
 308impl WorktreeListDelegate {
 309    fn new(
 310        workspace: WeakEntity<Workspace>,
 311        repo: Option<Entity<Repository>>,
 312        _window: &mut Window,
 313        cx: &mut Context<WorktreeList>,
 314    ) -> Self {
 315        let current_worktree_path = repo
 316            .as_ref()
 317            .map(|r| r.read(cx).work_directory_abs_path.to_path_buf());
 318
 319        Self {
 320            matches: vec![],
 321            all_worktrees: None,
 322            workspace,
 323            selected_index: 0,
 324            repo,
 325            last_query: Default::default(),
 326            modifiers: Default::default(),
 327            focus_handle: cx.focus_handle(),
 328            default_branch: None,
 329            forbidden_deletion_path: None,
 330            current_worktree_path,
 331        }
 332    }
 333
 334    fn create_worktree(
 335        &self,
 336        worktree_branch: &str,
 337        replace_current_window: bool,
 338        commit: Option<String>,
 339        window: &mut Window,
 340        cx: &mut Context<Picker<Self>>,
 341    ) {
 342        let Some(repo) = self.repo.clone() else {
 343            return;
 344        };
 345
 346        let branch = worktree_branch.to_string();
 347        let workspace = self.workspace.clone();
 348        cx.spawn_in(window, async move |_, cx| {
 349            let (receiver, new_worktree_path) = repo.update(cx, |repo, cx| {
 350                let worktree_directory_setting = ProjectSettings::get_global(cx)
 351                    .git
 352                    .worktree_directory
 353                    .clone();
 354                let new_worktree_path =
 355                    repo.path_for_new_linked_worktree(&branch, &worktree_directory_setting)?;
 356                let receiver = repo.create_worktree(
 357                    git::repository::CreateWorktreeTarget::NewBranch {
 358                        branch_name: branch.clone(),
 359                        base_sha: commit,
 360                    },
 361                    new_worktree_path.clone(),
 362                );
 363                anyhow::Ok((receiver, new_worktree_path))
 364            })?;
 365            receiver.await??;
 366
 367            workspace.update(cx, |workspace, cx| {
 368                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
 369                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
 370                    let project = workspace.project();
 371                    if let Some((parent_worktree, _)) =
 372                        project.read(cx).find_worktree(repo_path, cx)
 373                    {
 374                        let worktree_store = project.read(cx).worktree_store();
 375                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
 376                            if trusted_worktrees.can_trust(
 377                                &worktree_store,
 378                                parent_worktree.read(cx).id(),
 379                                cx,
 380                            ) {
 381                                trusted_worktrees.trust(
 382                                    &worktree_store,
 383                                    HashSet::from_iter([PathTrust::AbsPath(
 384                                        new_worktree_path.clone(),
 385                                    )]),
 386                                    cx,
 387                                );
 388                            }
 389                        });
 390                    }
 391                }
 392            })?;
 393
 394            let (connection_options, app_state, is_local) =
 395                workspace.update(cx, |workspace, cx| {
 396                    let project = workspace.project().clone();
 397                    let connection_options = project.read(cx).remote_connection_options(cx);
 398                    let app_state = workspace.app_state().clone();
 399                    let is_local = project.read(cx).is_local();
 400                    (connection_options, app_state, is_local)
 401                })?;
 402
 403            if is_local {
 404                workspace
 405                    .update_in(cx, |workspace, window, cx| {
 406                        workspace.open_workspace_for_paths(
 407                            OpenMode::Activate,
 408                            vec![new_worktree_path],
 409                            window,
 410                            cx,
 411                        )
 412                    })?
 413                    .await?;
 414            } else if let Some(connection_options) = connection_options {
 415                open_remote_worktree(
 416                    connection_options,
 417                    vec![new_worktree_path],
 418                    app_state,
 419                    workspace.clone(),
 420                    replace_current_window,
 421                    cx,
 422                )
 423                .await?;
 424            }
 425
 426            anyhow::Ok(())
 427        })
 428        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
 429            let msg = e.to_string();
 430            if msg.contains("git.worktree_directory") {
 431                Some(format!("Invalid git.worktree_directory setting: {}", e))
 432            } else {
 433                Some(msg)
 434            }
 435        });
 436    }
 437
 438    fn open_worktree(
 439        &self,
 440        worktree_path: &PathBuf,
 441        replace_current_window: bool,
 442        window: &mut Window,
 443        cx: &mut Context<Picker<Self>>,
 444    ) {
 445        let workspace = self.workspace.clone();
 446        let path = worktree_path.clone();
 447
 448        let Some((connection_options, app_state, is_local)) = workspace
 449            .update(cx, |workspace, cx| {
 450                let project = workspace.project().clone();
 451                let connection_options = project.read(cx).remote_connection_options(cx);
 452                let app_state = workspace.app_state().clone();
 453                let is_local = project.read(cx).is_local();
 454                (connection_options, app_state, is_local)
 455            })
 456            .log_err()
 457        else {
 458            return;
 459        };
 460        let open_mode = if replace_current_window {
 461            OpenMode::Activate
 462        } else {
 463            OpenMode::NewWindow
 464        };
 465
 466        if is_local {
 467            let open_task = workspace.update(cx, |workspace, cx| {
 468                workspace.open_workspace_for_paths(open_mode, vec![path], window, cx)
 469            });
 470            cx.spawn(async move |_, _| {
 471                open_task?.await?;
 472                anyhow::Ok(())
 473            })
 474            .detach_and_prompt_err(
 475                "Failed to open worktree",
 476                window,
 477                cx,
 478                |e, _, _| Some(e.to_string()),
 479            );
 480        } else if let Some(connection_options) = connection_options {
 481            cx.spawn_in(window, async move |_, cx| {
 482                open_remote_worktree(
 483                    connection_options,
 484                    vec![path],
 485                    app_state,
 486                    workspace,
 487                    replace_current_window,
 488                    cx,
 489                )
 490                .await
 491            })
 492            .detach_and_prompt_err(
 493                "Failed to open worktree",
 494                window,
 495                cx,
 496                |e, _, _| Some(e.to_string()),
 497            );
 498        }
 499
 500        cx.emit(DismissEvent);
 501    }
 502
 503    fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
 504        self.repo
 505            .as_ref()
 506            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
 507    }
 508
 509    fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 510        let Some(entry) = self.matches.get(idx).cloned() else {
 511            return;
 512        };
 513        if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
 514            return;
 515        }
 516        let Some(repo) = self.repo.clone() else {
 517            return;
 518        };
 519        let workspace = self.workspace.clone();
 520        let path = entry.worktree.path;
 521
 522        cx.spawn_in(window, async move |picker, cx| {
 523            let result = repo
 524                .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))
 525                .await?;
 526
 527            if let Err(e) = result {
 528                log::error!("Failed to remove worktree: {}", e);
 529                if let Some(workspace) = workspace.upgrade() {
 530                    cx.update(|_window, cx| {
 531                        show_error_toast(
 532                            workspace,
 533                            format!("worktree remove {}", path.display()),
 534                            e,
 535                            cx,
 536                        )
 537                    })?;
 538                }
 539                return Ok(());
 540            }
 541
 542            picker.update_in(cx, |picker, _, cx| {
 543                picker.delegate.matches.retain(|e| e.worktree.path != path);
 544                if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {
 545                    all_worktrees.retain(|w| w.path != path);
 546                }
 547                picker.delegate.refresh_forbidden_deletion_path(cx);
 548                if picker.delegate.matches.is_empty() {
 549                    picker.delegate.selected_index = 0;
 550                } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
 551                    picker.delegate.selected_index = picker.delegate.matches.len() - 1;
 552                }
 553                cx.notify();
 554            })?;
 555
 556            anyhow::Ok(())
 557        })
 558        .detach();
 559    }
 560
 561    fn refresh_forbidden_deletion_path(&mut self, cx: &App) {
 562        let Some(workspace) = self.workspace.upgrade() else {
 563            debug_panic!("Workspace should always be available or else the picker would be closed");
 564            self.forbidden_deletion_path = None;
 565            return;
 566        };
 567
 568        let visible_worktree_paths = workspace.read_with(cx, |workspace, cx| {
 569            workspace
 570                .project()
 571                .read(cx)
 572                .visible_worktrees(cx)
 573                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
 574                .collect::<Vec<_>>()
 575        });
 576
 577        self.forbidden_deletion_path = if visible_worktree_paths.len() == 1 {
 578            visible_worktree_paths.into_iter().next()
 579        } else {
 580            None
 581        };
 582    }
 583}
 584
 585async fn open_remote_worktree(
 586    connection_options: RemoteConnectionOptions,
 587    paths: Vec<PathBuf>,
 588    app_state: Arc<workspace::AppState>,
 589    workspace: WeakEntity<Workspace>,
 590    replace_current_window: bool,
 591    cx: &mut AsyncWindowContext,
 592) -> anyhow::Result<()> {
 593    let workspace_window = cx
 594        .window_handle()
 595        .downcast::<MultiWorkspace>()
 596        .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
 597
 598    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
 599        workspace.toggle_modal(window, cx, |window, cx| {
 600            RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
 601        });
 602
 603        let prompt = workspace
 604            .active_modal::<RemoteConnectionModal>(cx)
 605            .expect("Modal just created")
 606            .read(cx)
 607            .prompt
 608            .clone();
 609
 610        connect(
 611            ConnectionIdentifier::setup(),
 612            connection_options.clone(),
 613            prompt,
 614            window,
 615            cx,
 616        )
 617        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
 618    })?;
 619
 620    let session = connect_task.await;
 621
 622    workspace
 623        .update_in(cx, |workspace, _window, cx| {
 624            if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
 625                prompt.update(cx, |prompt, cx| prompt.finished(cx))
 626            }
 627        })
 628        .ok();
 629
 630    let Some(Some(session)) = session else {
 631        return Ok(());
 632    };
 633
 634    let new_project: Entity<project::Project> = cx.update(|_, cx| {
 635        project::Project::remote(
 636            session,
 637            app_state.client.clone(),
 638            app_state.node_runtime.clone(),
 639            app_state.user_store.clone(),
 640            app_state.languages.clone(),
 641            app_state.fs.clone(),
 642            true,
 643            cx,
 644        )
 645    })?;
 646
 647    let window_to_use = if replace_current_window {
 648        workspace_window
 649    } else {
 650        let workspace_position = cx
 651            .update(|_, cx| {
 652                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
 653            })?
 654            .await
 655            .context("fetching workspace position from db")?;
 656
 657        let mut options =
 658            cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
 659        options.window_bounds = workspace_position.window_bounds;
 660
 661        cx.open_window(options, |window, cx| {
 662            let workspace = cx.new(|cx| {
 663                let mut workspace =
 664                    Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
 665                workspace.centered_layout = workspace_position.centered_layout;
 666                workspace
 667            });
 668            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
 669        })?
 670    };
 671
 672    workspace::open_remote_project_with_existing_connection(
 673        connection_options,
 674        new_project,
 675        paths,
 676        app_state,
 677        window_to_use,
 678        None,
 679        cx,
 680    )
 681    .await?;
 682
 683    Ok(())
 684}
 685
 686impl PickerDelegate for WorktreeListDelegate {
 687    type ListItem = ListItem;
 688
 689    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 690        "Select worktree…".into()
 691    }
 692
 693    fn editor_position(&self) -> PickerEditorPosition {
 694        PickerEditorPosition::Start
 695    }
 696
 697    fn match_count(&self) -> usize {
 698        self.matches.len()
 699    }
 700
 701    fn selected_index(&self) -> usize {
 702        self.selected_index
 703    }
 704
 705    fn set_selected_index(
 706        &mut self,
 707        ix: usize,
 708        _window: &mut Window,
 709        _: &mut Context<Picker<Self>>,
 710    ) {
 711        self.selected_index = ix;
 712    }
 713
 714    fn update_matches(
 715        &mut self,
 716        query: String,
 717        window: &mut Window,
 718        cx: &mut Context<Picker<Self>>,
 719    ) -> Task<()> {
 720        let Some(all_worktrees) = self.all_worktrees.clone() else {
 721            return Task::ready(());
 722        };
 723
 724        cx.spawn_in(window, async move |picker, cx| {
 725            let main_worktree_path = all_worktrees
 726                .iter()
 727                .find(|wt| wt.is_main)
 728                .map(|wt| wt.path.clone());
 729
 730            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
 731                all_worktrees
 732                    .into_iter()
 733                    .map(|worktree| WorktreeEntry {
 734                        worktree,
 735                        positions: Vec::new(),
 736                        is_new: false,
 737                    })
 738                    .collect()
 739            } else {
 740                let candidates = all_worktrees
 741                    .iter()
 742                    .enumerate()
 743                    .map(|(ix, worktree)| {
 744                        StringMatchCandidate::new(
 745                            ix,
 746                            &worktree.directory_name(main_worktree_path.as_deref()),
 747                        )
 748                    })
 749                    .collect::<Vec<StringMatchCandidate>>();
 750                fuzzy::match_strings(
 751                    &candidates,
 752                    &query,
 753                    true,
 754                    true,
 755                    10000,
 756                    &Default::default(),
 757                    cx.background_executor().clone(),
 758                )
 759                .await
 760                .into_iter()
 761                .map(|candidate| WorktreeEntry {
 762                    worktree: all_worktrees[candidate.candidate_id].clone(),
 763                    positions: candidate.positions,
 764                    is_new: false,
 765                })
 766                .collect()
 767            };
 768            picker
 769                .update(cx, |picker, _| {
 770                    if !query.is_empty()
 771                        && !matches.first().is_some_and(|entry| {
 772                            entry.worktree.directory_name(main_worktree_path.as_deref()) == query
 773                        })
 774                    {
 775                        let query = query.replace(' ', "-");
 776                        matches.push(WorktreeEntry {
 777                            worktree: GitWorktree {
 778                                path: Default::default(),
 779                                ref_name: Some(format!("refs/heads/{query}").into()),
 780                                sha: Default::default(),
 781                                is_main: false,
 782                                is_bare: false,
 783                            },
 784                            positions: Vec::new(),
 785                            is_new: true,
 786                        })
 787                    }
 788                    let delegate = &mut picker.delegate;
 789                    delegate.matches = matches;
 790                    if delegate.matches.is_empty() {
 791                        delegate.selected_index = 0;
 792                    } else {
 793                        delegate.selected_index =
 794                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
 795                    }
 796                    delegate.last_query = query;
 797                })
 798                .log_err();
 799        })
 800    }
 801
 802    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 803        let Some(entry) = self.matches.get(self.selected_index()) else {
 804            return;
 805        };
 806        if entry.is_new {
 807            self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
 808        } else {
 809            self.open_worktree(&entry.worktree.path, !secondary, window, cx);
 810        }
 811
 812        cx.emit(DismissEvent);
 813    }
 814
 815    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 816        cx.emit(DismissEvent);
 817    }
 818
 819    fn render_match(
 820        &self,
 821        ix: usize,
 822        selected: bool,
 823        _window: &mut Window,
 824        cx: &mut Context<Picker<Self>>,
 825    ) -> Option<Self::ListItem> {
 826        let entry = &self.matches.get(ix)?;
 827        let path = entry.worktree.path.compact().to_string_lossy().to_string();
 828        let sha = entry
 829            .worktree
 830            .sha
 831            .clone()
 832            .chars()
 833            .take(7)
 834            .collect::<String>();
 835
 836        let (branch_name, sublabel) = if entry.is_new {
 837            (
 838                Label::new(format!(
 839                    "Create Worktree: \"{}\"",
 840                    entry.worktree.display_name()
 841                ))
 842                .truncate()
 843                .into_any_element(),
 844                format!(
 845                    "based off {}",
 846                    self.base_branch(cx).unwrap_or("the current branch")
 847                ),
 848            )
 849        } else {
 850            let main_worktree_path = self
 851                .all_worktrees
 852                .as_ref()
 853                .and_then(|wts| wts.iter().find(|wt| wt.is_main))
 854                .map(|wt| wt.path.as_path());
 855            let display_name = entry.worktree.directory_name(main_worktree_path);
 856            let first_line = display_name.lines().next().unwrap_or(&display_name);
 857            let positions: Vec<_> = entry
 858                .positions
 859                .iter()
 860                .copied()
 861                .filter(|&pos| pos < first_line.len())
 862                .collect();
 863
 864            (
 865                HighlightedLabel::new(first_line.to_owned(), positions)
 866                    .truncate()
 867                    .into_any_element(),
 868                path,
 869            )
 870        };
 871
 872        let focus_handle = self.focus_handle.clone();
 873
 874        let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
 875
 876        let delete_button = |entry_ix: usize| {
 877            IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
 878                .icon_size(IconSize::Small)
 879                .tooltip(move |_, cx| {
 880                    Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
 881                })
 882                .on_click(cx.listener(move |this, _, window, cx| {
 883                    this.delegate.delete_at(entry_ix, window, cx);
 884                }))
 885        };
 886
 887        let is_current = !entry.is_new
 888            && self
 889                .current_worktree_path
 890                .as_ref()
 891                .is_some_and(|current| *current == entry.worktree.path);
 892
 893        let entry_icon = if entry.is_new {
 894            IconName::Plus
 895        } else if is_current {
 896            IconName::Check
 897        } else {
 898            IconName::GitWorktree
 899        };
 900
 901        Some(
 902            ListItem::new(format!("worktree-menu-{ix}"))
 903                .inset(true)
 904                .spacing(ListItemSpacing::Sparse)
 905                .toggle_state(selected)
 906                .child(
 907                    h_flex()
 908                        .w_full()
 909                        .gap_2p5()
 910                        .child(
 911                            Icon::new(entry_icon)
 912                                .color(if is_current {
 913                                    Color::Accent
 914                                } else {
 915                                    Color::Muted
 916                                })
 917                                .size(IconSize::Small),
 918                        )
 919                        .child(v_flex().w_full().min_w_0().child(branch_name).map(|this| {
 920                            if entry.is_new {
 921                                this.child(
 922                                    Label::new(sublabel)
 923                                        .size(LabelSize::Small)
 924                                        .color(Color::Muted)
 925                                        .truncate(),
 926                                )
 927                            } else {
 928                                this.child(
 929                                    h_flex()
 930                                        .w_full()
 931                                        .min_w_0()
 932                                        .gap_1p5()
 933                                        .when_some(
 934                                            entry.worktree.branch_name().map(|b| b.to_string()),
 935                                            |this, branch| {
 936                                                this.child(
 937                                                    Label::new(branch)
 938                                                        .size(LabelSize::Small)
 939                                                        .color(Color::Muted),
 940                                                )
 941                                                .child(
 942                                                    Label::new("")
 943                                                        .alpha(0.5)
 944                                                        .color(Color::Muted)
 945                                                        .size(LabelSize::Small),
 946                                                )
 947                                            },
 948                                        )
 949                                        .child(
 950                                            Label::new(sha)
 951                                                .size(LabelSize::Small)
 952                                                .color(Color::Muted),
 953                                        )
 954                                        .child(
 955                                            Label::new("")
 956                                                .alpha(0.5)
 957                                                .color(Color::Muted)
 958                                                .size(LabelSize::Small),
 959                                        )
 960                                        .child(
 961                                            Label::new(sublabel)
 962                                                .truncate_start()
 963                                                .color(Color::Muted)
 964                                                .size(LabelSize::Small)
 965                                                .flex_1(),
 966                                        )
 967                                        .into_any_element(),
 968                                )
 969                            }
 970                        })),
 971                )
 972                .when(!entry.is_new && !is_current, |this| {
 973                    let focus_handle = self.focus_handle.clone();
 974                    let open_in_new_window_button =
 975                        IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
 976                            .icon_size(IconSize::Small)
 977                            .tooltip(move |_, cx| {
 978                                Tooltip::for_action_in(
 979                                    "Open in New Window",
 980                                    &menu::SecondaryConfirm,
 981                                    &focus_handle,
 982                                    cx,
 983                                )
 984                            })
 985                            .on_click(|_, window, cx| {
 986                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
 987                            });
 988
 989                    this.end_slot(
 990                        h_flex()
 991                            .gap_0p5()
 992                            .child(open_in_new_window_button)
 993                            .when(can_delete, |this| this.child(delete_button(ix))),
 994                    )
 995                    .show_end_slot_on_hover()
 996                }),
 997        )
 998    }
 999
1000    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
1001        Some("No worktrees found".into())
1002    }
1003
1004    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
1005        let focus_handle = self.focus_handle.clone();
1006        let selected_entry = self.matches.get(self.selected_index);
1007        let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
1008        let can_delete = selected_entry
1009            .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
1010        let is_current = selected_entry.is_some_and(|entry| {
1011            !entry.is_new
1012                && self
1013                    .current_worktree_path
1014                    .as_ref()
1015                    .is_some_and(|current| *current == entry.worktree.path)
1016        });
1017
1018        let footer_container = h_flex()
1019            .w_full()
1020            .p_1p5()
1021            .gap_0p5()
1022            .justify_end()
1023            .border_t_1()
1024            .border_color(cx.theme().colors().border_variant);
1025
1026        if is_creating {
1027            let from_default_button = self.default_branch.as_ref().map(|default_branch| {
1028                Button::new(
1029                    "worktree-from-default",
1030                    format!("Create from: {default_branch}"),
1031                )
1032                .key_binding(
1033                    KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
1034                        .map(|kb| kb.size(rems_from_px(12.))),
1035                )
1036                .on_click(|_, window, cx| {
1037                    window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
1038                })
1039            });
1040
1041            let current_branch = self.base_branch(cx).unwrap_or("current branch");
1042
1043            Some(
1044                footer_container
1045                    .when_some(from_default_button, |this, button| this.child(button))
1046                    .child(
1047                        Button::new(
1048                            "worktree-from-current",
1049                            format!("Create from: {current_branch}"),
1050                        )
1051                        .key_binding(
1052                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1053                                .map(|kb| kb.size(rems_from_px(12.))),
1054                        )
1055                        .on_click(|_, window, cx| {
1056                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1057                        }),
1058                    )
1059                    .into_any(),
1060            )
1061        } else {
1062            Some(
1063                footer_container
1064                    .when(can_delete, |this| {
1065                        this.child(
1066                            Button::new("delete-worktree", "Delete")
1067                                .key_binding(
1068                                    KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
1069                                        .map(|kb| kb.size(rems_from_px(12.))),
1070                                )
1071                                .on_click(|_, window, cx| {
1072                                    window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
1073                                }),
1074                        )
1075                    })
1076                    .when(!is_current, |this| {
1077                        this.child(
1078                            Button::new("open-in-new-window", "Open in New Window")
1079                                .key_binding(
1080                                    KeyBinding::for_action_in(
1081                                        &menu::SecondaryConfirm,
1082                                        &focus_handle,
1083                                        cx,
1084                                    )
1085                                    .map(|kb| kb.size(rems_from_px(12.))),
1086                                )
1087                                .on_click(|_, window, cx| {
1088                                    window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
1089                                }),
1090                        )
1091                    })
1092                    .child(
1093                        Button::new("open-in-window", "Open")
1094                            .key_binding(
1095                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1096                                    .map(|kb| kb.size(rems_from_px(12.))),
1097                            )
1098                            .on_click(|_, window, cx| {
1099                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1100                            }),
1101                    )
1102                    .into_any(),
1103            )
1104        }
1105    }
1106}