worktree_picker.rs

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