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