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