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