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