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}