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