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        cx,
 652    )
 653    .await?;
 654
 655    Ok(())
 656}
 657
 658impl PickerDelegate for WorktreeListDelegate {
 659    type ListItem = ListItem;
 660
 661    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
 662        "Select worktree…".into()
 663    }
 664
 665    fn editor_position(&self) -> PickerEditorPosition {
 666        PickerEditorPosition::Start
 667    }
 668
 669    fn match_count(&self) -> usize {
 670        self.matches.len()
 671    }
 672
 673    fn selected_index(&self) -> usize {
 674        self.selected_index
 675    }
 676
 677    fn set_selected_index(
 678        &mut self,
 679        ix: usize,
 680        _window: &mut Window,
 681        _: &mut Context<Picker<Self>>,
 682    ) {
 683        self.selected_index = ix;
 684    }
 685
 686    fn update_matches(
 687        &mut self,
 688        query: String,
 689        window: &mut Window,
 690        cx: &mut Context<Picker<Self>>,
 691    ) -> Task<()> {
 692        let Some(all_worktrees) = self.all_worktrees.clone() else {
 693            return Task::ready(());
 694        };
 695
 696        cx.spawn_in(window, async move |picker, cx| {
 697            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
 698                all_worktrees
 699                    .into_iter()
 700                    .map(|worktree| WorktreeEntry {
 701                        worktree,
 702                        positions: Vec::new(),
 703                        is_new: false,
 704                    })
 705                    .collect()
 706            } else {
 707                let candidates = all_worktrees
 708                    .iter()
 709                    .enumerate()
 710                    .map(|(ix, worktree)| {
 711                        let name = if worktree.is_main {
 712                            MAIN_WORKTREE_DISPLAY_NAME
 713                        } else {
 714                            worktree.display_name()
 715                        };
 716                        StringMatchCandidate::new(ix, name)
 717                    })
 718                    .collect::<Vec<StringMatchCandidate>>();
 719                fuzzy::match_strings(
 720                    &candidates,
 721                    &query,
 722                    true,
 723                    true,
 724                    10000,
 725                    &Default::default(),
 726                    cx.background_executor().clone(),
 727                )
 728                .await
 729                .into_iter()
 730                .map(|candidate| WorktreeEntry {
 731                    worktree: all_worktrees[candidate.candidate_id].clone(),
 732                    positions: candidate.positions,
 733                    is_new: false,
 734                })
 735                .collect()
 736            };
 737            picker
 738                .update(cx, |picker, _| {
 739                    if !query.is_empty()
 740                        && !matches.first().is_some_and(|entry| {
 741                            let name = if entry.worktree.is_main {
 742                                MAIN_WORKTREE_DISPLAY_NAME
 743                            } else {
 744                                entry.worktree.display_name()
 745                            };
 746                            name == query
 747                        })
 748                    {
 749                        let query = query.replace(' ', "-");
 750                        matches.push(WorktreeEntry {
 751                            worktree: GitWorktree {
 752                                path: Default::default(),
 753                                ref_name: Some(format!("refs/heads/{query}").into()),
 754                                sha: Default::default(),
 755                                is_main: false,
 756                            },
 757                            positions: Vec::new(),
 758                            is_new: true,
 759                        })
 760                    }
 761                    let delegate = &mut picker.delegate;
 762                    delegate.matches = matches;
 763                    if delegate.matches.is_empty() {
 764                        delegate.selected_index = 0;
 765                    } else {
 766                        delegate.selected_index =
 767                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
 768                    }
 769                    delegate.last_query = query;
 770                })
 771                .log_err();
 772        })
 773    }
 774
 775    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
 776        let Some(entry) = self.matches.get(self.selected_index()) else {
 777            return;
 778        };
 779        if entry.is_new {
 780            self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
 781        } else {
 782            self.open_worktree(&entry.worktree.path, !secondary, window, cx);
 783        }
 784
 785        cx.emit(DismissEvent);
 786    }
 787
 788    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
 789        cx.emit(DismissEvent);
 790    }
 791
 792    fn render_match(
 793        &self,
 794        ix: usize,
 795        selected: bool,
 796        _window: &mut Window,
 797        cx: &mut Context<Picker<Self>>,
 798    ) -> Option<Self::ListItem> {
 799        let entry = &self.matches.get(ix)?;
 800        let path = entry.worktree.path.compact().to_string_lossy().to_string();
 801        let sha = entry
 802            .worktree
 803            .sha
 804            .clone()
 805            .chars()
 806            .take(7)
 807            .collect::<String>();
 808
 809        let (branch_name, sublabel) = if entry.is_new {
 810            (
 811                Label::new(format!(
 812                    "Create Worktree: \"{}\"",
 813                    entry.worktree.display_name()
 814                ))
 815                .truncate()
 816                .into_any_element(),
 817                format!(
 818                    "based off {}",
 819                    self.base_branch(cx).unwrap_or("the current branch")
 820                ),
 821            )
 822        } else {
 823            let display_name = if entry.worktree.is_main {
 824                MAIN_WORKTREE_DISPLAY_NAME
 825            } else {
 826                entry.worktree.display_name()
 827            };
 828            let first_line = display_name.lines().next().unwrap_or(display_name);
 829            let positions: Vec<_> = entry
 830                .positions
 831                .iter()
 832                .copied()
 833                .filter(|&pos| pos < first_line.len())
 834                .collect();
 835
 836            (
 837                HighlightedLabel::new(first_line.to_owned(), positions)
 838                    .truncate()
 839                    .into_any_element(),
 840                path,
 841            )
 842        };
 843
 844        let focus_handle = self.focus_handle.clone();
 845
 846        let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
 847
 848        let delete_button = |entry_ix: usize| {
 849            IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
 850                .icon_size(IconSize::Small)
 851                .tooltip(move |_, cx| {
 852                    Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
 853                })
 854                .on_click(cx.listener(move |this, _, window, cx| {
 855                    this.delegate.delete_at(entry_ix, window, cx);
 856                }))
 857        };
 858
 859        let is_current = !entry.is_new
 860            && self
 861                .current_worktree_path
 862                .as_ref()
 863                .is_some_and(|current| *current == entry.worktree.path);
 864
 865        let entry_icon = if entry.is_new {
 866            IconName::Plus
 867        } else if is_current {
 868            IconName::Check
 869        } else {
 870            IconName::GitWorktree
 871        };
 872
 873        Some(
 874            ListItem::new(format!("worktree-menu-{ix}"))
 875                .inset(true)
 876                .spacing(ListItemSpacing::Sparse)
 877                .toggle_state(selected)
 878                .child(
 879                    h_flex()
 880                        .w_full()
 881                        .gap_2p5()
 882                        .child(
 883                            Icon::new(entry_icon)
 884                                .color(if is_current {
 885                                    Color::Accent
 886                                } else {
 887                                    Color::Muted
 888                                })
 889                                .size(IconSize::Small),
 890                        )
 891                        .child(v_flex().w_full().child(branch_name).map(|this| {
 892                            if entry.is_new {
 893                                this.child(
 894                                    Label::new(sublabel)
 895                                        .size(LabelSize::Small)
 896                                        .color(Color::Muted)
 897                                        .truncate(),
 898                                )
 899                            } else {
 900                                this.child(
 901                                    h_flex()
 902                                        .w_full()
 903                                        .min_w_0()
 904                                        .gap_1p5()
 905                                        .child(
 906                                            Label::new(sha)
 907                                                .size(LabelSize::Small)
 908                                                .color(Color::Muted),
 909                                        )
 910                                        .child(
 911                                            Label::new("")
 912                                                .alpha(0.5)
 913                                                .color(Color::Muted)
 914                                                .size(LabelSize::Small),
 915                                        )
 916                                        .child(
 917                                            Label::new(sublabel)
 918                                                .truncate_start()
 919                                                .color(Color::Muted)
 920                                                .size(LabelSize::Small)
 921                                                .flex_1(),
 922                                        )
 923                                        .into_any_element(),
 924                                )
 925                            }
 926                        })),
 927                )
 928                .when(!entry.is_new, |this| {
 929                    let focus_handle = self.focus_handle.clone();
 930                    let open_in_new_window_button =
 931                        IconButton::new(("open-new-window", ix), IconName::ArrowUpRight)
 932                            .icon_size(IconSize::Small)
 933                            .tooltip(move |_, cx| {
 934                                Tooltip::for_action_in(
 935                                    "Open in New Window",
 936                                    &menu::SecondaryConfirm,
 937                                    &focus_handle,
 938                                    cx,
 939                                )
 940                            })
 941                            .on_click(|_, window, cx| {
 942                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx);
 943                            });
 944
 945                    this.end_slot(
 946                        h_flex()
 947                            .gap_0p5()
 948                            .child(open_in_new_window_button)
 949                            .when(can_delete, |this| this.child(delete_button(ix))),
 950                    )
 951                    .show_end_slot_on_hover()
 952                }),
 953        )
 954    }
 955
 956    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
 957        Some("No worktrees found".into())
 958    }
 959
 960    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
 961        let focus_handle = self.focus_handle.clone();
 962        let selected_entry = self.matches.get(self.selected_index);
 963        let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
 964        let can_delete = selected_entry
 965            .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
 966
 967        let footer_container = h_flex()
 968            .w_full()
 969            .p_1p5()
 970            .gap_0p5()
 971            .justify_end()
 972            .border_t_1()
 973            .border_color(cx.theme().colors().border_variant);
 974
 975        if is_creating {
 976            let from_default_button = self.default_branch.as_ref().map(|default_branch| {
 977                Button::new(
 978                    "worktree-from-default",
 979                    format!("Create from: {default_branch}"),
 980                )
 981                .key_binding(
 982                    KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
 983                        .map(|kb| kb.size(rems_from_px(12.))),
 984                )
 985                .on_click(|_, window, cx| {
 986                    window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
 987                })
 988            });
 989
 990            let current_branch = self.base_branch(cx).unwrap_or("current branch");
 991
 992            Some(
 993                footer_container
 994                    .when_some(from_default_button, |this, button| this.child(button))
 995                    .child(
 996                        Button::new(
 997                            "worktree-from-current",
 998                            format!("Create from: {current_branch}"),
 999                        )
1000                        .key_binding(
1001                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1002                                .map(|kb| kb.size(rems_from_px(12.))),
1003                        )
1004                        .on_click(|_, window, cx| {
1005                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1006                        }),
1007                    )
1008                    .into_any(),
1009            )
1010        } else {
1011            Some(
1012                footer_container
1013                    .when(can_delete, |this| {
1014                        this.child(
1015                            Button::new("delete-worktree", "Delete")
1016                                .key_binding(
1017                                    KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
1018                                        .map(|kb| kb.size(rems_from_px(12.))),
1019                                )
1020                                .on_click(|_, window, cx| {
1021                                    window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
1022                                }),
1023                        )
1024                    })
1025                    .child(
1026                        Button::new("open-in-new-window", "Open in New Window")
1027                            .key_binding(
1028                                KeyBinding::for_action_in(
1029                                    &menu::SecondaryConfirm,
1030                                    &focus_handle,
1031                                    cx,
1032                                )
1033                                .map(|kb| kb.size(rems_from_px(12.))),
1034                            )
1035                            .on_click(|_, window, cx| {
1036                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
1037                            }),
1038                    )
1039                    .child(
1040                        Button::new("open-in-window", "Open")
1041                            .key_binding(
1042                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
1043                                    .map(|kb| kb.size(rems_from_px(12.))),
1044                            )
1045                            .on_click(|_, window, cx| {
1046                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
1047                            }),
1048                    )
1049                    .into_any(),
1050            )
1051        }
1052    }
1053}