worktree_picker.rs

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