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 =
322                    repo.create_worktree(branch.clone(), new_worktree_path.clone(), commit);
323                anyhow::Ok((receiver, new_worktree_path))
324            })?;
325            receiver.await??;
326
327            workspace.update(cx, |workspace, cx| {
328                if let Some(trusted_worktrees) = TrustedWorktrees::try_get_global(cx) {
329                    let repo_path = &repo.read(cx).snapshot().work_directory_abs_path;
330                    let project = workspace.project();
331                    if let Some((parent_worktree, _)) =
332                        project.read(cx).find_worktree(repo_path, cx)
333                    {
334                        let worktree_store = project.read(cx).worktree_store();
335                        trusted_worktrees.update(cx, |trusted_worktrees, cx| {
336                            if trusted_worktrees.can_trust(
337                                &worktree_store,
338                                parent_worktree.read(cx).id(),
339                                cx,
340                            ) {
341                                trusted_worktrees.trust(
342                                    &worktree_store,
343                                    HashSet::from_iter([PathTrust::AbsPath(
344                                        new_worktree_path.clone(),
345                                    )]),
346                                    cx,
347                                );
348                            }
349                        });
350                    }
351                }
352            })?;
353
354            let (connection_options, app_state, is_local) =
355                workspace.update(cx, |workspace, cx| {
356                    let project = workspace.project().clone();
357                    let connection_options = project.read(cx).remote_connection_options(cx);
358                    let app_state = workspace.app_state().clone();
359                    let is_local = project.read(cx).is_local();
360                    (connection_options, app_state, is_local)
361                })?;
362
363            if is_local {
364                workspace
365                    .update_in(cx, |workspace, window, cx| {
366                        workspace.open_workspace_for_paths(
367                            OpenMode::Replace,
368                            vec![new_worktree_path],
369                            window,
370                            cx,
371                        )
372                    })?
373                    .await?;
374            } else if let Some(connection_options) = connection_options {
375                open_remote_worktree(
376                    connection_options,
377                    vec![new_worktree_path],
378                    app_state,
379                    workspace.clone(),
380                    replace_current_window,
381                    cx,
382                )
383                .await?;
384            }
385
386            anyhow::Ok(())
387        })
388        .detach_and_prompt_err("Failed to create worktree", window, cx, |e, _, _| {
389            let msg = e.to_string();
390            if msg.contains("git.worktree_directory") {
391                Some(format!("Invalid git.worktree_directory setting: {}", e))
392            } else {
393                Some(msg)
394            }
395        });
396    }
397
398    fn open_worktree(
399        &self,
400        worktree_path: &PathBuf,
401        replace_current_window: bool,
402        window: &mut Window,
403        cx: &mut Context<Picker<Self>>,
404    ) {
405        let workspace = self.workspace.clone();
406        let path = worktree_path.clone();
407
408        let Some((connection_options, app_state, is_local)) = workspace
409            .update(cx, |workspace, cx| {
410                let project = workspace.project().clone();
411                let connection_options = project.read(cx).remote_connection_options(cx);
412                let app_state = workspace.app_state().clone();
413                let is_local = project.read(cx).is_local();
414                (connection_options, app_state, is_local)
415            })
416            .log_err()
417        else {
418            return;
419        };
420        let open_mode = if replace_current_window {
421            OpenMode::Replace
422        } else {
423            OpenMode::NewWindow
424        };
425
426        if is_local {
427            let open_task = workspace.update(cx, |workspace, cx| {
428                workspace.open_workspace_for_paths(open_mode, vec![path], window, cx)
429            });
430            cx.spawn(async move |_, _| {
431                open_task?.await?;
432                anyhow::Ok(())
433            })
434            .detach_and_prompt_err(
435                "Failed to open worktree",
436                window,
437                cx,
438                |e, _, _| Some(e.to_string()),
439            );
440        } else if let Some(connection_options) = connection_options {
441            cx.spawn_in(window, async move |_, cx| {
442                open_remote_worktree(
443                    connection_options,
444                    vec![path],
445                    app_state,
446                    workspace,
447                    replace_current_window,
448                    cx,
449                )
450                .await
451            })
452            .detach_and_prompt_err(
453                "Failed to open worktree",
454                window,
455                cx,
456                |e, _, _| Some(e.to_string()),
457            );
458        }
459
460        cx.emit(DismissEvent);
461    }
462
463    fn base_branch<'a>(&'a self, cx: &'a mut Context<Picker<Self>>) -> Option<&'a str> {
464        self.repo
465            .as_ref()
466            .and_then(|repo| repo.read(cx).branch.as_ref().map(|b| b.name()))
467    }
468
469    fn delete_at(&self, idx: usize, window: &mut Window, cx: &mut Context<Picker<Self>>) {
470        let Some(entry) = self.matches.get(idx).cloned() else {
471            return;
472        };
473        if !entry.can_delete(self.forbidden_deletion_path.as_ref()) {
474            return;
475        }
476        let Some(repo) = self.repo.clone() else {
477            return;
478        };
479        let workspace = self.workspace.clone();
480        let path = entry.worktree.path;
481
482        cx.spawn_in(window, async move |picker, cx| {
483            let result = repo
484                .update(cx, |repo, _| repo.remove_worktree(path.clone(), false))
485                .await?;
486
487            if let Err(e) = result {
488                log::error!("Failed to remove worktree: {}", e);
489                if let Some(workspace) = workspace.upgrade() {
490                    cx.update(|_window, cx| {
491                        show_error_toast(
492                            workspace,
493                            format!("worktree remove {}", path.display()),
494                            e,
495                            cx,
496                        )
497                    })?;
498                }
499                return Ok(());
500            }
501
502            picker.update_in(cx, |picker, _, cx| {
503                picker.delegate.matches.retain(|e| e.worktree.path != path);
504                if let Some(all_worktrees) = &mut picker.delegate.all_worktrees {
505                    all_worktrees.retain(|w| w.path != path);
506                }
507                picker.delegate.refresh_forbidden_deletion_path(cx);
508                if picker.delegate.matches.is_empty() {
509                    picker.delegate.selected_index = 0;
510                } else if picker.delegate.selected_index >= picker.delegate.matches.len() {
511                    picker.delegate.selected_index = picker.delegate.matches.len() - 1;
512                }
513                cx.notify();
514            })?;
515
516            anyhow::Ok(())
517        })
518        .detach();
519    }
520
521    fn refresh_forbidden_deletion_path(&mut self, cx: &App) {
522        let Some(workspace) = self.workspace.upgrade() else {
523            debug_panic!("Workspace should always be available or else the picker would be closed");
524            self.forbidden_deletion_path = None;
525            return;
526        };
527
528        let visible_worktree_paths = workspace.read_with(cx, |workspace, cx| {
529            workspace
530                .project()
531                .read(cx)
532                .visible_worktrees(cx)
533                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
534                .collect::<Vec<_>>()
535        });
536
537        self.forbidden_deletion_path = if visible_worktree_paths.len() == 1 {
538            visible_worktree_paths.into_iter().next()
539        } else {
540            None
541        };
542    }
543}
544
545async fn open_remote_worktree(
546    connection_options: RemoteConnectionOptions,
547    paths: Vec<PathBuf>,
548    app_state: Arc<workspace::AppState>,
549    workspace: WeakEntity<Workspace>,
550    replace_current_window: bool,
551    cx: &mut AsyncWindowContext,
552) -> anyhow::Result<()> {
553    let workspace_window = cx
554        .window_handle()
555        .downcast::<MultiWorkspace>()
556        .ok_or_else(|| anyhow::anyhow!("Window is not a Workspace window"))?;
557
558    let connect_task = workspace.update_in(cx, |workspace, window, cx| {
559        workspace.toggle_modal(window, cx, |window, cx| {
560            RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx)
561        });
562
563        let prompt = workspace
564            .active_modal::<RemoteConnectionModal>(cx)
565            .expect("Modal just created")
566            .read(cx)
567            .prompt
568            .clone();
569
570        connect(
571            ConnectionIdentifier::setup(),
572            connection_options.clone(),
573            prompt,
574            window,
575            cx,
576        )
577        .prompt_err("Failed to connect", window, cx, |_, _, _| None)
578    })?;
579
580    let session = connect_task.await;
581
582    workspace
583        .update_in(cx, |workspace, _window, cx| {
584            if let Some(prompt) = workspace.active_modal::<RemoteConnectionModal>(cx) {
585                prompt.update(cx, |prompt, cx| prompt.finished(cx))
586            }
587        })
588        .ok();
589
590    let Some(Some(session)) = session else {
591        return Ok(());
592    };
593
594    let new_project: Entity<project::Project> = cx.update(|_, cx| {
595        project::Project::remote(
596            session,
597            app_state.client.clone(),
598            app_state.node_runtime.clone(),
599            app_state.user_store.clone(),
600            app_state.languages.clone(),
601            app_state.fs.clone(),
602            true,
603            cx,
604        )
605    })?;
606
607    let window_to_use = if replace_current_window {
608        workspace_window
609    } else {
610        let workspace_position = cx
611            .update(|_, cx| {
612                workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
613            })?
614            .await
615            .context("fetching workspace position from db")?;
616
617        let mut options =
618            cx.update(|_, cx| (app_state.build_window_options)(workspace_position.display, cx))?;
619        options.window_bounds = workspace_position.window_bounds;
620
621        cx.open_window(options, |window, cx| {
622            let workspace = cx.new(|cx| {
623                let mut workspace =
624                    Workspace::new(None, new_project.clone(), app_state.clone(), window, cx);
625                workspace.centered_layout = workspace_position.centered_layout;
626                workspace
627            });
628            cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
629        })?
630    };
631
632    workspace::open_remote_project_with_existing_connection(
633        connection_options,
634        new_project,
635        paths,
636        app_state,
637        window_to_use,
638        cx,
639    )
640    .await?;
641
642    Ok(())
643}
644
645impl PickerDelegate for WorktreeListDelegate {
646    type ListItem = ListItem;
647
648    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
649        "Select worktree…".into()
650    }
651
652    fn editor_position(&self) -> PickerEditorPosition {
653        PickerEditorPosition::Start
654    }
655
656    fn match_count(&self) -> usize {
657        self.matches.len()
658    }
659
660    fn selected_index(&self) -> usize {
661        self.selected_index
662    }
663
664    fn set_selected_index(
665        &mut self,
666        ix: usize,
667        _window: &mut Window,
668        _: &mut Context<Picker<Self>>,
669    ) {
670        self.selected_index = ix;
671    }
672
673    fn update_matches(
674        &mut self,
675        query: String,
676        window: &mut Window,
677        cx: &mut Context<Picker<Self>>,
678    ) -> Task<()> {
679        let Some(all_worktrees) = self.all_worktrees.clone() else {
680            return Task::ready(());
681        };
682
683        cx.spawn_in(window, async move |picker, cx| {
684            let mut matches: Vec<WorktreeEntry> = if query.is_empty() {
685                all_worktrees
686                    .into_iter()
687                    .map(|worktree| WorktreeEntry {
688                        worktree,
689                        positions: Vec::new(),
690                        is_new: false,
691                    })
692                    .collect()
693            } else {
694                let candidates = all_worktrees
695                    .iter()
696                    .enumerate()
697                    .map(|(ix, worktree)| StringMatchCandidate::new(ix, worktree.display_name()))
698                    .collect::<Vec<StringMatchCandidate>>();
699                fuzzy::match_strings(
700                    &candidates,
701                    &query,
702                    true,
703                    true,
704                    10000,
705                    &Default::default(),
706                    cx.background_executor().clone(),
707                )
708                .await
709                .into_iter()
710                .map(|candidate| WorktreeEntry {
711                    worktree: all_worktrees[candidate.candidate_id].clone(),
712                    positions: candidate.positions,
713                    is_new: false,
714                })
715                .collect()
716            };
717            picker
718                .update(cx, |picker, _| {
719                    if !query.is_empty()
720                        && !matches
721                            .first()
722                            .is_some_and(|entry| entry.worktree.display_name() == query)
723                    {
724                        let query = query.replace(' ', "-");
725                        matches.push(WorktreeEntry {
726                            worktree: GitWorktree {
727                                path: Default::default(),
728                                ref_name: Some(format!("refs/heads/{query}").into()),
729                                sha: Default::default(),
730                                is_main: false,
731                            },
732                            positions: Vec::new(),
733                            is_new: true,
734                        })
735                    }
736                    let delegate = &mut picker.delegate;
737                    delegate.matches = matches;
738                    if delegate.matches.is_empty() {
739                        delegate.selected_index = 0;
740                    } else {
741                        delegate.selected_index =
742                            core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
743                    }
744                    delegate.last_query = query;
745                })
746                .log_err();
747        })
748    }
749
750    fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
751        let Some(entry) = self.matches.get(self.selected_index()) else {
752            return;
753        };
754        if entry.is_new {
755            self.create_worktree(&entry.worktree.display_name(), secondary, None, window, cx);
756        } else {
757            self.open_worktree(&entry.worktree.path, secondary, window, cx);
758        }
759
760        cx.emit(DismissEvent);
761    }
762
763    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
764        cx.emit(DismissEvent);
765    }
766
767    fn render_match(
768        &self,
769        ix: usize,
770        selected: bool,
771        _window: &mut Window,
772        cx: &mut Context<Picker<Self>>,
773    ) -> Option<Self::ListItem> {
774        let entry = &self.matches.get(ix)?;
775        let path = entry.worktree.path.to_string_lossy().to_string();
776        let sha = entry
777            .worktree
778            .sha
779            .clone()
780            .chars()
781            .take(7)
782            .collect::<String>();
783
784        let (branch_name, sublabel) = if entry.is_new {
785            (
786                Label::new(format!(
787                    "Create Worktree: \"{}\"",
788                    entry.worktree.display_name()
789                ))
790                .truncate()
791                .into_any_element(),
792                format!(
793                    "based off {}",
794                    self.base_branch(cx).unwrap_or("the current branch")
795                ),
796            )
797        } else {
798            let branch = entry.worktree.display_name();
799            let branch_first_line = branch.lines().next().unwrap_or(branch);
800            let positions: Vec<_> = entry
801                .positions
802                .iter()
803                .copied()
804                .filter(|&pos| pos < branch_first_line.len())
805                .collect();
806
807            (
808                HighlightedLabel::new(branch_first_line.to_owned(), positions)
809                    .truncate()
810                    .into_any_element(),
811                path,
812            )
813        };
814
815        let focus_handle = self.focus_handle.clone();
816
817        let can_delete = entry.can_delete(self.forbidden_deletion_path.as_ref());
818
819        let delete_button = |entry_ix: usize| {
820            IconButton::new(("delete-worktree", entry_ix), IconName::Trash)
821                .icon_size(IconSize::Small)
822                .tooltip(move |_, cx| {
823                    Tooltip::for_action_in("Delete Worktree", &DeleteWorktree, &focus_handle, cx)
824                })
825                .on_click(cx.listener(move |this, _, window, cx| {
826                    this.delegate.delete_at(entry_ix, window, cx);
827                }))
828        };
829
830        let entry_icon = if entry.is_new {
831            IconName::Plus
832        } else {
833            IconName::GitWorktree
834        };
835
836        Some(
837            ListItem::new(format!("worktree-menu-{ix}"))
838                .inset(true)
839                .spacing(ListItemSpacing::Sparse)
840                .toggle_state(selected)
841                .child(
842                    h_flex()
843                        .w_full()
844                        .gap_2p5()
845                        .child(
846                            Icon::new(entry_icon)
847                                .color(Color::Muted)
848                                .size(IconSize::Small),
849                        )
850                        .child(v_flex().w_full().child(branch_name).map(|this| {
851                            if entry.is_new {
852                                this.child(
853                                    Label::new(sublabel)
854                                        .size(LabelSize::Small)
855                                        .color(Color::Muted)
856                                        .truncate(),
857                                )
858                            } else {
859                                this.child(
860                                    h_flex()
861                                        .w_full()
862                                        .min_w_0()
863                                        .gap_1p5()
864                                        .child(
865                                            Label::new(sha)
866                                                .size(LabelSize::Small)
867                                                .color(Color::Muted),
868                                        )
869                                        .child(
870                                            Label::new("")
871                                                .alpha(0.5)
872                                                .color(Color::Muted)
873                                                .size(LabelSize::Small),
874                                        )
875                                        .child(
876                                            Label::new(sublabel)
877                                                .truncate()
878                                                .color(Color::Muted)
879                                                .size(LabelSize::Small)
880                                                .flex_1(),
881                                        )
882                                        .into_any_element(),
883                                )
884                            }
885                        })),
886                )
887                .when(can_delete, |this| {
888                    if selected {
889                        this.end_slot(delete_button(ix))
890                    } else {
891                        this.end_hover_slot(delete_button(ix))
892                    }
893                }),
894        )
895    }
896
897    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
898        Some("No worktrees found".into())
899    }
900
901    fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
902        let focus_handle = self.focus_handle.clone();
903        let selected_entry = self.matches.get(self.selected_index);
904        let is_creating = selected_entry.is_some_and(|entry| entry.is_new);
905        let can_delete = selected_entry
906            .is_some_and(|entry| entry.can_delete(self.forbidden_deletion_path.as_ref()));
907
908        let footer_container = h_flex()
909            .w_full()
910            .p_1p5()
911            .gap_0p5()
912            .justify_end()
913            .border_t_1()
914            .border_color(cx.theme().colors().border_variant);
915
916        if is_creating {
917            let from_default_button = self.default_branch.as_ref().map(|default_branch| {
918                Button::new(
919                    "worktree-from-default",
920                    format!("Create from: {default_branch}"),
921                )
922                .key_binding(
923                    KeyBinding::for_action_in(&WorktreeFromDefault, &focus_handle, cx)
924                        .map(|kb| kb.size(rems_from_px(12.))),
925                )
926                .on_click(|_, window, cx| {
927                    window.dispatch_action(WorktreeFromDefault.boxed_clone(), cx)
928                })
929            });
930
931            let current_branch = self.base_branch(cx).unwrap_or("current branch");
932
933            Some(
934                footer_container
935                    .when_some(from_default_button, |this, button| this.child(button))
936                    .child(
937                        Button::new(
938                            "worktree-from-current",
939                            format!("Create from: {current_branch}"),
940                        )
941                        .key_binding(
942                            KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
943                                .map(|kb| kb.size(rems_from_px(12.))),
944                        )
945                        .on_click(|_, window, cx| {
946                            window.dispatch_action(menu::Confirm.boxed_clone(), cx)
947                        }),
948                    )
949                    .into_any(),
950            )
951        } else {
952            Some(
953                footer_container
954                    .when(can_delete, |this| {
955                        this.child(
956                            Button::new("delete-worktree", "Delete")
957                                .key_binding(
958                                    KeyBinding::for_action_in(&DeleteWorktree, &focus_handle, cx)
959                                        .map(|kb| kb.size(rems_from_px(12.))),
960                                )
961                                .on_click(|_, window, cx| {
962                                    window.dispatch_action(DeleteWorktree.boxed_clone(), cx)
963                                }),
964                        )
965                    })
966                    .child(
967                        Button::new("open-in-new-window", "Open in New Window")
968                            .key_binding(
969                                KeyBinding::for_action_in(
970                                    &menu::SecondaryConfirm,
971                                    &focus_handle,
972                                    cx,
973                                )
974                                .map(|kb| kb.size(rems_from_px(12.))),
975                            )
976                            .on_click(|_, window, cx| {
977                                window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx)
978                            }),
979                    )
980                    .child(
981                        Button::new("open-in-window", "Open")
982                            .key_binding(
983                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
984                                    .map(|kb| kb.size(rems_from_px(12.))),
985                            )
986                            .on_click(|_, window, cx| {
987                                window.dispatch_action(menu::Confirm.boxed_clone(), cx)
988                            }),
989                    )
990                    .into_any(),
991            )
992        }
993    }
994}