worktree_picker.rs

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