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