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