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